Skip to content

Commit dadfef8

Browse files
authored
Merge pull request #221 from stevvooe/benchmark-with-labels-metric-vec
metricvec: handle hash collision for labeled metrics
2 parents 25db044 + 4db77b0 commit dadfef8

File tree

7 files changed

+364
-101
lines changed

7 files changed

+364
-101
lines changed

prometheus/counter.go

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -96,19 +96,15 @@ func NewCounterVec(opts CounterOpts, labelNames []string) *CounterVec {
9696
opts.ConstLabels,
9797
)
9898
return &CounterVec{
99-
MetricVec: MetricVec{
100-
children: map[uint64]Metric{},
101-
desc: desc,
102-
newMetric: func(lvs ...string) Metric {
103-
result := &counter{value: value{
104-
desc: desc,
105-
valType: CounterValue,
106-
labelPairs: makeLabelPairs(desc, lvs),
107-
}}
108-
result.init(result) // Init self-collection.
109-
return result
110-
},
111-
},
99+
MetricVec: newMetricVec(desc, func(lvs ...string) Metric {
100+
result := &counter{value: value{
101+
desc: desc,
102+
valType: CounterValue,
103+
labelPairs: makeLabelPairs(desc, lvs),
104+
}}
105+
result.init(result) // Init self-collection.
106+
return result
107+
}),
112108
}
113109
}
114110

prometheus/gauge.go

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,9 @@ func NewGaugeVec(opts GaugeOpts, labelNames []string) *GaugeVec {
7272
opts.ConstLabels,
7373
)
7474
return &GaugeVec{
75-
MetricVec: MetricVec{
76-
children: map[uint64]Metric{},
77-
desc: desc,
78-
newMetric: func(lvs ...string) Metric {
79-
return newValue(desc, GaugeValue, 0, lvs...)
80-
},
81-
},
75+
MetricVec: newMetricVec(desc, func(lvs ...string) Metric {
76+
return newValue(desc, GaugeValue, 0, lvs...)
77+
}),
8278
}
8379
}
8480

prometheus/histogram.go

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -301,13 +301,9 @@ func NewHistogramVec(opts HistogramOpts, labelNames []string) *HistogramVec {
301301
opts.ConstLabels,
302302
)
303303
return &HistogramVec{
304-
MetricVec: MetricVec{
305-
children: map[uint64]Metric{},
306-
desc: desc,
307-
newMetric: func(lvs ...string) Metric {
308-
return newHistogram(desc, opts, lvs...)
309-
},
310-
},
304+
MetricVec: newMetricVec(desc, func(lvs ...string) Metric {
305+
return newHistogram(desc, opts, lvs...)
306+
}),
311307
}
312308
}
313309

prometheus/summary.go

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -404,13 +404,9 @@ func NewSummaryVec(opts SummaryOpts, labelNames []string) *SummaryVec {
404404
opts.ConstLabels,
405405
)
406406
return &SummaryVec{
407-
MetricVec: MetricVec{
408-
children: map[uint64]Metric{},
409-
desc: desc,
410-
newMetric: func(lvs ...string) Metric {
411-
return newSummary(desc, opts, lvs...)
412-
},
413-
},
407+
MetricVec: newMetricVec(desc, func(lvs ...string) Metric {
408+
return newSummary(desc, opts, lvs...)
409+
}),
414410
}
415411
}
416412

prometheus/untyped.go

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,9 @@ func NewUntypedVec(opts UntypedOpts, labelNames []string) *UntypedVec {
7070
opts.ConstLabels,
7171
)
7272
return &UntypedVec{
73-
MetricVec: MetricVec{
74-
children: map[uint64]Metric{},
75-
desc: desc,
76-
newMetric: func(lvs ...string) Metric {
77-
return newValue(desc, UntypedValue, 0, lvs...)
78-
},
79-
},
73+
MetricVec: newMetricVec(desc, func(lvs ...string) Metric {
74+
return newValue(desc, UntypedValue, 0, lvs...)
75+
}),
8076
}
8177
}
8278

prometheus/vec.go

Lines changed: 171 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ package prometheus
1616
import (
1717
"fmt"
1818
"sync"
19+
20+
"github.com/prometheus/common/model"
1921
)
2022

