流量已经停了,监控曲线为什么还在往下滑?

一次突发流量,经过一分钟和五分钟的 rate 窗口,会变成两种形状。看懂计数器、滑动窗口与采样间隔,才知道监控图上的“现在”究竟包含多少过去。

流量已经停了,监控曲线为什么还在往下滑?

停止压测之后,终端已经不再发请求,监控上的请求速率却还挂在半空,过了一会儿才慢慢落下去。再把面板从最近十五分钟拉到最近六小时,刚才那个尖峰竟然又矮了一截。

遇到这种画面,我会先看查询里的方括号。里面那个 [5m] 很容易被当成无关紧要的默认值,却决定了屏幕上的“现在”究竟混着多少过去。

如果曲线来自 rate(),一个点说的通常不是这一瞬间发生了多少事,而是回看一段时间后,估算出的平均每秒速率。它可以及时拿到最新数据,同时对变化表现得很迟钝。这两件事并不矛盾。

先把里程表和速度表分开

假设有一个小服务,指标叫 demo_requests_total。每处理一个请求就加一,运行久了,数字从几千变成几万。它像汽车的里程表:累计走了多少路,不是此刻开得多快。

这种只能增加、重启时可能归零的指标叫计数器。正在排队的任务数、当前温度则可以正常升降,属于另一类指标。把后者直接塞进 rate(),下降可能被误当成计数器重置,结果就失去了原本想表达的含义。指标类型的区别可以对照 Prometheus 官方说明。

速度要靠两次读数之间的变化来算。假设一分钟前累计处理一千次,现在处理到七千次,这一分钟增加六千次,平均就是每秒一百次。单看“七千”没办法知道这个速度;单看现在也没办法知道过去一分钟发生了什么。

实际监控还有采样这一层。服务可以持续更新计数器,Prometheus 却只在抓取时拿到读数。每十五秒抓一次,就像每隔一段时间给里程表拍张照片。两张照片之间发生的事情,被折进了一个差值里。

下面这条查询给每条时间序列算最近五分钟的每秒速率:

rate(demo_requests_total[5m])

别把它硬背成“最后一个数减第一个数再除以三百”。真实算法还会处理可见的计数器重置,并对窗口边界做外推,采样点也未必恰好落在两端。这里先用理想的连续信号讲清窗口形状;后面的数字都是教学示意,不是某套系统的实测结果。

五分钟窗口,装着五分钟的历史

假设这个服务原本没有流量,从十二点整开始,稳定收到每秒一百次请求,持续足够长时间。

在十二点零一分向前看五分钟,只有最后一分钟有流量,前面四分钟都是零。把新增请求摊到整个窗口,平均每秒约二十次。十二点零二分,有流量的部分占两分钟,平均约四十次。直到五分钟过去,窗口里全部换成新状态,曲线才走到一百。

这不是请求花了五分钟才抵达监控系统,而是你主动问了一个带历史的问题。也不要把五分钟窗口理解成固定延迟五分钟:变化一进入窗口就会开始影响结果,只是旧状态要逐步退出,整条曲线并非简单向右平移。

再假设十二点十分流量突然停止。十一分时,回看的五分钟里仍然装着四分钟的旧流量;十二分还装着三分钟。旧数据一点点离开窗口,平均值才一点点下降。真实流量像直角台阶,窗口平均像斜坡。

三个五分钟窗口中,旧流量所占的时长依次减少;每格一分钟,数值为理想连续信号示意

我喜欢把它想成一把向过去伸出去的尺子。尺子右端跟着当前时间走,尺子覆盖到的所有历史都参与回答。你盯着的是尺子右端的时间标签,计算用的却是整把尺子下面的东西。

这个区别直接影响排障判断。刚刚缩容,五分钟平均仍然偏高,不一定意味着缩容没执行;流量刚降下来,曲线还有尾巴,也不一定意味着客户端还在重试。先检查原始计数器最近是否还在增加,再讨论延迟从哪里来,往往比盯着平滑曲线猜原因更有用。

不过,不能拿“窗口效应”解释所有迟滞。如果最新样本时间本身就落后,或者抓取不断失败,那是另一件事。历史平均正常存在,数据链路也可能同时出故障,两者需要分别查。

尖峰为什么变成了一座小山

换一个更短的假设:服务只忙了一分钟,每秒一百次,然后归零。

一分钟总共增加六千次请求。放进五分钟窗口,即使整段突发都被窗口包含,平均也只是每秒二十次。峰值从一百变成二十,并不表示丢掉了八成请求,而是同样的数量被摊到了更长的时间里。

更容易误解的是形状。突发结束后,只要这整段一分钟仍留在窗口里,平均值就可以保持一段平台;等窗口左端开始扫过那段流量,曲线才逐渐落下去。图上的“持续繁忙”,有时只是那一分钟还没走出回看范围。

同一分钟突发经过一分钟与五分钟平均后,峰值与尾部明显不同;所有曲线为理想连续信号示意

这解释了一个很实际的分歧:开发说尖峰到过一百,值班的人说图上明明最多二十。两个人可能都没看错,只是一个在谈短时速率,另一个在谈五分钟平均。

所以我不愿意只在面板标题里写一个含糊的“QPS”。如果这个数会被用来比较压测结果,至少应该同时看见统计窗口。每秒是输出单位,一分钟或五分钟是观察范围,它们不是互相替代的标注。

