Skip to content

Commit 3b5f219

Browse files
committed
util/metric: introduce HighCardinalityHistogram in aggregated metrics
This patch introduces `HighCardinalityHistogram` metric which is similar to the `HighCardinalityCounter` introduced in #153568. It relies on unordered cache with LRU eviction as child storage. The parent values represents the aggregation of all child metric values. The child metrics values are only exported and aggregated values is persisted in CRDB. It relies on LabelSliceCache to efficiently store label values at registry. The child metric eviction policy is combination of max cache size of 5000 and minimum retention time of 20 seconds. This guarantees that we would see the child metric values at least in one scrape with default interval of 10 seconds before getting evicted due to cache size. The child eviction won't impact the parent value. Epic: CRDB-53398 Part of: CRDB-53833 Release note: None
1 parent 3389d85 commit 3b5f219

7 files changed

+800
-2
lines changed

pkg/util/metric/aggmetric/BUILD.bazel

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ go_test(
4646
"//pkg/util/leaktest",
4747
"//pkg/util/metric",
4848
"//pkg/util/timeutil",
49-
"@com_github_cockroachdb_crlib//testutils/require",
5049
"@com_github_prometheus_client_model//go",
5150
"@com_github_prometheus_common//expfmt",
5251
"@com_github_stretchr_testify//require",

pkg/util/metric/aggmetric/histogram.go

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,3 +357,220 @@ func (sch *SQLChildHistogram) RecordValue(v int64) {
357357
func (sch *SQLChildHistogram) Value() metric.HistogramSnapshot {
358358
return sch.h.CumulativeSnapshot()
359359
}
360+
361+
// HighCardinalityHistogram is similar to AggHistogram but uses cache storage instead of B-tree,
362+
// allowing for automatic eviction of less frequently used child metrics.
363+
// This is useful when dealing with high cardinality metrics that might exceed resource limits.
364+
type HighCardinalityHistogram struct {
365+
h metric.IHistogram
366+
create func() metric.IHistogram
367+
childSet
368+
labelSliceCache *metric.LabelSliceCache
369+
ticker struct {
370+
// We use a RWMutex, because we don't want child histograms to contend when
371+
// recording values, unless we're rotating histograms for the parent & children.
372+
// In this instance, the "writer" for the RWMutex is the ticker, and the "readers"
373+
// are all the child histograms recording their values.
374+
syncutil.RWMutex
375+
*tick.Ticker
376+
}
377+
}
378+
379+
var _ metric.Iterable = (*HighCardinalityHistogram)(nil)
380+
var _ metric.PrometheusEvictable = (*HighCardinalityHistogram)(nil)
381+
var _ metric.WindowedHistogram = (*HighCardinalityHistogram)(nil)
382+
var _ metric.CumulativeHistogram = (*HighCardinalityHistogram)(nil)
383+
384+
// NewHighCardinalityHistogram constructs a new HighCardinalityHistogram that uses cache storage
385+
// with eviction for child metrics.
386+
func NewHighCardinalityHistogram(
387+
opts metric.HistogramOptions, childLabels ...string,
388+
) *HighCardinalityHistogram {
389+
create := func() metric.IHistogram {
390+
return metric.NewHistogram(opts)
391+
}
392+
h := &HighCardinalityHistogram{
393+
h: create(),
394+
create: create,
395+
}
396+
h.ticker.Ticker = tick.NewTicker(
397+
now(),
398+
opts.Duration/metric.WindowedHistogramWrapNum,
399+
func() {
400+
// Atomically rotate the histogram window for the
401+
// parent histogram, and all the child histograms.
402+
h.h.Tick()
403+
h.childSet.apply(func(childItem MetricItem) {
404+
childHist, ok := childItem.(*HighCardinalityChildHistogram)
405+
if !ok {
406+
panic(errors.AssertionFailedf(
407+
"unable to assert type of child for histogram %q when rotating histogram windows",
408+
opts.Metadata.Name))
409+
}
410+
childHist.h.Tick()
411+
})
412+
})
413+
h.initWithCacheStorageType(childLabels, opts.Metadata.Name)
414+
return h
415+
}
416+
417+
// GetName is part of the metric.Iterable interface.
418+
func (h *HighCardinalityHistogram) GetName(useStaticLabels bool) string {
419+
return h.h.GetName(useStaticLabels)
420+
}
421+
422+
// GetHelp is part of the metric.Iterable interface.
423+
func (h *HighCardinalityHistogram) GetHelp() string { return h.h.GetHelp() }
424+
425+
// GetMeasurement is part of the metric.Iterable interface.
426+
func (h *HighCardinalityHistogram) GetMeasurement() string { return h.h.GetMeasurement() }
427+
428+
// GetUnit is part of the metric.Iterable interface.
429+
func (h *HighCardinalityHistogram) GetUnit() metric.Unit { return h.h.GetUnit() }
430+
431+
// GetMetadata is part of the metric.Iterable interface.
432+
func (h *HighCardinalityHistogram) GetMetadata() metric.Metadata { return h.h.GetMetadata() }
433+
434+
// Inspect is part of the metric.Iterable interface.
435+
func (h *HighCardinalityHistogram) Inspect(f func(interface{})) {
436+
func() {
437+
h.ticker.Lock()
438+
defer h.ticker.Unlock()
439+
tick.MaybeTick(&h.ticker)
440+
}()
441+
f(h)
442+
}
443+
444+
// CumulativeSnapshot is part of the metric.CumulativeHistogram interface.
445+
func (h *HighCardinalityHistogram) CumulativeSnapshot() metric.HistogramSnapshot {
446+
return h.h.CumulativeSnapshot()
447+
}
448+
449+
// WindowedSnapshot is part of the metric.WindowedHistogram interface.
450+
func (h *HighCardinalityHistogram) WindowedSnapshot() metric.HistogramSnapshot {
451+
return h.h.WindowedSnapshot()
452+
}
453+
454+
// GetType is part of the metric.PrometheusExportable interface.
455+
func (h *HighCardinalityHistogram) GetType() *prometheusgo.MetricType {
456+
return h.h.GetType()
457+
}
458+
459+
// GetLabels is part of the metric.PrometheusExportable interface.
460+
func (h *HighCardinalityHistogram) GetLabels(useStaticLabels bool) []*prometheusgo.LabelPair {
461+
return h.h.GetLabels(useStaticLabels)
462+
}
463+
464+
// ToPrometheusMetric is part of the metric.PrometheusExportable interface.
465+
func (h *HighCardinalityHistogram) ToPrometheusMetric() *prometheusgo.Metric {
466+
return h.h.ToPrometheusMetric()
467+
}
468+
469+
// RecordValue records the histogram value for the given label values. If a
470+
// histogram with the given label values doesn't exist yet, it creates a new
471+
// histogram and records against it. RecordValue records value in parent metrics as well.
472+
func (h *HighCardinalityHistogram) RecordValue(v int64, labelValues ...string) {
473+
childMetric := h.GetOrAddChild(labelValues...)
474+
475+
h.ticker.RLock()
476+
defer h.ticker.RUnlock()
477+
478+
h.h.RecordValue(v)
479+
if childMetric != nil {
480+
childMetric.RecordValue(v)
481+
}
482+
}
483+
484+
// Each is part of the metric.PrometheusIterable interface.
485+
func (h *HighCardinalityHistogram) Each(
486+
labels []*prometheusgo.LabelPair, f func(metric *prometheusgo.Metric),
487+
) {
488+
h.EachWithLabels(labels, f, h.labelSliceCache)
489+
}
490+
491+
// InitializeMetrics is part of the PrometheusEvictable interface.
492+
func (h *HighCardinalityHistogram) InitializeMetrics(labelCache *metric.LabelSliceCache) {
493+
h.mu.Lock()
494+
defer h.mu.Unlock()
495+
496+
h.labelSliceCache = labelCache
497+
}
498+
499+
// GetOrAddChild returns the existing child histogram for the given label values,
500+
// or creates a new one if it doesn't exist. This is the preferred method for
501+
// cache-based storage to avoid panics on existing keys.
502+
func (h *HighCardinalityHistogram) GetOrAddChild(
503+
labelVals ...string,
504+
) *HighCardinalityChildHistogram {
505+
if len(labelVals) == 0 {
506+
return nil
507+
}
508+
509+
// Create a LabelSliceCacheKey from the labelVals.
510+
key := metric.LabelSliceCacheKey(metricKey(labelVals...))
511+
512+
child := h.getOrAddWithLabelSliceCache(h.GetMetadata().Name, h.createHighCardinalityChildHistogram, h.labelSliceCache, labelVals...)
513+
514+
h.labelSliceCache.Upsert(key, &metric.LabelSliceCacheValue{
515+
LabelValues: labelVals,
516+
})
517+
518+
return child.(*HighCardinalityChildHistogram)
519+
}
520+
521+
func (h *HighCardinalityHistogram) createHighCardinalityChildHistogram(
522+
key uint64, cache *metric.LabelSliceCache,
523+
) LabelSliceCachedChildMetric {
524+
return &HighCardinalityChildHistogram{
525+
LabelSliceCacheKey: metric.LabelSliceCacheKey(key),
526+
LabelSliceCache: cache,
527+
h: h.create(),
528+
createdAt: timeutil.Now(),
529+
}
530+
}
531+
532+
// HighCardinalityChildHistogram is a child of a HighCardinalityHistogram. When metrics are
533+
// collected by prometheus, each of the children will appear with a distinct label,
534+
// however, when cockroach internally collects metrics, only the parent is collected.
535+
type HighCardinalityChildHistogram struct {
536+
metric.LabelSliceCacheKey
537+
h metric.IHistogram
538+
*metric.LabelSliceCache
539+
createdAt time.Time
540+
}
541+
542+
func (h *HighCardinalityChildHistogram) CreatedAt() time.Time {
543+
return h.createdAt
544+
}
545+
546+
func (h *HighCardinalityChildHistogram) DecrementLabelSliceCacheReference() {
547+
h.LabelSliceCache.DecrementAndDeleteIfZero(h.LabelSliceCacheKey)
548+
}
549+
550+
// ToPrometheusMetric constructs a prometheus metric for this HighCardinalityChildHistogram.
551+
func (h *HighCardinalityChildHistogram) ToPrometheusMetric() *prometheusgo.Metric {
552+
return h.h.ToPrometheusMetric()
553+
}
554+
555+
func (h *HighCardinalityChildHistogram) labelValues() []string {
556+
lv, ok := h.LabelSliceCache.Get(h.LabelSliceCacheKey)
557+
if !ok {
558+
return nil
559+
}
560+
return lv.LabelValues
561+
}
562+
563+
// RecordValue records the histogram value.
564+
func (h *HighCardinalityChildHistogram) RecordValue(v int64) {
565+
h.h.RecordValue(v)
566+
}
567+
568+
// CumulativeSnapshot returns the cumulative histogram snapshot.
569+
func (h *HighCardinalityChildHistogram) CumulativeSnapshot() metric.HistogramSnapshot {
570+
return h.h.CumulativeSnapshot()
571+
}
572+
573+
// WindowedSnapshot returns the windowed histogram snapshot.
574+
func (h *HighCardinalityChildHistogram) WindowedSnapshot() metric.HistogramSnapshot {
575+
return h.h.WindowedSnapshot()
576+
}

pkg/util/metric/aggmetric/histogram_test.go

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,16 @@ import (
1212
"strconv"
1313
"strings"
1414
"testing"
15+
"time"
1516

1617
"github.com/cockroachdb/cockroach/pkg/base"
1718
"github.com/cockroachdb/cockroach/pkg/testutils/datapathutils"
1819
"github.com/cockroachdb/cockroach/pkg/testutils/echotest"
1920
"github.com/cockroachdb/cockroach/pkg/util/cache"
2021
"github.com/cockroachdb/cockroach/pkg/util/leaktest"
2122
"github.com/cockroachdb/cockroach/pkg/util/metric"
22-
"github.com/cockroachdb/crlib/testutils/require"
2323
"github.com/prometheus/common/expfmt"
24+
"github.com/stretchr/testify/require"
2425
)
2526

2627
func TestSQLHistogram(t *testing.T) {
@@ -86,3 +87,84 @@ func TestSQLHistogram(t *testing.T) {
8687
}
8788
echotest.Require(t, writePrometheusMetrics(t), datapathutils.TestDataPath(t, testFile))
8889
}
90+
91+
func TestHighCardinalityHistogram(t *testing.T) {
92+
defer leaktest.AfterTest(t)()
93+
const cacheSize = 10
94+
r := metric.NewRegistry()
95+
writePrometheusMetrics := WritePrometheusMetricsFunc(r)
96+
97+
h := NewHighCardinalityHistogram(metric.HistogramOptions{
98+
Metadata: metric.Metadata{
99+
Name: "histo_gram",
100+
},
101+
Duration: base.DefaultHistogramWindowInterval(),
102+
MaxVal: 100,
103+
SigFigs: 1,
104+
BucketConfig: metric.Percent100Buckets,
105+
}, "database", "application_name")
106+
107+
h.mu.children = &UnorderedCacheWrapper{
108+
cache: initialiseCacheStorageForTesting(),
109+
}
110+
r.AddMetric(h)
111+
112+
// Initialize with a label slice cache to test eviction
113+
labelSliceCache := metric.NewLabelSliceCache()
114+
h.InitializeMetrics(labelSliceCache)
115+
116+
for i := 0; i < cacheSize+5; i++ {
117+
h.RecordValue(int64(i+1), "1", strconv.Itoa(i))
118+
}
119+
120+
// Wait more than cache eviction time to make sure that keys are not evicted based on only cache size.
121+
time.Sleep(6 * time.Second)
122+
123+
testFile := "HighCardinalityHistogram_pre_eviction.txt"
124+
if metric.HdrEnabled() {
125+
testFile = "HighCardinalityHistogram_pre_eviction_hdr.txt"
126+
}
127+
128+
echotest.Require(t, writePrometheusMetrics(t), datapathutils.TestDataPath(t, testFile))
129+
130+
for i := 0; i < cacheSize+5; i++ {
131+
metricKey := metric.LabelSliceCacheKey(metricKey("1", strconv.Itoa(i)))
132+
labelSliceValue, ok := labelSliceCache.Get(metricKey)
133+
require.True(t, ok, "missing labelSliceValue in label slice cache")
134+
require.Equal(t, int64(1), labelSliceValue.Counter.Load(), "the value should be 1")
135+
require.Equal(t, []string{"1", strconv.Itoa(i)}, labelSliceValue.LabelValues, "label values are mismatching")
136+
137+
}
138+
139+
for i := 0 + cacheSize; i < cacheSize+5; i++ {
140+
h.RecordValue(int64(i+10), "2", strconv.Itoa(i))
141+
}
142+
143+
testFile = "HighCardinalityHistogram_post_eviction.txt"
144+
if metric.HdrEnabled() {
145+
testFile = "HighCardinalityHistogram_post_eviction_hdr.txt"
146+
}
147+
echotest.Require(t, writePrometheusMetrics(t), datapathutils.TestDataPath(t, testFile))
148+
149+
for i := 0; i < 5; i++ {
150+
metricKey := metric.LabelSliceCacheKey(metricKey("1", strconv.Itoa(i)))
151+
_, ok := labelSliceCache.Get(metricKey)
152+
require.False(t, ok, "labelSliceValue should not be present.")
153+
}
154+
155+
for i := 10; i < 15; i++ {
156+
metricKey := metric.LabelSliceCacheKey(metricKey("1", strconv.Itoa(i)))
157+
labelSliceValue, ok := labelSliceCache.Get(metricKey)
158+
require.True(t, ok, "missing labelSliceValue in label slice cache")
159+
require.Equal(t, int64(1), labelSliceValue.Counter.Load(), "the value should be 1")
160+
require.Equal(t, []string{"1", strconv.Itoa(i)}, labelSliceValue.LabelValues, "label values are mismatching")
161+
}
162+
163+
for i := 10; i < 15; i++ {
164+
metricKey := metric.LabelSliceCacheKey(metricKey("2", strconv.Itoa(i)))
165+
labelSliceValue, ok := labelSliceCache.Get(metricKey)
166+
require.True(t, ok, "missing labelSliceValue in label slice cache")
167+
require.Equal(t, int64(1), labelSliceValue.Counter.Load(), "the value should be 1")
168+
require.Equal(t, []string{"2", strconv.Itoa(i)}, labelSliceValue.LabelValues, "label values are mismatching")
169+
}
170+
}

0 commit comments

Comments
 (0)