1// Copyright 2016 The etcd Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package etcdserver 16 17import ( 18 "sync" 19 20 pb "go.etcd.io/etcd/etcdserver/etcdserverpb" 21 22 humanize "github.com/dustin/go-humanize" 23 "go.uber.org/zap" 24) 25 26const ( 27 // DefaultQuotaBytes is the number of bytes the backend Size may 28 // consume before exceeding the space quota. 29 DefaultQuotaBytes = int64(2 * 1024 * 1024 * 1024) // 2GB 30 // MaxQuotaBytes is the maximum number of bytes suggested for a backend 31 // quota. A larger quota may lead to degraded performance. 32 MaxQuotaBytes = int64(8 * 1024 * 1024 * 1024) // 8GB 33) 34 35// Quota represents an arbitrary quota against arbitrary requests. Each request 36// costs some charge; if there is not enough remaining charge, then there are 37// too few resources available within the quota to apply the request. 38type Quota interface { 39 // Available judges whether the given request fits within the quota. 40 Available(req interface{}) bool 41 // Cost computes the charge against the quota for a given request. 42 Cost(req interface{}) int 43 // Remaining is the amount of charge left for the quota. 44 Remaining() int64 45} 46 47type passthroughQuota struct{} 48 49func (*passthroughQuota) Available(interface{}) bool { return true } 50func (*passthroughQuota) Cost(interface{}) int { return 0 } 51func (*passthroughQuota) Remaining() int64 { return 1 } 52 53type backendQuota struct { 54 s *EtcdServer 55 maxBackendBytes int64 56} 57 58const ( 59 // leaseOverhead is an estimate for the cost of storing a lease 60 leaseOverhead = 64 61 // kvOverhead is an estimate for the cost of storing a key's metadata 62 kvOverhead = 256 63) 64 65var ( 66 // only log once 67 quotaLogOnce sync.Once 68 69 DefaultQuotaSize = humanize.Bytes(uint64(DefaultQuotaBytes)) 70 maxQuotaSize = humanize.Bytes(uint64(MaxQuotaBytes)) 71) 72 73// NewBackendQuota creates a quota layer with the given storage limit. 74func NewBackendQuota(s *EtcdServer, name string) Quota { 75 lg := s.getLogger() 76 quotaBackendBytes.Set(float64(s.Cfg.QuotaBackendBytes)) 77 78 if s.Cfg.QuotaBackendBytes < 0 { 79 // disable quotas if negative 80 quotaLogOnce.Do(func() { 81 lg.Info( 82 "disabled backend quota", 83 zap.String("quota-name", name), 84 zap.Int64("quota-size-bytes", s.Cfg.QuotaBackendBytes), 85 ) 86 }) 87 return &passthroughQuota{} 88 } 89 90 if s.Cfg.QuotaBackendBytes == 0 { 91 // use default size if no quota size given 92 quotaLogOnce.Do(func() { 93 if lg != nil { 94 lg.Info( 95 "enabled backend quota with default value", 96 zap.String("quota-name", name), 97 zap.Int64("quota-size-bytes", DefaultQuotaBytes), 98 zap.String("quota-size", DefaultQuotaSize), 99 ) 100 } 101 }) 102 quotaBackendBytes.Set(float64(DefaultQuotaBytes)) 103 return &backendQuota{s, DefaultQuotaBytes} 104 } 105 106 quotaLogOnce.Do(func() { 107 if s.Cfg.QuotaBackendBytes > MaxQuotaBytes { 108 lg.Warn( 109 "quota exceeds the maximum value", 110 zap.String("quota-name", name), 111 zap.Int64("quota-size-bytes", s.Cfg.QuotaBackendBytes), 112 zap.String("quota-size", humanize.Bytes(uint64(s.Cfg.QuotaBackendBytes))), 113 zap.Int64("quota-maximum-size-bytes", MaxQuotaBytes), 114 zap.String("quota-maximum-size", maxQuotaSize), 115 ) 116 } 117 lg.Info( 118 "enabled backend quota", 119 zap.String("quota-name", name), 120 zap.Int64("quota-size-bytes", s.Cfg.QuotaBackendBytes), 121 zap.String("quota-size", humanize.Bytes(uint64(s.Cfg.QuotaBackendBytes))), 122 ) 123 }) 124 return &backendQuota{s, s.Cfg.QuotaBackendBytes} 125} 126 127func (b *backendQuota) Available(v interface{}) bool { 128 // TODO: maybe optimize backend.Size() 129 return b.s.Backend().Size()+int64(b.Cost(v)) < b.maxBackendBytes 130} 131 132func (b *backendQuota) Cost(v interface{}) int { 133 switch r := v.(type) { 134 case *pb.PutRequest: 135 return costPut(r) 136 case *pb.TxnRequest: 137 return costTxn(r) 138 case *pb.LeaseGrantRequest: 139 return leaseOverhead 140 default: 141 panic("unexpected cost") 142 } 143} 144 145func costPut(r *pb.PutRequest) int { return kvOverhead + len(r.Key) + len(r.Value) } 146 147func costTxnReq(u *pb.RequestOp) int { 148 r := u.GetRequestPut() 149 if r == nil { 150 return 0 151 } 152 return costPut(r) 153} 154 155func costTxn(r *pb.TxnRequest) int { 156 sizeSuccess := 0 157 for _, u := range r.Success { 158 sizeSuccess += costTxnReq(u) 159 } 160 sizeFailure := 0 161 for _, u := range r.Failure { 162 sizeFailure += costTxnReq(u) 163 } 164 if sizeFailure > sizeSuccess { 165 return sizeFailure 166 } 167 return sizeSuccess 168} 169 170func (b *backendQuota) Remaining() int64 { 171 return b.maxBackendBytes - b.s.Backend().Size() 172} 173