Skip to content

Commit 7f85a26

Browse files
authored
Ingester bounds (#3992)
* Added global ingester limits. Signed-off-by: Peter Štibraný <[email protected]> * Add tests for global limits. Signed-off-by: Peter Štibraný <[email protected]> * Expose current limits used by ingester via metrics. Signed-off-by: Peter Štibraný <[email protected]> * Add max inflight requests limit. Signed-off-by: Peter Štibraný <[email protected]> * Added test for inflight push requests. Signed-off-by: Peter Štibraný <[email protected]> * Docs. Signed-off-by: Peter Štibraný <[email protected]> * Debug log. Signed-off-by: Peter Štibraný <[email protected]> * Test for unmarshalling. Signed-off-by: Peter Štibraný <[email protected]> * Nil default global limits. Signed-off-by: Peter Štibraný <[email protected]> * CHANGELOG.md Signed-off-by: Peter Štibraný <[email protected]> * Expose current ingestion rate as gauge. Signed-off-by: Peter Štibraný <[email protected]> * Expose number of inflight requests. Signed-off-by: Peter Štibraný <[email protected]> * Change ewmaRate to use RWMutex. Signed-off-by: Peter Štibraný <[email protected]> * Rename globalLimits to instanceLimits. Rename max_users to max_tenants. Removed extra parameter to `getOrCreateTSDB` Signed-off-by: Peter Štibraný <[email protected]> * Rename globalLimits to instanceLimits, fix users -> tenants, explain that these limits only work when using blocks engine. Signed-off-by: Peter Štibraný <[email protected]> * Rename globalLimits to instanceLimits, fix users -> tenants, explain that these limits only work when using blocks engine. Signed-off-by: Peter Štibraný <[email protected]> * Remove details from error messages. Signed-off-by: Peter Štibraný <[email protected]> * Comment. Signed-off-by: Peter Štibraný <[email protected]> * Fix series count when closing non-empty TSDB. Signed-off-by: Peter Štibraný <[email protected]> * Added new failure modes to benchmark. Signed-off-by: Peter Štibraný <[email protected]> * Fixed docs. Signed-off-by: Peter Štibraný <[email protected]> * Tick every second. Signed-off-by: Peter Štibraný <[email protected]> * Fix CHANGELOG.md Signed-off-by: Peter Štibraný <[email protected]> * Review feedback. Signed-off-by: Peter Štibraný <[email protected]> * Review feedback. Signed-off-by: Peter Štibraný <[email protected]> * Remove forgotten fmt.Println. Signed-off-by: Peter Štibraný <[email protected]> * Use error variables. Signed-off-by: Peter Štibraný <[email protected]>
1 parent f107e5d commit 7f85a26

11 files changed

+786
-86
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
* [ENHANCEMENT] Allow use of `y|w|d` suffixes for duration related limits and per-tenant limits. #4044
2828
* [ENHANCEMENT] Query-frontend: Small optimization on top of PR #3968 to avoid unnecessary Extents merging. #4026
2929
* [ENHANCEMENT] Add a metric `cortex_compactor_compaction_interval_seconds` for the compaction interval config value. #4040
30+
* [ENHANCEMENT] Ingester: added following per-ingester (instance) limits: max number of series in memory (`-ingester.instance-limits.max-series`), max number of users in memory (`-ingester.instance-limits.max-tenants`), max ingestion rate (`-ingester.instance-limits.max-ingestion-rate`), and max inflight requests (`-ingester.instance-limits.max-inflight-push-requests`). These limits are only used when using blocks storage. Limits can also be configured using runtime-config feature, and current values are exported as `cortex_ingester_instance_limits` metric. #3992.
3031
* [BUGFIX] Ruler-API: fix bug where `/api/v1/rules/<namespace>/<group_name>` endpoint return `400` instead of `404`. #4013
3132
* [BUGFIX] Distributor: reverted changes done to rate limiting in #3825. #3948
3233
* [BUGFIX] Ingester: Fix race condition when opening and closing tsdb concurrently. #3959

docs/configuration/config-file-reference.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,31 @@ lifecycler:
771771
# After what time a series is considered to be inactive.
772772
# CLI flag: -ingester.active-series-metrics-idle-timeout
773773
[active_series_metrics_idle_timeout: <duration> | default = 10m]
774+
775+
instance_limits:
776+
# Max ingestion rate (samples/sec) that ingester will accept. This limit is
777+
# per-ingester, not per-tenant. Additional push requests will be rejected.
778+
# Current ingestion rate is computed as exponentially weighted moving average,
779+
# updated every second. This limit only works when using blocks engine. 0 =
780+
# unlimited.
781+
# CLI flag: -ingester.instance-limits.max-ingestion-rate
782+
[max_ingestion_rate: <float> | default = 0]
783+
784+
# Max users that this ingester can hold. Requests from additional users will
785+
# be rejected. This limit only works when using blocks engine. 0 = unlimited.
786+
# CLI flag: -ingester.instance-limits.max-tenants
787+
[max_tenants: <int> | default = 0]
788+
789+
# Max series that this ingester can hold (across all tenants). Requests to
790+
# create additional series will be rejected. This limit only works when using
791+
# blocks engine. 0 = unlimited.
792+
# CLI flag: -ingester.instance-limits.max-series
793+
[max_series: <int> | default = 0]
794+
795+
# Max inflight push requests that this ingester can handle (across all
796+
# tenants). Additional requests will be rejected. 0 = unlimited.
797+
# CLI flag: -ingester.instance-limits.max-inflight-push-requests
798+
[max_inflight_push_requests: <int> | default = 0]
774799
```
775800

776801
### `querier_config`

pkg/cortex/modules.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ func (t *Cortex) initIngesterService() (serv services.Service, err error) {
424424
t.Cfg.Ingester.DistributorShardingStrategy = t.Cfg.Distributor.ShardingStrategy
425425
t.Cfg.Ingester.DistributorShardByAllLabels = t.Cfg.Distributor.ShardByAllLabels
426426
t.Cfg.Ingester.StreamTypeFn = ingesterChunkStreaming(t.RuntimeConfig)
427+
t.Cfg.Ingester.InstanceLimitsFn = ingesterInstanceLimits(t.RuntimeConfig)
427428
t.tsdbIngesterConfig()
428429

429430
t.Ingester, err = ingester.New(t.Cfg.Ingester, t.Cfg.IngesterClient, t.Overrides, t.Store, prometheus.DefaultRegisterer, util_log.Logger)

pkg/cortex/runtime_config.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ type runtimeConfigValues struct {
2727
Multi kv.MultiRuntimeConfig `yaml:"multi_kv_config"`
2828

2929
IngesterChunkStreaming *bool `yaml:"ingester_stream_chunks_when_using_blocks"`
30+
31+
IngesterLimits ingester.InstanceLimits `yaml:"ingester_limits"`
3032
}
3133

3234
// runtimeConfigTenantLimits provides per-tenant limit overrides based on a runtimeconfig.Manager
@@ -124,6 +126,20 @@ func ingesterChunkStreaming(manager *runtimeconfig.Manager) func() ingester.Quer
124126
}
125127
}
126128

129+
func ingesterInstanceLimits(manager *runtimeconfig.Manager) func() *ingester.InstanceLimits {
130+
if manager == nil {
131+
return nil
132+
}
133+
134+
return func() *ingester.InstanceLimits {
135+
val := manager.GetConfig()
136+
if cfg, ok := val.(*runtimeConfigValues); ok && cfg != nil {
137+
return &cfg.IngesterLimits
138+
}
139+
return nil
140+
}
141+
}
142+
127143
func runtimeConfigHandler(runtimeCfgManager *runtimeconfig.Manager, defaultLimits validation.Limits) http.HandlerFunc {
128144
return func(w http.ResponseWriter, r *http.Request) {
129145
cfg, ok := runtimeCfgManager.GetConfig().(*runtimeConfigValues)

pkg/ingester/ingester.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/prometheus/prometheus/pkg/labels"
1919
tsdb_record "github.com/prometheus/prometheus/tsdb/record"
2020
"github.com/weaveworks/common/httpgrpc"
21+
"go.uber.org/atomic"
2122
"golang.org/x/time/rate"
2223
"google.golang.org/grpc/codes"
2324

@@ -93,6 +94,9 @@ type Config struct {
9394
DistributorShardingStrategy string `yaml:"-"`
9495
DistributorShardByAllLabels bool `yaml:"-"`
9596

97+
DefaultLimits InstanceLimits `yaml:"instance_limits"`
98+
InstanceLimitsFn func() *InstanceLimits `yaml:"-"`
99+
96100
// For testing, you can override the address and ID of this ingester.
97101
ingesterClientFactory func(addr string, cfg client.Config) (client.HealthAndIngesterClient, error)
98102
}
@@ -121,6 +125,11 @@ func (cfg *Config) RegisterFlags(f *flag.FlagSet) {
121125
f.DurationVar(&cfg.ActiveSeriesMetricsUpdatePeriod, "ingester.active-series-metrics-update-period", 1*time.Minute, "How often to update active series metrics.")
122126
f.DurationVar(&cfg.ActiveSeriesMetricsIdleTimeout, "ingester.active-series-metrics-idle-timeout", 10*time.Minute, "After what time a series is considered to be inactive.")
123127
f.BoolVar(&cfg.StreamChunksWhenUsingBlocks, "ingester.stream-chunks-when-using-blocks", false, "Stream chunks when using blocks. This is experimental feature and not yet tested. Once ready, it will be made default and this config option removed.")
128+
129+
f.Float64Var(&cfg.DefaultLimits.MaxIngestionRate, "ingester.instance-limits.max-ingestion-rate", 0, "Max ingestion rate (samples/sec) that ingester will accept. This limit is per-ingester, not per-tenant. Additional push requests will be rejected. Current ingestion rate is computed as exponentially weighted moving average, updated every second. This limit only works when using blocks engine. 0 = unlimited.")
130+
f.Int64Var(&cfg.DefaultLimits.MaxInMemoryTenants, "ingester.instance-limits.max-tenants", 0, "Max users that this ingester can hold. Requests from additional users will be rejected. This limit only works when using blocks engine. 0 = unlimited.")
131+
f.Int64Var(&cfg.DefaultLimits.MaxInMemorySeries, "ingester.instance-limits.max-series", 0, "Max series that this ingester can hold (across all tenants). Requests to create additional series will be rejected. This limit only works when using blocks engine. 0 = unlimited.")
132+
f.Int64Var(&cfg.DefaultLimits.MaxInflightPushRequests, "ingester.instance-limits.max-inflight-push-requests", 0, "Max inflight push requests that this ingester can handle (across all tenants). Additional requests will be rejected. 0 = unlimited.")
124133
}
125134

126135
// Ingester deals with "in flight" chunks. Based on Prometheus 1.x
@@ -167,6 +176,10 @@ type Ingester struct {
167176

168177
// Prometheus block storage
169178
TSDBState TSDBState
179+
180+
// Rate of pushed samples. Only used by V2-ingester to limit global samples push rate.
181+
ingestionRate *ewmaRate
182+
inflightPushRequests atomic.Int64
170183
}
171184

172185
// ChunkStore is the interface we need to store chunks
@@ -176,6 +189,8 @@ type ChunkStore interface {
176189

177190
// New constructs a new Ingester.
178191
func New(cfg Config, clientConfig client.Config, limits *validation.Overrides, chunkStore ChunkStore, registerer prometheus.Registerer, logger log.Logger) (*Ingester, error) {
192+
defaultInstanceLimits = &cfg.DefaultLimits
193+
179194
if cfg.ingesterClientFactory == nil {
180195
cfg.ingesterClientFactory = client.MakeIngesterClient
181196
}
@@ -209,7 +224,6 @@ func New(cfg Config, clientConfig client.Config, limits *validation.Overrides, c
209224
i := &Ingester{
210225
cfg: cfg,
211226
clientConfig: clientConfig,
212-
metrics: newIngesterMetrics(registerer, true, cfg.ActiveSeriesMetricsEnabled),
213227

214228
limits: limits,
215229
chunkStore: chunkStore,
@@ -219,6 +233,7 @@ func New(cfg Config, clientConfig client.Config, limits *validation.Overrides, c
219233
registerer: registerer,
220234
logger: logger,
221235
}
236+
i.metrics = newIngesterMetrics(registerer, true, cfg.ActiveSeriesMetricsEnabled, i.getInstanceLimits, nil, &i.inflightPushRequests)
222237

223238
var err error
224239
// During WAL recovery, it will create new user states which requires the limiter.
@@ -301,14 +316,14 @@ func NewForFlusher(cfg Config, chunkStore ChunkStore, limits *validation.Overrid
301316

302317
i := &Ingester{
303318
cfg: cfg,
304-
metrics: newIngesterMetrics(registerer, true, false),
305319
chunkStore: chunkStore,
306320
flushQueues: make([]*util.PriorityQueue, cfg.ConcurrentFlushes),
307321
flushRateLimiter: rate.NewLimiter(rate.Inf, 1),
308322
wal: &noopWAL{},
309323
limits: limits,
310324
logger: logger,
311325
}
326+
i.metrics = newIngesterMetrics(registerer, true, false, i.getInstanceLimits, nil, &i.inflightPushRequests)
312327

313328
i.BasicService = services.NewBasicService(i.startingForFlusher, i.loopForFlusher, i.stopping)
314329
return i, nil
@@ -444,6 +459,17 @@ func (i *Ingester) Push(ctx context.Context, req *cortexpb.WriteRequest) (*corte
444459
return nil, err
445460
}
446461

462+
// We will report *this* request in the error too.
463+
inflight := i.inflightPushRequests.Inc()
464+
defer i.inflightPushRequests.Dec()
465+
466+
gl := i.getInstanceLimits()
467+
if gl != nil && gl.MaxInflightPushRequests > 0 {
468+
if inflight > gl.MaxInflightPushRequests {
469+
return nil, errTooManyInflightPushRequests
470+
}
471+
}
472+
447473
if i.cfg.BlocksStorageEnabled {
448474
return i.v2Push(ctx, req)
449475
}

pkg/ingester/ingester_v2.go

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ type userTSDB struct {
105105
seriesInMetric *metricCounter
106106
limiter *Limiter
107107

108+
instanceSeriesCount *atomic.Int64 // Shared across all userTSDB instances created by ingester.
109+
instanceLimitsFn func() *InstanceLimits
110+
108111
stateMtx sync.RWMutex
109112
state tsdbState
110113
pushesInFlight sync.WaitGroup // Increased with stateMtx read lock held, only if state == active or activeShipping.
@@ -214,6 +217,14 @@ func (u *userTSDB) PreCreation(metric labels.Labels) error {
214217
return nil
215218
}
216219

220+
// Verify ingester's global limit
221+
gl := u.instanceLimitsFn()
222+
if gl != nil && gl.MaxInMemorySeries > 0 {
223+
if series := u.instanceSeriesCount.Load(); series >= gl.MaxInMemorySeries {
224+
return errMaxSeriesLimitReached
225+
}
226+
}
227+
217228
// Total series limit.
218229
if err := u.limiter.AssertMaxSeriesPerUser(u.userID, int(u.Head().NumSeries())); err != nil {
219230
return err
@@ -233,6 +244,8 @@ func (u *userTSDB) PreCreation(metric labels.Labels) error {
233244

234245
// PostCreation implements SeriesLifecycleCallback interface.
235246
func (u *userTSDB) PostCreation(metric labels.Labels) {
247+
u.instanceSeriesCount.Inc()
248+
236249
metricName, err := extract.MetricNameFromLabels(metric)
237250
if err != nil {
238251
// This should never happen because it has already been checked in PreCreation().
@@ -243,6 +256,8 @@ func (u *userTSDB) PostCreation(metric labels.Labels) {
243256

244257
// PostDeletion implements SeriesLifecycleCallback interface.
245258
func (u *userTSDB) PostDeletion(metrics ...labels.Labels) {
259+
u.instanceSeriesCount.Sub(int64(len(metrics)))
260+
246261
for _, metric := range metrics {
247262
metricName, err := extract.MetricNameFromLabels(metric)
248263
if err != nil {
@@ -377,6 +392,9 @@ type TSDBState struct {
377392
// Timeout chosen for idle compactions.
378393
compactionIdleTimeout time.Duration
379394

395+
// Number of series in memory, across all tenants.
396+
seriesCount atomic.Int64
397+
380398
// Head compactions metrics.
381399
compactionsTriggered prometheus.Counter
382400
compactionsFailed prometheus.Counter
@@ -449,14 +467,15 @@ func NewV2(cfg Config, clientConfig client.Config, limits *validation.Overrides,
449467
i := &Ingester{
450468
cfg: cfg,
451469
clientConfig: clientConfig,
452-
metrics: newIngesterMetrics(registerer, false, cfg.ActiveSeriesMetricsEnabled),
453470
limits: limits,
454471
chunkStore: nil,
455472
usersMetadata: map[string]*userMetricsMetadata{},
456473
wal: &noopWAL{},
457474
TSDBState: newTSDBState(bucketClient, registerer),
458475
logger: logger,
476+
ingestionRate: newEWMARate(0.2, cfg.RateUpdatePeriod),
459477
}
478+
i.metrics = newIngesterMetrics(registerer, false, cfg.ActiveSeriesMetricsEnabled, i.getInstanceLimits, i.ingestionRate, &i.inflightPushRequests)
460479

461480
// Replace specific metrics which we can't directly track but we need to read
462481
// them from the underlying system (ie. TSDB).
@@ -511,11 +530,11 @@ func NewV2ForFlusher(cfg Config, limits *validation.Overrides, registerer promet
511530
i := &Ingester{
512531
cfg: cfg,
513532
limits: limits,
514-
metrics: newIngesterMetrics(registerer, false, false),
515533
wal: &noopWAL{},
516534
TSDBState: newTSDBState(bucketClient, registerer),
517535
logger: logger,
518536
}
537+
i.metrics = newIngesterMetrics(registerer, false, false, i.getInstanceLimits, nil, &i.inflightPushRequests)
519538

520539
i.TSDBState.shipperIngesterID = "flusher"
521540

@@ -613,6 +632,9 @@ func (i *Ingester) updateLoop(ctx context.Context) error {
613632
rateUpdateTicker := time.NewTicker(i.cfg.RateUpdatePeriod)
614633
defer rateUpdateTicker.Stop()
615634

635+
ingestionRateTicker := time.NewTicker(1 * time.Second)
636+
defer ingestionRateTicker.Stop()
637+
616638
var activeSeriesTickerChan <-chan time.Time
617639
if i.cfg.ActiveSeriesMetricsEnabled {
618640
t := time.NewTicker(i.cfg.ActiveSeriesMetricsUpdatePeriod)
@@ -628,6 +650,8 @@ func (i *Ingester) updateLoop(ctx context.Context) error {
628650
select {
629651
case <-metadataPurgeTicker.C:
630652
i.purgeUserMetricsMetadata()
653+
case <-ingestionRateTicker.C:
654+
i.ingestionRate.tick()
631655
case <-rateUpdateTicker.C:
632656
i.userStatesMtx.RLock()
633657
for _, db := range i.TSDBState.dbs {
@@ -680,6 +704,13 @@ func (i *Ingester) v2Push(ctx context.Context, req *cortexpb.WriteRequest) (*cor
680704
return nil, err
681705
}
682706

707+
il := i.getInstanceLimits()
708+
if il != nil && il.MaxIngestionRate > 0 {
709+
if rate := i.ingestionRate.rate(); rate >= il.MaxIngestionRate {
710+
return nil, errMaxSamplesPushRateLimitReached
711+
}
712+
}
713+
683714
db, err := i.getOrCreateTSDB(userID, false)
684715
if err != nil {
685716
return nil, wrapWithUser(err, userID)
@@ -841,6 +872,8 @@ func (i *Ingester) v2Push(ctx context.Context, req *cortexpb.WriteRequest) (*cor
841872
validation.DiscardedSamples.WithLabelValues(perMetricSeriesLimit, userID).Add(float64(perMetricSeriesLimitCount))
842873
}
843874

875+
i.ingestionRate.add(int64(succeededSamplesCount))
876+
844877
switch req.Source {
845878
case cortexpb.RULE:
846879
db.ingestedRuleSamples.add(int64(succeededSamplesCount))
@@ -1381,6 +1414,13 @@ func (i *Ingester) getOrCreateTSDB(userID string, force bool) (*userTSDB, error)
13811414
return nil, fmt.Errorf(errTSDBCreateIncompatibleState, ingesterState)
13821415
}
13831416

1417+
gl := i.getInstanceLimits()
1418+
if gl != nil && gl.MaxInMemoryTenants > 0 {
1419+
if users := int64(len(i.TSDBState.dbs)); users >= gl.MaxInMemoryTenants {
1420+
return nil, errMaxUsersLimitReached
1421+
}
1422+
}
1423+
13841424
// Create the database and a shipper for a user
13851425
db, err := i.createTSDB(userID)
13861426
if err != nil {
@@ -1408,6 +1448,9 @@ func (i *Ingester) createTSDB(userID string) (*userTSDB, error) {
14081448
seriesInMetric: newMetricCounter(i.limiter),
14091449
ingestedAPISamples: newEWMARate(0.2, i.cfg.RateUpdatePeriod),
14101450
ingestedRuleSamples: newEWMARate(0.2, i.cfg.RateUpdatePeriod),
1451+
1452+
instanceLimitsFn: i.getInstanceLimits,
1453+
instanceSeriesCount: &i.TSDBState.seriesCount,
14111454
}
14121455

14131456
// Create a new user database
@@ -1876,6 +1919,11 @@ func (i *Ingester) closeAndDeleteUserTSDBIfIdle(userID string) tsdbCloseCheckRes
18761919
tenantDeleted = true
18771920
}
18781921

1922+
// At this point there are no more pushes to TSDB, and no possible compaction. Normally TSDB is empty,
1923+
// but if we're closing TSDB because of tenant deletion mark, then it may still contain some series.
1924+
// We need to remove these series from series count.
1925+
i.TSDBState.seriesCount.Sub(int64(userDB.Head().NumSeries()))
1926+
18791927
dir := userDB.db.Dir()
18801928

18811929
if err := userDB.Close(); err != nil {
@@ -2031,3 +2079,21 @@ func wrappedTSDBIngestErr(ingestErr error, timestamp model.Time, labels []cortex
20312079

20322080
return fmt.Errorf(errTSDBIngest, ingestErr, timestamp.Time().UTC().Format(time.RFC3339Nano), cortexpb.FromLabelAdaptersToLabels(labels).String())
20332081
}
2082+
2083+
func (i *Ingester) getInstanceLimits() *InstanceLimits {
2084+
// Don't apply any limits while starting. We especially don't want to apply series in memory limit while replaying WAL.
2085+
if i.State() == services.Starting {
2086+
return nil
2087+
}
2088+
2089+
if i.cfg.InstanceLimitsFn == nil {
2090+
return defaultInstanceLimits
2091+
}
2092+
2093+
l := i.cfg.InstanceLimitsFn()
2094+
if l == nil {
2095+
return defaultInstanceLimits
2096+
}
2097+
2098+
return l
2099+
}

0 commit comments

Comments
 (0)