DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

Nginx 代理 SSE 后消息成批到达,直到连接结束才显示

SSE 直连及时、经过 Nginx 后积压时,按应用输出、事件分隔和代理响应缓冲逐层定位;只调整事件流端点,并区分请求缓冲与空闲超时。

SSE 接口直连应用时能逐条收到消息,经过 Nginx 后却长时间没有输出,随后一次出现多条,甚至等连接结束才全部出现。连接仍然保持、应用也持续产生事件时,需要找出响应在哪一层开始积压。

下面针对 Nginx 用 proxy_pass 转发 HTTP 应用的场景。PHP-FPM 的 fastcgi_pass、uWSGI 和 Kubernetes Ingress 有各自的配置入口,不能直接套用这里的 proxy_* 指令。

先把浏览器、应用和代理分开

在有权限访问应用的测试环境,用同一个 SSE 路径分别请求应用和 Nginx 入口。将地址换成实际测试地址;需要登录的接口还要使用相同身份,避免把登录页面当成事件流。

curl -N -i http://127.0.0.1:8080/events
curl -N -i https://events.example.com/events

-N 关闭 curl 自己的输出缓冲,让终端尽量及时显示收到的内容。两条命令可在两个终端运行,观察同一批测试事件;结束时按 Ctrl+C。预期响应应为 200,内容类型为 text/event-stream,并持续出现事件内容。不要只用 curl -I 检查响应头,它不能显示事件是否按时到达。

如果直连也成批出现,先处理应用、框架或应用服务器的输出缓冲,确保生成事件后确实把数据刷新到连接。Nginx 无法提前发送自己尚未收到的数据。如果 curl 已逐条显示,浏览器却没有触发回调,检查事件格式:例如发送的字符串 data: ready\n\n 末尾需要空行;只写一行但没有事件分隔空行,浏览器会继续等待。这里的 \n 表示真正的换行,不是两个字面字符。

只有直连及时、经过代理后才积压时,才继续缩小到 Nginx 及其前后的代理层。存在 CDN 或第二层反向代理时,应逐跳比较,不能把所有延迟都归给第一台 Nginx。

在 SSE 路径关闭响应缓冲

proxy_buffering 默认开启。Nginx 会把上游响应放进缓冲区,必要时写入临时文件;关闭后则随接收过程向客户端传送。对 SSE,通常只需要调整事件流路径,不必改变整个站点。

下面是放进现有 server 块的示例。它假设应用在 127.0.0.1:8080 提供 /events,并且该路径已经具备所需认证。若已有对应 location,修改原块,不要再添加一个冲突的块。

location = /events {
    proxy_pass http://127.0.0.1:8080;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header Connection "";
    proxy_buffering off;
    proxy_cache off;
}

proxy_pass 没有附加 URI,因此保留 /events 请求路径。proxy_cache off 只在这个端点明确不使用代理缓存,并覆盖上层继承的缓存配置。缓存与响应缓冲是两项不同机制。

示例显式使用 HTTP/1.1 转发,并清空发给上游的 Connection 头。它控制 Nginx 到应用这一段,不会把浏览器到 Nginx 的 HTTP/2 连接改为 HTTP/1.1。不要根据旧配置教程推断自己安装版本的默认协议。

还需保留原来必要的代理头和访问控制。当前层一旦出现 proxy_set_header,就不再继承上层整组 proxy_set_header;若应用依赖上层设置的外部协议或认证相关请求头,应把所需设置完整保留在这个块中。不要为了复制示例删掉原有鉴权规则。

修改前保存当前站点配置,在对应实例执行配置检查,通过后按该实例的管理方式重载。常规 Nginx 命令示例为:

nginx -t
nginx -s reload

两条命令按顺序执行,检查失败时不要重载。容器或非默认配置文件的部署,应在正确实例内使用相应参数,避免检查的是另一份配置。

应用也可以只关闭这一条响应的缓冲

若同一路径既有普通响应又有事件流,应用可以在 SSE 响应开始前设置下面的响应头,让 Nginx 按该响应关闭缓冲:

Content-Type: text/event-stream
X-Accel-Buffering: no

这里必须是上游应用返回给 Nginx 的响应头。把它放进浏览器请求,或只用 Nginx 的 add_header 附加到发给浏览器的响应,都不能替代对当前代理响应缓冲的配置。无法修改应用时,使用前面的 proxy_buffering off 路径设置。

如果应用已经返回这个头,但仍无效,检查匹配的配置是否有 proxy_ignore_headers X-Accel-Buffering:它会关闭对该上游响应头的处理。修改时只调整需要改变的字段,保留同一指令里其他仍有用途的设置。Nginx 默认不向客户端转发上游的 X-Accel-* 头,因此浏览器没看到这个头,并不能证明应用没发;应在受控环境检查应用的直连响应。

proxy_request_buffering off 控制的是客户端请求体发往上游之前的缓冲,不能代替这里的响应缓冲设置。也不需要为了这一个症状把所有站点缓存、所有缓冲区或 HTTP 分块传输一并关闭。

及时到达后仍断线,要另查空闲超时

消息已经逐条到达,但长时间没有业务事件时连接断开,是另一种现象。proxy_read_timeout 默认是 60s,它限制相邻两次读取上游数据之间的等待时间,并非整条 SSE 连接最多只能持续 60 秒。

应用可在空闲期间定期发送并刷新 SSE 注释,例如字符串 : ping\n\n。注释不会触发普通消息事件,但能让连接有数据经过。心跳间隔需要短于实际链路中相关空闲超时,并留出调度余量;若数据仍被应用缓冲,生成心跳却没有发送出去也无效。确需增加读取超时时,应只调整该端点,并同时考虑长连接占用。

重新连接后,用测试事件确认消息随产生过程到达,而非集中在关闭连接后出现;再覆盖超过预期空闲间隔的场景,检查错误日志中是否还有上游读取超时。关闭响应缓冲会让上游更直接地受到慢客户端影响,发布前还应观察连接数和应用资源。出现异常时恢复保存的配置,检查通过后重载,并重新建立 SSE 连接验证恢复结果。