This commit is contained in:
Łukasz Mierzwa 2025-03-05 21:34:23 +01:00 committed by GitHub
commit 19345549f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 400 additions and 40 deletions

26
cmd/prometheus/labels.go Normal file
View file

@ -0,0 +1,26 @@
// Copyright 2017 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !stringlabels
package main
import (
"log/slog"
"github.com/prometheus/prometheus/tsdb"
)
func mapCommonLabelSymbols(_ *tsdb.DB, _ *slog.Logger) error {
return nil
}

View file

@ -0,0 +1,136 @@
// Copyright 2017 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build stringlabels
package main
import (
"cmp"
"context"
"fmt"
"log/slog"
"slices"
"strings"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/tsdb"
"github.com/prometheus/prometheus/tsdb/index"
)
// countBlockSymbols reads given block index and counts how many time each string
// occurs on time series labels.
func countBlockSymbols(ctx context.Context, block *tsdb.Block) (map[string]int, error) {
names := map[string]int{}
ir, err := block.Index()
if err != nil {
return names, err
}
labelNames, err := ir.LabelNames(ctx)
if err != nil {
return names, err
}
for _, name := range labelNames {
name = strings.Clone(name)
if _, ok := names[name]; !ok {
names[name] = 0
}
values, err := ir.LabelValues(ctx, name)
if err != nil {
return names, err
}
for _, value := range values {
value = strings.Clone(value)
if _, ok := names[value]; !ok {
names[value] = 0
}
p, err := ir.Postings(ctx, name, value)
if err != nil {
return names, err
}
refs, err := index.ExpandPostings(p)
if err != nil {
return names, err
}
names[name] += len(refs)
names[value] += len(refs)
}
}
return names, ir.Close()
}
type labelCost struct {
name string
cost int
}
// selectBlockStringsToMap takes a block and returns a list of strings that are most commonly
// present on all time series.
// List is sorted starting with the most frequent strings.
func selectBlockStringsToMap(block *tsdb.Block) ([]string, error) {
names, err := countBlockSymbols(context.Background(), block)
if err != nil {
return nil, fmt.Errorf("failed to build list of common strings in block %s: %w", block.Meta().ULID, err)
}
costs := make([]labelCost, 0, len(names))
for name, count := range names {
costs = append(costs, labelCost{name: name, cost: (len(name) - 1) * count})
}
slices.SortFunc(costs, func(a, b labelCost) int {
return cmp.Compare(b.cost, a.cost)
})
mappedLabels := make([]string, 0, 256)
for i, c := range costs {
if i >= 256 {
break
}
mappedLabels = append(mappedLabels, c.name)
}
return mappedLabels, nil
}
func mapCommonLabelSymbols(db *tsdb.DB, logger *slog.Logger) error {
var block *tsdb.Block
for _, b := range db.Blocks() {
if block == nil || b.MaxTime() > block.MaxTime() {
block = b
}
}
if block == nil {
logger.Info("No tsdb blocks found, can't map common label strings")
return nil
}
logger.Info(
"Finding most common label strings in last block",
slog.String("block", block.String()),
)
mappedLabels, err := selectBlockStringsToMap(block)
if err != nil {
return err
}
logger.Info("Mapped common label strings", slog.Int("count", len(mappedLabels)))
labels.MapLabels(mappedLabels)
return nil
}

View file

