两个TTL不一致如何制造一个必现的409

前言

线上有一个很奇怪的问题:OpenAI-compatible endpoint 第一次请求正常,放一段时间后再请求会稳定返回:

1
409 sampling run is not active

再多等一会,它又可能自己恢复。

最初看起来像某个模型坏了,因为不同模型出现问题的时间并不一致;Trio 自己的 Sample API 当时又是正常的。继续复现后才发现,问题和模型没有关系,真正的规律是“同一个 API Key 和模型空闲超过 15 分钟”。

最后定位到的是一个很典型的分布式系统问题:Router 和 Actor 各自维护了一份生命周期,但两个 TTL 不一样。

请求链路

OpenAI 请求会经过:

1
2
3
4
5
OpenAI SDK
→ Control
→ Model Router
→ Actor
→ vLLM

Router 为了避免每次请求都创建 ServiceSession 和 SamplingRun,会按 credential_id + model 缓存一份路由材料:

  • ServiceSession ID
  • SamplingRun ID
  • Actor endpoint
  • Actor JWT
  • JWT 过期时间

Actor 则在本机维护 SamplingRun 的真实运行状态。一个 Run 长时间没有请求时,会被空闲回收。

单看两边的实现都很合理,放在一起就出问题了。

两个都合理的TTL

Router 当时用 Actor JWT 的剩余有效期判断缓存能否复用:

1
2
3
JWT有效期:1小时
提前刷新:10分钟
Route Cache可复用时间:约50分钟

Actor 的 Run 空闲超时则是:

1
run_idle_timeout:15分钟

于是同一份路由材料在两个服务眼里会有两种状态:

1
2
3
0~15分钟:Router认为有效,Actor也认为Run有效
15~50分钟:Router认为有效,Actor已经回收Run
50分钟以后:Router缓存过期,重新创建Run

这正好解释了“请求一开始成功,等十几分钟失败,再过很久又恢复”。

在 15~50 分钟这个窗口中,Router 会继续拿旧 SamplingRun ID 和旧 JWT 请求 Actor;Actor 查到 Run 已经不是 ACTIVE,于是返回 409。

完整修复

最小修复方案是让 Router Cache TTL 小于 Actor idle timeout,例如把缓存改成 10 分钟。

这能减少问题,但它把两个服务的内部配置耦合在了一起:

1
Router必须知道Actor的回收时间

以后 Actor 调整 idle timeout,Router 也要同步修改。只要部署配置漂移,问题还会回来。

更稳妥的方式是让 Router 能识别“缓存指向的 Run 已经失效”,然后自愈。

最终实现的刷新流程是:

1
2
3
4
5
6
7
1. Router命中缓存并请求Actor
2. Actor返回409 sampling_run_inactive
3. Router精确识别这个错误
4. 删除 credential_id + model 对应缓存
5. 复用原ServiceSession创建新SamplingRun
6. 获取新的Actor JWT
7. 原请求只重试一次

这里必须“精确识别”,不能看到所有 409 就重试。参数冲突、业务状态冲突等 409 仍然应该原样返回,否则会把真实错误伪装成一次刷新。

也不能无限重试。旧 Run 失效属于一个确定、可恢复的前置条件错误,刷新一次足够;第二次还失败就应该结束请求。

缓存失效不是只删Redis

这次问题还有一个容易忽略的点:缓存 value 里不是普通查询结果,而是一组有生命周期的远程资源凭证。

删除 Redis key 只是本地动作,后续还要决定:

  • 是否复用原 ServiceSession
  • 是否需要关闭旧 Run
  • 新 Run 创建失败时缓存保持什么状态
  • 并发请求是否会重复创建 Run
  • 原请求在什么条件下可以安全重放

因此 route cache 本质上并不只是性能缓存,它是一个分布式资源句柄缓存。命中不代表资源仍然存在,TTL 也不能代替服务端的真实状态。

最后

这次排查最有价值的地方,是把“偶发 409”还原成了一个确定的时间窗口。

分布式系统里,只要同一个资源在两个服务中各有一份生命周期,就不能只检查某一边的代码是否合理。50 分钟的凭证缓存和 15 分钟的资源空闲回收单独看都没错,组合起来却制造了一个 35 分钟宽的稳定故障窗口。

相关记录:

作者

Noah Shen

发布于

2026-07-10

更新于

2026-07-10

许可协议

评论