Skip to content
预计 1 分钟右侧目录可快速定位

流式响应为什么会堵住:Backpressure 与 Gateway Timeout ​

前置:客户端断开后上游该不该停:Disconnect 与 Cancellation Propagation
核心问题:客户端消费事件太慢时,数据会堆在哪里,系统怎样把压力向上游传播而不是无限 buffer 到 OOM?

慢客户端可以把正常 Run 拖成内存事故 ​

模型每秒产生几十个 token delta,Tool/Node 还不断产生状态事件;手机网络上的 SSE 客户端却只能每秒读几条。如果服务端为这个连接维护无界 asyncio.Queue,Runtime 越正常工作,内存反而越快增长。

这就是 streaming backpressure:producer 产生事件的速度超过 transport/consumer 能发送的速度。 它与 broker backlog、Runtime Ready Queue 是不同层的问题。

先把事件分成“可压缩”和“不可丢” ​

不是每个事件都要逐条可靠发送。

例如:

text
token.delta          → 可合并/采样;通常不需要 durable replay
progress.updated     → 可覆盖旧值
run.waiting_approval → 关键状态事件;必须能恢复/重新查询
run.completed        → 关键 terminal fact;必须持久化
artifact.created     → 通常需要稳定 reference

因此慢 consumer 时可以把多个 token delta 合并成较大 chunk,却不能因为 buffer 满就丢掉 approval.required 或唯一的 run.completed 事实。后两者应该首先存在于 durable state/event store,stream 只是 delivery channel。

Bounded buffer 的满载策略必须显式 ​

最简单的 producer/consumer 模型:

python
queue = asyncio.Queue(maxsize=256)

真正重要的是 maxsize 到达后怎么办:

  • producer 等待:把 backpressure 传回上游;
  • coalesce:合并高频 delta/progress;
  • drop non-critical:只允许对明确可丢事件;
  • disconnect slow consumer:保护服务端资源;
  • switch to polling/resume cursor:客户端稍后按 durable event id 补关键状态。

如果 Runtime 核心 loop 被单个慢浏览器永久阻塞,也不合理。通常需要把 durable event commit 与具体 connection delivery 解耦。

Gateway Timeout 是另一条独立边界 ​

反向代理、CDN、Load Balancer 可能限制:

text
connect timeout
idle/read timeout
request duration
response buffering

应用允许 SSE 连接 30 分钟,不代表中间层也允许。连接可能在 Run 仍然 RUNNING 时被 proxy 断开。

因此协议不能建立这个错误等式:

text
stream connected == Run alive
stream disconnected == Run cancelled

Run lifetime 应由 durable Runtime policy 决定。客户端重新连接后,通过 run_id + last_event_id/cursor 查询当前状态或继续消费事件。

Heartbeat 解决 idle,不解决所有长连接问题 ​

定期 heartbeat/comment 可以避免某些 idle timeout,也帮助客户端发现连接失活;但它不能保证所有 gateway 都允许无限 request duration,也不能解决代理 buffering 配置。

如果基础设施天然不适合超长流,设计应允许:

text
POST /runs → 202 + run_id
GET /runs/{id}
GET /runs/{id}/events (SSE, reconnectable)

而不是要求创建 Run 的原始 HTTP request 一直不结束。

Backpressure 还要跨到 Event Store 设计 ​

若客户端断开后需要补事件,事件必须有稳定顺序/cursor,例如:

text
run_id
event_seq
event_type
payload/ref
created_at

但不必把每个 token delta 都永久存库。可以持久化状态变化和重要 artifact,瞬时 streaming delta 只用于体验。这样 durable storage 不会因为 token stream 爆炸,又能保证恢复时不会丢关键事实。

怎么验证 ​

用一个故意很慢的 consumer,把读取速度降到 producer 的 1/20,持续运行足够长时间,断言:内存/buffer 有上限;关键 terminal/approval 事件仍可通过 durable API 获得;可压缩 delta 被合并;断开连接后 Run 按 policy 继续。

再在测试/预发环境设置短 gateway idle timeout,确认 heartbeat 或 reconnect path 有效。真正需要验收的是“连接可以断,Run 事实不能乱”。

自测 ​

  1. Producer 每秒产生 1000 个 event,客户端只能消费 100 个。没有 backpressure 时内存和 latency 会怎样变化?
  2. Gateway timeout 与后端 Run 仍在正常执行可以同时发生。系统应怎样避免把网关超时误记成 Run 失败?

Sources ​

AI Engineering · Agent · Harness · Backend Systems