← 中文资源

简体中文版本

从服务端视角调试 SSE 重连风暴

识别所有 EventSource 客户端同时重连的典型特征,在服务端加心跳帧,并修复客户端退避,终止空闲代理放大断连。

阅读英文原文

目标

对中间设备而言,SSE 连接看起来是空闲的:服务器只在有新消息时才说话。到第 60 秒代理就会掐断连接,所有浏览器在同一刻重连,网关遭遇被误认为流量洪峰的同步尖峰。两个修复终结风暴:心跳让流保持“可见的活跃”,带抖动的客户端退避让仍然发生的重连彼此错开。

步骤要点

  1. 把网关连接日志按分钟分桶统计:周期性尖峰(梳状图)是同步超时的签名,自然流失是平的。
  2. 在服务端 SSE 响应里每 15 秒写入一行注释心跳 “: ping”,中间设备看到活跃流量而不再判空闲。
  3. 用 curl -N 验证心跳确实逐秒到达、连接能活过之前致命的 60 秒。
  4. 客户端 EventSource 的 onerror 里做指数退避加随机抖动(500ms×2^n,封顶 30 秒),服务重启后重连散布在几十秒内。
  5. 两个修复上线后重跑分钟分桶统计,“每分钟新连接超 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;