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  }