-
Notifications
You must be signed in to change notification settings - Fork 1.2k
metricvec: handle hash collision for labeled metrics #221
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
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 |
---|---|---|
|
@@ -16,6 +16,8 @@ package prometheus | |
import ( | ||
"fmt" | ||
"sync" | ||
|
||
"github.com/prometheus/common/model" | ||
) | ||
|
||
// MetricVec is a Collector to bundle metrics of the same name that | ||
|
@@ -25,10 +27,31 @@ import ( | |
// provided in this package. | ||
type MetricVec struct { | ||
mtx sync.RWMutex // Protects the children. | ||
children map[uint64]Metric | ||
children map[uint64][]metricWithLabelValues | ||
desc *Desc | ||
|
||
newMetric func(labelValues ...string) Metric | ||
newMetric func(labelValues ...string) Metric | ||
hashAdd func(h uint64, s string) uint64 // replace hash function for testing collision handling | ||
hashAddByte func(h uint64, b byte) uint64 | ||
} | ||
|
||
// newMetricVec returns an initialized MetricVec. The concrete value is | ||
// returned for embedding into another struct. | ||
func newMetricVec(desc *Desc, newMetric func(lvs ...string) Metric) MetricVec { | ||
return MetricVec{ | ||
children: map[uint64][]metricWithLabelValues{}, | ||
desc: desc, | ||
newMetric: newMetric, | ||
hashAdd: hashAdd, | ||
hashAddByte: hashAddByte, | ||
} | ||
} | ||
|
||
// metricWithLabelValues provides the metric and its label values for | ||
// disambiguation on hash collision. | ||
type metricWithLabelValues struct { | ||
values []string | ||
metric Metric | ||
} | ||
|
||
// Describe implements Collector. The length of the returned slice | ||
|
@@ -42,8 +65,10 @@ func (m *MetricVec) Collect(ch chan<- Metric) { | |
m.mtx.RLock() | ||
defer m.mtx.RUnlock() | ||
|
||
for _, metric := range m.children { | ||
ch <- metric | ||
for _, metrics := range m.children { | ||
for _, metric := range metrics { | ||
ch <- metric.metric | ||
} | ||
} | ||
} | ||
|
||
|
@@ -77,16 +102,7 @@ func (m *MetricVec) GetMetricWithLabelValues(lvs ...string) (Metric, error) { | |
return nil, err | ||
} | ||
|
||
m.mtx.RLock() | ||
metric, ok := m.children[h] | ||
m.mtx.RUnlock() | ||
if ok { | ||
return metric, nil | ||
} | ||
|
||
m.mtx.Lock() | ||
defer m.mtx.Unlock() | ||
return m.getOrCreateMetric(h, lvs...), nil | ||
return m.getOrCreateMetric(h, lvs), nil | ||
} | ||
|
||
// GetMetricWith returns the Metric for the given Labels map (the label names | ||
|
@@ -107,20 +123,7 @@ func (m *MetricVec) GetMetricWith(labels Labels) (Metric, error) { | |
return nil, err | ||
} | ||
|
||
m.mtx.RLock() | ||
metric, ok := m.children[h] | ||
m.mtx.RUnlock() | ||
if ok { | ||
return metric, nil | ||
} | ||
|
||
lvs := make([]string, len(labels)) | ||
for i, label := range m.desc.variableLabels { | ||
lvs[i] = labels[label] | ||
} | ||
m.mtx.Lock() | ||
defer m.mtx.Unlock() | ||
return m.getOrCreateMetric(h, lvs...), nil | ||
return m.getOrCreateMetric(h, labels), nil | ||
} | ||
|
||
// WithLabelValues works as GetMetricWithLabelValues, but panics if an error | ||
|
@@ -168,11 +171,7 @@ func (m *MetricVec) DeleteLabelValues(lvs ...string) bool { | |
if err != nil { | ||
return false | ||
} | ||
if _, ok := m.children[h]; !ok { | ||
return false | ||
} | ||
delete(m.children, h) | ||
return true | ||
return m.deleteByHash(h, lvs) | ||
} | ||
|
||
// Delete deletes the metric where the variable labels are the same as those | ||
|
@@ -193,10 +192,31 @@ func (m *MetricVec) Delete(labels Labels) bool { | |
if err != nil { | ||
return false | ||
} | ||
if _, ok := m.children[h]; !ok { | ||
|
||
return m.deleteByHash(h, labels) | ||
} | ||
|
||
// deleteByHash removes the metric from the hash bucket h. If there are | ||
// multiple matches in the bucket, use lvs to select a metric and remove only | ||
// that metric. | ||
// | ||
// lvs MUST be of type Labels or []string or this method will panic. | ||
func (m *MetricVec) deleteByHash(h uint64, values interface{}) bool { | ||
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'm not really a fan of interface type arguments. We're losing the advantages of the type system. Is this really necessary? 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. To maintain correctness and zero-alloc behavior without repeating code, it is necessary. Any errors are caught during test time. What is the issue? This is private code. Are you just afraid? 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. We could, of course, repeat the code. Go isn't really afraid of that. But in this case, it would be a lot of code. I'm fine with the interface usage as it avoids the allocation in L117 of the old code. It might be good, though, to document the requirements for 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. You're right @stevvooe, I couldn't come up with a typed implementation either without causing more allocations. We'll need to wait until golang's escape analysis becomes better. 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. @grobie @beorn7 Just made a polymorphic implementation and confirmed we get allocations. Up to you, but it might be better to allow allocations and wait for better escape analysis. It would avoid the needless complexity in this code. A simple string slice header allocation on each lookup would let us delete a massive swath of code. 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 guess we're more interested in as little as possible overhead of the client than a nice and short code base. @beorn7 is the gatekeeper of this repository though. 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. Yes, avoiding allocations in this code path is essential. It is very likely to be called very frequently by users. Current code is the best solution, I guess. |
||
metrics, ok := m.children[h] | ||
if !ok { | ||
return false | ||
} | ||
delete(m.children, h) | ||
|
||
i := m.findMetric(metrics, values) | ||
if i >= len(metrics) { | ||
return false | ||
} | ||
|
||
if len(metrics) > 1 { | ||
m.children[h] = append(metrics[:i], metrics[i+1:]...) | ||
} else { | ||
delete(m.children, h) | ||
} | ||
return true | ||
} | ||
|
||
|
@@ -216,7 +236,8 @@ func (m *MetricVec) hashLabelValues(vals []string) (uint64, error) { | |
} | ||
h := hashNew() | ||
for _, val := range vals { | ||
h = hashAdd(h, val) | ||
h = m.hashAdd(h, val) | ||
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. This should actually be followed by 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. Good catch. I'll use the 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.
Since the hashing is completely internal here, it doesn't really matter. Even if somebody constructed a collision, it doesn't matter anymore as you have implemented the detection here. Still I'd prefer consistency. 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.
The main problem there is that if you're debugging and print in the wrong place, things explode. Also, if you ever want to use these keys to binary search, which may make sense in certain cases, you'll have to generate one with a different separator that doesn't mess with tuple-oriented sort order.
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. This is about hashing, not about tuples. Only add this character while creating the hash, nowhere else. https://github.com/prometheus/common/blob/master/model/signature.go 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. Good sir, we are hashing tuples. 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. For certain definition of tuples, certainly. But we are not modifying the "tuple". The separator byte is only added during hashing, there is no way it will ever be printed or used in sorting. 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 don't see this link but I've used the constant To give you context, we had a sort error in the docker registry API due to the use of a |
||
h = m.hashAddByte(h, model.SeparatorByte) | ||
} | ||
return h, nil | ||
} | ||
|
@@ -231,19 +252,125 @@ func (m *MetricVec) hashLabels(labels Labels) (uint64, error) { | |
if !ok { | ||
return 0, fmt.Errorf("label name %q missing in label map", label) | ||
} | ||
h = hashAdd(h, val) | ||
h = m.hashAdd(h, val) | ||
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. See above. |
||
h = m.hashAddByte(h, model.SeparatorByte) | ||
} | ||
return h, nil | ||
} | ||
|
||
func (m *MetricVec) getOrCreateMetric(hash uint64, labelValues ...string) Metric { | ||
metric, ok := m.children[hash] | ||
// getOrCreateMetric retrieves the metric by hash and label value or creates it | ||
// and returns the new one. | ||
// | ||
// lvs MUST be of type Labels or []string or this method will panic. | ||
// | ||
// This function holds the mutex. | ||
func (m *MetricVec) getOrCreateMetric(hash uint64, lvs interface{}) Metric { | ||
m.mtx.RLock() | ||
metric, ok := m.getMetric(hash, lvs) | ||
m.mtx.RUnlock() | ||
if ok { | ||
return metric | ||
} | ||
|
||
m.mtx.Lock() | ||
defer m.mtx.Unlock() | ||
metric, ok = m.getMetric(hash, lvs) | ||
if !ok { | ||
// Copy labelValues. Otherwise, they would be allocated even if we don't go | ||
// down this code path. | ||
copiedLabelValues := append(make([]string, 0, len(labelValues)), labelValues...) | ||
metric = m.newMetric(copiedLabelValues...) | ||
m.children[hash] = metric | ||
lvs := m.copyLabelValues(lvs) | ||
metric = m.newMetric(lvs...) | ||
m.children[hash] = append(m.children[hash], metricWithLabelValues{values: lvs, metric: metric}) | ||
} | ||
return metric | ||
} | ||
|
||
// getMetric while handling possible collisions in the hash space. Must be | ||
// called while holding read mutex. | ||
// | ||
// lvs must be of type Labels or []string. | ||
func (m *MetricVec) getMetric(h uint64, lvs interface{}) (Metric, bool) { | ||
metrics, ok := m.children[h] | ||
if ok { | ||
return m.selectMetric(metrics, lvs) | ||
} | ||
|
||
return nil, false | ||
} | ||
|
||
func (m *MetricVec) selectMetric(metrics []metricWithLabelValues, lvs interface{}) (Metric, bool) { | ||
i := m.findMetric(metrics, lvs) | ||
|
||
if i < len(metrics) { | ||
return metrics[i].metric, true | ||
} | ||
|
||
return nil, false | ||
} | ||
|
||
// findMetric returns the index of the matching metric or len(metrics) if not | ||
// found. | ||
func (m *MetricVec) findMetric(metrics []metricWithLabelValues, lvs interface{}) int { | ||
for i, metric := range metrics { | ||
if m.matchLabels(metric.values, lvs) { | ||
return i | ||
} | ||
} | ||
|
||
return len(metrics) | ||
} | ||
|
||
func (m *MetricVec) matchLabels(values []string, lvs interface{}) bool { | ||
switch lvs := lvs.(type) { | ||
case []string: | ||
if len(values) != len(lvs) { | ||
return false | ||
} | ||
|
||
for i, v := range values { | ||
if v != lvs[i] { | ||
return false | ||
} | ||
} | ||
|
||
return true | ||
case Labels: | ||
if len(lvs) != len(values) { | ||
return false | ||
} | ||
|
||
for i, k := range m.desc.variableLabels { | ||
if values[i] != lvs[k] { | ||
return false | ||
} | ||
} | ||
|
||
return true | ||
default: | ||
// If we reach this condition, there is an unexpected type being used | ||
// as a labels value. Either add branch here for the new type or fix | ||
// the bug causing the type to be passed in. | ||
panic("unsupported type") | ||
} | ||
} | ||
|
||
// copyLabelValues copies the labels values into common string slice format to | ||
// use when allocating the metric and to keep track of hash collision | ||
// ambiguity. | ||
// | ||
// lvs must be of type Labels or []string or this method will panic. | ||
func (m *MetricVec) copyLabelValues(lvs interface{}) []string { | ||
var labelValues []string | ||
switch lvs := lvs.(type) { | ||
case []string: | ||
labelValues = make([]string, len(lvs)) | ||
copy(labelValues, lvs) | ||
case Labels: | ||
labelValues = make([]string, len(lvs)) | ||
for i, k := range m.desc.variableLabels { | ||
labelValues[i] = lvs[k] | ||
} | ||
default: | ||
panic(fmt.Sprintf("unsupported type for lvs: %#v", lvs)) | ||
} | ||
|
||
return labelValues | ||
} |
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 doc comment is probably a good place to document the requirements for
interface{}
("... is of type []string or Labels").