如何在Grafana云项目中修复一个重复计算的Prometheus错误

66 阅读6分钟

作为Grafana实验室的一名软件工程师,我最近参与了一个项目,该项目涉及生成PromQL查询。我们验证生成的查询的正确性的方法之一是使用一套集成测试。这些测试将针对Prometheus查询引擎的本地实例和一些测试数据执行生成的PromQL查询,并验证结果是否符合预期。

集成测试非常有用,帮助我们发现了PromQL查询生成器的问题,在一个案例中,还发现了Prometheus的一个错误。在这篇博文中,我将谈谈我们是如何发现这个bug的,是什么原因造成的,以及如何修复。

看到双重性

在给PromQL查询生成器添加了一个功能后,我决定做一个负责任的软件工程师,添加集成测试来验证新的行为。

或者,至少,我试着这样做:

由于某些原因,我写的一个新测试无法通过。查询生成器输出的是预期的PromQL查询。然而,普罗米修斯本身似乎也有问题。当评估生成的查询时,返回的结果是不正确的:

该测试做了以下的事情:

  • 创建一个名为foo的时间序列,样本值为[5, 10, 15, 20, 25],间隔时间为30秒。
  • 运行PromQL查询sum_over_time(foo[30s]) ,步长为60秒(即每隔60秒,将过去30秒的样本值相加)。

因此,我们预期的结果是:

  • 0s时5个(0s时的单个样本,值为1)
  • 60s时25个(30s时取样,数值为10,加上60s时取样,数值为15)
  • 120s时45个(90s时取样,数值为20,加上120s时取样,数值为25)。

然而,60年代的实际结果是35而不是25:

在失败的测试中尝试了不同的样本值,我开始注意到一个模式。

例如,以30秒的间隔加载样本值[0, 1, 10, 100, 1000],并使用与前一个例子相同的查询和步骤,60秒的结果是12而不是11:

该系列中的第二个样本被重复计算了。在这个例子中,30秒的样本(值为1)被重复计算了,导致60秒的结果是1+1+10=12,而不是1+10=11。

回到最初的例子,30秒的样本(数值为10)被重复计算,导致60秒的结果为10+10+15=35,而不是10+15=25。

寻找答案

现在我对这个问题有了一个大致的了解,是时候仔细看看在查询评估过程中发生了什么。带着可重复的失败测试案例、调试器和相关的背景音乐,我潜入了Prometheus代码库。

为了计算一个系列的sum_over_time() ,Prometheus查询引擎首先需要提取相关时间范围内的原始样本。它通过使用一个迭代器来做到这一点,该迭代器可以按照时间戳递增的顺序遍历一个系列的样本。查询引擎使用的两种迭代器方法是。Seek(),它将迭代器推进到第一个等于或大于指定时间戳的样本,以及Next() ,它只是移动到下一个样本。

有多种迭代器的实现方式。memSafeIterator 是用来查询内存中的样本的。它由以下部分组成。1)一个内部迭代器,读取以压缩格式存储的样本(xorIterator),和2)一个缓冲区,以未压缩的格式存储最后四个样本。如果需要最后四个样本中的任何一个,memSafeIterator 从其缓冲区中读取。否则,它只是返回其内部迭代器的结果。

在我们失败的测试中,memSafeIterator 与其内部迭代器不同步。当调用Next() ,两个迭代器都会前进一个样本。然而,当调用Seek() ,只有内部迭代器会更新其当前样本。这意味着memSafeIterator 没有正确地从缓冲区读取数据。

为了使解释更清楚,让我们通过一个(简化的)演练,看看这个迭代器错误是如何导致集成测试失败的。

开始的时候,内部的xorIteratormemSafeIterator 都是指向0s的初始样本。为了评估0s处的sum_over_time() 查询,需要读取0s处的样本值。由于该样本不在缓冲区内,所以使用内部迭代器的结果(即5):

然后在60s时评估查询。这意味着所有从30s到60s的样本值都被加起来。Seek() 方法被调用以移动到该时间范围内最早的样本(在这种情况下是30s的样本)。有了这个错误,只有内部迭代器将其当前的样本更新为30s的样本。memSafeIterator ,保持在0s的样本。

memSafeIterator 它不认为它已经达到了它的缓冲区,所以当读取当前样本时,它使用内部迭代器的结果来代替。内部迭代器返回10作为时间范围内的第一个值:

由于还没有到达时间范围的终点,Next() ,以获得下一个样本。这使得memSafeIterator ,推进到30s的样本。它检测到它已经到达缓冲区,因此停止从内部迭代器中读取。取而代之的是,再次读取30s的样本值,但这次是从缓冲区:

Next() 再次调用 ,将其推进到当前时间范围内的最后一个样本,即60s样本,其值为15。memSafeIterator

将收集到的样本值相加(10+10+15),我们得到错误的答案是35。

修复方法

memSafeIterator 是一个Go结构。它的内部迭代器是一个嵌入式字段。这意味着如果一个方法没有为 ,但为内部迭代器定义了,内部迭代器的方法将被调用。memSafeIterator

在这种情况下,memSafeIterator 没有一个Seek() 方法,但它的内部迭代器有,所以调用memSafeIterator.Seek() 只会导致内部迭代器执行其Seek() 方法并推进其当前样本。

为了完成调查,我给memSafeIterator 添加了一个Seek() 方法,该方法正确地更新了它的当前样本,并将该修正提交给 Prometheus,最后,检查了我们的集成测试现在是否成功。

总结

自从我9个月前加入Grafana实验室以来,我一直在研究指标摄取和查询翻译,了解了几个监控系统,并开始做出开源贡献。重复计算的Prometheus bug只是我遇到的有趣问题的一个例子。