凌晨两点我打开手机,看到备份任务的告警:已连续失败 11 天。
不是告警失灵,是从来没有告警。那个任务是我半年前挂在 crontab 上的,跑挂的当天我在开会,之后就一直没人提。翻日志才知道,磁盘满了,备份脚本找不到源文件,直接 exit 1。它安安静静地死了十一天,我一次都没察觉。
这件事之后,我花了一个周末把手上十来个自动化任务全部过了一遍。它们大致分三类:定时备份、抓取入库、周期性汇总。都不复杂,但没有一个能在我睡着的时候可靠地叫醒我。下面是我后来落地的做法,按我实际踩坑的顺序写。
先防「没跑」,再防「跑挂了」
大多数人加告警的第一反应是:脚本出错时发条消息。这个思路覆盖不到最危险的那类故障——任务根本没被启动。cron 条目被误删、机器重启后服务没起来、上游改了 API 导致进程卡在等待、容器 OOM 被杀掉,这些情况下脚本内一行错误处理都不会执行,因为它压根没跑到那一行。
我的解决办法是反向确认:每个任务跑完之后,主动向外上报一次「我活着」。外部服务如果在一个预期窗口内没收到上报,就判定失败。这就是常说的心跳或 dead man’s switch。我用的是 healthchecks.io,免费额度对我这十来个任务够用,具体配额我记不清了;自建也不难,一张 job_state 表加一个每分钟扫一次的 cron 就行。
我把所有任务统一收敛到一个包装脚本,crontab 里只调它:
1 |
|
有两点要注意。上游那个 curl 必须带 || true,否则网络抖一下,上报失败会让整个任务返回非零,反而制造假告警。另外超时时间要设成该任务平常耗时的 1.5 倍左右,而不是拍脑袋写个 24 小时。备份任务平时跑 8 分钟,我把窗口设成 15 分钟;抓取任务波动大,通常 3 到 10 分钟,我设了 30 分钟。设太松等于没有告警,设太紧天天误报。
按「谁该在多长时间内响应」分三档
一开始我把所有告警都发进同一个 IM 群,结果两周之后我学会了无视那个群。所有告警都一个级别,等于没有级别。
现在我分三档,判断标准只有一个:这件事能不能等到明天早上。
P0 走短信加电话,能把我从床上拽起来。目前够格的只有一个:对账任务,因为它失败意味着账目和实际收款对不上,拖一晚处理成本会翻倍。我一度想把备份也提成 P0,后来想清楚备份失败一晚并不会立刻出事,降成了 P1。
P1 走 IM 并 @ 我,工作时间两小时内看。抓取入库、数据同步这类。
P2 进每日早报,攒到早上九点一起看。清理临时文件、索引重建、周报生成这类,晚半天没有任何影响。
分级之后还有一个必要的补丁:抑制。同一任务的同类故障,三十分钟内只发一次,否则一个死循环能把手机刷爆。我用一个最简单的文件锁实现,key 是任务名加退出码,锁的有效期就是抑制窗口。另外,如果任务在告警发出后重试成功,我会补一条恢复通知——只有恢复通知能让下一次告警重新被信任。
还有一类容易被忽略的合并:如果三个任务同时失败,而且日志都指向同一个数据库连接超时,那其实是同一个故障。我在告警层做了一个很粗糙的归并,同一分钟内相同错误摘要的告警合成一条,标题写「3 个任务因同一原因失败」。这比三条独立告警有用得多。
跑完了但结果是错的
心跳只能证明进程活着,证明不了它干对了活。我遇到过最难受的一次是抓取任务跑了,退出码 0,日志干干净净,但入库 0 条——对方改了页面结构,解析器返回了空列表,脚本认为「没抓到新的」是正常情况。
所以每个有产出的任务,我都会加一条结果断言。断言写在任务最后,不通过就 exit 1,复用同一套告警通道:
1 | rows=$(psql -tAc "select count(*) from articles where fetched_at > now() - interval '1 hour'") |
阈值怎么定是个取舍。固定值简单,但业务量本身在变,旺季天天误报,淡季又漏报。我现在的做法是取过去七天同期数量的中位数,低于它的一半就报,这个规则我还在观察,可能还要调。另一类断言更好写:产出文件的大小下限、汇总任务里最终金额的正负号、导出任务的行数和源表行数的比值——后者如果偏离 1 太多,基本就是漏了过滤条件。
这类「结果异常」的告警和「进程失败」的告警应该分开标,因为排查路径完全不同。前者通常不是代码问题,是外部世界变了。
告警文本是写给凌晨三点的自己
我见过最多的告警内容是 script failed。凌晨三点看到这五个字,等于什么都没看到。
现在我的每条告警固定给五样东西:任务名、上次成功时间、退出码、最后一行错误、日志链接,外加一句「你可以先做什么」。最后那句最重要,它不写排查全过程,只写第一步动作,比如「先看日志尾部二十行,确认能否安全重跑」,或者「检查上游是否改版,重跑前先清掉脏数据」。
1 | def build_alert(job, last_ok, code, summary, log_url): |
「上次成功时间」这一项是我后来加的,它比错误信息更能说明严重程度。同样是退出码 1,上次成功是十分钟前还是十一天前,处理优先级差得很远。
最后是清理噪音。我每月花十分钟翻一遍告警记录,标准很粗暴:这条规则在过去三个月里有没有让我真正动手处理过。没有的,要么把阈值调宽,要么直接降级到早报。上个月砍掉了两条,一条是每周末都因为磁盘使用率 81% 报警、但我从来没管过,另一条是某个外部接口偶发的 502,重试就好了。砍完之后每周告警量从十几条降到五条以内,现在我看到告警会真的点开。
下周我打算把抓取任务那条固定阈值换成滚动中位数,先把过去三十天的入库量拉出来算一遍,看看按新规则会误报几次。如果超过两次,我就不换。
评论