Skip to content

Commit 21da3b8

Browse files
authored
Refactor target label handling in target allocator for improved performance (#4587)
* Refactor target relabelling Manually build labels for targets * Use scratchbuilder Reuse the sorted label name buffer
1 parent d36edd1 commit 21da3b8

File tree

4 files changed

+113
-53
lines changed

4 files changed

+113
-53
lines changed

cmd/otel-allocator/internal/prehook/relabel.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"slices"
88

99
"github.com/go-logr/logr"
10+
"github.com/prometheus/prometheus/model/labels"
1011
"github.com/prometheus/prometheus/model/relabel"
1112

1213
"github.com/open-telemetry/opentelemetry-operator/cmd/otel-allocator/internal/target"
@@ -36,19 +37,23 @@ func (tf *relabelConfigTargetFilter) Apply(targets []*target.Item) []*target.Ite
3637
return targets
3738
}
3839

40+
builder := labels.NewBuilder(labels.EmptyLabels())
3941
writeIndex := 0
4042
for _, tItem := range targets {
41-
keepTarget := true
42-
lset := tItem.Labels
43-
for _, cfg := range tf.relabelCfg[tItem.JobName] {
44-
lset, keepTarget = relabel.Process(lset, cfg)
45-
if !keepTarget {
46-
break // inner loop
47-
}
48-
}
43+
builder.Reset(tItem.Labels)
44+
keepTarget := relabel.ProcessBuilder(builder, tf.relabelCfg[tItem.JobName]...)
4945

5046
if keepTarget {
51-
targets[writeIndex] = target.NewItem(tItem.JobName, tItem.TargetURL, tItem.Labels, tItem.CollectorName, target.WithRelabeledLabels(lset))
47+
// Compute hash immediately while we have the builder, skipping meta labels.
48+
// This avoids materializing the filtered labels.
49+
hash := target.HashFromBuilder(builder, tItem.JobName)
50+
targets[writeIndex] = target.NewItem(
51+
tItem.JobName,
52+
tItem.TargetURL,
53+
tItem.Labels,
54+
tItem.CollectorName,
55+
target.WithHash(hash),
56+
)
5257
writeIndex++
5358
}
5459
}

