目标
对中间设备而言,SSE 连接看起来是空闲的:服务器只在有新消息时才说话。到第 60 秒代理就会掐断连接,所有浏览器在同一刻重连,网关遭遇被误认为流量洪峰的同步尖峰。两个修复终结风暴:心跳让流保持“可见的活跃”,带抖动的客户端退避让仍然发生的重连彼此错开。
步骤要点
- 把网关连接日志按分钟分桶统计:周期性尖峰(梳状图)是同步超时的签名,自然流失是平的。
- 在服务端 SSE 响应里每 15 秒写入一行注释心跳 “: ping”,中间设备看到活跃流量而不再判空闲。
- 用 curl -N 验证心跳确实逐秒到达、连接能活过之前致命的 60 秒。
- 客户端 EventSource 的 onerror 里做指数退避加随机抖动(500ms×2^n,封顶 30 秒),服务重启后重连散布在几十秒内。
- 两个修复上线后重跑分钟分桶统计,“每分钟新连接超 100”的分钟数应归零。
排错提示
有心跳仍掉线,是中间代理在缓冲整个响应而不是流式转发,用 curl -N 确认字节是否增量到达并关闭代理缓冲;客户端不认 Last-Event-Id 时,服务端必须每条事件带 id 字段并支持从偏移恢复,否则客户端全量重放会放大负载;退避设了上限就不会永久失联,同时监听网络恢复事件让醒着的客户端立即重连。
适用边界
心跳以少量常驻带宽换取连接稳定,间隔要小于最严格的中间设备超时;只修服务端时行为不端的第三方客户端仍能制造风暴,需对新 SSE 连接加速率限制兜底。
代码示例
// Server: keep the stream alive for intermediaries
setInterval(() => res.write(": ping\n\n"), 15000);
// Client: stagger reconnects after a drop
const delay = Math.min(30000, 500 * 2 ** attempt) + Math.random() * 1000;