为什么SSE模式需要考虑代理的穿透情况

涉及到 SSE 连接的一个核心矛盾:SSE 是长连接,而 HTTP 基础设施默认假设连接是短暂的

问题的根源在于:代理是为短请求-响应设计的,SSE 则是持久的单向流。两者在几个关键假设上正面冲突。

问题一:缓冲(最常见的坑)

Nginx 默认开启 proxy_buffering,其逻辑是先把后端响应缓存在内存/磁盘里,等完整响应到达后再转给客户端——这对普通 API 是性能优化,对 SSE 则完全破坏了流式体验。用户会感知到"思考了很久,然后一次性输出全部文字"。

问题二:超时(idle timeout 与长连接)

这里有两个不同的超时机制叠加:proxy_read_timeout 是代理等待后端"下一个字节"的时限——LLM 在复杂推理时可能几秒内不产出任何 token,代理误判为后端无响应而断连。keepalive_timeout 则是 TCP 层的空闲连接回收,与应用层无关。

SSE 协议有内置的心跳机制应对这个问题——服务端可以定期发送注释行 : ping 加双换行,这对客户端无意义但能刷新代理的 idle 计时器。

问题三:协议升级与 HTTPS 终止

代理在 SSE 路径上实际上做了多件事:TLS 终止(解密后再加密)、协议版本转换(HTTP/2 到 HTTP/1.1 或反向)、以及响应头的增删改。每一环都可能引入问题——比如某些代理会在协议降级时把 Transfer-Encoding: chunked 错误处理成固定长度响应,导致流被截断。

各层代理的具体行为差异

组件默认行为SSE 所需配置
Nginxproxy_buffering on,60s read timeoutproxy_buffering off + proxy_read_timeout 300s
AWS ALB60s idle timeout调大 idle timeout,或服务端发心跳
Cloudflare100s 无活动超时,免费版不支持流式企业版 + 禁用 Polish/Rocket Loader
Kubernetes Ingress依赖底层 Nginx/Envoy 配置annotations 中设置 buffering 和 timeout
CDN(通用)默认缓存 text/event-stream 内容响应头加 Cache-Control: no-store

根本矛盾总结

代理的存在意义是"聚合、优化、保护"——它假设响应是有限的、可缓存的、可以被整体检查的。SSE 则要求"连接越透明越好"——数据不应被持有、超时应当宽松、帧边界不能被破坏。

这不是 SSE 的缺陷,而是两种设计哲学的摩擦。解决路径有两个方向:要么把代理配置成"透明管道"(buffering off + 宽超时 + 心跳),要么升级到 HTTP/2 或 WebSocket,让协议本身的流式语义更强,减少对应用层约定的依赖。