ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Prometheus迁移VictoriaMetrics后changes()误报根因与避坑改写

Prometheus迁移VictoriaMetrics后changes()误报根因与避坑改写 最近帮一个团队排查告警抖动发现他们从Prometheus迁移到VictoriaMetrics之后所有“配置变更检测”的规则都在频繁误报。同一个metric、同一句changes(metric[5m]) 0两个引擎跑出来的结果竟然不一样数据源完全没变。定位到最后问题出在changes函数对缺值样本的处理语义上。这个差异在指标采集稀疏、scrape偶发失败时特别容易踩到而且不迁移根本意识不到。这篇文章就围绕这个场景做一次完整复盘缺值在PromQL里到底怎么表示、changes在两个引擎里各自怎么数变化、以及生产环境里怎么改查询才能避免误判。无论你是在做Prometheus迁移、还是只想把告警写得更稳都值得看完。1. 缺值场景下 changes() 到底在数什么1.1 PromQL 官方语义按“窗口内样本”依次比较PromQL文档里changes()的定义很简单返回指定时间范围内一个序列值变化的次数。但“变化”到底怎么定义要看实现。Prometheus的changes()实现大致是把metric[window]选出来的样本按时间排好拿第一个样本作为初始基线从第二个开始逐个与上一个比较值不同就计数加一。这个逻辑背后有两层隐含意思第一窗口内至少要两个样本才有机会返回非零第二窗口外哪怕紧挨着一个样本它也不参与比较。如果窗口内恰好只有一个样本比如抓取从某个时间点断掉、到下个时间点恢复你在恢复时刻查询changes(metric[10m])窗口内其实只有一个恢复后的样本。Prometheus把这个单点当作基线没有后续样本可比直接返回0。也就是说在官方语义里“只有一个点”等于“看不出任何变化”无论这个点的值跟之前相比跳了多少。这个行为在大多数场景下是合理的它要求确凿的“窗口内两次观测值不同”才算变化。但也正是这个“保守”在VictoriaMetrics里变成了另一种处理方式。1.2 VictoriaMetrics 的差异比较基线来自窗口外VictoriaMetrics在实现聚合函数时有一套统一的rollup机制。计算每个窗口的结果之前引擎会先尝试取窗口左侧最近一个原始样本记为prevValue。这个prevValue本来是给rate()、increase()这类增量函数用的没有窗口前的值就没法算增量。但changes()也复用了这套机制于是行为出现了分叉。在VM里changes(metric[10m])会先把prevValue拿出来跟窗口内第一个样本比较不一样就计数如果窗口内完全没有样本则返回0。所以同样的场景窗口前一个样本值是1窗口内只有恢复后的一个点值为2VM会认为“从1变成2确实发生了一次变化”返回1。一个返回0一个返回1放到告警表达式里就是“不报警”和“误报警”的区别。这里我说明一下VM并不是所有情况下都比Prometheus多数一次而是多了一个“窗外信息源”。当窗外样本与窗口内首样本相同时两边结果一致只有跨过断点后值本身发生了跳变差异才会被放大。1.3 一个类比起点路牌这个差异用生活场景类比会很快理解。假设你要数一段路上“车牌号变了几次”。Prometheus只统计你走进路段之后看到的车如果整段路只看到一辆红色车它就说“看不出变化因为起点有没有车不知道”VictoriaMetrics会先看一眼路段起点处的路牌发现上一辆车是蓝色、现在是红色于是记录一次变化。两种说法都有道理一个更保守一个信息更全。但用在告警里结果可能完全不同。指标上表现最明显的场景就是序列断点之后恢复的第一个点。恢复点作为窗口内唯一样本Prometheus默认“无法判断变化”VM默认“已经变过一次”。前者可能漏报后者可能误报。理解这个起点路牌的概念后面所有坑都能串起来。2. 用实验验证Prometheus 与 VictoriaMetrics 的差异2.1 构造一条带 gap 的样本序列下面这组样本很有代表性时间上我用相对分钟表示T代表最新时刻方便你换算到自己环境里时间值T-20m1T-12m1T-10m缺T-5m2T2也就是说T-12m 到 T-5m 之间有一个空洞T-10m 这个点完全没有样本。这种pattern在真实环境里很常见一次scrape超时、一次exporter重启、一次网络抖动都会留下类似的缺口。设计这个序列时我特意把左侧样本放在T-12m而不是T-15m目的是让后面的查询窗口边界不依赖“左边界是否包含”这类实现细节实验结果更干净。下面的验证基于VictoriaMetrics单机版v1.93.x和Prometheus 2.45.x大家可以参考。不同版本可能有细微差异但核心语义在较长版本区间内是稳定的。2.2 三个查询与预期结果针对这条序列在两个引擎上跑三句几乎一样的查询全部用timeT定位到最新时刻。Q1changes(demo_metric[16m])窗口覆盖T-16m到T。窗口内样本是T-12m1、T-5m2、T2三个点。Prometheus基线取T-12m1T-5m2发生变化计数1T2与上一个相同保持1。结果1。VictoriaMetricsprev取T-20m1与T-12m1相同T-5m2变化计数1T2不变。结果1。两边一致。Q2changes(demo_metric[10m])窗口覆盖T-10m到T。窗口内样本只有T-5m2、T2两个点。Prometheus基线取T-5m2T2相同结果0。VictoriaMetricsprev取T-12m1与T-5m2不同计数1T2相同。结果1。出现明显差异。Q3changes(demo_metric[5m])在T-5m时刻查询窗口覆盖T-10m到T-5m。窗口内只有一个样本T-5m2。Prometheus单点结果0。VictoriaMetricsprev取T-12m1窗口内T-5m2不同结果1。差异更极端这就是“恢复点单样本”的典型场景。三个查询覆盖了“窗口内多样本、双样本、单样本”三种形态后两种直接把差异暴露出来。2.3 在自己环境里复现实验VictoriaMetrics里复现很简单。单机版启动后用/api/v1/import接口把历史样本直接推过去。下面的bash脚本会按当前时间推入1、1、2、2四个点TS$(( $(date %s) * 1000 )) T20$((TS - 20*60*1000)) T12$((TS - 12*60*1000)) T5$((TS - 5*60*1000)) curl -X POST http://localhost:8428/api/v1/import \ -H Content-Type: application/json \ -d [ {\metric\:{\__name__\:\demo_metric\},\timestamps\:[${T20},${T12},${T5},${TS}],\values\:[1,1,2,2]} ]导入后在同一个终端会话里继续查询curl http://localhost:8428/api/v1/query?querychanges(demo_metric[10m]) \ --data-urlencode time${TS} curl http://localhost:8428/api/v1/query?querychanges(demo_metric[5m]) \ --data-urlencode time${T5}为了排除查询缓存影响VM请求可以加nocache1参数。Prometheus侧复现稍微麻烦一点因为没有这么简单的一次性历史导入接口。两个可行的方案用promtool tsdb create-blocks-from openmetrics把带时间戳的OpenMetrics文本转成TSDB块放进存储目录后启动Prometheus查询或者开启--web.enable-remote-write-receiver用remote write推样本但需要处理protobuf编码。如果不想折腾Prometheus环境直接查看promql/functions.go里的funcChanges实现逻辑和我上面推演的一致VM侧可以扫rollup.go里changes相关实现重点看它对prevValue的引用。提示导入测试样本时时间戳单位必须是毫秒别写成秒否则查询窗口会全部落在空区间拿到一堆0反而干扰判断。2.4 实验结果解读两张表汇总一下查询窗口内样本PrometheusVictoriaMetricschanges(x[16m]) at T1, 2, 211changes(x[10m]) at T2, 201changes(x[5m]) at T-5m201核心结论就一句话窗口内的“比较基线”来源不同。Prometheus取窗口内第一个样本VM取窗口外最近一个样本。当窗口内样本越稀疏窗口外基线的影响就越容易被放大。你甚至可以把VM的行为理解为“携带上一窗口状态进入当前窗口”这和它计算rate时保持一致。3. 边界、NaN 与 lookback三个隐藏细节3.1 窗口左侧边界到底谁有资格当基线先厘清一个容易混淆的点VM不是永远比Prometheus多数一次只有当窗口前一个样本存在且与窗口内第一个样本不同时才会多计。如果窗口前样本与窗口内第一个样本相同两边结果一致。更准确的说法是Prometheus继承的是从“窗口内第一帧”开始观察VM继承的是从“窗口外前一帧”开始观察差别在于入射角。这类边界问题在增量指标上大家早就见识过了rate()在窗口左侧没有样本时结果会不同increase()也会因为窗口边界选取而出现差异。changes()只是又撞上了同一个绕不开的边界。理解了这一点以后碰到任何跨窗口聚合函数你都会有排查思路先问引擎取到了窗口外的哪些信息。在Prometheus里[10m]这类的范围选择器决定的是“哪些点进入窗口”而不是“哪些点参与比较”。进入窗口的点少参与比较的基数就少VM则额外知道窗口外一个点的值。这个信息差就是所有差异的根源。3.2 连续双样本变化与重复值还有一个容易被忽略的点当窗口前样本等于窗口内第一个样本时VM的“额外比较”不会增加计数所以很多查询两边结果相同。例如序列是1,1,1,1窗口前1窗口内第一个1VM从1开始比较后面全是1结果0Prometheus结果也是0。只有窗口前样本和窗口内首样本不同才会产生差异。所以误报往往是“断裂处的值本身发生了跳变”而不是窗口本身有变化。另外如果窗口内存在连续相同值比如[2,2,2]无论Prometheus还是VM都只会记录一次“从上一个值到2”的变化。changes()不会把重复出现算作多次变化这一点两个引擎一致。真正要小心的反而是值在窗口内来回跳变比如[1,2,1,2]两边都会数出3次这个没有歧义但告警阈值如果设成0任何一次抖动都会触发。3.3 NaN 算不算一次“变化”严格说NaN不是缺值但生产环境里经常有人用它表示“无效值”——比如除零、JSON解析失败、业务自定义的无效状态。Prometheus本身对NaN的处理有个历史遗留的坑在funcChanges里样本比较用的是!而Go语言里NaN ! NaN为真所以连续两个NaN样本会被当成“值变了”计一次。这意味着如果你真的把NaN写进了序列Prometheus的changes统计会变得反直觉。VM这边因为rollup基础层普遍会对NaN做清理和特殊对待不同小版本之间行为有过调整我不建议你只看这篇文章的结论最好用真实样本在自己的版本上验证一遍。验证方法很简单推两条值都是NaN、时间间隔很短的样本然后查changes(metric[5m])。返回0说明引擎跳过了NaN返回1说明把NaN当作普通采样值参与比较。实测NaN行为时先确认你的采集链路是否真的把NaN样本存下来了。很多抓取路径会在落地前就把NaN丢弃你其实根本没有NaN样本可查那就谈不上“参与比较”。3.4 VM 的 lookback 机制会影响这些吗VictoriaMetrics有个-search.lookback参数默认5分钟它控制即时向量查询时多久以内的样本会被当成“当前值”。严格来说它不直接改变changes()对窗口内样本的选择因为range窗口是显式给定的。但有一个间接影响VM在准备prevValue时如果窗口前样本距今太远可能受lookback逻辑影响取不到或者取到一个“展平”后的值。在默认参数下常见的断点场景都是分钟级不会触发这个问题。但如果你把-search.lookback调大或者用长跨度窗口查询稀疏序列建议多做一轮验证。VM的rollup机制本身比Prometheus信息量更大参数一改行为可能跟着变不能想当然。迁移到VM时这类边界行为应该被当成默认风险记录下来而不是遇到一次改一次。4. 生产环境中的避坑改写与告警排查4.1 五分钟判断你的引擎属于哪一派如果你不确定当前部署用的是哪套行为最笨也最可靠的办法是造一条只有两个值的序列然后精准踩在“窗口内只有一个样本”的时刻查changes。例如导入时间戳0和100秒值分别是1和2然后在time100时查changes(metric[5m])返回0Prometheus原生语义基线用窗口内首样本。返回1VictoriaMetrics语义基线用了窗口外样本。这套验证脚本建议直接放进迁移验收用例以后升级版本也能回归。我们实践里就是这么干的已经帮我们排查过两次“为什么告警行为和以前不一样”的疑问。不要依赖文档里模棱两可的措辞直接造数据验证最可靠。4.2 让告警表达更稳的三条改写路径第一条路径保持Prometheus原生语义加保护条件。查询写作changes(metric[10m]) 0 and count_over_time(metric[10m]) 1count_over_time统计窗口内样本数大于1才允许changes参与逻辑判断。这样窗口内只有单样本时无论VM返回0还是1最终结果都被压成0。适合“必须确认窗口内真有两个以上观测值才判变更”的场景也是我们最常用的规避手段。第二条路径主动填gap消除断点。VM里可以用keepLastValue结合子查询把短时间空洞先填上再数变化changes(keepLastValue(metric, 5m)[1h:1m]) 0keepLastValue的第二个参数表示向后填充的最大间隔超过这个间隔的gap不会被硬填这能避免把长时间无数据也强行补成“无变化”。子查询会让计算量上涨窗口和步长不要无脑调大生产环境先在小范围验证性能。第三条路径换个更贴语义的函数。如果关心的是计数器复位次数用resets()如果关心的是绝对值漂移直接用max_over_time(...) - min_over_time(...) 阈值这种写法。很多场景其实不需要数变化次数强行用changes反而引入它对缺值的额外解读。能少用一处边界敏感函数就少一个未来要排查的坑。4.3 告警误判排查速查表症状可能原因验证方法解决迁移到VM后changes告警更频繁VM把窗口外prev计入变化对同一时间点分别查changes和count_over_time加count_over_time保护断点恢复后必报警恢复点作为窗口内唯一样本被计数定位到恢复点时刻查changes用keepLastValue填gap或加保护条件偶发性误报无法稳定复现scrape抖动导致样本稀疏拉出该时间窗口的原始样本分布调整窗口长度使其至少包含两个样本连续NaN样本导致结果偏大引擎把NaN参与不等比较推两个NaN样本验证在采集端清洗NaN或不要依赖changes统计NaN这张表基本覆盖了我们在迁移排障中遇到的高频场景。每次遇到changes相关误报我都建议先按表里“验证方法”那一列操作不要上来就改告警阈值否则只是把敏感度调低了问题仍在。4.4 一个真实排障片段最后分享一个实际案例。某服务的配置指纹指标每2分钟抓取一次正常情况下很稳定但连续两次scrape失败后配置指纹从一个旧值跳到了新值。团队用changes(config_fingerprint[5m]) 0做变更告警。在Prometheus上如果5分钟窗口内只捞到了新值那一个样本告警不触发换到VM后这个查询变成了1告警立刻触发。排查时我们先把窗口拉长到15分钟发现两边都返回1说明“变更确实发生了”问题只在于“告警窗口到底应不应该包含这次变更”。最终确认业务只关心5分钟内发生的变更于是加了count_over_time(config_fingerprint[5m]) 1保护。这个案例里没有谁对谁错只是把口径和窗口对齐了。我个人在实际操作中的体会是changes的语义差异不是设计缺陷而是“窗口内观察”和“窗口外回溯”两种哲学的区别。只要你把比较基线这件事想清楚绝大多数误报都能提前用一条保护条件挡住。最后再分享一个小技巧做Prometheus到VictoriaMetrics的迁移之前专门写一组带gap的测试序列把changes、increase、resets这几个边界敏感的聚合函数全部跑一遍结果记录成文档。这份文档会是你未来排查告警误报时最值钱的家底。
返回列表