2123
// MetricVec is a Collector to bundle metrics of the same name that
@@ -25,10 +27,31 @@ import (
2527
// provided in this package.
2628
type MetricVec struct {
2729
mtx sync.RWMutex // Protects the children.
28-
children map[uint64]Metric
30+
children map[uint64][]metricWithLabelValues
2931
desc *Desc
3032

31-
newMetric func(labelValues ...string) Metric
33+
newMetric func(labelValues ...string) Metric
34+
hashAdd func(h uint64, s string) uint64 // replace hash function for testing collision handling
35+
hashAddByte func(h uint64, b byte) uint64
36+
}
37+
38+
// newMetricVec returns an initialized MetricVec. The concrete value is
39+
// returned for embedding into another struct.
40+
func newMetricVec(desc *Desc, newMetric func(lvs ...string) Metric) MetricVec {
41+
return MetricVec{
42+
children: map[uint64][]metricWithLabelValues{},
43+
desc: desc,
44+
newMetric: newMetric,
45+
hashAdd: hashAdd,
46+
hashAddByte: hashAddByte,
47+
}
48+
}
49+
50+
// metricWithLabelValues provides the metric and its label values for
51+
// disambiguation on hash collision.
52+
type metricWithLabelValues struct {
53+
values []string
54+
metric Metric
3255
}
3356

3457
// Describe implements Collector. The length of the returned slice
@@ -42,8 +65,10 @@ func (m *MetricVec) Collect(ch chan<- Metric) {
4265
m.mtx.RLock()
4366
defer m.mtx.RUnlock()
4467

45-
for _, metric := range m.children {
46-
ch <- metric
68+
for _, metrics := range m.children {
69+
for _, metric := range metrics {
70+
ch <- metric.metric
71+
}
4772
}
4873
}
4974

@@ -77,16 +102,7 @@ func (m *MetricVec) GetMetricWithLabelValues(lvs ...string) (Metric, error) {
77102
return nil, err
78103
}
79104

80-
m.mtx.RLock()
81-
metric, ok := m.children[h]
82-
m.mtx.RUnlock()
83-
if ok {
84-
return metric, nil
85-
}
86-
87-
m.mtx.Lock()
88-
defer m.mtx.Unlock()
89-
return m.getOrCreateMetric(h, lvs...), nil
105+
return m.getOrCreateMetric(h, lvs), nil
90106
}
91107

92108
// GetMetricWith returns the Metric for the given Labels map (the label names
@@ -107,20 +123,7 @@ func (m *MetricVec) GetMetricWith(labels Labels) (Metric, error) {
107123
return nil, err
108124
}
109125

110-
m.mtx.RLock()
111-
metric, ok := m.children[h]
112-
m.mtx.RUnlock()
113-
if ok {
114-
return metric, nil
115-
}
116-
117-
lvs := make([]string, len(labels))
118-
for i, label := range m.desc.variableLabels {
119-
lvs[i] = labels[label]
120-
}
121-
m.mtx.Lock()
122-
defer m.mtx.Unlock()
123-
return m.getOrCreateMetric(h, lvs...), nil
126+
return m.getOrCreateMetric(h, labels), nil
124127
}
125128

126129
// WithLabelValues works as GetMetricWithLabelValues, but panics if an error
@@ -168,11 +171,7 @@ func (m *MetricVec) DeleteLabelValues(lvs ...string) bool {
168171
if err != nil {
169172
return false
170173
}
171-
if _, ok := m.children[h]; !ok {
172-
return false
173-
}
174-
delete(m.children, h)
175-
return true
174+
return m.deleteByHash(h, lvs)
176175
}
177176

178177
// Delete deletes the metric where the variable labels are the same as those
@@ -193,10 +192,31 @@ func (m *MetricVec) Delete(labels Labels) bool {
193192
if err != nil {
194193
return false
195194
}
196-
if _, ok := m.children[h]; !ok {
195+
196+
return m.deleteByHash(h, labels)
197+
}
198+
199+
// deleteByHash removes the metric from the hash bucket h. If there are
200+
// multiple matches in the bucket, use lvs to select a metric and remove only
201+
// that metric.
202+
//
203+
// lvs MUST be of type Labels or []string or this method will panic.
204+
func (m *MetricVec) deleteByHash(h uint64, values interface{}) bool {
205+
metrics, ok := m.children[h]
206+
if !ok {
197207
return false
198208
}
199-
delete(m.children, h)
209+
210+
i := m.findMetric(metrics, values)
211+
if i >= len(metrics) {
212+
return false
213+
}
214+
215+
if len(metrics) > 1 {
216+
m.children[h] = append(metrics[:i], metrics[i+1:]...)
217+
} else {
218+
delete(m.children, h)
219+
}
200220
return true
201221
}
202222

@@ -216,7 +236,8 @@ func (m *MetricVec) hashLabelValues(vals []string) (uint64, error) {
216236
}
217237
h := hashNew()
218238
for _, val := range vals {
219-
h = hashAdd(h, val)
239+
h = m.hashAdd(h, val)
240+
h = m.hashAddByte(h, model.SeparatorByte)
220241
}
221242
return h, nil
222243
}
@@ -231,19 +252,125 @@ func (m *MetricVec) hashLabels(labels Labels) (uint64, error) {
231252
if !ok {
232253
return 0, fmt.Errorf("label name %q missing in label map", label)
233254
}
234-
h = hashAdd(h, val)
255+
h = m.hashAdd(h, val)
256+
h = m.hashAddByte(h, model.SeparatorByte)
235257
}
236258
return h, nil
237259
}
238260

