先接受一件事:任务一定会挂
做自动化最开始的想法是"让它别出错"。跑了几个月之后想法变了——出错是常态,网络抖一下、目标页面改版、磁盘满了、前一次还没跑完下一次又触发了,任何一条都能让任务挂掉。真正要设计的不是"不出错",而是"出错之后自己能爬起来,并且不会把事情搞得更糟"。
坑一:两次触发抢同一个文件
我们的线索同步脚本每 5 分钟跑一次,正常情况下几秒就结束。直到有一次服务端积压,一次跑了 4 分钟,下一次触发又开始了。两个进程同时读写同一个 SQLite,结果是一条线索被推了两次,另一条的状态被覆盖成"已推送"但其实根本没推。
失败的写法:脚本开头直接读库、结尾直接写库,没有任何互斥。看起来"大多数时候没问题",所以一直没被发现。
改法:开头写一个锁文件,拿到锁才继续;拿不到就直接退出(这一轮跳过,下一轮再来,比并发写坏数据强)。锁文件里记 PID 和开始时间,如果锁的持有时间超过阈值(比如 15 分钟),判定为上一次异常退出留下的僵尸锁,清理后再抢——这一步不能省,否则一次崩溃会让任务永久停摆。
坑二:把"失败"写进了"已处理"清单
这个坑最隐蔽。我们有个月度采集任务,跑成功之后会写一个标记文件,下次看到标记就跳过,避免重复采集。逻辑本身没错,错在标记是在流程开头写的,不是结尾。于是某个月采集到一半网络断了,标记已经写下了,接下来一整年它都"执行过"了。
同样的毛病在售电周报上更严重:先跑的任务写了"今日已跑"标记,后面那个负责产出详细版的任务每次都读到标记、每次都跳过。结果是我们连续好几周以为周报在正常出,其实一份都没有。直到某天手动查目录才发现。
规则:标记必须在全部步骤成功之后才写,而且要写"完成了哪一步",不是笼统的"今天跑过了"。这样中途挂掉,下次能从断点继续,而不是从头再来或者干脆不跑。
坑三:重跑一次就重复产出一份
修好上面两点之后,新问题来了:任务失败重跑,昨天的文章又生成了一遍,还多推了一条通知。原因是没有幂等判断。
正确的做法是先查"今天这个东西是不是已经有了",有了就只做"保在线"的动作,不重复生成。我们现在每个产出型任务开头都会检查目标文件(或清单)里今天的记录是否存在——存在就跳过生成,直接走部署和校验。这样重跑十次和跑一次结果一样。
一个容易忽略的细节:幂等判断要放在推进指针之前。如果先取了下一个主题、再发现今天已经有了,主题指针就白推了一次,第二天会跳过一个题目。
留下的三样东西
- 锁文件:防并发,带超时自清理,不靠"应该不会同时跑"这种假设。
- 分步状态:每一步跑完登记成功/失败/跳过,写进一个 JSON。第二天先看昨天哪几步没跑完,补上再跑今天的。没有这个,中断就变成了永久丢失。
- 可回读的告警:任务跑完要发通知,但"命令执行成功"不等于"通知送达"。我们的做法是发完立刻回读消息列表确认,读不到就重试,重试还失败就明确写"推送失败"。这一步看起来多余,恰恰是它挡住了好几次"以为通知了其实没有"。
最后的体会
自愈设计没有多高深,本质上就是把"我猜它成功了"换成"我确认它成功了"。每一次都问一句:如果这一步挂了,下一次运行会怎么样?会重做、会跳过、还是会假装没事?想清楚这三个答案,自动化的可靠度就能上一个台阶。
一句话总结:失败不可怕,可怕的是失败被系统记成了成功。