云原生监控Prometheus对样本rate计算,出现标签重复?

0 - 本案例所涉及的知识点

云原生、微服务,带你了解大规模容器下的监控方式,通过各个案例分析,熟悉prometheus的内部原理。

涉及知识点:go prometheus

1 - 案例概要

收到用户反馈,使用grafana通过设置prometheus数据源,绘制容器的网络指标时,出现异常。

云原生监控Prometheus对样本rate计算,出现标签重复?_第1张图片
1.png

2 - 故障重现

服务端prometheus版本为2.9.2,通过访问内置UI地址 prometheus-server:9090,查询数据重现故障,错误截图如下,

云原生监控Prometheus对样本rate计算,出现标签重复?_第2张图片
2.png

通过浏览器开发者模式,获取当前具体的请求接口可表示为:

curl -X POST prometheus-server:9090/api/v1/query
Content-Type: application/x-www-form-urlencoded

query: sum(rate(container_network_receive_bytes_total{pod_name="magick-img-converter-747d899b66-lsrn6"}[1m])) by (pod_name)
time: 1563457283.943

响应的HTTP状态码422 Unprocessable Entity,错误信息:

{
    "status": "error",
    "errorType": "execution",
    "error": "vector cannot contain metrics with the same labelset"
}

3 - 初步假设

看字面意思是查询返回的样本结果集合中存在相同的标签,至于是什么标签相同,只能通过查询源码定位问题了。

4 - 理论知识

4.1 - Prometheus基础数据结构

首先熟悉Prometheus几个基础数据结构

// 一个数据点
// T表示该数据点时间(UNIX时间戳),V表示该时间点对应的样本值
type Point struct {
    T int64
    V float64
}

// 标签为一个键值对,均为字符串格式
type Label struct {
    Name, Value string
}

// 由多个标签组成的标签组
type Labels []Label

// 一个样本数据,由一个数据点与一个标签组组合而成
// 只有单一的时间点,也就是只包含一个时间点的值
type Sample struct {
    Point  Point
    Metric labels.Labels
}

// 一个序列数据,由多个数据点与一个标签组组合而成
// 由于包含多个时间点,可以反映某一事物、现象等随时间的变化状态或程度
type Series struct {
    Points []Point       `json:"values"`
    Metric labels.Labels `json:"metric"`
}

// 一个向量,由多个样本组成的数组
type Vector []Sample

// 一个矩阵,由多个序列组成的数组
type Matrix []Series

Sample与Series的区别:

  • Sample是样本:一个标签组合,只包含一个时间点以及该点的时间数据
  • Series是序列:一个标签组合,由多个时间点以及对应的数据组合而成

Vector与Matrix的区别:

  • Vector是一个向量数据结构,由多个样本组成,这些样本以时间为方向,具有相同的方向

  • Matrix是一个矩阵数据结构,由多个序列数据组成

  • Vector由不通的标签与数据点组成

  • Series由相同的标签与多个数据点组成

4.2 - 通过关键字匹配定位源码

  • 异常关键字出现的代码位置

源码文件:github.com/prometheus/prometheus/promql/engine.go

func (ev *evaluator) rangeEval(f func([]Value, *EvalNodeHelper) Vector, exprs ...Expr) Matrix {
    ... ...
    enh.ts = ts
    result := f(args, enh)
    if result.ContainsSameLabelset() {
        ev.errorf("vector cannot contain metrics with the same labelset")
    }
    enh.out = result[:0] // Reuse result vector.
    ... ...
}
func (ev *evaluator) eval(expr Expr) Value {
    ... ...
    case *Call:
    ......
            if len(ss.Points) > 0 {
                if ev.currentSamples < ev.maxSamples {
                    mat = append(mat, ss)
                    ev.currentSamples += len(ss.Points)
                } else {
                    ev.error(ErrTooManySamples(env))
                }
            }
        }

        if mat.ContainsSameLabelset() {
            ev.errorf("vector cannot contain metrics with the same labelset")
        }

        putPointSlice(points)

        return mat

    ... ...

    case *UnaryExpr:
        mat := ev.eval(e.Expr).(Matrix)
        if e.Op == ItemSUB {
            for i := range mat {
                mat[i].Metric = dropMetricName(mat[i].Metric)
                for j := range mat[i].Points {
                    mat[i].Points[j].V = -mat[i].Points[j].V
                }
            }
            if mat.ContainsSameLabelset() {
                ev.errorf("vector cannot contain metrics with the same labelset")
            }
        }
        return mat
    ... ...
}
  • 关键函数result.ContainsSameLabelset的实现源码

该函数主要用于检测在查询返回的数据中,是否存在相同的标签集合

源码文件:github.com/prometheus/prometheus/promql/value.go

// 向量类型的实现
func (vec Vector) ContainsSameLabelset() bool {
    l := make(map[uint64]struct{}, len(vec))
    for _, s := range vec {
        hash := s.Metric.Hash()
        if _, ok := l[hash]; ok {
            return true
        }
        l[hash] = struct{}{}
    }
    return false
}

// 矩阵类型的实现
func (m Matrix) ContainsSameLabelset() bool {
    l := make(map[uint64]struct{}, len(m))
    for _, ss := range m {
        hash := ss.Metric.Hash()
        if _, ok := l[hash]; ok {
            return true
        }
        l[hash] = struct{}{}
    }
    return false
}
  • 判断样本标签组唯一性,函数s.Metric.Hash()的实现

源码文件:github.com/prometheus/prometheus/pkg/labels/labels.go

