-
Notifications
You must be signed in to change notification settings - Fork 812
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enforce series and sample limits on streaming queries to ingester from querier #3873
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ package distributor | |
|
||
import ( | ||
"context" | ||
"fmt" | ||
"io" | ||
"time" | ||
|
||
|
@@ -19,15 +20,15 @@ import ( | |
) | ||
|
||
// Query multiple ingesters and returns a Matrix of samples. | ||
func (d *Distributor) Query(ctx context.Context, from, to model.Time, matchers ...*labels.Matcher) (model.Matrix, error) { | ||
func (d *Distributor) Query(ctx context.Context, userID string, from, to model.Time, matchers ...*labels.Matcher) (model.Matrix, error) { | ||
var matrix model.Matrix | ||
err := instrument.CollectedRequest(ctx, "Distributor.Query", d.queryDuration, instrument.ErrorCode, func(ctx context.Context) error { | ||
req, err := ingester_client.ToQueryRequest(from, to, matchers) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
replicationSet, err := d.GetIngestersForQuery(ctx, matchers...) | ||
replicationSet, err := d.GetIngestersForQuery(ctx, userID, matchers...) | ||
if err != nil { | ||
return err | ||
} | ||
|
@@ -46,20 +47,20 @@ func (d *Distributor) Query(ctx context.Context, from, to model.Time, matchers . | |
} | ||
|
||
// QueryStream multiple ingesters via the streaming interface and returns big ol' set of chunks. | ||
func (d *Distributor) QueryStream(ctx context.Context, from, to model.Time, matchers ...*labels.Matcher) (*ingester_client.QueryStreamResponse, error) { | ||
func (d *Distributor) QueryStream(ctx context.Context, userID string, from, to model.Time, matchers ...*labels.Matcher) (*ingester_client.QueryStreamResponse, error) { | ||
var result *ingester_client.QueryStreamResponse | ||
err := instrument.CollectedRequest(ctx, "Distributor.QueryStream", d.queryDuration, instrument.ErrorCode, func(ctx context.Context) error { | ||
req, err := ingester_client.ToQueryRequest(from, to, matchers) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
replicationSet, err := d.GetIngestersForQuery(ctx, matchers...) | ||
replicationSet, err := d.GetIngestersForQuery(ctx, userID, matchers...) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
result, err = d.queryIngesterStream(ctx, replicationSet, req) | ||
result, err = d.queryIngesterStream(ctx, userID, replicationSet, req) | ||
if err != nil { | ||
return err | ||
} | ||
|
@@ -74,12 +75,7 @@ func (d *Distributor) QueryStream(ctx context.Context, from, to model.Time, matc | |
|
||
// GetIngestersForQuery returns a replication set including all ingesters that should be queried | ||
// to fetch series matching input label matchers. | ||
func (d *Distributor) GetIngestersForQuery(ctx context.Context, matchers ...*labels.Matcher) (ring.ReplicationSet, error) { | ||
userID, err := tenant.TenantID(ctx) | ||
if err != nil { | ||
return ring.ReplicationSet{}, err | ||
} | ||
|
||
func (d *Distributor) GetIngestersForQuery(ctx context.Context, userID string, matchers ...*labels.Matcher) (ring.ReplicationSet, error) { | ||
// If shuffle sharding is enabled we should only query ingesters which are | ||
// part of the tenant's subring. | ||
if d.cfg.ShardingStrategy == util.ShardingStrategyShuffle { | ||
|
@@ -172,7 +168,10 @@ func (d *Distributor) queryIngesters(ctx context.Context, replicationSet ring.Re | |
} | ||
|
||
// queryIngesterStream queries the ingesters using the new streaming API. | ||
func (d *Distributor) queryIngesterStream(ctx context.Context, replicationSet ring.ReplicationSet, req *ingester_client.QueryRequest) (*ingester_client.QueryStreamResponse, error) { | ||
func (d *Distributor) queryIngesterStream(ctx context.Context, userID string, replicationSet ring.ReplicationSet, req *ingester_client.QueryRequest) (*ingester_client.QueryStreamResponse, error) { | ||
maxSeries := d.limits.MaxSeriesPerQuery(userID) | ||
maxSamples := d.limits.MaxSamplesPerQuery(userID) | ||
|
||
// Fetch samples from multiple ingesters | ||
results, err := replicationSet.Do(ctx, d.cfg.ExtraQueryDelay, func(ctx context.Context, ing *ring.InstanceDesc) (interface{}, error) { | ||
client, err := d.ingesterPool.GetClientFor(ing.Addr) | ||
|
@@ -204,6 +203,10 @@ func (d *Distributor) queryIngesterStream(ctx context.Context, replicationSet ri | |
|
||
result.Chunkseries = append(result.Chunkseries, resp.Chunkseries...) | ||
result.Timeseries = append(result.Timeseries, resp.Timeseries...) | ||
|
||
if len(result.Chunkseries) > maxSeries || len(result.Timeseries) > maxSeries { | ||
return nil, fmt.Errorf("exceeded maximum number of series in a query (limit %d)", maxSeries) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are the implications on the returned status code? I've the feeling this may be detected as a storage error and we return a 5xx error (while it should be a 4xx) but I haven't deeply checked it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think it would work better as an httpgrpc error? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bboreham I don't remember how the error code propagation works in detail but I would start testing it. Do you have time/interest to work on it, otherwise I can takeover, cause I'm interested into this limit too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I checked it and we have to return a |
||
} | ||
} | ||
return result, nil | ||
}) | ||
|
@@ -214,6 +217,7 @@ func (d *Distributor) queryIngesterStream(ctx context.Context, replicationSet ri | |
hashToChunkseries := map[string]ingester_client.TimeSeriesChunk{} | ||
hashToTimeSeries := map[string]ingester_client.TimeSeries{} | ||
|
||
sampleCount := 0 | ||
for _, result := range results { | ||
response := result.(*ingester_client.QueryStreamResponse) | ||
|
||
|
@@ -231,15 +235,24 @@ func (d *Distributor) queryIngesterStream(ctx context.Context, replicationSet ri | |
key := ingester_client.LabelsToKeyString(ingester_client.FromLabelAdaptersToLabels(series.Labels)) | ||
existing := hashToTimeSeries[key] | ||
existing.Labels = series.Labels | ||
previousCount := len(existing.Samples) | ||
if existing.Samples == nil { | ||
existing.Samples = series.Samples | ||
} else { | ||
existing.Samples = mergeSamples(existing.Samples, series.Samples) | ||
} | ||
hashToTimeSeries[key] = existing | ||
sampleCount += len(existing.Samples) - previousCount | ||
if sampleCount > maxSamples { | ||
return nil, fmt.Errorf("exceeded maximum number of samples in a query (limit %d)", maxSamples) | ||
} | ||
} | ||
} | ||
|
||
if len(hashToChunkseries) > maxSeries || len(hashToTimeSeries) > maxSeries { | ||
return nil, fmt.Errorf("exceeded maximum number of series in a query (limit %d)", maxSeries) | ||
} | ||
|
||
resp := &ingester_client.QueryStreamResponse{ | ||
Chunkseries: make([]ingester_client.TimeSeriesChunk, 0, len(hashToChunkseries)), | ||
Timeseries: make([]ingester_client.TimeSeries, 0, len(hashToTimeSeries)), | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't we enforce the limits here as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes; my focus was on things that blow up, which a range query is much more likely to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
Query()
is used for range queries too, no? It's used when the "gRPC streaming" is disabled, while "QueryStream()" is called when it's enabled.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, sorry, I got confused.
For non-streaming queries and chunks, ingester enforces the limits already.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where is it done? I can't find it.