产线刚搭起来那几个月,我最怕的不是失败,是失败之后要从头再来。
一条成片三小时,第 137 步挂了——可能是显存、可能是下游超时、可能只是磁盘满了——然后前面 136 步的成果全部作废,从零开始。
那段时间我做过最蠢的事,是给整条产线加了一个「总重试」:失败就整个重来。结果是同一个任务连续重来三次,每次都在同一个地方挂,浪费了九个小时才发现是一个必然失败的输入。
后来我把「失败」当成产线的一等公民来设计,下面是几条最有用的原则。
每一步的产物都落盘,而且命名要能推出「是哪一步、给哪个输入」
这是断点续跑的地基。听起来理所当然,但很多产线在早期为了省事,中间产物只存在内存里,或者用时间戳命名——跑完之后根本对不上哪个文件属于哪一步。
我最后用的命名规则很朴素:<任务ID>/<步骤序号>-<步骤名>.<扩展名>。看到目录就知道跑到了哪一步、缺了哪一步。
顺带解决了一个隐蔽的问题:并发跑多个任务时,随机命名会让你完全无法反推哪份产物属于哪个任务。每个任务一个独立目录,零竞态。
续跑的前提是幂等:同一步跑两次,结果必须一样
有了落盘的产物,续跑逻辑就很简单:跑之前先看这一步的产物在不在,在就跳过。
但这要求每一步都是幂等的——给同样的输入,产出同样的结果,不依赖「上一次跑到哪」这种隐藏状态。
我踩过的坑是一个「追加写入」的步骤:它往一个文件末尾追加内容,重跑一次就追加两遍。改成「先写临时文件,成功后原子替换」之后才真正幂等。
任何不幂等的步骤都是断点续跑的漏洞,续跑到它那里就会产出错误结果,而且不报错。
把失败分成三类,各走各的路
不是所有失败都该重试。我最后分成三类:
- 瞬时失败(网络抖动、下游限流、显存瞬间不足):原地重试,指数退避带抖动,最多三次。
- 输入失败(这个输入本身就处理不了):立刻停,标记这个任务,不要重试,重试一万次结果都一样。
- 环境失败(磁盘满、依赖服务挂了):停整条产线,告警,等人来。继续跑只会产生一堆残缺产物。
区分的依据是错误类型和错误原文。这要求每一步失败时把原始错误完整保留下来,不要包装成一句「处理失败」——我曾经因为一层包装把真正的根因截掉,对着「处理失败」四个字排查了一整晚。
然后才是「快」
做完上面三件事,我才回头去优化速度。
原因很简单:一条能从第 137 步接着走的产线,失败一次的代价是几十秒;一条每次失败都从头再来的产线,就算快一倍,失败一次的代价还是三小时。可靠性带来的时间收益,比任何单点优化都大。
现在这条产线跑一次仍然要两个多小时。但过去半年它的重跑总时长,加起来不到一个下午。
有朋友问过我,这套东西是不是过度设计。
我的答案是:如果一条产线一辈子只跑一次,是的。但只要它会跑第二次,「能从哪里接着走」就是它最值钱的能力,没有之一。


SERVICES.waline即生效。想说话可以先去 留言板,那边我每条都回。