一条 SSE 如何穿过多层网关还不卡顿
AI 对话、告警推送、长任务进度,这类场景常用 SSE。链路一旦变成「浏览器 → Nginx → Gateway → BFF → 业务服务」,任一层默认缓冲都会把「流式」攒成「最后一次性吐出」。下面记一遍怎么把 SSE 真的流起来。
一、SSE 的基本帧格式
SSE 是单向长连接,服务端按帧推送:
1 | event: token |
帧与帧之间空行分隔。浏览器端用 EventSource,或 fetch + ReadableStream,都能逐帧解析。
二、典型链路
sequenceDiagram
participant B as Browser
participant N as Nginx
participant G as API Gateway
participant F as BFF
participant S as Service
B->>N: GET /api/stream (Accept text/event-stream)
N->>G: proxy_pass
G->>F: 转发
F->>S: 上游 SSE/内部流
S-->>F: chunk
F-->>G: chunk
G-->>N: chunk
N-->>B: chunk
Note over N,G: 任一层 buffering 都会攒成包
读图: 浏览器到业务服务中间有多层代理;任一环缓冲,整条链路就不再「逐帧到达」。
三、缓冲问题定位
flowchart TB
S[症状: 等很久突然全出来] --> R{逐层排查}
R --> N1[Nginx proxy_buffering?]
R --> G1[Gateway 是否 await 完整响应?]
R --> F1[BFF 是否 pipe/流式?]
R --> P1[公司代理/HTTPS中间盒?]
N1 -->|开| Fix1[proxy_buffering off]
G1 -->|是| Fix2[改为流式转发]
F1 -->|否| Fix3[用 ReadableStream pipe]
P1 -->|是| Fix4[绕过或加心跳]
四、各层配置要点
Nginx
1 | location /api/stream { |
Gateway(Spring Cloud Gateway 示意)
flowchart LR
A[请求进入] --> B{是否 SSE?}
B -->|是| C[流式转发 不缓冲 body]
B -->|否| D[正常路由]
C --> E[保持连接 透传 chunk]
关键:不要把上游 body 读成完整 String 再转发,用响应式/流式 API 透传。
BFF(NestJS 示意)
1 | ('/stream') |
不要 await 上游成完整字符串再 res.send,用 pipe / streaming response。
浏览器
1 | // fetch 流式读,便于带 Authorization |
五、心跳保活
长时间无数据的连接,容易被中间设备掐掉。服务端定时发注释行(不触发事件):
1 | : keep-alive |
浏览器 EventSource 会忽略注释行,连接仍保持活跃。间隔建议 15–30 秒。
六、鉴权怎么带
flowchart TB
A[SSE 鉴权] --> B{方案选择}
B --> C[短时 ticket 放 query<br/>一次性 短TTL]
B --> D[fetch 流式读<br/>带 Authorization Header]
C --> E[服务端校验后销毁 ticket]
D --> F[标准 Bearer Token]
不要把长期 Refresh Token 塞进 URL——访问日志、浏览器历史都会留痕。
七、断线重连
stateDiagram-v2
[*] --> 已连接
已连接 --> 断开: 网络抖动/超时
断开 --> 重连中: 自动重连
重连中 --> 已连接: 成功(带 Last-Event-ID)
重连中 --> 重连中: 失败(指数退避)
已连接 --> [*]: 主动关闭
EventSource原生支持Last-Event-ID,服务端可据此续传fetch方案需自己维护游标,断线后带上cursor参数
八、排障口诀
| 症状 | 优先怀疑 |
|---|---|
| 等很久突然全出来 | 某层 buffering |
| 一分钟断 | 空闲超时 / 无心跳 |
| 只自己环境不行 | 公司代理或 HTTPS 中间盒 |
| 多实例偶发丢 | Sticky / 连接打到不同副本 |
| 前几帧正常后面断 | 响应被提前 close |
九、小结
SSE「卡顿」多半不是业务模型慢,而是 代理把流攒成了包。从最外层 Nginx 往内逐层确认「不缓冲、可心跳、超时匹配」,通常就能恢复逐 token / 逐事件的体验。