乐趣区

关于prometheus:promethus删除value的label

一. 概述

prometheus 拉取 exporter 中的指标进行解析时,对于 labels,若 label value=””,则会将该 label 去掉;

也就是说,对于 label value=””,不会存储到 tsdb,通过 prom API 也查问不到该 label。

二. 源码剖析

1. 原理

scape 当前,会通过两次删除,将 value=”” 的 Label 删掉:

  • 第一次:对拉取的 text 进行 label 解析后,在增加 target 的 label 时,删除 value=”” 的 label;
  • 第二次:将解析后的 labels、t/ v 数据,存储到 head 时,删除 value=”” 的 label;
// scrape/scrape.go
func (sl *scrapeLoop) append(app storage.Appender, b []byte, contentType string, ts time.Time) (total, added, seriesAdded int, err error) {
    ...
    for {
        var (
            et          textparse.Entry
            sampleAdded bool
        )
        if et, err = p.Next(); err != nil {
            if err == io.EOF {err = nil}
            break
        }
        ...
        met, tp, v := p.Series()
        ce, ok := sl.cache.get(yoloString(met))        // 查找 cache
        if !ok {
            var lset labels.Labels
            mets := p.Metric(&lset)                    // 解析原始 labels,保留到 lset
            lset = sl.sampleMutator(lset)              // 增加 target 的 label,含第一次删除的逻辑
            ...
            ref, err = app.Add(lset, t, v)             // 增加到 head,含第二次删除的逻辑
        }
    }
    ...
}

2. 第一次删除:解析 labels

scrape 当前,会进行拉取对象的文本解析;
解析 label 时,会增加 target 的 label,此时会删除 value=”” 的 label;

执行过程:

  • 首先,结构 Builder 对象,删除 value=“”的 label 由 Builder 对象实现;
  • 而后,依据 honor=true/false,解决与 target.label 的抵触;

    • 删除 value=”” 的 label 的操作,在 Builder.Set(lkey, lvalue) 中;
  • 最初,执行 relabel 操作;
// scrape/scrape.go
func mutateSampleLabels(lset labels.Labels, target *Target, honor bool, rc []*relabel.Config) labels.Labels {lb := labels.NewBuilder(lset)                     // 应用 Builder 对象进行结构

    if honor {                                        // honor=true 时,解决与 target.label 的抵触,当 labelKey 抵触时,间接应用 scrape 的 label,不顾及 target 的 label
        for _, l := range target.Labels() {if !lset.Has(l.Name) {lb.Set(l.Name, l.Value)
            }
        }
    } else {                                          // 默认 honor=false 时,解决与 target.label 的抵触,当 labelKey 抵触时,将 scrape 的 label 批改为:exported_labelKey,lableValue
        for _, l := range target.Labels() {
            // existingValue will be empty if l.Name doesn't exist.
            existingValue := lset.Get(l.Name)
            if existingValue != "" {lb.Set(model.ExportedLabelPrefix+l.Name, existingValue)
            }
            // It is now safe to set the target label.
            lb.Set(l.Name, l.Value)
        }
    }

    res := lb.Labels()                              // 输入 Builder 结构实现的 Labels

    if len(rc) > 0 {                                // 执行 relabel 操作
        res = relabel.Process(res, rc...)
    }

    return res
}

1).Builder 对象

  • base:scrape 过去的原始 labels;
  • add: 增加的 target 的 label;
  • del: 删除的 Labels;
// pkg/labels/labels.go
type Builder struct {
    base Labels
    del  []string
    add  []Label}

当初始化 Builder 时,会将 base 中 label value=”” 的增加到 Builder.Del 中:

// pkg/labels/labels.go
func NewBuilder(base Labels) *Builder {
    b := &Builder{del: make([]string, 0, 5),
        add: make([]Label, 0, 5),
    }
    b.Reset(base)
    return b
}
func (b *Builder) Reset(base Labels) {
    b.base = base
    b.del = b.del[:0]
    b.add = b.add[:0]
    for _, l := range b.base {
        if l.Value == ""{                // label value="" 时,增加到 Builder.Del
            b.del = append(b.del, l.Name)
        }
    }
}

2). 依据 Honor 解决与 target.label 的抵触

  • honor=true 时,解决与 target.label 的抵触,当 labelKey 抵触时,间接应用 scrape 的 label,不顾及 target 的 label;
  • honor=false 时,解决与 target.label 的抵触,当 labelKey 抵触时,将 scrape 的 label 批改为:exported_labelKey,lableValue;
  • 默认 honor=false;

3).Builder.Set(lkey,lvalue) 删除 value=”” 的 label

value=”” 时,将其增加到 Builder.Del 中;

// pkg/labels/labels.go
func (b *Builder) Set(n, v string) *Builder {
    if v == "" {
        // Empty labels are the same as missing labels.
        return b.Del(n)
    }
    for i, a := range b.add {
        if a.Name == n {b.add[i].Value = v
            return b
        }
    }
    b.add = append(b.add, Label{Name: n, Value: v})
    return b
}

4). 输入 Builder 结构实现的 Labels

输入时,将排除掉 Builder.Del 中的 label:

// pkg/labels/labels.go
func (b *Builder) Labels() Labels {
    ...
    // In the general case, labels are removed, modified or moved
    // rather than added.
    res := make(Labels, 0, len(b.base))
Outer:
    for _, l := range b.base {
        for _, n := range b.del {    // 去掉 b.Del
            if l.Name == n {continue Outer}
        }
        ...
        res = append(res, l)
    }
    res = append(res, b.add...)
    sort.Sort(res)

    return res
}

3. 第二次删除:

labels、t/ v 数据筹备结束后,会将 labels、t/ v 增加到 head 中:

// tsdb/head.go
func (a *headAppender) Add(lset labels.Labels, t int64, v float64) (uint64, error) {
    ...
    // Ensure no empty labels have gotten through.
    lset = lset.WithoutEmpty()        // 这里删除 value="" 的 label
    ...
    s, created, err := a.head.getOrCreate(lset.Hash(), lset)
    ...
    if created {
        a.series = append(a.series, record.RefSeries{
            Ref:    s.ref,
            Labels: lset,
        })
    }
    return s.ref, a.AddFast(s.ref, t, v)
}

删除 value=”” 的 label 的代码,其实现形式:

  • 个别的做法是,新建 Lables 对象,遍历老 labels 对象,将 value!=”” 的 label 增加进去;
  • 上面的代码应用了绝对内存高效的做法:

    • 绝大多数场景下,不存在 value=”” 的 label,间接返回原 labelset;
    • 若发现 value=”” 的 label,则新建 Labels 对象,将 value!=”” 的 label 增加进去;
// pkg/labels/labels.go
// WithoutEmpty returns the labelset without empty labels.
// May return the same labelset.
func (ls Labels) WithoutEmpty() Labels {
    for _, v := range ls {
        if v.Value != ""{                // 判断 value 是否""
            continue
        }
        els := make(Labels, 0, len(ls)-1)
        for _, v := range ls {
            if v.Value != ""{            // 判断 value 是否""
                els = append(els, v)
            }
        }
        return els
    }
    return ls
}

三. 总结

prometheus 会删除 value=”” 的 label,也不会存入 TSDB,通过以下方面实现这一点:

  • 解析实现 scrape 的 label 后,为 series 增加 target 的 label 时,会过滤掉 value=”” 的 label;

    • 通过 Builder 实现;
  • 拿到残缺的 lables 后,存入 TSDB 前,再次过滤掉 value=”” 的 label;
退出移动版