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 if lg != nil { 82 lg.Info( 83 "disabled backend quota", 84 zap.String("quota-name", name), 85 zap.Int64("quota-size-bytes", s.Cfg.QuotaBackendBytes), 86 ) 87 } else { 88 plog.Warningf("disabling backend quota") 89 } 90 }) 91 return &passthroughQuota{} 92 } 93 94 if s.Cfg.QuotaBackendBytes == 0 { 95 // use default size if no quota size given 96 quotaLogOnce.Do(func() { 97 if lg != nil { 98 lg.Info( 99 "enabled backend quota with default value", 100 zap.String("quota-name", name), 101 zap.Int64("quota-size-bytes", DefaultQuotaBytes), 102 zap.String("quota-size", DefaultQuotaSize), 103 ) 104 } 105 }) 106 quotaBackendBytes.Set(float64(DefaultQuotaBytes)) 107 return &backendQuota{s, DefaultQuotaBytes} 108 } 109 110 quotaLogOnce.Do(func() { 111 if s.Cfg.QuotaBackendBytes > MaxQuotaBytes { 112 if lg != nil { 113 lg.Warn( 114 "quota exceeds the maximum value", 115 zap.String("quota-name", name), 116 zap.Int64("quota-size-bytes", s.Cfg.QuotaBackendBytes), 117 zap.String("quota-size", humanize.Bytes(uint64(s.Cfg.QuotaBackendBytes))), 118 zap.Int64("quota-maximum-size-bytes", MaxQuotaBytes), 119 zap.String("quota-maximum-size", maxQuotaSize), 120 ) 121 } else { 122 plog.Warningf("backend quota %v exceeds maximum recommended quota %v", s.Cfg.QuotaBackendBytes, MaxQuotaBytes) 123 } 124 } 125 if lg != nil { 126 lg.Info( 127 "enabled backend quota", 128 zap.String("quota-name", name), 129 zap.Int64("quota-size-bytes", s.Cfg.QuotaBackendBytes), 130 zap.String("quota-size", humanize.Bytes(uint64(s.Cfg.QuotaBackendBytes))), 131 ) 132 } 133 }) 134 return &backendQuota{s, s.Cfg.QuotaBackendBytes} 135} 136 137func (b *backendQuota) Available(v interface{}) bool { 138 // TODO: maybe optimize backend.Size() 139 return b.s.Backend().Size()+int64(b.Cost(v)) < b.maxBackendBytes 140} 141 142func (b *backendQuota) Cost(v interface{}) int { 143 switch r := v.(type) { 144 case *pb.PutRequest: 145 return costPut(r) 146 case *pb.TxnRequest: 147 return costTxn(r) 148 case *pb.LeaseGrantRequest: 149 return leaseOverhead 150 default: 151 panic("unexpected cost") 152 } 153} 154 155func costPut(r *pb.PutRequest) int { return kvOverhead + len(r.Key) + len(r.Value) } 156 157func costTxnReq(u *pb.RequestOp) int { 158 r := u.GetRequestPut() 159 if r == nil { 160 return 0 161 } 162 return costPut(r) 163} 164 165func costTxn(r *pb.TxnRequest) int { 166 sizeSuccess := 0 167 for _, u := range r.Success { 168 sizeSuccess += costTxnReq(u) 169 } 170 sizeFailure := 0 171 for _, u := range r.Failure { 172 sizeFailure += costTxnReq(u) 173 } 174 if sizeFailure > sizeSuccess { 175 return sizeFailure 176 } 177 return sizeSuccess 178} 179 180func (b *backendQuota) Remaining() int64 { 181 return b.maxBackendBytes - b.s.Backend().Size() 182} 183