-
Notifications
You must be signed in to change notification settings - Fork 557
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 max series for metrics queries #4525
base: main
Are you sure you want to change the base?
Conversation
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.
Thank you for adding docs.
604930d
to
2c986ed
Compare
|
||
metrics: | ||
# Maximum number of time series returned for a metrics query. | ||
[max_response_series: <int> | default = 1000] |
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.
this is an interesting choice. normally we would communicate the max series through a query param from the frontend to the queriers. the negative of your approach is that we have to make sure that 2 settings are aligned or tempo may appear subtly broken. the advantage is that we don't repeatedly marshal something like series=1000
once for every subquery.
can you bring this up with the team and see if we have consensus either way?
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.
this does make me wonder if we should have a shared section of the config for querying like we do for storage. that feels like overkill for one setting tho.
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.
switching to passing the config in the request proto since it needs to get passed all the way to the metrics evaluator
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.
should this be removed from the docs then?
…n all data points
4aa4ee3
to
a073454
Compare
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.
How does the metric query look now? Are the series evenly distributed? I'm asking because we have an issue with exemplars where we enforce a similar limit, and they appear to be skewed
|
||
metrics: | ||
# Maximum number of time series returned for a metrics query. | ||
[max_response_series: <int> | default = 1000] |
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.
should this be removed from the docs then?
@@ -62,9 +72,22 @@ func NewQueryRange(req *tempopb.QueryRangeRequest) (Combiner, error) { | |||
diff := diffResponse(prevResp, resp) | |||
// store resp for next diff | |||
prevResp = resp | |||
prevTotalSeries := totalSeries | |||
totalSeries += len(diff.Series) |
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.
i don't think this logic is correct for streaming. this assumes that all of diff.Series
is new. i think you should just check resp.Series directly as it represents the complete response at this time.
also, it seems odd that in quit we use combiner.MaxSeriesReached()
but we use a different calculation in diff and finalize. can we not use combiner.MaxSeriesReached()
in all cases?
@@ -34,6 +34,7 @@ func newQueryRangeStreamingGRPCHandler(cfg Config, next pipeline.AsyncRoundTripp | |||
if req.Step == 0 { | |||
req.Step = traceql.DefaultQueryRangeStep(req.Start, req.End) | |||
} | |||
req.MaxSeries = uint32(cfg.Metrics.Sharder.MaxResponseSeries) |
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.
i'm a little weirded out by just overwriting this value. should we 400 if this is set to non-zero b/c we don't accept it being set externally? do we have any other settings that use this pattern we can pull from?
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.
perhaps we should treat it the same way we do limits? if it's specified in the request (and not larger than the max) honor it else use the max?
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.
this makes sense
@@ -271,6 +273,7 @@ func (s *queryRangeSharder) buildBackendRequests(ctx context.Context, tenantID s | |||
FooterSize: m.FooterSize, | |||
// DedicatedColumns: dc, for perf reason we pass dedicated columns json in directly to not have to realloc object -> proto -> json | |||
Exemplars: exemplars, | |||
MaxSeries: uint32(s.cfg.MaxResponseSeries), |
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.
this method is cloning the upstream request. should we do that here as well instead of copying the value from the config
@@ -115,6 +116,7 @@ func (s queryRangeSharder) RoundTrip(pipelineRequest pipeline.Request) (pipeline | |||
} | |||
} | |||
req.Exemplars = maxExemplars | |||
req.MaxSeries = uint32(s.cfg.MaxResponseSeries) |
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.
i find the code that is setting the value on the request confusing. we are doing it in the handler, again here, and in the backend requests below. i'm finding this confusing.
modules/generator/instance.go
Outdated
@@ -540,16 +541,24 @@ func (i *instance) QueryRange(ctx context.Context, req *tempopb.QueryRangeReques | |||
for _, p := range processors { | |||
err = p.QueryRange(ctx, req, rawEval, jobEval) | |||
if err != nil { | |||
fmt.Printf("error in query range: %v\n", err) |
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.
remove?
bunch of similar test lines below
|
||
// listens for series counts and updates the total series count. | ||
go func() { | ||
for count := range seriesCountCh { |
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.
can we find a simpler way to do this? there's a lot of moving parts here to just count the current series.
i'm also concerned about the cost of repeatedly calling .Results()
on the series. does this require a lot of work? how does it compare for different functions like rates/quantiles/avgs/etc?
also, the metrics aggregated at this level are different than those in the frontend. for instance quantile_over_time
will produce considerably more series here than at the frontend b/c it is a histogram here that is aggregated into quantiles in the frontend. how does this work into our solution?
also also, if series are truncated here do we pass that back in the response?
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.
Correct me if I'm wrong but I thought the series from "jobEval.ObserveSeries" are already aggregated series. So at worst, we would have the truncated amount of series. But if the environment has more than one metrics-generator, that is additional series that would get aggregated again. I think this would be suffice but what do you think?
The .Results() is calling this https://github.com/grafana/tempo/blob/main/pkg/traceql/engine_metrics.go#L716
Since these are the raw not-yet aggregated results, it's a lot harder to count them. Since (I think) we avoided calling them for performance and calling it only once at the end https://github.com/grafana/tempo/blob/main/modules/generator/instance.go#L548 do you think maybe we should just not keep track of the series on the head and wal blocks?
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.
Correct me if I'm wrong but I thought the series from "jobEval.ObserveSeries" are already aggregated series. So at worst, we would have the truncated amount of series.
It's dependent on the metric type and aggregation level. So when you aggregate quantiles at the lower levels you are actually aggregating histograms and passing those up to the frontend. A histogram has considerably more series than a quantile. Up to N times more where N is the number of buckets in the histogram
When you aggregate a simple rates at all levels it is the same number of series.
The .Results() is calling this https://github.com/grafana/tempo/blob/main/pkg/traceql/engine_metrics.go#L716
yeah, that looks potentially expensive. I did track down all the .Observe calls at the series level and the count they are returning seems a lot cheaper
@@ -326,6 +326,10 @@ type QueryRangeCombiner struct { | |||
req *tempopb.QueryRangeRequest | |||
eval *MetricsFrontendEvaluator | |||
metrics *tempopb.SearchMetrics | |||
|
|||
maxSeries int |
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.
you only need 2 of these 3, right?
why not remove maxSeriesReached and change the function below to
func (q *QueryRangeCombiner) MaxSeriesReached() bool {
return q.seriesCount >= q.maxSeries
}
@@ -313,15 +313,15 @@ type VectorAggregator interface { | |||
// RangeAggregator sorts spans into time slots | |||
// TODO - for efficiency we probably combine this with VectorAggregator (see todo about CountOverTimeAggregator) | |||
type RangeAggregator interface { | |||
Observe(s Span) | |||
Observe(s Span) int |
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.
at the span level the GroupingAggregator is stored directly on the MetricsAggregate object and is the only thing we store there:
a.agg = NewGroupingAggregator(a.op.String(), func() RangeAggregator {
return NewStepAggregator(q.Start, q.End, q.Step, innerAgg)
}, a.by, byFunc, byFuncLabel)
of the span level aggregators it also seems like we actually only need to count series in the GroupingAggregator. should we change a.agg to be a concrete type and only add the ability to count series to that object instead of polluting all these interfaces? and meaningless data (0s) from some Aggregators?
@@ -34,6 +34,7 @@ func newQueryRangeStreamingGRPCHandler(cfg Config, next pipeline.AsyncRoundTripp | |||
if req.Step == 0 { | |||
req.Step = traceql.DefaultQueryRangeStep(req.Start, req.End) | |||
} | |||
req.MaxSeries = uint32(cfg.Metrics.Sharder.MaxResponseSeries) |
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.
perhaps we should treat it the same way we do limits? if it's specified in the request (and not larger than the max) honor it else use the max?
@@ -167,6 +167,7 @@ func (m *MetricsCompare) observe(span Span) { | |||
} | |||
totals[i]++ | |||
}) | |||
return len(m.seriesAgg.Results()) |
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.
can we get tests on these all these series aggregators observe's returning total series?
What this PR does: Add config to enforce max time series returned in a metrics query. This is enforced at 4 levels: front-end combiner, querier combiner, metrics-generator local blocks, and metrics evaluation. The configuration is set in the query-frontend config and is passed to all levels as
maxSeries
in theQueryRangeRequest
proto.new config: max_response_series <default 1000>
Setting the value to 0 will disable this feature.
approach : Keep track of number of series for every
observe
andobserveSeries
call and exit as soon as the limit is reached. Whatever series were generated up to this point will be truncated at the limit and returned as partial results. This may mean that partial results are not useful as each series could potentially contain just one data point.Which issue(s) this PR fixes:
Fixes #4219
Checklist
CHANGELOG.md
updated - the order of entries should be[CHANGE]
,[FEATURE]
,[ENHANCEMENT]
,[BUGFIX]