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