Go AI 后端超时树:每个下游都要有自己的时间预算

AI 后端链路经常很长:认证、读取配置、检索知识库、重排、调用模型、写日志、保存结果、推送通知。很多服务只在最外层设置一个总超时,内部下游随意等待。结果某个慢依赖吃掉全部时间,模型还没开始就已经接近超时。Go 服务里,超时应该像一棵树,每个下游都有自己的预算。

超时树的目标,是让请求在有限时间内做出可解释的取舍。

一、先定义总预算

flowchart TD
  A[Request 8s] --> B[Auth 300ms]
  A --> C[Retrieval 1500ms]
  A --> D[Rerank 800ms]
  A --> E[LLM 5000ms]
  A --> F[Persist 300ms]

用户请求如果承诺 8 秒内返回,就要把 8 秒拆给各个阶段。认证不能等 2 秒,检索不能无限重试,持久化也不能卡住主流程。预算拆清后,服务才能在某个下游变慢时及时降级。

预算不是平均分。模型调用可能需要最多时间,认证和配置读取应该很短,日志写入可以异步。每个阶段的预算要根据业务价值和依赖特性分配。

二、Context 要逐层派生

ctx, cancel := context.WithTimeout(parent, 1500*time.Millisecond)
defer cancel()
docs, err := retriever.Search(ctx, query)

Go 的 context 很适合传递取消和超时,但要避免所有下游共用一个没有边界的 ctx。每个阶段可以从父 ctx 派生子 ctx,设置自己的 timeout。父请求取消时,所有子任务都应该停止。

同时要避免忘记 cancel。即使请求提前返回,也要释放 timer 和相关资源。长链路服务里,这些小疏漏会慢慢变成 goroutine 泄漏和资源浪费。

三、降级策略要跟预算绑定

fallback:
  retrieval_timeout: use_cached_docs
  rerank_timeout: skip_rerank
  persist_timeout: async_retry

超时不是只有失败一种结果。检索超时可以使用缓存,重排超时可以跳过,日志写入超时可以异步补偿,模型超时可以返回部分结果或提示稍后查看。每个下游都要提前定义超时后的动作。

降级也要可见。响应里可以带上内部标记,日志里记录哪个阶段降级。否则用户觉得回答质量下降,团队却不知道这次请求跳过了重排或用了旧缓存。

四、观测要按阶段打点

{
  "trace_id": "tr_001",
  "retrieval_ms": 420,
  "rerank_ms": 180,
  "llm_ms": 4630,
  "timeout_stage": null
}

只有总耗时,看不出问题。每个阶段的耗时、超时、重试和降级都要进入 trace 或结构化日志。这样当 TP99 升高时,能知道是检索慢、模型慢,还是持久化卡住。

阶段打点还可以反过来调整预算。某个阶段长期只用 50ms,却给了 1 秒预算;另一个阶段经常超时,却对结果很关键,就需要优化或扩容。预算应该跟着数据演进。

还要把客户端取消传到底。用户关闭连接后,检索、模型调用和持久化不应该继续无意义消耗资源。超时树和取消树最好一起设计,让请求退出时下游能及时收手。

五、总结

Go AI 后端要把请求总超时拆成超时树,为每个下游分配独立时间预算,并用 context 逐层派生、绑定降级策略和阶段观测。

超时不是最后一刻才发现来不及,而是从请求进入系统开始,就清楚每一步最多能花多久。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