@ -853,6 +853,12 @@ func main() {
cfg.web.Flags = map[string]string{} cfg.web.Flags = map[string]string{}
cfg.tsdb.PreInitFunc = func(db *tsdb.DB) {
if err = mapCommonLabelSymbols(db, logger); err != nil {
logger.Warn("Failed to map common strings in labels", slog.Any("err", err))
}
}
// Exclude kingpin default flags to expose only Prometheus ones. // Exclude kingpin default flags to expose only Prometheus ones.
boilerplateFlags := kingpin.New("", "").Version("") boilerplateFlags := kingpin.New("", "").Version("")
for _, f := range a.Model().Flags { for _, f := range a.Model().Flags {
@ -1797,6 +1803,7 @@ type tsdbOptions struct {
CompactionDelayMaxPercent int CompactionDelayMaxPercent int
EnableOverlappingCompaction bool EnableOverlappingCompaction bool
EnableOOONativeHistograms bool EnableOOONativeHistograms bool
PreInitFunc tsdb.PreInitFunc
} }
func (opts tsdbOptions) ToTSDBOptions() tsdb.Options { func (opts tsdbOptions) ToTSDBOptions() tsdb.Options {
@ -1821,6 +1828,7 @@ func (opts tsdbOptions) ToTSDBOptions() tsdb.Options {
EnableDelayedCompaction: opts.EnableDelayedCompaction, EnableDelayedCompaction: opts.EnableDelayedCompaction,
CompactionDelayMaxPercent: opts.CompactionDelayMaxPercent, CompactionDelayMaxPercent: opts.CompactionDelayMaxPercent,
EnableOverlappingCompaction: opts.EnableOverlappingCompaction, EnableOverlappingCompaction: opts.EnableOverlappingCompaction,
PreInitFunc: opts.PreInitFunc,
} }
} }

View file

@ -23,6 +23,124 @@ import (
"github.com/cespare/xxhash/v2" "github.com/cespare/xxhash/v2"
) )
var (
// List of labels that should be mapped to a single byte value.
// Obviously can't have more than 256 here.
mappedLabels = []string{}
mappedLabelIndex = map[string]byte{}
)
// MapLabels takes a list of strings that shuld use a single byte storage
// inside labels, making them use as little memory as possible.
// Since we use a single byte mapping we can only have 256 such strings.
//
// We MUST store empty string ("") as one of the values here and if you
// don't pass it into MapLabels() then it will be injected.
//
// If you pass more strings than 256 then extra strings will be ignored.
func MapLabels(names []string) {
// We must always store empty string. Push it to the front of the slice if not present.
if !slices.Contains(names, "") {
names = append([]string{""}, names...)
}
mappedLabels = make([]string, 0, 256)
mappedLabelIndex = make(map[string]byte, 256)
for i, name := range names {
if i >= 256 {
break
}
mappedLabels = append(mappedLabels, name)
mappedLabelIndex[name] = byte(i)
}
}
func init() {
names := []string{
// Empty string, this must be present here.
"",
// These label names are always present on every time series.
MetricName,
InstanceName,
"job",
// Common label names.
BucketLabel,
"code",
"handler",
"quantile",
// Meta metric names injected by Prometheus itself.
"scrape_body_size_bytes",
"scrape_duration_seconds",
"scrape_sample_limit",
"scrape_samples_post_metric_relabeling",
"scrape_samples_scraped",
"scrape_series_added",
"scrape_timeout_seconds",
// Common metric names from client libraries.
"process_cpu_seconds_total",
"process_max_fds",
"process_network_receive_bytes_total",
"process_network_transmit_bytes_total",
"process_open_fds",
"process_resident_memory_bytes",
"process_start_time_seconds ",
"process_virtual_memory_bytes",
"process_virtual_memory_max_bytes",
// client_go specific metrics
"go_gc_heap_frees_by_size_bytes_bucket",
"go_gc_heap_allocs_by_size_bytes_bucket",
"net_conntrack_dialer_conn_failed_total",
"go_sched_pauses_total_other_seconds_bucket",
"go_sched_pauses_total_gc_seconds_bucket",
"go_sched_pauses_stopping_other_seconds_bucket",
"go_sched_pauses_stopping_gc_seconds_bucket",
"go_sched_latencies_seconds_bucket",
"go_gc_pauses_seconds_bucket",
"go_gc_duration_seconds",
// node_exporter metrics
"node_cpu_seconds_total",
"node_scrape_collector_success",
"node_scrape_collector_duration_seconds",
"node_cpu_scaling_governor",
"node_cpu_guest_seconds_total",
"node_hwmon_temp_celsius",
"node_hwmon_sensor_label",
"node_hwmon_temp_max_celsius",
"node_cooling_device_max_state",
"node_cooling_device_cur_state",
"node_softnet_times_squeezed_total",
"node_softnet_received_rps_total",
"node_softnet_processed_total",
"node_softnet_flow_limit_count_total",
"node_softnet_dropped_total",
"node_softnet_cpu_collision_total",
"node_softnet_backlog_len",
"node_schedstat_waiting_seconds_total",
"node_schedstat_timeslices_total",
"node_schedstat_running_seconds_total",
"node_cpu_scaling_frequency_min_hertz",
"node_cpu_scaling_frequency_max_hertz",
"node_cpu_scaling_frequency_hertz",
"node_cpu_frequency_min_hertz",
"node_cpu_frequency_max_hertz",
"node_hwmon_temp_crit_celsius",
"node_hwmon_temp_crit_alarm_celsius",
"node_cpu_core_throttles_total",
"node_thermal_zone_temp",
"node_hwmon_temp_min_celsius",
"node_hwmon_chip_names",
"node_filesystem_readonly",
"node_filesystem_device_error",
"node_filesystem_size_bytes",
"node_filesystem_free_bytes",
"node_filesystem_files_free",
"node_filesystem_files",
"node_filesystem_avail_bytes",
}
MapLabels(names)
}
// Labels is implemented by a single flat string holding name/value pairs. // Labels is implemented by a single flat string holding name/value pairs.
// Each name and value is preceded by its length in varint encoding. // Each name and value is preceded by its length in varint encoding.
// Names are in order. // Names are in order.
@ -30,12 +148,15 @@ type Labels struct {
data string data string
} }
func decodeSize(data string, index int) (int, int) { func decodeSize(data string, index int) (int, int, bool) {
// Fast-path for common case of a single byte, value 0..127. // Fast-path for common case of a single byte, value 0..127.
b := data[index] b := data[index]
index++ index++
if b == 0x0 {
return 1, index, true
}
if b < 0x80 { if b < 0x80 {
return int(b), index return int(b), index, false
} }
size := int(b & 0x7F) size := int(b & 0x7F)
for shift := uint(7); ; shift += 7 { for shift := uint(7); ; shift += 7 {
@ -48,15 +169,27 @@ func decodeSize(data string, index int) (int, int) {
break break
} }
} }
return size, index return size, index, false
} }
func decodeString(data string, index int) (string, int) { func decodeString(data string, index int) (string, int) {
var size int var size int
size, index = decodeSize(data, index) var mapped bool
size, index, mapped = decodeSize(data, index)
if mapped {
b := data[index]
return mappedLabels[int(b)], index + size
}
return data[index : index+size], index + size return data[index : index+size], index + size
} }
func encodeShortString(s string) (int, byte) {
if i, ok := mappedLabelIndex[s]; ok {
return 0, i
}
return len(s), 0
}
// Bytes returns ls as a byte slice. // Bytes returns ls as a byte slice.
// It uses non-printing characters and so should not be used for printing. // It uses non-printing characters and so should not be used for printing.
func (ls Labels) Bytes(buf []byte) []byte { func (ls Labels) Bytes(buf []byte) []byte {
@ -197,11 +330,24 @@ func (ls Labels) Get(name string) string {
return "" // Prometheus does not store blank label names. return "" // Prometheus does not store blank label names.
} }
for i := 0; i < len(ls.data); { for i := 0; i < len(ls.data); {
var size int var size, next int
size, i = decodeSize(ls.data, i) var mapped bool
var lName, lValue string
size, next, mapped = decodeSize(ls.data, i) // Read the key index and size.
if mapped { // Key is a mapped string, so decode it fully and move i to the value index.
lName, i = decodeString(ls.data, i)
if lName == name {
lValue, _ = decodeString(ls.data, i)
return lValue
}
if lName[0] > name[0] { // Stop looking if we've gone past.
break
}
} else { // Value is stored raw in the data string.
i = next // Move index to the start of the key string.
if ls.data[i] == name[0] { if ls.data[i] == name[0] {
lName := ls.data[i : i+size] lName = ls.data[i : i+size]
i += size i += size // We got the key string, move the index to the start of the value.
if lName == name { if lName == name {
lValue, _ := decodeString(ls.data, i) lValue, _ := decodeString(ls.data, i)
return lValue return lValue
@ -212,8 +358,9 @@ func (ls Labels) Get(name string) string {
} }
i += size i += size
} }
size, i = decodeSize(ls.data, i) }
i += size size, i, _ = decodeSize(ls.data, i) // Read the value index and size.
i += size // move the index past the value so we can read the next key.
} }
return "" return ""
} }
@ -224,11 +371,22 @@ func (ls Labels) Has(name string) bool {
return false // Prometheus does not store blank label names. return false // Prometheus does not store blank label names.
} }
for i := 0; i < len(ls.data); { for i := 0; i < len(ls.data); {
var size int var size, next int
size, i = decodeSize(ls.data, i) var mapped bool
var lName string
size, next, mapped = decodeSize(ls.data, i)
if mapped {
lName, i = decodeString(ls.data, i)
if lName == name {
return true
}
if lName[0] > name[0] { // Stop looking if we've gone past.
break
}
} else {
i = next
if ls.data[i] == name[0] { if ls.data[i] == name[0] {
lName := ls.data[i : i+size] lName = ls.data[i : i+size]
i += size
if lName == name { if lName == name {
return true return true
} }
@ -236,9 +394,10 @@ func (ls Labels) Has(name string) bool {
if ls.data[i] > name[0] { // Stop looking if we've gone past. if ls.data[i] > name[0] { // Stop looking if we've gone past.
break break
} }
}
i += size i += size
} }
size, i = decodeSize(ls.data, i) size, i, _ = decodeSize(ls.data, i)
i += size i += size
} }
return false return false
@ -356,10 +515,10 @@ func Compare(a, b Labels) int {
// Now we know that there is some difference before the end of a and b. // Now we know that there is some difference before the end of a and b.
// Go back through the fields and find which field that difference is in. // Go back through the fields and find which field that difference is in.
firstCharDifferent, i := i, 0 firstCharDifferent, i := i, 0
size, nextI := decodeSize(a.data, i) size, nextI, _ := decodeSize(a.data, i)
for nextI+size <= firstCharDifferent { for nextI+size <= firstCharDifferent {
i = nextI + size i = nextI + size
size, nextI = decodeSize(a.data, i) size, nextI, _ = decodeSize(a.data, i)
} }
// Difference is inside this entry. // Difference is inside this entry.
aStr, _ := decodeString(a.data, i) aStr, _ := decodeString(a.data, i)
@ -385,9 +544,9 @@ func (ls Labels) Len() int {
count := 0 count := 0
for i := 0; i < len(ls.data); { for i := 0; i < len(ls.data); {
var size int var size int
size, i = decodeSize(ls.data, i) size, i, _ = decodeSize(ls.data, i)
i += size i += size
size, i = decodeSize(ls.data, i) size, i, _ = decodeSize(ls.data, i)
i += size i += size
count++ count++
} }
@ -422,7 +581,7 @@ func (ls Labels) Validate(f func(l Label) error) error {
func (ls Labels) DropMetricName() Labels { func (ls Labels) DropMetricName() Labels {
for i := 0; i < len(ls.data); { for i := 0; i < len(ls.data); {
lName, i2 := decodeString(ls.data, i) lName, i2 := decodeString(ls.data, i)
size, i2 := decodeSize(ls.data, i2) size, i2, _ := decodeSize(ls.data, i2)
i2 += size i2 += size
if lName == MetricName { if lName == MetricName {
if i == 0 { // Make common case fast with no allocations. if i == 0 { // Make common case fast with no allocations.
@ -518,12 +677,27 @@ func marshalLabelsToSizedBuffer(lbls []Label, data []byte) int {
func marshalLabelToSizedBuffer(m *Label, data []byte) int { func marshalLabelToSizedBuffer(m *Label, data []byte) int {
i := len(data) i := len(data)
i -= len(m.Value)
size, b := encodeShortString(m.Value)
if size == 0 {
i--
data[i] = b
} else {
i -= size
copy(data[i:], m.Value) copy(data[i:], m.Value)
i = encodeSize(data, i, len(m.Value)) }
i -= len(m.Name) i = encodeSize(data, i, size)
size, b = encodeShortString(m.Name)
if size == 0 {
i--
data[i] = b
} else {
i -= size
copy(data[i:], m.Name) copy(data[i:], m.Name)
i = encodeSize(data, i, len(m.Name)) }
i = encodeSize(data, i, size)
return len(data) - i return len(data) - i
} }
@ -581,9 +755,16 @@ func labelsSize(lbls []Label) (n int) {
func labelSize(m *Label) (n int) { func labelSize(m *Label) (n int) {
// strings are encoded as length followed by contents. // strings are encoded as length followed by contents.
l := len(m.Name) l, _ := encodeShortString(m.Name)
if l == 0 {
l++
}
n += l + sizeVarint(uint64(l)) n += l + sizeVarint(uint64(l))
l = len(m.Value)
l, _ = encodeShortString(m.Value)
if l == 0 {
l++
}
n += l + sizeVarint(uint64(l)) n += l + sizeVarint(uint64(l))
return n return n
} }

View file

@ -82,8 +82,8 @@ func labelsWithHashCollision() (labels.Labels, labels.Labels) {
if ls1.Hash() != ls2.Hash() { if ls1.Hash() != ls2.Hash() {
// These ones are the same when using -tags stringlabels // These ones are the same when using -tags stringlabels
ls1 = labels.FromStrings("__name__", "metric", "lbl", "HFnEaGl") ls1 = labels.FromStrings("__name__", "metric", "lbl", "D3opXYk")
ls2 = labels.FromStrings("__name__", "metric", "lbl", "RqcXatm") ls2 = labels.FromStrings("__name__", "metric", "lbl", "G1__3.m")
} }
if ls1.Hash() != ls2.Hash() { if ls1.Hash() != ls2.Hash() {

View file

@ -224,6 +224,9 @@ type Options struct {
// PostingsDecoderFactory allows users to customize postings decoders based on BlockMeta. // PostingsDecoderFactory allows users to customize postings decoders based on BlockMeta.
// By default, DefaultPostingsDecoderFactory will be used to create raw posting decoder. // By default, DefaultPostingsDecoderFactory will be used to create raw posting decoder.
PostingsDecoderFactory PostingsDecoderFactory PostingsDecoderFactory PostingsDecoderFactory
// PreInitFunc is a function that will be called before the HEAD is initialized.
PreInitFunc PreInitFunc
} }
type NewCompactorFunc func(ctx context.Context, r prometheus.Registerer, l *slog.Logger, ranges []int64, pool chunkenc.Pool, opts *Options) (Compactor, error) type NewCompactorFunc func(ctx context.Context, r prometheus.Registerer, l *slog.Logger, ranges []int64, pool chunkenc.Pool, opts *Options) (Compactor, error)
@ -234,6 +237,8 @@ type BlockQuerierFunc func(b BlockReader, mint, maxt int64) (storage.Querier, er
type BlockChunkQuerierFunc func(b BlockReader, mint, maxt int64) (storage.ChunkQuerier, error) type BlockChunkQuerierFunc func(b BlockReader, mint, maxt int64) (storage.ChunkQuerier, error)
type PreInitFunc func(*DB)
// DB handles reads and writes of time series falling into // DB handles reads and writes of time series falling into
// a hashed partition of a seriedb. // a hashed partition of a seriedb.
type DB struct { type DB struct {
@ -1011,6 +1016,10 @@ func open(dir string, l *slog.Logger, r prometheus.Registerer, opts *Options, rn
minValidTime = inOrderMaxTime minValidTime = inOrderMaxTime
} }
if db.opts.PreInitFunc != nil {
db.opts.PreInitFunc(db)
}
if initErr := db.head.Init(minValidTime); initErr != nil { if initErr := db.head.Init(minValidTime); initErr != nil {
db.head.metrics.walCorruptionsTotal.Inc() db.head.metrics.walCorruptionsTotal.Inc()
var e *errLoadWbl var e *errLoadWbl

View file

@ -6247,8 +6247,8 @@ func labelsWithHashCollision() (labels.Labels, labels.Labels) {
if ls1.Hash() != ls2.Hash() { if ls1.Hash() != ls2.Hash() {
// These ones are the same when using -tags stringlabels // These ones are the same when using -tags stringlabels
ls1 = labels.FromStrings("__name__", "metric", "lbl", "HFnEaGl") ls1 = labels.FromStrings("__name__", "metric", "lbl", "D3opXYk")
ls2 = labels.FromStrings("__name__", "metric", "lbl", "RqcXatm") ls2 = labels.FromStrings("__name__", "metric", "lbl", "G1__3.m")
} }
if ls1.Hash() != ls2.Hash() { if ls1.Hash() != ls2.Hash() {