去年这个时候,我的内容生产线还是一条直线:一个环节坏了,整条线停。
上游模型接口抖一下,队列里排着的三十个任务全部作废;某个渲染引擎在竖版分辨率下死锁,那一批片子当天出不来。我当时的处理方式很朴素——盯着日志,坏了就手动重跑。跑了三个月,我发现自己一天要手动介入七八次,而每一次介入的动作都高度重复。
于是我把这条线重写了一遍。核心只有一句话:每个环节都必须有主选和兜底,而且切换是自动的。
一、先分清「重试」和「降级」
这是我最开始就犯的错。我以为加了重试就有了容错,结果并没有。
重试解决的是同一条路径上的瞬时故障:网络抖动、限流、临时 5xx。这类问题的特征是「再试一次可能就好了」,退避几秒重发,成功率很高。
降级解决的是这条路径本身走不通:模型不支持这个参数、这个引擎在这种输入下必然死锁、这个供应商的账户余额没了。这类问题重试一万次也是同样的结果,只有换一条路才有出口。
把两者混在一起会出现一个很典型的症状:日志里全是重试,但没有一次成功。你以为系统在努力工作,其实它在原地打转。
所以现在我的每个环节都写成两层:
第 1 层(API 层):网络异常 / 429 / 5xx → 指数退避重试 3 次
第 2 层(业务层):结果校验不过 / 明确的能力不匹配 → 换引擎重来
两层的重试配额彼此隔离
最后那句是重点。我曾经把它们写在一个循环里,结果一次网络闪断把「换引擎」的机会也一起吃掉了——业务层还剩两次机会,但异常直接把整个函数打穿,兜底引擎根本没被调用过。表面上看是「模型不稳定」,实际上是我的重试结构有个洞。
二、降级触发的阈值怎么定
这是被问得最多的问题。我的答案是:盯两个信号,命中任一就降级,不要靠感觉。
信号一:同一动作连续失败 2 次。为什么是 2 而不是 3、5?因为第一次失败可能是偶发,第二次同样的失败基本可以确认是系统性的。再往上加次数,只是把等待时间线性拉长,换不来多少额外的成功率。我实测过 2 / 3 / 5 三档,2 和 3 的最终成功率差不到一个百分点,但 2 的平均耗时少了将近四成。
信号二:需求本身超出主引擎的能力边界。这个不需要等失败,可以前置判断。比如某个环节需要大批量的精确帧同步,而主引擎在这件事上本来就不擅长,那就不要浪费两次失败再降级,直接在入口路由过去。
第二个信号需要你真的知道每个引擎的能力边界,这是没法偷懒的部分——只能靠一次次踩坑,然后把结论写进文档。我现在每个引擎都有一份「能做什么 / 不能做什么 / 什么参数会直接崩」的清单,路由逻辑读的就是这份清单。
三、最容易被忽略的一条:别让兜底把问题藏起来
这是我踩得最疼的一个坑,值得单独说。
有一段时间我给所有环节都加了「兜底」:任何异常都 catch 住,返回一个默认值,让流程继续往下走。看起来很健壮——线上再也不报错了。
三周后我才发现,某个环节其实早就坏了,一直在返回默认值,下游拿着这个默认值生产了两百多条废品。而我完全不知道,因为日志是干净的。
从那之后我的原则变了:
兜底是换一条路继续走,不是假装走通了。
具体落到代码上:
- 降级成功 → 记一条 WARN,标明「主引擎失败,已切到 X」,并计数
- 所有路径都失败 → 大声失败,抛出去,让任务标红,不要返回默认值
- 任何一次降级都要能被统计出来 —— 如果某条路径的降级率从 2% 涨到 40%,那说明主引擎出了系统性问题,得去修根因,而不是让兜底一直扛着
第三条救过我不止一次。降级率是个特别灵敏的健康指标,比错误率灵敏得多,因为它在系统「还能用」的阶段就已经开始报警了。
四、错误信息的截断方向,居然是反的
一个很小但很值得说的细节。
我以前记录异常是这么写的: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:]
开头留一点用于识别是哪一类错误,末尾留足够多用于定位根因。就这么一个改动,让我排查问题的速度快了一大截。
五、它后来跑出了生产线
写完这套东西之后,我发现自己看很多事情的方式变了。
出门旅行订酒店,我会顺手记下步行距离内的第二个选择;重要的会要飞过去,我会看一眼同一天晚一班的航班还有没有票;给家里配网络,我留了一条完全独立的备用线路,不和主线共用同一家运营商。
这不是焦虑,恰恰相反——因为有后路,所以主路径可以走得更激进。
我现在敢在主链路上用最新、最强、但还不够稳的引擎,因为我知道它挂了会自动切走,最坏情况是慢一点、糙一点,但不会是「什么都没有」。如果没有兜底,我只敢用最保守的那个选择,那条线的天花板就永远只有保守方案的水平。
主引擎 + 降级兜底,本质上是一种让你敢于冒险的结构。
那些还没解决的
诚实地说,这套东西远没有做完:
- 降级的质量落差没有量化。现在切到兜底引擎,我只知道「它成功了」,但产出质量降了多少,没有指标。我需要一个自动质检来打分,否则「成功」这个词是有水分的。
- 多环节同时降级会产生复合效应。单个环节降一档还能接受,三个环节同时降一档,最终产物可能已经不合格了。目前我还没有一个全链路的质量下限判据。
- 兜底路径的测试覆盖不足。这是最讽刺的:兜底代码平时不跑,最需要它的时候才第一次跑。我现在会定期主动把主引擎掐掉,逼系统走一遍降级路径——但还没做成自动化的常规演练。
第三条我觉得最要紧。没被演练过的兜底,等于没有兜底。
有人在留言里问我,这么搞是不是过度工程了。我的判断标准很简单:如果一个故障会让你在半夜爬起来手动处理,那它值得被自动化;如果它一年发生一次、影响面很小,那记一条日志就够了。
不要为了完备去做完备。为了睡个好觉去做。


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