服务事故复盘 · 27

一次棋局错误,如何暴露一串服务回归

8 月 30 日晚,一盘带有非半目贴目的棋先触发了 KataGo 参数错误,随后同一轮排查又发现流式响应过大、注册运行时不兼容、Token 重复创建和旧客户端同步失败。这里公开时间线、实际影响、修复证据和仍需完成的工作。

  1. 第一条可见错误出现在 20:47

    2026 年 8 月 30 日 20:47,一次 Web 全局复盘把非整数、非半整数的贴目提交给 Modal。KataGo 拒绝了该参数,任务在 0 毫秒 GPU 用时后失败。错误页保留了查询标识和搜索量,却没有把“导入棋谱中的贴目不受支持”提前解释给用户。

    20:47 的非法贴目请求成为入口,后续重试和第二类供应商错误沿时间轴分开记录。
  2. 三十次失败只涉及两条重试链

    20:47 至 21:13 的数据库记录里,非法贴目错误出现 5 次,来自同一用户的同一盘棋;响应过大错误出现 25 次,来自另一名用户。同期 Modal 完成了 43 个任务。因此这是两条被自动或手动重试放大的故障链,不是 30 名用户同时失去服务,也不是整个分析系统在该时段完全中断。

    影响面按用户、棋局和成功任务重新计数,避免把 30 次尝试写成 30 名受影响用户。
  3. 非法贴目本应在供应商调用前停止

    KataGo 只接受 -400 至 400 之间的整数或半整数贴目。旧路径把不合约的值一直送到供应商,用户最终看到内部错误。现在 Web、REST 与流式入口共享同一校验,在计费和 GPU 调度前返回明确的 HTTP 400。系统不会静默四舍五入,因为改变贴目就改变了要分析的棋局。

    参数校验已经移到计费与 GPU 调度之前,非法值不再穿过 Worker 到达 KataGo。
  4. 终态事件重复了整场流式结果

    第二条故障来自 Modal 流:中间事件已经逐项传回结果,结束事件又附带完整结果数组。全局复盘包含大量局面时,终态事件超过供应商响应大小限制。修复后,中间结果保持流式传输,结束事件只保留 usage 等小型汇总;正式生产 App 已用 64 visits 查询验证,返回 1 个结果和完整用量。

    流式结果继续逐项到达,complete 事件从重复载荷缩成只含 usage 的轻量回执。
  5. 同轮排查发现三项独立兼容性回归

    注册失败不是非法贴目造成的:密码派生迭代数超过 Cloudflare Worker 的运行时上限,账户尚未写入就中止。客户端同步还使用了 Edge 不支持的 redirect 取值;部分旧客户端在重连时缺少稳定设备槽位,因而反复创建 API Token。这三项与棋局错误没有直接因果,但都说明服务端更新缺少旧客户端与边缘运行时的发布契约。

    注册、重定向和 Token 是三条独立支路;它们在同次回归审查中被发现,但不共享根因。
  6. 恢复同时覆盖主路径和回退路径

    8 月 31 日 15:54,兼容性修复进入 Cloudflare;16:15,REST 回退改为始终消费 RunPod 流并在终态缺少 output 时重建结果;随后 Modal Fast 正式重部署。16:18 的生产 RPC 在 NVIDIA L4 上返回 1 个结果、70 visits。新 Worker 上线后也出现了新的成功注册记录,证明注册写入链路已经恢复。

    Cloudflare、Modal 与 RunPod 分别验收,部署成功回执没有被当成生产行为成功的替代品。
  7. 下一次发布必须带着兼容矩阵前进

    后续发布门禁将覆盖 Web、新旧原生客户端、REST、WSS、注册、自动 Token 轮换、Modal 流和 RunPod 回退,并为大棋谱设置响应大小测试。运营侧还需要为注册写入前失败补充脱敏事件,让“没有账户记录”和“没有发起请求”不再无法区分。本次发布未上传新的前端 source map,业务不受影响,但前端异常的符号化会延迟,这也列入发布凭据检查。

    新的发布契约把客户端、协议、供应商、回退与可观测性放进同一张上线前矩阵。

已经落地的修复

  • 贴目在计费和供应商调用前校验
  • Modal 终态不再重复完整结果
  • RunPod 回退始终消费并重建流式输出
  • 注册参数符合 Cloudflare Worker 运行时限制
  • 同设备自动轮换 Token,上限由 5 调整为 20
  • Edge 重定向与旧 IGS 字节流保持兼容
  • WebGL 丢失时棋盘降级显示而不是空白
查看全部指南