239-
func (m *MetricVec) getOrCreateMetric(hash uint64, labelValues ...string) Metric {
240-
metric, ok := m.children[hash]
261+
// getOrCreateMetric retrieves the metric by hash and label value or creates it
262+
// and returns the new one.
263+
//
264+
// lvs MUST be of type Labels or []string or this method will panic.
265+
//
266+
// This function holds the mutex.
267+
func (m *MetricVec) getOrCreateMetric(hash uint64, lvs interface{}) Metric {
268+
m.mtx.RLock()
269+
metric, ok := m.getMetric(hash, lvs)
270+
m.mtx.RUnlock()
271+
if ok {
272+
return metric
273+
}
274+
275+
m.mtx.Lock()
276+
defer m.mtx.Unlock()
277+
metric, ok = m.getMetric(hash, lvs)
241278
if !ok {
242-
// Copy labelValues. Otherwise, they would be allocated even if we don't go
243-
// down this code path.
244-
copiedLabelValues := append(make([]string, 0, len(labelValues)), labelValues...)
245-
metric = m.newMetric(copiedLabelValues...)
246-
m.children[hash] = metric
279+
lvs := m.copyLabelValues(lvs)
280+
metric = m.newMetric(lvs...)
281+
m.children[hash] = append(m.children[hash], metricWithLabelValues{values: lvs, metric: metric})
247282
}
248283
return metric
249284
}
285+
286+
// getMetric while handling possible collisions in the hash space. Must be
287+
// called while holding read mutex.
288+
//
289+
// lvs must be of type Labels or []string.
290+
func (m *MetricVec) getMetric(h uint64, lvs interface{}) (Metric, bool) {
291+
metrics, ok := m.children[h]
292+
if ok {
293+
return m.selectMetric(metrics, lvs)
294+
}
295+
296+
return nil, false
297+
}
298+
299+
func (m *MetricVec) selectMetric(metrics []metricWithLabelValues, lvs interface{}) (Metric, bool) {
300+
i := m.findMetric(metrics, lvs)
301+
302+
if i < len(metrics) {
303+
return metrics[i].metric, true
304+
}
305+
306+
return nil, false
307+
}
308+
309+
// findMetric returns the index of the matching metric or len(metrics) if not
310+
// found.
311+
func (m *MetricVec) findMetric(metrics []metricWithLabelValues, lvs interface{}) int {
312+
for i, metric := range metrics {
313+
if m.matchLabels(metric.values, lvs) {
314+
return i
315+
}
316+
}
317+
318+
return len(metrics)
319+
}
320+
321+
func (m *MetricVec) matchLabels(values []string, lvs interface{}) bool {
322+
switch lvs := lvs.(type) {
323+
case []string:
324+
if len(values) != len(lvs) {
325+
return false
326+
}
327+
328+
for i, v := range values {
329+
if v != lvs[i] {
330+
return false
331+
}
332+
}
333+
334+
return true
335+
case Labels:
336+
if len(lvs) != len(values) {
337+
return false
338+
}
339+
340+
for i, k := range m.desc.variableLabels {
341+
if values[i] != lvs[k] {
342+
return false
343+
}
344+
}
345+
346+
return true
347+
default:
348+
// If we reach this condition, there is an unexpected type being used
349+
// as a labels value. Either add branch here for the new type or fix
350+
// the bug causing the type to be passed in.
351+
panic("unsupported type")
352+
}
353+
}
354+
355+
// copyLabelValues copies the labels values into common string slice format to
356+
// use when allocating the metric and to keep track of hash collision
357+
// ambiguity.
358+
//
359+
// lvs must be of type Labels or []string or this method will panic.
360+
func (m *MetricVec) copyLabelValues(lvs interface{}) []string {
361+
var labelValues []string
362+
switch lvs := lvs.(type) {
363+
case []string:
364+
labelValues = make([]string, len(lvs))
365+
copy(labelValues, lvs)
366+
case Labels:
367+
labelValues = make([]string, len(lvs))
368+
for i, k := range m.desc.variableLabels {
369+
labelValues[i] = lvs[k]
370+
}
371+
default:
372+
panic(fmt.Sprintf("unsupported type for lvs: %#v", lvs))
373+
}
374+
375+
return labelValues
376+
}

0 commit comments

Comments
 (0)