任务挂在 61% 四个小时没人发现:给长跑任务装一颗心跳

上周我们的一条批量管线挂了。凌晨三点开始跑,十一点我去看面板,进度卡在 61%,任务状态还是 running,日志最后一行停在 03:42,进程活着,CPU 0%。

专属插画
任务挂在 61% 四个小时没人发现:给长跑任务装一颗心跳

任务挂在 61% 四个小时没人发现:给长跑任务装一颗心跳

上周我们的一条批量管线挂了。凌晨三点开始跑,十一点我去看面板,进度卡在 61%,任务状态还是 running,日志最后一行停在 03:42,进程活着,CPU 0%。

查下去发现是 SDK 内部重试层把请求挂住了:外层 timeout 600 秒没问题,但内部重试一层层堆,累计跑了三个小时,每次都「合规」。进程活、状态活,人就死了。

从那以后上了三件事,都不贵,但那天要是都在,二十分钟内就会有人被叫醒。

一、心跳:别问任务「活着吗」,问「你在干什么」

最先扔掉的是基于状态判断。running 是一个状态,不是证据。心跳才是证据:任务在每一个 stage 结束时往心跳文件写一行 `stage=xxx p=61 eta=42m at=`,带进度号和真实时间戳。

watchdog 每 10 分钟检查两个条件:

- 心跳超过 15 分钟没刷新 → 判挂起

- 连续 30 分钟进度数字不动,但文件还在写 → 判空转

两类分开处理是刻意的:前者是「装死」,立刻叫人;后者是「某一环变慢但还在走」,严重级别低一级,只进日报。分开之后,要叫人的次数少了四分之三。

二、超时分层:挂起往往不是外层的锅

当晚那次挂起,外层 600 秒超时根本没触发,是内部重试一圈一圈叠起来的。教训是:超时要说清楚是哪一层的。

- 单个 HTTP 请求:90 秒硬超时,超时即报错,不做内层重试

- 单个业务 stage:15 分钟,第一次触发发告警,第二次直接杀

- 整个任务:6 小时,只记录不杀

第三层不杀,是因为挂在中段时断点续跑经常能救回来(2026-08-21 那篇写过),直接杀等于把「挂起」升级成「全丢」。

真正省事的一步是把三层的超时值放进同一个配置文件,并在测试里断言「内层必须严格小于外层」。靠配置互相约束,比靠人记靠谱——总有人手一松把某层调大,全套保护就失效了。

三、杀进程分两步:先 SIGTERM 等 30 秒

以前杀任务是直接 kill -9。问题出在:有的任务正写到 checkpoint 一半,文件写了一半,下次 resume 从坏的 checkpoint 直接崩。

现在杀进程分两步,是关键改动:

1. 发 SIGTERM,任务的信号处理器停下来补一件事——把当前 stage 标记为 interrupted,未写完的 checkpoint 换成 .tmp

2. 等 30 秒还活着,再 SIGKILL

这 30 秒里进程还有能力做收尾,和裸 kill -9 的区别就在这个。看起来琐碎,但把「挂起后 resume 失败」基本变成了「resume 一次就好」。

检测到挂起之后做什么

检测到挂起,不要让 watchdog 自己启动补救。理由是 watchdog 和业务同机,挂起的时候机器往往已经是奇怪状态——内存满、盘满,直接补救容易踩同一个坑。

我们的路径:检测挂起 → 推送通知,附一行现场信息(stage、最后进度、心跳停了多久)→ on-call 二选一:resume 或丢弃。决策最多三秒,因为决策需要的信息都在通知里。

一句话收个尾

- running 是状态,心跳文件才是证据

- 超时分层写,内层严格小于外层,且靠配置断言兜底

- 杀进程给 30 秒收尾窗口,别裸 kill -9

- watchdog 只叫人,不自救;决策要三秒内能做完

上了这套之后,同一条管线再没出过「挂了八小时没人知道」的事故。最久的一次挂起,从发生到有人在手机上看到通知,是 18 分钟。

留言区

欢迎分享你的想法!

发表留言

0/500

加载留言中…