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