原文首发于 kuricat.com
现象
在使用 PromQL 简单查询 一个 Summary 类型的 Metrics 时(假设 Metrics 名字 A
), 出现了一个比较诡异的现象.
首先 , 我们不使用函数来进行查询, 仅仅列出原始结果:
// 对应的 PromQL
A{quantile="0.5"}
接着, 我们对上述数据 使用 函数 avg
取 平均值.
// 对应的 PromQL
avg(A{quantile="0.5"})
这时, 诡异的事情就出现了, 按照常理, 如果这时我们使用 avg 函数对 A
求平均值 (avg(A{quantile="0.5"})
), 应该会计算得到 Metrics A 的平均值曲线, 可是在使用 avg 函数后, 却发现图像上无数据.
接着通过查看 Prometheus 的原始查询响应, 发现实际上是有数据返回, 但是由于全部都是 NaN
, 所以 Graph 上没有图像.
当时笔者以为是 有一些数据是不连续的, 导致影响了 avg 函数
的执行, 遂开始尝试使用 sum
, 等函数来验证这个问题是由数据不连续所引发的. 结果发现这些函数都出现了上述问题. 但比较意外的是 max
和 min
函数却都表现正常.
解决方案
这里先贴上 解决方案, 可以通过添加一个对 A 向量的筛选, 去掉 Value 为 NaN 的样本, 示例代码如下所示:
// before
avg(A{label="label"})
// after
avg(A{label="label"} > 0)
// ^^^^^^ 注意这里
即可让结果按照预期显示.
原因
这个问题是在最近出现的, 当时我们在对 PHP 侧的 Metrics 的 生产和 收集做一些改造, 将 Metrics 的生产和 收集 转由 Golang 中间件
负责. 但在我们改造完成后, 发现 Grafana 中, 部分和 Summary 有关的 Dashboard 无法正常展示图像.
那么接着就是定位问题, 通过直接查看 Grafana 对 Prometheus 的查询的响应体, 发现响应在正常返回, 但都是 NaN, 那说明问题并不是由 PromQL 表达式错误引起的.
那么既然不是 PromQL 表达式的问题, 那 异常原因 有没有可能与 PromQL 的处理机制
以及 这里出现的 NaN 响应
有关? 在笔者印象中, 在 Prometheus/client_golang
里只有 Summary 指标 会有 NaN 相关的返回, 所以我们接着到 prometheus/client_golang Issue 列表中 搜索相关的关键词, 还就真的找到两个相关的 Issue #860 和 #8860 , 通过 阅读 Issue , 就找到了解决方案.
这里的 解决方案其实很简单. 以上面的代码段为例, 我们只需要在 A
向量 的后面 加上一个 >0
来筛掉 值为 NaN 的样本即可. 但由于 笔者通常用 写函数调用的思维 在写 PromQL,并没有很快的意识到 这里的A
是一个向量而不是一个普通的变量, 遂才写了错误的表达式造成来这次的问题.
NaN 数据的 产生
NaN 的产生 通常出现在 prometheus/client_golang 的 Summary 实现上。
在 Summary 的 配置中, 有一个 叫 MaxAge
的配置项 用于表示 刚刚收到的样本 将会在 该 Summary 的 样本集中保存多久. 在超过 MaxAge 时间后(并且无新数据加入),prometheus/client_golang
会将 Summary 的各个 quantile(分位数)
的 Value 置为 NaN, 类似下面这样.
// before
a_metrics{quantile="0.2"} 181
a_metrics{quantile="0.5"} 181
a_metrics{quantile="0.9"} 181
a_metrics{quantile="0.99"} 181
a_metrics_sum 3.246959e+06
a_metrics_count 17939
// after (after maxAge)
a_metrics{quantile="0.2"} NaN
a_metrics{quantile="0.5"} NaN
a_metrics{quantile="0.9"} NaN
a_metrics{quantile="0.99"} NaN
a_metrics_sum 3.246959e+06
a_metrics_count 17939
接着这些数据会被 Prometheus 采集, 然后储存在 Prometheus 中.
为什么不设置成 0 , 而设置成 NaN
那这样就引出另一个问题, 为什么要把 过时数据的值 设置成 NaN 而不设置成 0 呢? 如果设置成 0 就不会影响 PromQL 的运算了.
关于这一点, 其实 已经由类似的讨论在 Issue 中, #85 (2015) ,
-
beorn7
: 在很长时间之后, 我们希望 Summary 能观察到衰减在图像上
而不是冻结最后一个的样本的值在图像上
, 如果 Summary 的图像展示冻结是不准确的, 我们甚至没有办法根据这个冻结的结果判断现在服务是否正常. 下面两种方式都能获取到 图像的衰减, 但是我(beorn7)不确定哪一种会更好 -
Summary 将所有 的分位数 设置为 0 并返回, 但这是一种错误! 因为你的 Summary 其实此时根本没有值, 而你在图像上看起来却是 0 , 这可能导致一些错误的判断
-
NaN 会更加忠实的反映你的 Summary 的当前状态, 但这大概很容易会让人感到疑惑,
-
juliusv
: 0 值是一个错误的指标, 它根本不能反映现实, 只是告诉你 “噢, 那个很好”,冻结最后一个值在图像上
的方案我觉得可能没问题, 但是 NaN 似乎是更加正确的方案, 我更加推荐它 -
关于这个 Issue 后续还有
哪些值需要被设置成 NaN
的讨论, 如果有兴趣可以直接去 Issue 里了解, 这里就不过度展开
NaN 数据为什么会干扰到 PromQL 的查询
NaN 的特性
根据 IEEE 754
的标准, Golang 需要使用一个 NaN 的值来标识 not-a-number(不是一个数值)
, 例如在下面这段代码, 就会得到一个 实际上为 NaN 的返回值.
q := math.Sqrt(-1) // 对 -1 开平方 (只能对正数开平方)
fmt.Print(q) // NaN
根据 IEEE 754
, 对 NaN
进行算数运算, 实质上是在和一个 不可表示的值(它甚至可能不是一个值, 是一个范围) 做运算, 所以结果将会得到一个 NaN
, 就像下面的例子
NaN := math.NaN()
fmt.Println(NaN + 2) // NaN
fmt.Println(NaN - 2) // NaN
fmt.Println(NaN * 2) // NaN
fmt.Println(NaN / 2) // NaN
fmt.Println(math.Pow(NaN, 10)) // NaN
Prometheus 的函数处理逻辑
既然明白了 NaN
的特性, 接着我们来看 sum
函数 和 avg
函数 的源码是如何处理运行和处理数据的, 想直接跳往文件位置的读者可以点击这两个链接直接跳去相关文件: sum / avg
// sum
func funcSumOverTime(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) Vector {
return aggrOverTime(vals, enh, func(values []Point) float64 {
var sum float64
for _, v := range values {
sum += v.V // 这里可以看到, 直接累加全部的收集到的 Metrics 的值,
}
return sum
})
}
// avg
func funcAvgOverTime(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) Vector {
return aggrOverTime(vals, enh, func(values []Point) float64 {
var mean, count float64
for _, v := range values {
count++
mean += (v.V - mean) / count // 这里也是类似, 把和现在差值直接加上去
}
return mean
})
}
通过上面的代码我们可以得知, 如果 Metrics 的值里面混有 NaN 的值, 那么会直接污染整个结果, 导致输出的结果就像上面那样, 全部都是 NaN. rate 和 stddev 函数同理, 这里就不一一介绍了.
但问题又来了, 为什么 Max 和 Min 函数不受影响 ?
max 和 min 函数不受影响的原因
同样的, 我们还是直奔源码, 你也可以点这两个链接直接跳往文件位置: max / min
// Max
func funcMaxOverTime(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) Vector {
return aggrOverTime(vals, enh, func(values []Point) float64 {
max := values[0].V
for _, v := range values {
if v.V > max || math.IsNaN(max) { // 过滤 NaN
max = v.V
}
}
return max
})
}
// Min
func funcMinOverTime(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) Vector {
return aggrOverTime(vals, enh, func(values []Point) float64 {
min := values[0].V
for _, v := range values {
if v.V < min || math.IsNaN(min) { // 过滤 NaN
min = v.V
}
}
return min
})
}
我们可以看到这里 Max 和 Min 的实现代码中, 使用 IsNaN 函数
对 值进行了一个判断, 过滤掉了病毒式的 NaN 感染, 所以 Max 和 Min 函数仍然可以表现正常. 同时还发现了一个 Issue #4385 , 提交 Issue 的用户 jacksontj
表示 NaN 干扰了他的 PromQL 的 运算结果, 并提了一个 PR 来修正这个错误.
总结
至此, 这个问题的产生原因
和 解决方案
就介绍完毕了. 实际上, 笔者被这个问题大约折磨了有两个星期, 一开始笔者觉得是 函数用法不对 或者 数据有间断 以及 多余的标签干扰, 然后一直尝试各种各样的 PromQL 函数, 比如 label_replace
.
结果到最后还是没有解决问题, Google 了很久也无果(毕竟如果连问题原因都没定位到, Google 也无能为力). 直到有一天 点开 Prometheus 的原始查询响应 发现返回了很多 NaN, 开始怀疑是不是 NaN 的问题, 遂开始 Google 相关的关键字才找到解决方法.
这里又回到 Leader 念叨了很多遍的话, 定位问题的 方式很关键 , 这次这个问题能拖这么久就是压根没定位到问题, 然后乱试……
最后总算是解决了这个问题, 可喜可贺, 所以这里记录下来, 供大家参考
Ref
- Prometheus Docs https://prometheus.io/docs/prometheus/latest/querying/functions/
- Prometheus Issue https://github.com/prometheus/prometheus/issues/860
- Prometheus Issue https://github.com/grafana/grafana/issues/8860
- Prometheus Issue https://github.com/prometheus/prometheus/issues/4385
- Prometheus Issue https://github.com/prometheus/prometheus/pull/4386
- wikipedia https://zh.wikipedia.org/wiki/NaN
今天的文章PromQL 处理 NaN 数据 输出结果异常分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/20354.html