cmd/otel-allocator/internal/prehook/relabel_test.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ func TestApply(t *testing.T) {
244244
allocatorPrehook.SetConfig(relabelCfg)
245245
remainingItems := allocatorPrehook.Apply(targets)
246246
assert.Len(t, remainingItems, numRemaining)
247-
assert.Equal(t, remainingItems, expectedTargetMap)
247+
assert.Equal(t, expectedTargetMap, remainingItems)
248248

249249
// clear out relabelCfg to test with empty values
250250
for key := range relabelCfg {
@@ -370,7 +370,15 @@ func MakeTargetFromProm(rCfgs []*relabel.Config, rawTarget *target.Item) (*targe
370370
return nil, nil
371371
}
372372

373-
newTarget := target.NewItem(rawTarget.JobName, rawTarget.TargetURL, rawTarget.Labels, rawTarget.CollectorName, target.WithRelabeledLabels(lset))
373+
// Compute the hash from the builder, skipping meta labels
374+
hash := target.HashFromBuilder(lb, rawTarget.JobName)
375+
newTarget := target.NewItem(
376+
rawTarget.JobName,
377+
rawTarget.TargetURL,
378+
rawTarget.Labels,
379+
rawTarget.CollectorName,
380+
target.WithHash(hash),
381+
)
374382
return newTarget, nil
375383
}
376384

cmd/otel-allocator/internal/target/discovery.go

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"context"
88
"hash"
99
"hash/fnv"
10+
"maps"
11+
"slices"
1012
"sync"
1113
"time"
1214

@@ -26,6 +28,8 @@ import (
2628
allocatorWatcher "github.com/open-telemetry/opentelemetry-operator/cmd/otel-allocator/internal/watcher"
2729
)
2830

31+
const labelBuilderPreallocSize = 100
32+
2933
type Discoverer struct {
3034
log logr.Logger
3135
manager *discovery.Manager
@@ -199,7 +203,11 @@ func (m *Discoverer) Reload() {
199203

200204
// processTargetGroups processes the target groups and returns a map of targets.
201205
func (m *Discoverer) processTargetGroups(jobName string, groups []*targetgroup.Group, intoTargets []*Item) {
202-
builder := labels.NewBuilder(labels.Labels{})
206+
// the builder for group labels
207+
groupBuilder := labels.NewScratchBuilder(labelBuilderPreallocSize)
208+
209+
// a slice for sorting target label names, we allocate it here to avoid doing it in the hot loop
210+
targetLabelNames := make([]string, 0, labelBuilderPreallocSize)
203211

204212
begin := time.Now()
205213
defer func() {
@@ -208,18 +216,32 @@ func (m *Discoverer) processTargetGroups(jobName string, groups []*targetgroup.G
208216
var count float64 = 0
209217
index := 0
210218
for _, tg := range groups {
211-
builder.Reset(labels.EmptyLabels())
219+
groupBuilder.Reset()
212220
for ln, lv := range tg.Labels {
213-
builder.Set(string(ln), string(lv))
221+
groupBuilder.Add(string(ln), string(lv))
214222
}
215-
groupLabels := builder.Labels()
223+
groupBuilder.Sort()
216224
for _, t := range tg.Targets {
217225
count++
218-
builder.Reset(groupLabels)
219-
for ln, lv := range t {
220-
builder.Set(string(ln), string(lv))
226+
// ScratchBuilder is a struct containing a slice of labels. By assigning to a new variable, we get a copy
227+
// of the struct, with a new slice pointing to the same underlying array. As long as we don't mutate the
228+
// original slice and only append to it, we can avoid copying the group labels.
229+
targetBuilder := groupBuilder
230+
targetLabelNames = targetLabelNames[:0]
231+
232+
// We can't sort the whole builder slice, because that would modify the underlying groupBuilder. Instead,
233+
// we sort the labels in a separate slice. As a result, the group labels and the target labels are sorted
234+
// subslices of the builder slice, which is in itself not sorted. This is fine, as we don't care what the
235+
// order of labels is - just that it's consistent, so the hash is always the same.
236+
for ln := range maps.Keys(t) {
237+
targetLabelNames = append(targetLabelNames, string(ln))
238+
}
239+
slices.Sort(targetLabelNames)
240+
for _, ln := range targetLabelNames {
241+
lv := t[model.LabelName(ln)]
242+
targetBuilder.Add(ln, string(lv))
221243
}
222-
item := NewItem(jobName, string(t[model.AddressLabel]), builder.Labels(), "")
244+
item := NewItem(jobName, string(t[model.AddressLabel]), targetBuilder.Labels(), "")
223245
intoTargets[index] = item
224246
index++
225247
}

cmd/otel-allocator/internal/target/target.go

Lines changed: 59 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,26 @@ package target
55

66
import (
77
"encoding/binary"
8-
"slices"
98
"strconv"
109
"strings"
10+
"sync"
1111

1212
"github.com/cespare/xxhash/v2"
1313
"github.com/prometheus/common/model"
1414
"github.com/prometheus/prometheus/model/labels"
1515
)
1616

17+
// seps is the separator used between label name/value pairs in hash computation.
18+
// This matches Prometheus's label hashing approach.
19+
var seps = []byte{'\xff'}
20+
21+
// hasherPool is a pool of xxhash digesters for efficient hash computation.
22+
var hasherPool = sync.Pool{
23+
New: func() any {
24+
return xxhash.New()
25+
},
26+
}
27+
1728
// nodeLabels are labels that are used to identify the node on which the given
1829
// target is residing. To learn more about these labels, please refer to:
1930
// https://prometheus.io/docs/prometheus/latest/configuration/configuration/#kubernetes_sd_config
@@ -37,40 +48,53 @@ func (h ItemHash) String() string {
3748

3849
// Item represents a target to be scraped.
3950
type Item struct {
40-
JobName string
41-
TargetURL string
42-
Labels labels.Labels
43-
// relabeledLabels contains the final labels after Prometheus relabeling processing.
44-
relabeledLabels labels.Labels
45-
CollectorName string
46-
hash ItemHash
51+
JobName string
52+
TargetURL string
53+
Labels labels.Labels
54+
CollectorName string
55+
hash ItemHash
4756
}
4857

4958
type ItemOption func(*Item)
5059

51-
func WithRelabeledLabels(lbs labels.Labels) ItemOption {
60+
// WithHash sets a precomputed hash on the item.
61+
// Use this when the hash has been computed during relabeling to avoid recomputation.
62+
func WithHash(hash ItemHash) ItemOption {
5263
return func(i *Item) {
53-
// In Prometheus, labels with the MetaLabelPrefix are discarded after relabeling, which means they are not used in hash calculation.
54-
// For details, see https://github.com/prometheus/prometheus/blob/e6cfa720fbe6280153fab13090a483dbd40bece3/scrape/target.go#L534.
55-
writeIndex := 0
56-
relabeledLabels := make(labels.Labels, len(lbs))
57-
for _, l := range lbs {
58-
if !strings.HasPrefix(l.Name, model.MetaLabelPrefix) {
59-
relabeledLabels[writeIndex] = l
60-
writeIndex++
61-
}
62-
}
63-
i.relabeledLabels = slices.Clip(relabeledLabels[:writeIndex])
64+
i.hash = hash
6465
}
6566
}
6667

6768
func (t *Item) Hash() ItemHash {
6869
if t.hash == 0 {
69-
t.hash = ItemHash(LabelsHashWithJobName(t.relabeledLabels, t.JobName))
70+
t.hash = ItemHash(LabelsHashWithJobName(t.Labels, t.JobName))
7071
}
7172
return t.hash
7273
}
7374

75+
// HashFromBuilder computes a hash from a labels.Builder, skipping meta labels.
76+
// This is used during relabeling to compute the hash efficiently without materializing
77+
// the filtered labels.
78+
func HashFromBuilder(builder *labels.Builder, jobName string) ItemHash {
79+
hash := hasherPool.Get().(*xxhash.Digest)
80+
hash.Reset()
81+
builder.Range(func(l labels.Label) {
82+
// Skip meta labels - they are discarded after relabeling in Prometheus.
83+
// For details, see https://github.com/prometheus/prometheus/blob/e6cfa720fbe6280153fab13090a483dbd40bece3/scrape/target.go#L534
84+
if strings.HasPrefix(l.Name, model.MetaLabelPrefix) {
85+
return
86+
}
87+
_, _ = hash.WriteString(l.Name)
88+
_, _ = hash.Write(seps)
89+
_, _ = hash.WriteString(l.Value)
90+
_, _ = hash.Write(seps)
91+
})
92+
_, _ = hash.WriteString(jobName)
93+
result := hash.Sum64()
94+
hasherPool.Put(hash)
95+
return ItemHash(result)
96+
}
97+
7498
func (t *Item) GetNodeName() string {
7599
relevantLabels := t.Labels.MatchLabels(true, relevantLabelNames...)
76100
for _, label := range nodeLabels {
@@ -95,14 +119,12 @@ func (t *Item) GetEndpointSliceName() string {
95119
// NewItem Creates a new target item.
96120
// INVARIANTS:
97121
// * Item fields must not be modified after creation.
98-
func NewItem(jobName string, targetURL string, labels labels.Labels, collectorName string, opts ...ItemOption) *Item {
122+
func NewItem(jobName string, targetURL string, itemLabels labels.Labels, collectorName string, opts ...ItemOption) *Item {
99123
item := &Item{
100-
JobName: jobName,
101-
TargetURL: targetURL,
102-
Labels: labels,
103-
// relabeledLabels defaults to original labels if WithRelabeledLabels is not specified.
104-
relabeledLabels: labels,
105-
CollectorName: collectorName,
124+
JobName: jobName,
125+
TargetURL: targetURL,
126+
Labels: itemLabels,
127+
CollectorName: collectorName,
106128
}
107129
for _, opt := range opts {
108130
opt(item)
@@ -116,10 +138,13 @@ func NewItem(jobName string, targetURL string, labels labels.Labels, collectorNa
116138
// The scrape manager adds it later. Address is already included in the labels, so it is not needed here.
117139
func LabelsHashWithJobName(ls labels.Labels, jobName string) uint64 {
118140
labelsHash := ls.Hash()
119-
labelsHashBytes := make([]byte, 8)
120-
_, _ = binary.Encode(labelsHashBytes, binary.LittleEndian, labelsHash) // nolint: errcheck // this can only fail if the buffer size is wrong
121-
hash := xxhash.New()
122-
_, _ = hash.Write(labelsHashBytes) // nolint: errcheck // xxhash.Write can't fail
123-
_, _ = hash.Write([]byte(jobName)) // nolint: errcheck // xxhash.Write can't fail
124-
return hash.Sum64()
141+
var labelsHashBytes [8]byte
142+
binary.LittleEndian.PutUint64(labelsHashBytes[:], labelsHash)
143+
hash := hasherPool.Get().(*xxhash.Digest)
144+
hash.Reset()
145+
_, _ = hash.Write(labelsHashBytes[:]) // nolint: errcheck // xxhash.Write can't fail
146+
_, _ = hash.WriteString(jobName) // nolint: errcheck // xxhash.Write can't fail
147+
result := hash.Sum64()
148+
hasherPool.Put(hash)
149+
return result
125150
}

0 commit comments

Comments
 (0)