KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
01 · 选型:什么时候用 SSE 而不是 WebSocket — keel 龙骨
这一章回答:四种实时通道各自的成本与失效模式,以及一个可以直接用的决策树。
这一章回答:四种实时通道各自的成本与失效模式,以及一个可以直接用的决策树。
"实时推送"有四种常见做法,选错的代价不是性能差一点,而是在某个环节彻底不可用(例如 WebSocket 被企业代理拦掉、轮询把数据库压垮)。先分清它们的性质。
一、四种通道
| 通道 | 方向 | 协议 | 开销 | 断线恢复 | 典型失效 |
|---|---|---|---|---|---|
| 短轮询 | 客户端拉 | HTTP | 每次完整请求头 | 天然无状态 | QPS 与延迟成正比,空转率高 |
| 长轮询 | 客户端拉 | HTTP | 一次请求挂起直到有数据 | 需自己带游标 | 代理超时、连接数、服务端挂起成本 |
| SSE | 服务端推 | HTTP(长连接) | 一个连接 + 文本帧 | 协议内建(Last-Event-ID) |
代理缓冲、压缩、空闲超时 |
| WebSocket | 双向 | WS(升级握手) | 一个连接 + 二进制/文本帧 | 需自己实现 | 代理/防火墙不支持升级、负载均衡需额外配置 |
二、SSE 的真实优势与真实限制
优势(这些是实打实的工程收益):
- 就是普通 HTTP:不需要协议升级,穿过企业代理、CDN、API 网关的成功率远高于 WebSocket;
- 断线重连是协议自带的:浏览器
EventSource会自动重连并带上Last-Event-ID,服务端只要支持续播即可(第 04 章); - 文本协议可调试:
curl就能看流,tcpdump/日志可读,不需要专门工具; - 与 HTTP 基础设施复用:鉴权、限流、日志、追踪都在原有的 HTTP 中间件里。
限制(这些会直接约束设计):
- 单向:服务端 → 客户端。客户端要发数据必须另开普通请求(Agent 场景通常正好:发起是一次 POST,回传是一条 SSE);
- 只能是 GET,且浏览器
EventSource不能自定义请求头 → 鉴权要用 Cookie、query 参数或一次性 ticket(第 08 章); - HTTP/1.1 下同域最多 6 条连接(HTTP/2 下复用同一连接,限制解除)→ 多开几个 SSE 会挤占其它请求;
- 只能传文本(二进制要 base64)。
三、决策树
需要客户端高频向服务端发数据(如协作编辑、游戏)?
├─ 是 → WebSocket
└─ 否(服务端单向推为主)
├─ 数据是偶发的、允许秒级延迟?(如消息红点)
│ └─ 用轮询或长轮询,别上长连接(长连接是持续成本)
└─ 需要持续、低延迟的单向流(token 流、进度、通知)
├─ 环境里有没有会拦 WebSocket 的代理/网关? → 有 → SSE
├─ 需不需要浏览器自动重连? → 需要 → SSE
├─ 需不需要传二进制? → 需要 → WebSocket
└─ 其余情况 → SSE(默认选择,成本最低)
默认选 SSE,只在明确需要双向或二进制时才上 WebSocket。这条建议的理由不是"SSE 更高级",而是它的失败模式更少、可观测性更好、与现有 HTTP 基建零摩擦。
四、成本对比:长连接不是免费的
一条长连接占用的资源(这是容量规划的基础,第 06 章会展开):
| 资源 | 每条连接的占用 |
|---|---|
| 服务端连接/文件描述符 | 1 |
| 后端 worker / 协程 | 1(异步下是协程,仍有栈与状态开销) |
| nginx 连接 | 1(上游 + 下游各一条) |
| 内存 | 几 KB ~ 几十 KB(缓冲、上下文) |
| 数据库连接(如果连接期间持有) | 可能是 1 ——这是最危险的 |
最后一行是关键:如果 SSE 处理函数里持有一个数据库事务或连接,1 万条长连接 = 1 万个数据库连接,系统会在几分钟内崩溃。这也是第 03 章要强调的"推数据时不要持有连接"。
五、与业务形态的匹配
| 业务形态 | 推荐 | 理由 |
|---|---|---|
| 大模型 token 流 | SSE | 天然单向、需要断线续播、文本 |
| Agent 中间过程(思考/工具调用) | SSE + 结构化事件 | 多类型事件,event: 字段天然支持 |
| 后台任务进度 | SSE(短生命周期)或轮询 | 任务短的话轮询更简单 |
| 通知/红点 | 轮询 或 SSE | 量小就别上长连接 |
| 实时协作/白板 | WebSocket | 双向、高频、低延迟 |
| 音视频信令 | WebSocket | 双向且需要即时往返 |
六、什么时候不该用长连接
三条判断:
- 数据更新频率低于分钟级 → 轮询。长连接要持续付出心跳、连接、监控成本;
- 用户可能长时间挂在页面上但不需要实时数据 → 用可见性/活跃度判断,页面隐藏时断开(省资源);
- 一次性的结果 → 直接用普通请求 + 前端 loading,不要为了"看起来高级"上 SSE。
反模式:为了展示技术能力给所有接口套 SSE。长连接是一种负债,只有持续的数据流才值得为它付费。
动手:可观察结果
| 产出 | 判断标准 |
|---|---|
| 一份选型决策记录 | 对一个具体场景,用决策树给出结论并写明被否决方案的原因 |
| 四种通道的最小实现对比 | 同一个"进度推送"场景各写一份,对比代码量与失败模式 |
| 一次 WebSocket 被拦的验证 | 在有代理/网关的环境验证握手失败,确认 SSE 可穿透 |
| 连接成本测算 | 列出单条连接占用的 fd / 协程 / 内存,估算 1 万连接时的总量 |
完成标志:给你一个"要实时"的需求,你能在 5 分钟内说出用哪种通道、为什么,以及它在什么条件下会失效。
故障注入
| 注入方式 | 观察 |
|---|---|
| 用 HTTP/1.1 同时开 7 条 SSE | 第 7 条是否阻塞,其它请求是否被饿死 |
| 在支持 WebSocket 的环境禁用协议升级 | WS 握手是否失败,SSE 是否仍可用 |
| 把数据频率降到 5 分钟一次仍用 SSE | 心跳与连接成本是否远超收益 |
| SSE handler 里持有一个数据库事务 | 并发 100 条连接时数据库连接是否被耗尽 |
| 关掉心跳让连接空闲 5 分钟 | 负载均衡/代理是否主动断开 |
自测题
- SSE 相比 WebSocket 的三个工程优势是什么?分别对应什么失效场景?
- 浏览器的
EventSource有哪三条限制?每条会如何约束你的设计? - 什么情况下应该坚持用轮询而不是上长连接?给出两条判据。
- 一条 SSE 长连接会占用哪些资源?其中最危险的一项是什么?
- 你的项目里哪些场景适合 SSE、哪些不适合?各举一例并说明理由。