Nginx WebSocket 与 SSE 并存:proxy_buffering 冲突排查

    发布时间:2026-09-22 12:30 更新时间:2026-09-22 12:30 阅读量:0

    很多站长会在同一个站点里同时用上 WebSocket 和 SSE:聊天、通知走 WebSocket,进度推送、日志滚动用 SSE。前端联调时一切正常,上线到 Nginx 反向代理后面却出现怪现象——SSE 的事件不是实时到达,而是一批批涌出来;或者 WebSocket 心跳正常,但同一 upstream 上的 SSE 请求把连接池占满。这类问题八成不是后端代码写错了,而是 Nginx 默认开启了响应缓冲,把本该边产生边发送的流式内容攒在内存里。本文把两类长连接的差异讲清楚,并给出排查与配置思路,具体参数含义以 Nginx 官方文档和你的实际版本为准。

    先分清 WebSocket 与 SSE 在代理层的差别

    WebSocket 走的是 HTTP Upgrade 握手,握手成功后连接从 HTTP 协议切换为全双工帧协议,Nginx 只做隧道转发,不再按 HTTP 响应体解析,所以 proxy_buffering 对升级后的 WebSocket 帧基本不起作用。真正影响 WebSocket 的是握手阶段的 Upgrade/Connection 头、proxy_read_timeout 和 keepalive 设置,这部分站内已有文章覆盖,这里不重复。

    SSE 不一样,它始终是普通 HTTP 响应,Content-Type 为 text/event-stream,响应体是持续追加的文本流。Nginx 默认 proxy_buffering on,会先把上游返回的数据读进缓冲区,攒够一定量或上游关闭响应后才转发给客户端。对于一次性的短响应,这能提升吞吐;对于永不结束的 SSE 流,等于人为制造延迟。所以同一个 server 块里两类接口并存时,必须让 SSE 路径单独关闭缓冲,而不是全局关掉——全局关闭会让普通接口的代理性能下降。

    location /ws/ {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
    }
    
    location /sse/ {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_buffering off;
        proxy_cache off;
        chunked_transfer_encoding on;
        proxy_read_timeout 3600s;
    }
    

    注意 SSE 块里把 Connection 置空,避免复用 WebSocket 那套 upgrade 语义;chunked_transfer_encoding on 是默认值,显式写出便于对照,若上游本身用 HTTP/1.1 chunked 输出,Nginx 会保持分块转发,不会因为关了 buffering 就退化成 Content-Length。

    排查步骤:从抓包到配置逐层定位

    第一步先确认问题出在 Nginx 还是后端。用 curl 直连后端端口,加 -N 关闭 curl 自己的缓冲,观察事件是否实时到达:

    # 直连后端,观察 SSE 事件间隔
    curl -N -H 'Accept: text/event-stream' http://127.0.0.1:8080/sse/progress
    
    

    再经 Nginx 访问同一路径,对比行为差异

    curl -N -H 'Accept: text/event-stream' https://example.com/sse/progress

    如果直连实时、过 Nginx 变批量,基本可以锁定缓冲问题。第二步查看响应头,确认代理层有没有改写传输方式:

    curl -sI -H 'Accept: text/event-stream' https://example.com/sse/progress
    

    关注 Content-Type 是否为 text/event-stream

    以及是否存在 Transfer-Encoding: chunked

    第三步检查 Nginx 错误日志与上游日志的时间戳是否对得上。如果 Nginx 侧记录的上游响应时间很短,但客户端收到数据很晚,说明数据卡在代理缓冲区。还可以用 ss 观察连接状态,确认长连接是否被中途断开:

    ss -tnp state established '( sport = :443 or dport = :8080 )'
    

    第四步核对是否有多层代理。CDN、云负载均衡、前置的另一台 Nginx 都可能各自开启缓冲,只改最外层往往不够,需要逐层确认,具体行为以各服务商文档为准。

    容易踩的坑与配套参数

    关闭 proxy_buffering 后,若上游产生数据的速度远快于客户端消费速度,数据会堆在 Nginx 与客户端的 socket 里,内存压力转移到 worker 进程。可以在 SSE 块里配合 proxy_max_temp_file_size 0 禁止写临时文件,并适当调小 proxy_buffers 数量,避免单个长连接占用过多内存。

    另一个常见坑是 gzip。对 text/event-stream 开启 gzip 会引入压缩缓冲,进一步破坏实时性,建议在 SSE 路径上关闭:

    location /sse/ {
        gzip off;
        proxy_buffering off;
        proxy_cache off;
        add_header X-Accel-Buffering no;
    }
    

    add_header X-Accel-Buffering no 是给后端应用的提示,某些语言框架会读取该头决定是否自行缓冲;真正生效的仍是 Nginx 的 proxy_buffering 指令。此外,SSE 依赖长连接,要确保 proxy_read_timeout 足够大,并在后端定期发送注释行心跳,否则空闲连接可能被中间设备回收。

    最后提醒一点:不要为了省事在 http 或 server 层全局关闭 proxy_buffering。普通动态接口失去缓冲后,慢客户端会拖住上游连接,反而更容易引发 upstream 超时。正确做法是按 location 精确区分路径,让长连接流式转发、短请求正常缓冲。改完配置用 nginx -t 校验再 reload,然后分别用浏览器和 curl 复测 WebSocket 与 SSE,确认两类连接互不干扰。若仍有个别延迟,优先怀疑上游框架自身的输出缓冲和中间链路,而不是继续堆 Nginx 参数。

    继续阅读

    📑 📅
    Linux磁盘IO高却查不出:iotop与blkio限速实战 2026-09-22
    服务器被DDoS打满带宽:自查连接分布与限速缓解 2026-09-22
    Nginx 代理 WebSocket 频繁断连:Upgrade 头、超时与保活配置 2026-09-22
    systemd timer 替代 crontab 实战:OnCalendar、随机延迟与失败重试 2026-09-22
    Docker容器时间漂移与crond定时任务错乱:TZ与宿主时间源协同 2026-09-21
    MySQL备份文件损坏怎么验证:一致性校验与恢复演练 2026-09-22
    Linux 改完 fstab 重启起不来:UUID 混用与救援恢复 2026-09-22
    Nginx 反代下 Cookie 域与 Path 错乱排查实战 2026-09-23
    MySQL单表过亿后的分页优化:延迟关联与覆盖索引 2026-09-23
    Docker 容器启动即退出排查:exit code、logs 与前台进程 2026-09-23