长任务在实际生产当中的一些问题
你的 Job 跑了 7 天,第 6 天断电了。然后呢?
在实际生产中, 我们经常处理一些”重型”任务:大模型预训练、大规模数据清洗、跨度数天的分布式回测。这些任务安安静静跑着,不出事你都忘了它的存在。一出事就是 P0。
面试时,我有问过这样一道场景题:“你有一个需要运行 7 天的任务,跑到第 6 天晚上,Spot Instance 被回收了。你怎么保证这 6 天的工作不白费?”
90% 的同学第一反应是:”加个 try-catch,重启一下?”
在分布式系统里,故障不是”万一”,是”必然”。如果你觉得程序能顺顺利利跑完 168 小时,说明你还没见过真正的生产环境。
01 “加个断点续传不就行了?”
聪明一点的会说 Checkpointing。方向对。但 90% 的人对 Checkpoint 的理解,停留在”把进度存下来”。
我带过一个项目,几百个并行 Worker 处理海量图片数据。一个junior写了这样的逻辑:
# 他觉得没问题的版本
for data in tasks:
result = process(data)
save_to_db(result) # 写结果
update_progress(offset) # 更新进度
我问他:“如果 update_progress 成功了,但 save_to_db 因为网络抖动延迟了,这时候机器挂了。重启之后会怎样?”
他想了一会儿说:”进度记录在前面,数据没写进去......那这部分数据就跳过了。”
对。丢了。 而且你不知道丢了多少,不知道丢了哪些。下游如果是计费系统,你猜后果是什么。
他又说:”那我调一下顺序,先写数据,再更新进度。”
我说:“那如果数据写成功了,进度没更新,机器挂了呢?”
“......那恢复的时候会重复处理这批数据。”
“对。所以你现在面临的问题是:不管你怎么排顺序,两个独立的写操作之间,永远存在一个窗口,机器可以在这个窗口里挂掉。 先写谁都不对。”
他说:”那用transaction。把两个操作放进同一个transaction里。”
“如果两个写入的目标是同一个database,可以。但如果一个写 DB,一个写 S3 呢?跨系统的transaction你打算怎么做?成本你算过吗?”
到这里他基本答不上来了。
这就是 Checkpointing 真正的难点。不是”存不存”的问题,是”两个write operaiton的原子性”问题。 你的 Checkpoint 写入和实际数据写入,要么同时成功,要么同时失败。一旦存在不一致的中间状态,恢复的时候要么丢数据,要么重复数据。
工业界处理这个问题有几条路:Write-Ahead Logging,Atomic Rename,Two-Phase Commit。每条路的适用场景和代价完全不同。
我早年也踩过这个坑。当时做一个数据迁移任务,Checkpoint 逻辑写得”看起来”没问题,但 Checkpoint 写入和数据写入之间没有做原子保证。上线之后,有一次 EC2 实例在凌晨被回收,恢复后大概 3000 条记录被重复写入了下游。那个下游恰好是个敏感的系统。也算是血与泪的教训了。
02 Signals:你的程序知道自己快挂了吗?
Checkpoint 解决的是”记忆”——程序恢复之后知道从哪里继续。但还有一个前置问题:你的程序知不知道自己”要挂了”?
Linux 里,进程被终止前会收到信号。SIGTERM 是礼貌的通知:”你该走了,收拾一下。” SIGKILL 是直接断头,没有任何余地。
Kubernetes 做 Pod 滚动更新或者缩容时,先发 SIGTERM,给你一个 terminationGracePeriodSeconds(默认 30 秒)。超时没退出,SIGKILL。AWS Spot Instance 被回收前,会提前大约 2 分钟发通知。
问题来了。你的长任务收到 SIGTERM 时在干什么?
如果你的代码长这样:
for record in get_all_records():
process(record)
什么都不会发生。SIGTERM 来了,进程直接退出。没有 Checkpoint,没有清理。6 天的工作,没了。
正确的做法是注册 Signal Handler:
import signal, sys
shutdown_flag = False
def handle_sigterm(signum, frame):
global shutdown_flag
shutdown_flag = True # 只设标志位,不在 handler 里做重活
signal.signal(signal.SIGTERM, handle_sigterm)
for record in get_all_records():
if shutdown_flag:
save_checkpoint(current_position) # 收到信号,存盘
cleanup()
sys.exit(0)
process(record)
代码不复杂。但我面了这么多年,能在白板上写出这个的 NG,不超过十分之一。
而且这还只是最基础的版本。我在面试里会继续追问,能扛住的基本都是 Strong Hire 的候选人:
Grace Period 不够用怎么办? 你的 Checkpoint 要写远端存储,网络抖了一下,30 秒不够。SIGKILL 来了,Checkpoint 写到一半。恢复的时候读到一个 corrupted 文件,你的任务不是从断点恢复——是直接 crash。
这时候怎么处理?一个常见的工业做法是 Atomic Rename:先写临时文件,写完之后 rename 成正式文件名。POSIX 文件系统上 rename 是原子操作。如果 rename 之前进程被杀了,临时文件还在,但正式文件没被覆盖,恢复时读到的是上一次完整的 Checkpoint。
这个思路不难理解,但你得先意识到问题存在,才可能给出方案。面试里大部分人卡在第一步——根本没想到 Checkpoint 本身也会写坏。
03 为什么这些东西能决定面试结果
我 review 同学简历项目的时候,最常发现的问题是:缺乏对”不可靠性”的敬畏。
很多”分布式爬虫”或”大数据处理”项目,在本地跑得挺好。但只要我追问一层,系统就站不住了。
比如:任务重启了,老任务还在后台占着资源怎么办?任务断了 72 小时之后重新拉起,TB 级状态从远端加载回来,内存和 IO 扛得住吗?你的 Checkpoint 记录的是”第几行”,但源数据被重排了,恢复之后的一致性谁来保证?
这些问题每一个展开都能聊半个小时。教科书上不会教,LeetCode 上刷不到。
这也是 2026 年面试趋势的一个明显变化:System Design 的考察越来越偏向故障处理。 不是让你画一个完美的架构图,是让你回答——”这个系统坏了怎么办。”
面试官不是在考你会不会 Checkpointing 这个具体技术。他考的是你面对故障时的思维方式。这个思维方式会出现在每一轮 System Design 里:你设计的系统,在某个组件挂了的时候,是默默丢数据,还是有明确的降级和恢复策略?
能回答后者的人,和不能的人,在面试官的评价表上是两个级别。
04 这个 Gap 怎么补
靠自己摸索,需要进大厂、搞挂几次线上服务、写几份 post-mortem。大概 3-5 年的周期。但现在的市场,没有 3-5 年给你试错。一次面试机会浪费了,可能就是半年冷冻期。
这也是我在做的辅导里的核心内容之一。我不教刷题。我做的事情是:把你现有的项目拆开,找出那些”生产环境下一定会出问题”的地方,然后重新构建。 不是在简历上加几个 buzzword。是你真的能在面试里讲清楚——为什么这样设计,哪里会出问题,出了问题怎么恢复。
这篇文章聊了 Checkpoint 的原子性和 Signal 处理,这是长任务设计里最表层的两个问题。往下还有 Zombie Task 的 Fencing 机制,Asynchronous snapshot strategies for large, distributed state, 跨系统 Exactly-once 的实际代价......每一个都是面试里能让你脱颖而出,或者让你当场哑火的分水岭。
不过令人尴尬的是,学校不教这些。公司也不会等你学会了再给你 Offer。现在这个时代还是要靠自己去努力发觉自己这些unknown unknowns,主动采取措施学习。