func (ls Labels) Hash() uint64 {
    b := make([]byte, 0, 1024)

    // 对标签名与标签值通过'\xff'特殊字符分割,然后由xxhash类库计算
    // 结果格式如:2011129674386607102
    for _, v := range ls {
        b = append(b, v.Name...)
        b = append(b, sep)
        b = append(b, v.Value...)
        b = append(b, sep)
    }
    return xxhash.Sum64(b)
}

也就是在这计算中Vector(向量)结构或Matrix(矩阵)结构,不能存在相同的Labels(标签组)。

5 - 疑点提出

  1. Prometheus的持久存储方案是内部开发团队自实现的,是这个引起的?
  2. Prometheus本地存储设置只保留最近24h的数据,难道是这部分数据异常?
  3. 分别关闭本地与远程存储,如果均正常,难道是查询返回了相同的两条指标?
  4. 向量与矩阵里面的样本或序列数据是不能存在相同的标签组合?

6 - 实践检验

6.1 - 实验前准备

下载源码,通过以下命令可编译二进制可执行文件

cd github.com/prometheus/prometheus
go build cmd/prometheus/main.go

6.2 - 验证疑点1

由于prometheus本身不适合做长久数据存储,所以我们内部设计了一套持久存储的方案,结合配置remote_read、remote_write实现。

为了验证疑点1,可以先把prometheus.yml中的remote_read设置关闭,然后重载prometheus配置。

remote_read:
#- url: http://api.example.com/read
#  read_recent: true

在使用promQL语句查询:

云原生监控Prometheus对样本rate计算,出现标签重复?_第3张图片
4.png

停止remote_read设置之后就可以正常使用了,但这里也并不能确定就是设置remote_read的问题。

6.2 - 验证疑点2

通过关闭远程数据读取可以解决问题,那通过删除本地数据呢?通过以下步骤:

  1. 设置prometheus.yml取消原先对remote_read的注释。
  2. 关闭prometehus进程,清除本地存储目录下的所有数据,实际由storage.tsdb.path设置,如:data/*
  3. 重新启动prometheus进程。

在刚启动时候,可以正常查询出数据:

云原生监控Prometheus对样本rate计算,出现标签重复?_第4张图片
4.png

等待一会后,出现同样问题:

云原生监控Prometheus对样本rate计算,出现标签重复?_第5张图片
6.png

6.3 - 验证疑点3

在同时开启本地与远程数据存储的情况下,通过promQL查询以下语句:

container_network_receive_bytes_total{pod_name="magick-img-converter-747d899b66-lsrn6"}[1m]

接口返回内容:

云原生监控Prometheus对样本rate计算,出现标签重复?_第6张图片
7.png

确实是访问两个相同标签组的数据,但是时间不同,对这结果集使用rate函数计算,也就出现了原先的问题。

rate(container_network_receive_bytes_total{pod_name="magick-img-converter-747d899b66-lsrn6"}[1m])

6.4 - 验证疑点4

相同的标签组合是指:一组同样的标签名与标签值。这里通过更改prometheus代码,植入测试语句,获取从远程或本地的数据,对比差异,查询到底哪里导致这个问题的产生。

由以上几个疑点验证可知,问题出现在eval函数的case *Call语句中。

func (ev *evaluator) eval(expr Expr) Value {
    ... ...
    case *Call:
        ... ...
        sel := e.Args[matrixArgIndex].(*MatrixSelector)
        // 由这个函数获取数据,填充sel变量
        checkForSeriesSetExpansion(ev.ctx, sel)
        mat := make(Matrix, 0, len(sel.series)) // Output matrix.
        ... ...
        // Process all the calls for one time series at a time.
        it := storage.NewBuffer(selRange)
        for i, s := range sel.series {
            // DEBUG
            // 通过打印出当前sel中所有序列的标签组合
            fmt.Println("i:", i, "before:", ss.Labels())

            points = points[:0]
            it.Reset(s.Iterator())
            ss := Series{
                // 移除掉内部__name__这个标签
                Metric: dropMetricName(sel.series[i].Labels()),
                Points: getPointSlice(numSteps),
            }
            ... ...
            if len(ss.Points) > 0 {
                if ev.currentSamples < ev.maxSamples {
                    // DEBUG
                    // 打印出移除标签后,实际加入到mat中的数据
                    fmt.Println("i:", i, "after:", ss.Labels())

                    mat = append(mat, ss)
                    ev.currentSamples += len(ss.Points)
                } else {
                    ev.error(ErrTooManySamples(env))
                }
            }
        }

        if mat.ContainsSameLabelset() {
            ev.errorf("vector cannot contain metrics with the same labelset")
        }

        putPointSlice(points)

        return mat
    ... ...
}

编译prometheus,执行该二进制,查询语句,然后观察日志的输出:

云原生监控Prometheus对样本rate计算,出现标签重复?_第7张图片
8.png
云原生监控Prometheus对样本rate计算,出现标签重复?_第8张图片
9.png

这里可以看到,存在两个数据,在未移除标签name之前,上图存在两个name,下图只有一个name,当移除之后,他们就存在相同的标签组了。

7 - 问题定位

查询指标数据时,prometheus分别从本地存储与远程存储获取数据,然后进行合并,远程存储获取的数据相比本地多了一个name标签。

8 - 解决方案

8.1 - 方案1

修复远程存储返回多余的name标签。

8.2 - 方案2

修改prometheus代码,如果存在重复的name标签键值,则想办法去重。

9 - 总结讨论

通过方案1,我们就解决了这个问题,同时也继续跟踪prometheus源码,能否为它提交PR,增强健壮性。

如存在疑问点,欢迎留言讨论,本人关注云原生生态,如果您也喜欢,欢迎关注我,一起学习,共同进步。

你可能感兴趣的:(云原生监控Prometheus对样本rate计算,出现标签重复?)