github.com/thanos-io/thanos@v0.32.5/pkg/store/limiter.go (about) 1 // Copyright (c) The Thanos Authors. 2 // Licensed under the Apache License 2.0. 3 4 package store 5 6 import ( 7 "sync" 8 9 "github.com/alecthomas/units" 10 "github.com/pkg/errors" 11 "github.com/prometheus/client_golang/prometheus" 12 "github.com/prometheus/client_golang/prometheus/promauto" 13 "go.uber.org/atomic" 14 15 "github.com/thanos-io/thanos/pkg/extkingpin" 16 "github.com/thanos-io/thanos/pkg/store/storepb" 17 ) 18 19 type ChunksLimiter interface { 20 // Reserve num chunks out of the total number of chunks enforced by the limiter. 21 // Returns an error if the limit has been exceeded. This function must be 22 // goroutine safe. 23 Reserve(num uint64) error 24 } 25 26 type SeriesLimiter interface { 27 // Reserve num series out of the total number of series enforced by the limiter. 28 // Returns an error if the limit has been exceeded. This function must be 29 // goroutine safe. 30 Reserve(num uint64) error 31 } 32 33 type BytesLimiter interface { 34 // Reserve bytes out of the total amount of bytes enforced by the limiter. 35 // Returns an error if the limit has been exceeded. This function must be 36 // goroutine safe. 37 Reserve(num uint64) error 38 } 39 40 // ChunksLimiterFactory is used to create a new ChunksLimiter. The factory is useful for 41 // projects depending on Thanos (eg. Cortex) which have dynamic limits. 42 type ChunksLimiterFactory func(failedCounter prometheus.Counter) ChunksLimiter 43 44 // SeriesLimiterFactory is used to create a new SeriesLimiter. 45 type SeriesLimiterFactory func(failedCounter prometheus.Counter) SeriesLimiter 46 47 // BytesLimiterFactory is used to create a new BytesLimiter. 48 type BytesLimiterFactory func(failedCounter prometheus.Counter) BytesLimiter 49 50 // Limiter is a simple mechanism for checking if something has passed a certain threshold. 51 type Limiter struct { 52 limit uint64 53 reserved atomic.Uint64 54 55 // Counter metric which we will increase if limit is exceeded. 56 failedCounter prometheus.Counter 57 failedOnce sync.Once 58 } 59 60 // NewLimiter returns a new limiter with a specified limit. 0 disables the limit. 61 func NewLimiter(limit uint64, ctr prometheus.Counter) *Limiter { 62 return &Limiter{limit: limit, failedCounter: ctr} 63 } 64 65 // Reserve implements ChunksLimiter. 66 func (l *Limiter) Reserve(num uint64) error { 67 if l == nil { 68 return nil 69 } 70 if l.limit == 0 { 71 return nil 72 } 73 if reserved := l.reserved.Add(num); reserved > l.limit { 74 // We need to protect from the counter being incremented twice due to concurrency 75 // while calling Reserve(). 76 l.failedOnce.Do(l.failedCounter.Inc) 77 return errors.Errorf("limit %v violated (got %v)", l.limit, reserved) 78 } 79 return nil 80 } 81 82 // NewChunksLimiterFactory makes a new ChunksLimiterFactory with a static limit. 83 func NewChunksLimiterFactory(limit uint64) ChunksLimiterFactory { 84 return func(failedCounter prometheus.Counter) ChunksLimiter { 85 return NewLimiter(limit, failedCounter) 86 } 87 } 88 89 // NewSeriesLimiterFactory makes a new SeriesLimiterFactory with a static limit. 90 func NewSeriesLimiterFactory(limit uint64) SeriesLimiterFactory { 91 return func(failedCounter prometheus.Counter) SeriesLimiter { 92 return NewLimiter(limit, failedCounter) 93 } 94 } 95 96 // NewBytesLimiterFactory makes a new BytesLimiterFactory with a static limit. 97 func NewBytesLimiterFactory(limit units.Base2Bytes) BytesLimiterFactory { 98 return func(failedCounter prometheus.Counter) BytesLimiter { 99 return NewLimiter(uint64(limit), failedCounter) 100 } 101 } 102 103 // SeriesSelectLimits are limits applied against individual Series calls. 104 type SeriesSelectLimits struct { 105 SeriesPerRequest uint64 106 SamplesPerRequest uint64 107 } 108 109 func (l *SeriesSelectLimits) RegisterFlags(cmd extkingpin.FlagClause) { 110 cmd.Flag("store.limits.request-series", "The maximum series allowed for a single Series request. The Series call fails if this limit is exceeded. 0 means no limit.").Default("0").Uint64Var(&l.SeriesPerRequest) 111 cmd.Flag("store.limits.request-samples", "The maximum samples allowed for a single Series request, The Series call fails if this limit is exceeded. 0 means no limit. NOTE: For efficiency the limit is internally implemented as 'chunks limit' considering each chunk contains a maximum of 120 samples.").Default("0").Uint64Var(&l.SamplesPerRequest) 112 } 113 114 var _ storepb.StoreServer = &limitedStoreServer{} 115 116 // limitedStoreServer is a storepb.StoreServer that can apply series and sample limits against individual Series requests. 117 type limitedStoreServer struct { 118 storepb.StoreServer 119 newSeriesLimiter SeriesLimiterFactory 120 newSamplesLimiter ChunksLimiterFactory 121 failedRequestsCounter *prometheus.CounterVec 122 } 123 124 // NewLimitedStoreServer creates a new limitedStoreServer. 125 func NewLimitedStoreServer(store storepb.StoreServer, reg prometheus.Registerer, selectLimits SeriesSelectLimits) storepb.StoreServer { 126 return &limitedStoreServer{ 127 StoreServer: store, 128 newSeriesLimiter: NewSeriesLimiterFactory(selectLimits.SeriesPerRequest), 129 newSamplesLimiter: NewChunksLimiterFactory(selectLimits.SamplesPerRequest), 130 failedRequestsCounter: promauto.With(reg).NewCounterVec(prometheus.CounterOpts{ 131 Name: "thanos_store_selects_dropped_total", 132 Help: "Number of select queries that were dropped due to configured limits.", 133 }, []string{"reason"}), 134 } 135 } 136 137 func (s *limitedStoreServer) Series(req *storepb.SeriesRequest, srv storepb.Store_SeriesServer) error { 138 seriesLimiter := s.newSeriesLimiter(s.failedRequestsCounter.WithLabelValues("series")) 139 chunksLimiter := s.newSamplesLimiter(s.failedRequestsCounter.WithLabelValues("chunks")) 140 limitedSrv := newLimitedServer(srv, seriesLimiter, chunksLimiter) 141 if err := s.StoreServer.Series(req, limitedSrv); err != nil { 142 return err 143 } 144 145 return nil 146 } 147 148 var _ storepb.Store_SeriesServer = &limitedServer{} 149 150 // limitedServer is a storepb.Store_SeriesServer that tracks statistics about sent series. 151 type limitedServer struct { 152 storepb.Store_SeriesServer 153 seriesLimiter SeriesLimiter 154 samplesLimiter ChunksLimiter 155 } 156 157 func newLimitedServer(upstream storepb.Store_SeriesServer, seriesLimiter SeriesLimiter, chunksLimiter ChunksLimiter) *limitedServer { 158 return &limitedServer{ 159 Store_SeriesServer: upstream, 160 seriesLimiter: seriesLimiter, 161 samplesLimiter: chunksLimiter, 162 } 163 } 164 165 func (i *limitedServer) Send(response *storepb.SeriesResponse) error { 166 series := response.GetSeries() 167 if series == nil { 168 return i.Store_SeriesServer.Send(response) 169 } 170 171 if err := i.seriesLimiter.Reserve(1); err != nil { 172 return errors.Wrapf(err, "failed to send series") 173 } 174 if err := i.samplesLimiter.Reserve(uint64(len(series.Chunks) * MaxSamplesPerChunk)); err != nil { 175 return errors.Wrapf(err, "failed to send samples") 176 } 177 178 return i.Store_SeriesServer.Send(response) 179 }