凌晨两点,你的手机亮起一条告警:夜间匹配任务输出为0,连续三个执行窗口都这样。你从床上爬起来,打开日志,发现程序没有崩溃、数据没有异常,只是它真的什么都没匹配到。一切正常,为什么要被叫醒?问题出在那条“输出计数<1触发报警”的规则上——它没办法区分“程序坏了”和“程序安静地做完工作”。这是输出计数警报最常见的误判场景。
很多人给定时任务配监控时,直觉上就会用一个输出计数阈值:每次运行结束后产生的记录、发送的邮件、处理的行数,如果变成0,就证明出事了。这种想法简单直接,支撑着成千上万个运维报警。但恰恰因为它只看最终结果的表面数字,当系统因为各种合理原因没有产出时,报警也会照响不误。相当于守门员不看场上局势,只要球没进就认定防守失败。
这种“零输出即故障”的逻辑忽略了流水线里真正决定报警性质的东西——为什么输出变成了0。如果把整个流程拆开来看,上游输入可能枯竭、处理能力被配额限制、下游存储被写满,任何一种情况都会导致最终产出归零,但这些原因有的需要立刻处理,有的完全无害。可输出计数警报不分青红皂白,只盯住最后的那个0,等于在用一个数字掩盖所有可能的状态。
举个例子:某个夜间匹配器,负责把新入库的商品和一批老用户做相似度匹配,结果写进一张 matches 表。最初设计的告警是:如果扫描到了新物品,最终却没有生成任何匹配行,就记一条日志,再由监控系统统计这条日志的出现次数,连续三个周期都这样,就触发报警。这就是典型的“输出计数”派。它隐含一个前提——只要任务正常跑起来,就一定会有匹配结果。但这个前提在很多生产场景里是脆弱的。
有一次线上真的报警了,业务方排查下来才发现,输入数据一切正常,相似度计算也没有报错,真正的原因出在用户池上:当天用户池因为配额策略被提前打满,匹配任务被限流,根本拿不到足够多的用户来参与计算。也就是说,任务确实跑完了,也没报错,但因为资源被限制,最终产生的匹配记录为0。
这个0不意味着服务宕机,也不意味着数据损坏,它只代表一个运维层面的资源限制。但可惜的是,原始的计数告警识别不出这种差异,它把“配额不够”和“进程崩溃”划进了同一个报警等级里。
这次经历暴露了一个普遍的设计缺陷:用输出数量的简单阈值来推断系统健康度,会混淆故障原因和正常波动。于是团队在调整阈值之前,先做了一个关键的诊断步骤。他们写了一条只读的 SQL 查询,直接从输入源、中间临时表到最终输出表走一遍全链路漏斗,目的是检验警报到底有没有逮住它所声称的故障。这一查就发现,报警周期的输入项是充足的,相似度计算的通过率也在正常范围,唯独下游匹配数被压成了0——这根本不是管道断裂,而是配额导致的“无声拒绝”。
如果当时直接去调高阈值,比如把报警条件改成“连续10次0输出才通知”,可能会掩盖真正的问题,让真实故障延迟暴露。既然单纯调阈值解决不了根因,那就必须让监控手段具备区分能力。团队的思路很鲜明:把智能往前移,放到发射日志的组件里,而不是在告警规则里堆叠复杂的逻辑。
原来的做法是任务结束只要 created == 0 就写一条模糊的警告日志;改进后改为在任务内部增加一个分类器,它根据任务运行期间的内部信号,判断“为什么最终没有输出”。这些内部信号包括:上游输入是否为空、计算过程是否抛了异常、下游写入是否被限流、资源配额是否耗尽等。分类器得到一个明确的原因标签后,代码会在输出日志里写明,例如:
matcher_zero_results reason=quota_exhausted items=121
matcher_zero_results reason=input_empty items=0
matcher_zero_results reason=processor_error items=87
这样一来,日志行从以前的一句模糊警告,变成了一条带原因签名的事件。监控系统仍然可以基于日志数量来触发报警,但报警规则可以精确匹配某个原因标签。对于 quota_exhausted 这类不紧急的原因,可以只记录指标但不报警,或降低报警等级;对于 processor_error 则保留紧急通知。报警的阈值甚至不需要修改:原来要求连续3个周期出现 warning 日志才叫醒人,现在依然保持3/3的条件,只是那些无害的周期不再产生 warning 级日志,而是换成一条不带报警的指标。既保留了原有的敏感度,又排除了误报噪音。
这个改造的核心并不是在监控端玩花样,而是把诊断能力内置到了业务代码里。在分类器实现上,可以抽象成一个纯函数,输入内部计数器(如上游可用项数、计算成功数、写入被拒数),输出一个标准化的原因标签。任务脚本只负责传递这些计数器,日志发射器再根据标签决定日志级别。这样一套组合,让“零输出”从一句含义模糊的告警,变成了可观察、可分类、可按级别响应的系统信号。夜间再次被叫醒时,你看一眼原因标签就知道:是配额又打满了,还是真的有什么东西坏在了管道里。
热门跟贴