Skip to content

Commit c328187

Browse files
authored
MQE: Add support for avg aggregation (#9201)
* MQE: Add support for avg aggregation * Optimise avg to not copy histograms unless needed * Reorder sum for consistency and readability * Update CHANGELOG * Add benchmarks * Fix variable naming * Add extra tests * Split out collecting histograms+floats * Shortcut if not data for series * Only allocate incrementalMeans if needed * Fix typo * Fix typo
1 parent 2bc501a commit c328187

File tree

9 files changed

+519
-92
lines changed

9 files changed

+519
-92
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
* [CHANGE] Querier: allow wrapping errors with context errors only when the former actually correspond to `context.Canceled` and `context.DeadlineExceeded`. #9175
3333
* [FEATURE] Alertmanager: Added `-alertmanager.log-parsing-label-matchers` to control logging when parsing label matchers. This flag is intended to be used with `-alertmanager.utf8-strict-mode-enabled` to validate UTF-8 strict mode is working as intended. The default value is `false`. #9173
3434
* [FEATURE] Alertmanager: Added `-alertmanager.utf8-migration-logging-enabled` to enable logging of tenant configurations that are incompatible with UTF-8 strict mode. The default value is `false`. #9174
35-
* [FEATURE] Querier: add experimental streaming PromQL engine, enabled with `-querier.query-engine=mimir`. #8422 #8430 #8454 #8455 #8360 #8490 #8508 #8577 #8660 #8671 #8677 #8747 #8850 #8872 #8838 #8911 #8909 #8923 #8924 #8925 #8932 #8933 #8934 #8962 #8986 #8993 #8995 #9008 #9017 #9018 #9019 #9120 #9121 #9136 #9139 #9140 #9145 #9191 #9194 #9196 #9212
35+
* [FEATURE] Querier: add experimental streaming PromQL engine, enabled with `-querier.query-engine=mimir`. #8422 #8430 #8454 #8455 #8360 #8490 #8508 #8577 #8660 #8671 #8677 #8747 #8850 #8872 #8838 #8911 #8909 #8923 #8924 #8925 #8932 #8933 #8934 #8962 #8986 #8993 #8995 #9008 #9017 #9018 #9019 #9120 #9121 #9136 #9139 #9140 #9145 #9191 #9194 #9196 #9201 #9212
3636
* [FEATURE] Experimental Kafka-based ingest storage. #6888 #6894 #6929 #6940 #6951 #6974 #6982 #7029 #7030 #7091 #7142 #7147 #7148 #7153 #7160 #7193 #7349 #7376 #7388 #7391 #7393 #7394 #7402 #7404 #7423 #7424 #7437 #7486 #7503 #7508 #7540 #7621 #7682 #7685 #7694 #7695 #7696 #7697 #7701 #7733 #7734 #7741 #7752 #7838 #7851 #7871 #7877 #7880 #7882 #7887 #7891 #7925 #7955 #7967 #8031 #8063 #8077 #8088 #8135 #8176 #8184 #8194 #8216 #8217 #8222 #8233 #8503 #8542 #8579 #8657 #8686 #8688 #8703 #8706 #8708 #8738 #8750 #8778 #8808 #8809 #8841 #8842 #8845 #8853 #8886 #8988
3737
* What it is:
3838
* When the new ingest storage architecture is enabled, distributors write incoming write requests to a Kafka-compatible backend, and the ingesters asynchronously replay ingested data from Kafka. In this architecture, the write and read path are de-coupled through a Kafka-compatible backend. The write path and Kafka load is a function of the incoming write traffic, the read path load is a function of received queries. Whatever the load on the read path, it doesn't affect the write path.
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
// SPDX-License-Identifier: AGPL-3.0-only
2+
// Provenance-includes-location: https://github.com/prometheus/prometheus/blob/main/promql/engine.go
3+
// Provenance-includes-license: Apache-2.0
4+
// Provenance-includes-copyright: The Prometheus Authors
5+
6+
package aggregations
7+
8+
import (
9+
"math"
10+
11+
"github.com/prometheus/prometheus/model/histogram"
12+
"github.com/prometheus/prometheus/promql"
13+
14+
"github.com/grafana/mimir/pkg/streamingpromql/floats"
15+
"github.com/grafana/mimir/pkg/streamingpromql/functions"
16+
"github.com/grafana/mimir/pkg/streamingpromql/limiting"
17+
"github.com/grafana/mimir/pkg/streamingpromql/types"
18+
)
19+
20+
type AvgAggregationGroup struct {
21+
floats []float64
22+
floatMeans []float64
23+
floatCompensatingMeans []float64 // Mean, or "compensating value" for Kahan summation.
24+
incrementalMeans []bool // True after reverting to incremental calculation of the mean value.
25+
floatPresent []bool
26+
histograms []*histogram.FloatHistogram
27+
histogramPointCount int
28+
29+
// Keeps track of how many series we have encountered thus far for the group at this point
30+
// This is necessary to do per point (instead of just counting the groups) as a series may have
31+
// stale or non-existent values that are not added towards the count.
32+
groupSeriesCounts []float64
33+
}
34+
35+
func (g *AvgAggregationGroup) AccumulateSeries(data types.InstantVectorSeriesData, steps int, start int64, interval int64, memoryConsumptionTracker *limiting.MemoryConsumptionTracker, emitAnnotationFunc functions.EmitAnnotationFunc) error {
36+
defer types.PutInstantVectorSeriesData(data, memoryConsumptionTracker)
37+
if len(data.Floats) == 0 && len(data.Histograms) == 0 {
38+
// Nothing to do
39+
return nil
40+
}
41+
42+
var err error
43+
44+
if g.groupSeriesCounts == nil {
45+
g.groupSeriesCounts, err = types.Float64SlicePool.Get(steps, memoryConsumptionTracker)
46+
if err != nil {
47+
return err
48+
}
49+
g.groupSeriesCounts = g.groupSeriesCounts[:steps]
50+
}
51+
52+
err = g.accumulateFloats(data, steps, start, interval, memoryConsumptionTracker)
53+
if err != nil {
54+
return err
55+
}
56+
err = g.accumulateHistograms(data, steps, start, interval, memoryConsumptionTracker, emitAnnotationFunc)
57+
if err != nil {
58+
return err
59+
}
60+
61+
return nil
62+
}
63+
64+
func (g *AvgAggregationGroup) accumulateFloats(data types.InstantVectorSeriesData, steps int, start int64, interval int64, memoryConsumptionTracker *limiting.MemoryConsumptionTracker) error {
65+
var err error
66+
if len(data.Floats) > 0 && g.floats == nil {
67+
// First series with float values for this group, populate it.
68+
g.floats, err = types.Float64SlicePool.Get(steps, memoryConsumptionTracker)
69+
if err != nil {
70+
return err
71+
}
72+
73+
g.floatCompensatingMeans, err = types.Float64SlicePool.Get(steps, memoryConsumptionTracker)
74+
if err != nil {
75+
return err
76+
}
77+
78+
g.floatPresent, err = types.BoolSlicePool.Get(steps, memoryConsumptionTracker)
79+
if err != nil {
80+
return err
81+
}
82+
83+
g.floats = g.floats[:steps]
84+
g.floatCompensatingMeans = g.floatCompensatingMeans[:steps]
85+
g.floatPresent = g.floatPresent[:steps]
86+
}
87+
88+
for _, p := range data.Floats {
89+
idx := (p.T - start) / interval
90+
g.groupSeriesCounts[idx]++
91+
if !g.floatPresent[idx] {
92+
// The first point is just taken as the value
93+
g.floats[idx] = p.F
94+
g.floatPresent[idx] = true
95+
continue
96+
}
97+
98+
if g.incrementalMeans == nil || !g.incrementalMeans[idx] {
99+
newV, newC := floats.KahanSumInc(p.F, g.floats[idx], g.floatCompensatingMeans[idx])
100+
if !math.IsInf(newV, 0) {
101+
// The sum doesn't overflow, so we propagate it to the
102+
// group struct and continue with the regular
103+
// calculation of the mean value.
104+
g.floats[idx], g.floatCompensatingMeans[idx] = newV, newC
105+
continue
106+
}
107+
// If we are here, we know that the sum _would_ overflow. So
108+
// instead of continuing to sum up, we revert to incremental
109+
// calculation of the mean value from here on.
110+
if g.floatMeans == nil {
111+
g.floatMeans, err = types.Float64SlicePool.Get(steps, memoryConsumptionTracker)
112+
if err != nil {
113+
return err
114+
}
115+
g.floatMeans = g.floatMeans[:steps]
116+
}
117+
if g.incrementalMeans == nil {
118+
// First time we are using an incremental mean. Track which samples will be incremental.
119+
g.incrementalMeans, err = types.BoolSlicePool.Get(steps, memoryConsumptionTracker)
120+
if err != nil {
121+
return err
122+
}
123+
g.incrementalMeans = g.incrementalMeans[:steps]
124+
}
125+
g.incrementalMeans[idx] = true
126+
g.floatMeans[idx] = g.floats[idx] / (g.groupSeriesCounts[idx] - 1)
127+
g.floatCompensatingMeans[idx] /= g.groupSeriesCounts[idx] - 1
128+
}
129+
if math.IsInf(g.floatMeans[idx], 0) {
130+
if math.IsInf(p.F, 0) && (g.floatMeans[idx] > 0) == (p.F > 0) {
131+
// The `floatMean` and `s.F` values are `Inf` of the same sign. They
132+
// can't be subtracted, but the value of `floatMean` is correct
133+
// already.
134+
continue
135+
}
136+
if !math.IsInf(p.F, 0) && !math.IsNaN(p.F) {
137+
// At this stage, the mean is an infinite. If the added
138+
// value is neither an Inf or a Nan, we can keep that mean
139+
// value.
140+
// This is required because our calculation below removes
141+
// the mean value, which would look like Inf += x - Inf and
142+
// end up as a NaN.
143+
continue
144+
}
145+
}
146+
currentMean := g.floatMeans[idx] + g.floatCompensatingMeans[idx]
147+
g.floatMeans[idx], g.floatCompensatingMeans[idx] = floats.KahanSumInc(
148+
p.F/g.groupSeriesCounts[idx]-currentMean/g.groupSeriesCounts[idx],
149+
g.floatMeans[idx],
150+
g.floatCompensatingMeans[idx],
151+
)
152+
}
153+
return nil
154+
}
155+
156+
func (g *AvgAggregationGroup) accumulateHistograms(data types.InstantVectorSeriesData, steps int, start int64, interval int64, memoryConsumptionTracker *limiting.MemoryConsumptionTracker, emitAnnotationFunc functions.EmitAnnotationFunc) error {
157+
var err error
158+
if len(data.Histograms) > 0 && g.histograms == nil {
159+
// First series with histogram values for this group, populate it.
160+
g.histograms, err = types.HistogramSlicePool.Get(steps, memoryConsumptionTracker)
161+
if err != nil {
162+
return err
163+
}
164+
g.histograms = g.histograms[:steps]
165+
}
166+
167+
var lastUncopiedHistogram *histogram.FloatHistogram
168+
169+
for i, p := range data.Histograms {
170+
idx := (p.T - start) / interval
171+
g.groupSeriesCounts[idx]++
172+
173+
if g.histograms[idx] == invalidCombinationOfHistograms {
174+
// We've already seen an invalid combination of histograms at this timestamp. Ignore this point.
175+
continue
176+
}
177+
178+
if g.histograms[idx] == nil {
179+
if lastUncopiedHistogram == p.H {
180+
// We've already used this histogram for a previous point due to lookback.
181+
// Make a copy of it so we don't modify the other point.
182+
g.histograms[idx] = p.H.Copy()
183+
g.histogramPointCount++
184+
continue
185+
}
186+
// This is the first time we have seen this histogram.
187+
// It is safe to store it and modify it later without copying, as we'll make copies above if the same histogram is used for subsequent points.
188+
g.histograms[idx] = p.H
189+
g.histogramPointCount++
190+
lastUncopiedHistogram = p.H
191+
continue
192+
}
193+
194+
// Check if the next point in data.Histograms is the same as the current point (due to lookback)
195+
// If it is, create a copy before modifying it.
196+
toAdd := p.H
197+
if i+1 < len(data.Histograms) && data.Histograms[i+1].H == p.H {
198+
toAdd = p.H.Copy()
199+
}
200+
201+
_, err = toAdd.Sub(g.histograms[idx])
202+
if err != nil {
203+
// Unable to subtract histograms (likely due to invalid combination of histograms). Make sure we don't emit a sample at this timestamp.
204+
g.histograms[idx] = invalidCombinationOfHistograms
205+
g.histogramPointCount--
206+
207+
if err := functions.NativeHistogramErrorToAnnotation(err, emitAnnotationFunc); err != nil {
208+
// Unknown error: we couldn't convert the error to an annotation. Give up.
209+
return err
210+
}
211+
continue
212+
}
213+
214+
toAdd.Div(g.groupSeriesCounts[idx])
215+
_, err = g.histograms[idx].Add(toAdd)
216+
if err != nil {
217+
// Unable to add histograms together (likely due to invalid combination of histograms). Make sure we don't emit a sample at this timestamp.
218+
g.histograms[idx] = invalidCombinationOfHistograms
219+
g.histogramPointCount--
220+
221+
if err := functions.NativeHistogramErrorToAnnotation(err, emitAnnotationFunc); err != nil {
222+
// Unknown error: we couldn't convert the error to an annotation. Give up.
223+
return err
224+
}
225+
continue
226+
}
227+
}
228+
return nil
229+
}
230+
231+
// reconcileAndCountFloatPoints will return the number of points with a float present.
232+
// It also takes the opportunity whilst looping through the floats to check if there
233+
// is a conflicting Histogram present. If both are present, an empty vector should
234+
// be returned. So this method removes the float+histogram where they conflict.
235+
func (g *AvgAggregationGroup) reconcileAndCountFloatPoints() (int, bool) {
236+
// It would be possible to calculate the number of points when constructing
237+
// the series groups. However, it requires checking each point at each input
238+
// series which is more costly than looping again here and just checking each
239+
// point of the already grouped series.
240+
// See: https://github.com/grafana/mimir/pull/8442
241+
// We also take two different approaches here: One with extra checks if we
242+
// have both Floats and Histograms present, and one without these checks
243+
// so we don't have to do it at every point.
244+
floatPointCount := 0
245+
haveMixedFloatsAndHistograms := false
246+
if len(g.floatPresent) > 0 && len(g.histograms) > 0 {
247+
for idx, present := range g.floatPresent {
248+
if present {
249+
if g.histograms[idx] != nil {
250+
// If a mix of histogram samples and float samples, the corresponding vector element is removed from the output vector entirely
251+
// and a warning annotation is emitted.
252+
g.floatPresent[idx] = false
253+
g.histograms[idx] = nil
254+
g.histogramPointCount--
255+
256+
haveMixedFloatsAndHistograms = true
257+
} else {
258+
floatPointCount++
259+
}
260+
}
261+
}
262+
} else {
263+
for _, p := range g.floatPresent {
264+
if p {
265+
floatPointCount++
266+
}
267+
}
268+
}
269+
return floatPointCount, haveMixedFloatsAndHistograms
270+
}
271+
272+
func (g *AvgAggregationGroup) ComputeOutputSeries(start int64, interval int64, memoryConsumptionTracker *limiting.MemoryConsumptionTracker) (types.InstantVectorSeriesData, bool, error) {
273+
floatPointCount, hasMixedData := g.reconcileAndCountFloatPoints()
274+
var floatPoints []promql.FPoint
275+
var err error
276+
277+
if floatPointCount > 0 {
278+
floatPoints, err = types.FPointSlicePool.Get(floatPointCount, memoryConsumptionTracker)
279+
if err != nil {
280+
return types.InstantVectorSeriesData{}, hasMixedData, err
281+
}
282+
283+
for i, havePoint := range g.floatPresent {
284+
if havePoint {
285+
t := start + int64(i)*interval
286+
var f float64
287+
if g.incrementalMeans != nil && g.incrementalMeans[i] {
288+
f = g.floatMeans[i] + g.floatCompensatingMeans[i]
289+
} else {
290+
f = (g.floats[i] + g.floatCompensatingMeans[i]) / g.groupSeriesCounts[i]
291+
}
292+
floatPoints = append(floatPoints, promql.FPoint{T: t, F: f})
293+
}
294+
}
295+
}
296+
297+
var histogramPoints []promql.HPoint
298+
if g.histogramPointCount > 0 {
299+
histogramPoints, err = types.HPointSlicePool.Get(g.histogramPointCount, memoryConsumptionTracker)
300+
if err != nil {
301+
return types.InstantVectorSeriesData{}, hasMixedData, err
302+
}
303+
304+
for i, h := range g.histograms {
305+
if h != nil && h != invalidCombinationOfHistograms {
306+
t := start + int64(i)*interval
307+
histogramPoints = append(histogramPoints, promql.HPoint{T: t, H: h.Compact(0)})
308+
}
309+
}
310+
}
311+
312+
types.Float64SlicePool.Put(g.floats, memoryConsumptionTracker)
313+
types.Float64SlicePool.Put(g.floatMeans, memoryConsumptionTracker)
314+
types.Float64SlicePool.Put(g.floatCompensatingMeans, memoryConsumptionTracker)
315+
types.BoolSlicePool.Put(g.floatPresent, memoryConsumptionTracker)
316+
types.HistogramSlicePool.Put(g.histograms, memoryConsumptionTracker)
317+
types.BoolSlicePool.Put(g.incrementalMeans, memoryConsumptionTracker)
318+
types.Float64SlicePool.Put(g.groupSeriesCounts, memoryConsumptionTracker)
319+
320+
return types.InstantVectorSeriesData{Floats: floatPoints, Histograms: histogramPoints}, hasMixedData, nil
321+
}

pkg/streamingpromql/aggregations/common.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type AggregationGroup interface {
2222
type AggregationGroupFactory func() AggregationGroup
2323

2424
var AggregationGroupFactories = map[parser.ItemType]AggregationGroupFactory{
25+
parser.AVG: func() AggregationGroup { return &AvgAggregationGroup{} },
2526
parser.MAX: func() AggregationGroup { return NewMinMaxAggregationGroup(true) },
2627
parser.MIN: func() AggregationGroup { return NewMinMaxAggregationGroup(false) },
2728
parser.SUM: func() AggregationGroup { return &SumAggregationGroup{} },

pkg/streamingpromql/aggregations/min_max.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func (g *MinMaxAggregationGroup) minAccumulatePoint(idx int64, f float64) {
5151

5252
func (g *MinMaxAggregationGroup) AccumulateSeries(data types.InstantVectorSeriesData, steps int, start int64, interval int64, memoryConsumptionTracker *limiting.MemoryConsumptionTracker, _ functions.EmitAnnotationFunc) error {
5353
if (len(data.Floats) > 0 || len(data.Histograms) > 0) && g.floatValues == nil {
54-
// Even if we have histograms, we have to populate the float slices, as we'll treat histograms as if they have value 0.
54+
// Even if we only have histograms, we have to populate the float slices, as we'll treat histograms as if they have value 0.
5555
// This is consistent with Prometheus but may not be the desired value: https://github.com/prometheus/prometheus/issues/14711
5656

5757
var err error

0 commit comments

Comments
 (0)