技术2026 · 08 / 288 分钟♡ 142评论 23

主引擎 + 降级兜底:我把工具链做成了容灾系统

一个从内容生产线蔓延到生活每个角落的思维方式——先走主路径,走不通就降级,永远留一条后路。

深夜的生产线 · 三块屏都在跑

去年这个时候,我的内容生产线还是一条直线:一个环节坏了,整条线停。

上游模型接口抖一下,队列里排着的三十个任务全部作废;某个渲染引擎在竖版分辨率下死锁,那一批片子当天出不来。我当时的处理方式很朴素——盯着日志,坏了就手动重跑。跑了三个月,我发现自己一天要手动介入七八次,而每一次介入的动作都高度重复。

于是我把这条线重写了一遍。核心只有一句话:每个环节都必须有主选和兜底,而且切换是自动的。

一、先分清「重试」和「降级」

这是我最开始就犯的错。我以为加了重试就有了容错,结果并没有。

重试解决的是同一条路径上的瞬时故障:网络抖动、限流、临时 5xx。这类问题的特征是「再试一次可能就好了」,退避几秒重发,成功率很高。

降级解决的是这条路径本身走不通:模型不支持这个参数、这个引擎在这种输入下必然死锁、这个供应商的账户余额没了。这类问题重试一万次也是同样的结果,只有换一条路才有出口。

把两者混在一起会出现一个很典型的症状:日志里全是重试,但没有一次成功。你以为系统在努力工作,其实它在原地打转。

所以现在我的每个环节都写成两层:

第 1 层(API 层):网络异常 / 429 / 5xx → 指数退避重试 3 次
第 2 层(业务层):结果校验不过 / 明确的能力不匹配 → 换引擎重来
两层的重试配额彼此隔离

最后那句是重点。我曾经把它们写在一个循环里,结果一次网络闪断把「换引擎」的机会也一起吃掉了——业务层还剩两次机会,但异常直接把整个函数打穿,兜底引擎根本没被调用过。表面上看是「模型不稳定」,实际上是我的重试结构有个洞。

二、降级触发的阈值怎么定

这是被问得最多的问题。我的答案是:盯两个信号,命中任一就降级,不要靠感觉。

信号一:同一动作连续失败 2 次。为什么是 2 而不是 3、5?因为第一次失败可能是偶发,第二次同样的失败基本可以确认是系统性的。再往上加次数,只是把等待时间线性拉长,换不来多少额外的成功率。我实测过 2 / 3 / 5 三档,2 和 3 的最终成功率差不到一个百分点,但 2 的平均耗时少了将近四成。

信号二:需求本身超出主引擎的能力边界。这个不需要等失败,可以前置判断。比如某个环节需要大批量的精确帧同步,而主引擎在这件事上本来就不擅长,那就不要浪费两次失败再降级,直接在入口路由过去。

第二个信号需要你真的知道每个引擎的能力边界,这是没法偷懒的部分——只能靠一次次踩坑,然后把结论写进文档。我现在每个引擎都有一份「能做什么 / 不能做什么 / 什么参数会直接崩」的清单,路由逻辑读的就是这份清单。

三、最容易被忽略的一条:别让兜底把问题藏起来

这是我踩得最疼的一个坑,值得单独说。

有一段时间我给所有环节都加了「兜底」:任何异常都 catch 住,返回一个默认值,让流程继续往下走。看起来很健壮——线上再也不报错了。

三周后我才发现,某个环节其实早就坏了,一直在返回默认值,下游拿着这个默认值生产了两百多条废品。而我完全不知道,因为日志是干净的

从那之后我的原则变了:

兜底是换一条路继续走,不是假装走通了

具体落到代码上:

第三条救过我不止一次。降级率是个特别灵敏的健康指标,比错误率灵敏得多,因为它在系统「还能用」的阶段就已经开始报警了。

四、错误信息的截断方向,居然是反的

一个很小但很值得说的细节。

我以前记录异常是这么写的:str(e)[:300]——取前 300 个字符。跑了很久都没觉得有问题,直到有一次线上失败,我打开日志想看根因,发现满屏都是那条命令行参数,真正的报错信息在末尾,被截掉了。

对于绝大多数异常,信息密度最高的部分在末尾:最内层的报错、真正的原因、那个关键的字段名。开头往往是调用栈的外壳、命令行、参数堆。

现在我一律写成保头 + 保尾:

def brief(e, head=110, tail=420):
    s = str(e)
    return s if len(s) <= head + tail else s[:head] + " ... " + s[-tail:]

开头留一点用于识别是哪一类错误,末尾留足够多用于定位根因。就这么一个改动,让我排查问题的速度快了一大截。

五、它后来跑出了生产线

写完这套东西之后,我发现自己看很多事情的方式变了。

出门旅行订酒店,我会顺手记下步行距离内的第二个选择;重要的会要飞过去,我会看一眼同一天晚一班的航班还有没有票;给家里配网络,我留了一条完全独立的备用线路,不和主线共用同一家运营商。

这不是焦虑,恰恰相反——因为有后路,所以主路径可以走得更激进。

我现在敢在主链路上用最新、最强、但还不够稳的引擎,因为我知道它挂了会自动切走,最坏情况是慢一点、糙一点,但不会是「什么都没有」。如果没有兜底,我只敢用最保守的那个选择,那条线的天花板就永远只有保守方案的水平。

主引擎 + 降级兜底,本质上是一种让你敢于冒险的结构

那些还没解决的

诚实地说,这套东西远没有做完:

第三条我觉得最要紧。没被演练过的兜底,等于没有兜底。


有人在留言里问我,这么搞是不是过度工程了。我的判断标准很简单:如果一个故障会让你在半夜爬起来手动处理,那它值得被自动化;如果它一年发生一次、影响面很小,那记一条日志就够了。

不要为了完备去做完备。为了睡个好觉去做。

这篇里的照片Frames全部相册 →
第 214 次重跑,这次过了
第 214 次重跑,这次过了家 · 书房 · Sony A7C II · 85mm f/2.0
← 更早一篇
演讲实录 · 一条流水线的三次重写,和我改掉的三个错觉

关注公众号「十年追随」

新文章第一时间推给你。留言我都会回,公众号里也能收到回复通知。 不发广告,一周最多两条。

十年追随 公众号二维码
评论Comments23 条 · 匿名也能评,昵称头像会记住
评论服务(Waline)待部署 —— 香港 VPS 起来后填 SERVICES.waline 即生效。
想说话可以先去 留言板,那边我每条都回。
关注公众号「十年追随」更新第一时间推给你 · 留言也能收到我的回复
看二维码