Prometheus 计数器重置实验:为什么先聚合会把 145 算成 285
进程重启以后,累计完成数从 120 变成 5。把相邻采样值直接相减,会得到负数;把负数改成零,又会丢掉重启后的五次完成。更隐蔽的错误发生在聚合顺序上:两个实例各自都正常上报,先加总再计算增量,却能把逐实例计算的145放大成285。
先固定一组能够重放的输入。实例 A 每分钟的样本是 100 110 120 5 15 25,实例 B 是 100 120 140 160 180 200。A 中途出现一次计数下降,B 始终递增。这里的数字是测试夹具,不是生产采样,也不代表实际重启恰好发生在某一分钟。
把 145 和 285 都写成可执行断言
实验固定使用 promtool 2.55.1。它随同对应的 Prometheus 发行包提供,不需要启动监控服务、创建数据库或接入真实应用。确认本机命令版本后,把下面文件保存为 counter.test.yml。
rule_files: []
evaluation_interval: 1m
tests:
- name: counter_reset
interval: 1m
input_series:
- series: 'jobs_total{instance="a"}'
values: '100 110 120 5 15 25'
- series: 'jobs_total{instance="b"}'
values: '100 120 140 160 180 200'
promql_expr_test:
- expr: resets(jobs_total{instance="a"}[5m])
eval_time: 5m
exp_samples:
- labels: '{instance="a"}'
value: 1
- expr: increase(jobs_total{instance="a"}[5m])
eval_time: 5m
exp_samples:
- labels: '{instance="a"}'
value: 45
- expr: rate(jobs_total{instance="a"}[5m])
eval_time: 5m
exp_samples:
- labels: '{instance="a"}'
value: 0.15000000000000002
- expr: sum(increase(jobs_total[5m]))
eval_time: 5m
exp_samples:
- labels: '{}'
value: 145
- expr: increase((sum(jobs_total))[5m:1m])
eval_time: 5m
exp_samples:
- labels: '{}'
value: 285
运行:
promtool --version
promtool test rules counter.test.yml
本次在该版本上运行通过,末尾输出 SUCCESS。文件一共检查五个结果:A 被识别到一次重置、A 的窗口增量为 45、对应每秒速率约为 0.15、两实例先求增量再相加得到 145、先相加再求增量则得到 285。速率期望值保留了这个版本实际使用的浮点表示,尾部的小数差异没有改变其每秒约 0.15 的含义。
测试文件没有录制规则,所以 rule_files 是空列表。测试中的样本间隔是一个分钟,查询在第五分钟求值;文件中的窗口与样本时间相互配合,145 和 285 是这组输入下的结果。随意删掉一个起点、把窗口改成四分钟,再要求同一个整数通过,已经换了一道题。
这组期望还依赖明确的版本边界。Prometheus 3.0 将范围选择器的左端点改为不包含、右端点仍包含;本夹具在第五分钟查询五分钟窗口,恰好有一个样本落在左边界。升级后应使用目标版本重新运行并核对预期,不能把2.55.1的45、145和285直接当成所有版本的通用答案。计数重置应在聚合之前处理的原则,与这组固定窗口的具体数值,要分别保留。
聚合为什么把一次重置放大了
先看 A 自己。100 到 110 增加 10,110 到 120 再增加 10;计数器下降到 5 时,重置修正把新周期中已经出现的 5 保留下来;随后再增加 10 和 10,总计 45。B 从 100 增加到 200,贡献 100。两份增量相加是 145,仍保留了各自计数周期的身份。
如果先把每分钟的样本相加,得到的序列变成 200 230 260 165 195 225。原本只属于 A 的重置,现在表现为“整个聚合计数器”从 260 下降到 165。修正规则只能看见这一条合成序列,它无法从 165 中拆出 A 的 5 和 B 的 160。于是新周期的 165 被整体当成重置后的累计量,得到 285。
问题不在于 sum 或 increase 哪个函数算错,而在于聚合之前就丢掉了重置归属。两个实例可能在不同时间启动、在不同位置清零,没有一个共同的累计周期。业务上能相加的完成量,并不意味着它们的累计样本适合先合并成一条新计数器。
因此,多实例完成速率应保留原序列计算速率,再汇总:
sum by (job) (rate(jobs_total[5m]))
上面的业务查询假定原指标确实带有 job 标签;测试夹具只保留 instance,不额外制造一个不存在的标签。若需要按队列、地区或业务类型观察,就在聚合结果中保留对应的有限维度。不要为了让表达式变短,先把每台进程的周期信息揉到一起,再对结果套一个时间窗口。

重置修正不能恢复没有被采到的事件
重置发生前,A 可能还从 120 增加到了 128,只是下次抓取之前进程退出。抓取到的序列仍然是 120、5,监控系统不知道丢失的那 8 次。increase 能根据已有样本修正下降,不能把没有进入采样记录的事件还原出来。要核对精确的业务账目,仍需使用对应的业务记录,不能把监控窗口估算当作账本。
另一个限制是下降本身的解释。resets 在这里把相邻浮点样本的下降识别为重置,这不等于确认发生了一次操作系统重启。应用主动清零、更换数据来源、错误地把非累计指标命名成计数器,都可能制造同样的外观。遇到重置异常增多,应该再查进程启动时间、部署事件和指标实现,不能只凭一条下降曲线给重启次数下结论。
标签改变也需要单独看。若新进程换了 instance,对时序库来说可能是旧序列结束、新序列开始,原序列没有出现可见的下降。为观察服务总量做有限标签聚合是正常需求,但在原始计算阶段仍应按各自序列处理。把实例标识复用、重写或随机化,会进一步影响查询能看到的连续性。
窗口内出现小数并不自动说明埋点错了
实际抓取很难与查询窗口完美对齐。increase 会把已有采样跨度外推到窗口边界,因而累计值只按整数增加,窗口增量也可能出现小数。测试里规整的一分钟采样只是为了让重置与聚合错误更清楚,不能据此要求所有生产查询都返回整数。
把所有增量强制取整会掩盖估算性质,也会给小流量指标带来额外偏差。图表展示可以选择适合人的精度;告警表达式和后续计算则应保留原始结果,并明确自己判断的是窗口平均行为。rate 的单位是每秒,increase 的单位是该窗口的估计增量,不要把每秒数再标成五分钟总数。
同样,不应拿 rate 去处理可正常上下波动的库存、温度或当前队列长度。那些下降是业务状态变化,不是累计周期重置。先确认指标本身记录什么,再选函数,比看到曲线为负就给它套一个重置修正更可靠。
让错误的查询留在回归文件里
最后一项断言故意保留 increase((sum(jobs_total))[5m:1m]) 的 285。它不是推荐表达式,而是一份可执行反例:后续重构查询、调整录制规则时,可以明确看见两种顺序的差别。若删除了实例 A 的重置样本,两种表达式在某些输入上可能又变得接近,错误便很容易被正常运行的样本遮住。
维护监控规则时,至少要保留有重置、无重置和没有足够样本的不同输入。最后一种情况可能没有速率结果,不能因为面板空白就补成“完成了零次”。测试夹具能够隔离表达式语义;真实环境还需要核对抓取间隔、丢点、标签生命周期和采集是否可用。遇到增量突刺,先在原始实例序列中找到下降,再核对聚合顺序,通常比直接调高告警阈值更容易定位。