服务事故复盘 · 27
一次棋局错误,如何暴露一串服务回归
8 月 30 日晚,一盘带有非半目贴目的棋先触发了 KataGo 参数错误,随后同一轮排查又发现流式响应过大、注册运行时不兼容、Token 重复创建和旧客户端同步失败。这里公开时间线、实际影响、修复证据和仍需完成的工作。
第一条可见错误出现在 20:47
2026 年 8 月 30 日 20:47,一次 Web 全局复盘把非整数、非半整数的贴目提交给 Modal。KataGo 拒绝了该参数,任务在 0 毫秒 GPU 用时后失败。错误页保留了查询标识和搜索量,却没有把“导入棋谱中的贴目不受支持”提前解释给用户。
20:47 的非法贴目请求成为入口,后续重试和第二类供应商错误沿时间轴分开记录。 三十次失败只涉及两条重试链
20:47 至 21:13 的数据库记录里,非法贴目错误出现 5 次,来自同一用户的同一盘棋;响应过大错误出现 25 次,来自另一名用户。同期 Modal 完成了 43 个任务。因此这是两条被自动或手动重试放大的故障链,不是 30 名用户同时失去服务,也不是整个分析系统在该时段完全中断。
影响面按用户、棋局和成功任务重新计数,避免把 30 次尝试写成 30 名受影响用户。 非法贴目本应在供应商调用前停止
KataGo 只接受 -400 至 400 之间的整数或半整数贴目。旧路径把不合约的值一直送到供应商,用户最终看到内部错误。现在 Web、REST 与流式入口共享同一校验,在计费和 GPU 调度前返回明确的 HTTP 400。系统不会静默四舍五入,因为改变贴目就改变了要分析的棋局。
参数校验已经移到计费与 GPU 调度之前,非法值不再穿过 Worker 到达 KataGo。 终态事件重复了整场流式结果
第二条故障来自 Modal 流:中间事件已经逐项传回结果,结束事件又附带完整结果数组。全局复盘包含大量局面时,终态事件超过供应商响应大小限制。修复后,中间结果保持流式传输,结束事件只保留 usage 等小型汇总;正式生产 App 已用 64 visits 查询验证,返回 1 个结果和完整用量。
流式结果继续逐项到达,complete 事件从重复载荷缩成只含 usage 的轻量回执。 同轮排查发现三项独立兼容性回归
注册失败不是非法贴目造成的:密码派生迭代数超过 Cloudflare Worker 的运行时上限,账户尚未写入就中止。客户端同步还使用了 Edge 不支持的 redirect 取值;部分旧客户端在重连时缺少稳定设备槽位,因而反复创建 API Token。这三项与棋局错误没有直接因果,但都说明服务端更新缺少旧客户端与边缘运行时的发布契约。
注册、重定向和 Token 是三条独立支路;它们在同次回归审查中被发现,但不共享根因。 恢复同时覆盖主路径和回退路径
8 月 31 日 15:54,兼容性修复进入 Cloudflare;16:15,REST 回退改为始终消费 RunPod 流并在终态缺少 output 时重建结果;随后 Modal Fast 正式重部署。16:18 的生产 RPC 在 NVIDIA L4 上返回 1 个结果、70 visits。新 Worker 上线后也出现了新的成功注册记录,证明注册写入链路已经恢复。
Cloudflare、Modal 与 RunPod 分别验收,部署成功回执没有被当成生产行为成功的替代品。 下一次发布必须带着兼容矩阵前进
后续发布门禁将覆盖 Web、新旧原生客户端、REST、WSS、注册、自动 Token 轮换、Modal 流和 RunPod 回退,并为大棋谱设置响应大小测试。运营侧还需要为注册写入前失败补充脱敏事件,让“没有账户记录”和“没有发起请求”不再无法区分。本次发布未上传新的前端 source map,业务不受影响,但前端异常的符号化会延迟,这也列入发布凭据检查。
新的发布契约把客户端、协议、供应商、回退与可观测性放进同一张上线前矩阵。
已经落地的修复
- 贴目在计费和供应商调用前校验
- Modal 终态不再重复完整结果
- RunPod 回退始终消费并重建流式输出
- 注册参数符合 Cloudflare Worker 运行时限制
- 同设备自动轮换 Token,上限由 5 调整为 20
- Edge 重定向与旧 IGS 字节流保持兼容
- WebGL 丢失时棋盘降级显示而不是空白