github.com/thanos-io/thanos@v0.32.5/pkg/receive/limiter.go (about) 1 // Copyright (c) The Thanos Authors. 2 // Licensed under the Apache License 2.0. 3 4 package receive 5 6 import ( 7 "context" 8 "fmt" 9 "sync" 10 "time" 11 12 "github.com/go-kit/log" 13 "github.com/go-kit/log/level" 14 "github.com/prometheus/client_golang/prometheus/promauto" 15 "github.com/thanos-io/thanos/pkg/extkingpin" 16 17 "github.com/pkg/errors" 18 "github.com/prometheus/client_golang/prometheus" 19 "github.com/thanos-io/thanos/pkg/extprom" 20 "github.com/thanos-io/thanos/pkg/gate" 21 ) 22 23 // Limiter is responsible for managing the configuration and initialization of 24 // different types that apply limits to the Receive instance. 25 type Limiter struct { 26 sync.RWMutex 27 requestLimiter requestLimiter 28 HeadSeriesLimiter headSeriesLimiter 29 writeGate gate.Gate 30 registerer prometheus.Registerer 31 configPathOrContent fileContent 32 logger log.Logger 33 configReloadCounter prometheus.Counter 34 configReloadFailedCounter prometheus.Counter 35 receiverMode ReceiverMode 36 configReloadTimer time.Duration 37 } 38 39 // headSeriesLimiter encompasses active/head series limiting logic. 40 type headSeriesLimiter interface { 41 QueryMetaMonitoring(context.Context) error 42 isUnderLimit(tenant string) (bool, error) 43 } 44 45 type requestLimiter interface { 46 AllowSizeBytes(tenant string, contentLengthBytes int64) bool 47 AllowSeries(tenant string, amount int64) bool 48 AllowSamples(tenant string, amount int64) bool 49 } 50 51 // fileContent is an interface to avoid a direct dependency on kingpin or extkingpin. 52 type fileContent interface { 53 Content() ([]byte, error) 54 Path() string 55 } 56 57 // NewLimiter creates a new *Limiter given a configuration and prometheus 58 // registerer. 59 func NewLimiter(configFile fileContent, reg prometheus.Registerer, r ReceiverMode, logger log.Logger, configReloadTimer time.Duration) (*Limiter, error) { 60 limiter := &Limiter{ 61 writeGate: gate.NewNoop(), 62 requestLimiter: &noopRequestLimiter{}, 63 HeadSeriesLimiter: NewNopSeriesLimit(), 64 logger: logger, 65 receiverMode: r, 66 configReloadTimer: configReloadTimer, 67 } 68 69 if reg != nil { 70 limiter.registerer = NewUnRegisterer(reg) 71 limiter.configReloadCounter = promauto.With(limiter.registerer).NewCounter( 72 prometheus.CounterOpts{ 73 Namespace: "thanos", 74 Subsystem: "receive", 75 Name: "limits_config_reload_total", 76 Help: "How many times the limit configuration was reloaded", 77 }, 78 ) 79 limiter.configReloadFailedCounter = promauto.With(limiter.registerer).NewCounter( 80 prometheus.CounterOpts{ 81 Namespace: "thanos", 82 Subsystem: "receive", 83 Name: "limits_config_reload_err_total", 84 Help: "How many times the limit configuration failed to reload.", 85 }, 86 ) 87 } 88 89 if configFile == nil { 90 return limiter, nil 91 } 92 93 limiter.configPathOrContent = configFile 94 if err := limiter.loadConfig(); err != nil { 95 return nil, errors.Wrap(err, "load tenant limits config") 96 } 97 98 return limiter, nil 99 } 100 101 // StartConfigReloader starts the automatic configuration reloader based off of 102 // the file indicated by pathOrContent. It starts a Go routine in the given 103 // *run.Group. 104 func (l *Limiter) StartConfigReloader(ctx context.Context) error { 105 if !l.CanReload() { 106 return nil 107 } 108 109 return extkingpin.PathContentReloader(ctx, l.configPathOrContent, l.logger, func() { 110 level.Info(l.logger).Log("msg", "reloading limit config") 111 if err := l.loadConfig(); err != nil { 112 if failedReload := l.configReloadCounter; failedReload != nil { 113 failedReload.Inc() 114 } 115 errMsg := fmt.Sprintf("error reloading tenant limits config from %s", l.configPathOrContent.Path()) 116 level.Error(l.logger).Log("msg", errMsg, "err", err) 117 } 118 if reloadCounter := l.configReloadCounter; reloadCounter != nil { 119 reloadCounter.Inc() 120 } 121 }, l.configReloadTimer) 122 } 123 124 func (l *Limiter) CanReload() bool { 125 if l.configPathOrContent == nil { 126 return false 127 } 128 if l.configPathOrContent.Path() == "" { 129 return false 130 } 131 return true 132 } 133 134 func (l *Limiter) loadConfig() error { 135 config, err := ParseLimitConfigContent(l.configPathOrContent) 136 if err != nil { 137 return err 138 } 139 l.Lock() 140 defer l.Unlock() 141 maxWriteConcurrency := config.WriteLimits.GlobalLimits.MaxConcurrency 142 if maxWriteConcurrency > 0 { 143 l.writeGate = gate.New( 144 extprom.WrapRegistererWithPrefix( 145 "thanos_receive_write_request_concurrent_", 146 l.registerer, 147 ), 148 int(maxWriteConcurrency), 149 gate.WriteRequests, 150 ) 151 } 152 l.requestLimiter = newConfigRequestLimiter( 153 l.registerer, 154 &config.WriteLimits, 155 ) 156 seriesLimitIsActivated := func() bool { 157 if config.WriteLimits.DefaultLimits.HeadSeriesLimit != 0 { 158 return true 159 } 160 for _, tenant := range config.WriteLimits.TenantsLimits { 161 if tenant.HeadSeriesLimit != nil && *tenant.HeadSeriesLimit != 0 { 162 return true 163 } 164 } 165 return false 166 } 167 if (l.receiverMode == RouterOnly || l.receiverMode == RouterIngestor) && seriesLimitIsActivated() { 168 l.HeadSeriesLimiter = NewHeadSeriesLimit(config.WriteLimits, l.registerer, l.logger) 169 } 170 return nil 171 } 172 173 // RequestLimiter is a safe getter for the request limiter. 174 func (l *Limiter) RequestLimiter() requestLimiter { 175 l.RLock() 176 defer l.RUnlock() 177 return l.requestLimiter 178 } 179 180 // WriteGate is a safe getter for the write gate. 181 func (l *Limiter) WriteGate() gate.Gate { 182 l.RLock() 183 defer l.RUnlock() 184 return l.writeGate 185 } 186 187 // ParseLimitConfigContent parses the limit configuration from the path or 188 // content. 189 func ParseLimitConfigContent(limitsConfig fileContent) (*RootLimitsConfig, error) { 190 if limitsConfig == nil { 191 return &RootLimitsConfig{}, nil 192 } 193 limitsContentYaml, err := limitsConfig.Content() 194 if err != nil { 195 return nil, errors.Wrap(err, "get content of limit configuration") 196 } 197 parsedConfig, err := ParseRootLimitConfig(limitsContentYaml) 198 if err != nil { 199 return nil, errors.Wrap(err, "parse limit configuration") 200 } 201 return parsedConfig, nil 202 } 203 204 type nopConfigContent struct{} 205 206 var _ fileContent = (*nopConfigContent)(nil) 207 208 // Content returns no content and no error. 209 func (n nopConfigContent) Content() ([]byte, error) { 210 return nil, nil 211 } 212 213 // Path returns an empty path. 214 func (n nopConfigContent) Path() string { 215 return "" 216 } 217 218 // NewNopConfig creates a no-op config content (no configuration). 219 func NewNopConfig() nopConfigContent { 220 return nopConfigContent{} 221 }