长窗口并没有天生更正确。容量规划需要稳定趋势,平滑会帮忙;定位几十秒的突发,平滑又可能藏起重要细节。两个面板可以并排保留:一个回答“最近整体有多忙”,另一个回答“短时间内发生了什么”。不要强迫一条线同时承担这两个任务。

把窗口改短,为什么又出现空白

既然长窗口会压低尖峰,改成 [10s] 会不会更准确?如果每十五秒才抓一次,十秒窗口里通常连两个点都凑不齐。算不出速率的时候,结果可能是缺失,面板表现为空白,而不是零。

零是“观察到了没有增长”,缺失是“没有足够信息做这次计算”。如果为了画面连续,把缺失值补成零,抓取出问题反而会看起来像业务很清闲。

短窗口还会放大样本位置的影响。同样是一阵请求,有时刚好跨过两个抓取点,有时集中在两个点之间。窗口轻轻移动一下,包含的样本组合变了,计算结果也会跳。画得更尖锐,不等于更接近每一秒的真实过程。

抓取时刻间隔十五秒;十秒窗口只包含一个样本,六十秒窗口包含多个样本,端点采用左开右闭示意

Grafana 为这类查询提供了 $__rate_interval。官方给出的计算方式会同时考虑面板间隔与抓取间隔,并让回看范围至少达到所用抓取间隔的四倍。它是给样本数量留余地的办法,不是保证任何监控都精确的魔法。抓取间隔配置得不对,变量也不会自动知道实际发生了什么。使用方法和边界见 Grafana 文档。

这里至少有三个不同的时间:服务被多久抓一次、每个结果向前看多久、图上隔多久求一个值。页面刷新频率又是第四个。把网页从三十秒刷新改成五秒,不会让服务原本十五秒一次的采样凭空多出信息。

可以用一个小练习分清它们:固定查询窗口,只修改面板显示范围,观察查询步长有没有变化;再固定显示范围,只修改窗口,观察峰值和尾部。一次只动一项,就知道变化来自哪里。

还有一种很适合用来校准直觉的场景:任务执行得很少,几分钟才完成一次。假设一分钟内恰好完成一个任务,平均每秒速率就是六十分之一。面板出现小数没有什么奇怪,它在表达频率,并不是说真的完成了零点几个任务。

如果把这种指标的单位仍写成“个”,读者很容易误会;写成“次/秒”才能对应查询的含义。想看某段时间累计完成多少,又是另一个问题,不能把速率曲线最后一个点直接当成总数。低频事件还特别容易受窗口位置影响:那一次完成刚进入窗口时有值,走出去后又归零,看起来像周期性的山包。

这时我会先确认读者要做什么决定。如果只是确认任务最近有没有完成,直接显示最近完成时间可能更有用;如果要比较长期吞吐,再看稳定的速率趋势。为了让数字看起来更平稳而不断放大窗口,最后可能把真正关心的事情也抹掉了。

放大缩小,为什么同一个峰还会变

Prometheus 的范围查询可以理解成:沿着时间轴,每隔一个步长,把同一条表达式重新算一遍。步长控制计算结果的疏密,窗口控制每次计算回看多远。这是两个维度,官方的 查询基础说明 对此有明确区分。

假设某个尖峰只持续十几秒,图上却每隔一分钟才取一个计算结果,峰顶就可能落在两个求值时刻之间。曲线连线再漂亮,也不能替代没有算出来的那个点。

另外,面板使用自动窗口变量时,拉长整个时间范围可能连查询窗口也一起改大。于是你做的表面上只是缩放,背后却同时改变了“在哪些时刻看”和“每次看多少历史”。两张截图看起来像在比较同一件事,统计口径可能已经不同。

复盘事故时,我会把查询表达式、回看窗口、查询步长和时间范围一起保存。只留下截图,过几天很难判断峰值变化到底来自业务,还是来自面板。需要严肃比较两次压测,就固定这些条件,再核对各自的采样是否完整。

这也提醒我们别从低分辨率的长周期图里硬找秒级细节。它适合发现某个小时异常,不适合证明异常只持续了几秒。发现大概位置后再缩小范围、检查原始样本,是更踏实的做法。

irate 更灵敏,也没有变成摄像机

有人会建议把 rate 换成 irate。后者使用范围内最后两个数据点,更接近最近一次采样间隔内的变化率。它确实能更快反映一些变化,但仍然只能利用采样得到的信息。

例如两次抓取隔了十五秒,中间增加一千五百次请求。无论这些请求均匀分布在十五秒内,还是集中在其中一秒,两个端点都一样。只凭这两张“里程表照片”,无法还原那一秒的最高速度。这是信息本来就没被记录,不是换函数能补回来。

irate 更容易跳动,也因此不能只为了让告警“快一点”,就不加判断地替换原查询。告警究竟要捕捉短促毛刺,还是持续超载,应该先说清楚。Prometheus 对两个函数的定义、重置处理和适用建议都在 查询函数文档。

还有一个容易被忘掉的顺序:多个实例合起来看时,先给各自计数器算速率,再求和。某个实例重启会把自己的计数器归零,提前混合原始计数可能掩盖这件事。下面保留每条序列的独立处理,再汇总到服务:

sum by (service) (rate(demo_requests_total[5m]))

这条写法也不会恢复抓取之间没有记录的请求细节。监控适合用来判断趋势、发现异常;要精确解释某次请求为什么慢,还需要对应的日志、追踪或更合适的测量。

下次看见流量停了、曲线还在滑落,我会先问:这条线此刻回答的,是最近十五秒、最近一分钟,还是最近五分钟的问题?把问题读对,才轮得到判断答案是不是错了。

评论