bigywater.tech
微服务 · 25

WebSocket 与 SSE 穿过网关的配置

WebSocket 和 SSE 穿过 nginx 要配什么?空闲多久会被断开?client 断开后上游多久发现?长连接会占掉多少网关连接?这篇用 websockets 17.2 和 nginx 1.31.6 逐项量出来。

HTTP 426
只有 proxy_pass(或只转发 Upgrade 头)时,WebSocket 握手得到的状态;完整写法才是 101
2.0 s 与 7 s
proxy_read_timeout 2 s 时,心跳间隔 2.5 s 的连接 2.0 s 就断了;0.5 s 和 1.5 s 的在 7 秒内一直保持
31 / 45
worker_connections = 64 时,同时能保持的 WebSocket 数(每个占网关两条连接),直连上游是 45 / 45

握手

实测 · 本机执行输出
  配置                | 结果                                                            | 往返中位数   | p99    
  ------------------+---------------------------------------------------------------+---------+--------
  (a) 只有 proxy_pass | InvalidStatus: server rejected WebSocket connection: HTTP 426 | -       | -      
  (b) 只转发 Upgrade 头 | InvalidStatus: server rejected WebSocket connection: HTTP 426 | -       | -      
  (c) 完整写法          | 握手成功(101)                                                     | 0.18 ms | 0.41 ms
  (d) 直连上游          | 握手成功(101)                                                     | 0.06 ms | 0.09 ms

要同时转发 Upgrade 和 Connection: upgrade,并用 HTTP/1.1;否则上游拒绝。

空闲超时

实测 · 本机执行输出
  客户端                 | 结果                 
  --------------------+--------------------
  (a) 不发任何东西          | 2.0 s 被关闭(关闭码 1006)
  (b) 每 0.5 s 一个 ping | 7 秒内一直保持           
  (c) 每 1.5 s 一个 ping | 7 秒内一直保持           
  (d) 每 2.5 s 一个 ping | 2.0 s 被关闭(关闭码 1006)
  proxy_read_timeout 管的是“两次从上游读到数据之间的间隔”:WebSocket 的 ping 帧由 client 发给上游,上游回 pong,pong 到了才算 nginx 读到数据,所以心跳间隔要比 proxy_read_timeout 短(这里 0.5 s 和 1.5 s 都小于 2 s,2.5 s 大于)

proxy_read_timeout 管两次读到数据的间隔,心跳间隔要比它短。

SSE 的断开

实测 · 本机执行输出
  路径                                      | client 关闭之后,上游发现写失败用了
  ----------------------------------------+----------------------
  直连上游                                    | 0.02 s               
  经过 nginx(默认)                            | 0.23 s               
  经过 nginx + proxy_ignore_client_abort on | 0.44 s               

client 断开后,数据在流动时上游一两个事件内就会发现,与 ignore_client_abort 的差别约一个事件。

占用的连接

实测 · 本机执行输出
  路径                              | 尝试建立 | 成功握手并保持 | 失败的类型                                              | error.log: not enough
  --------------------------------+------+---------+----------------------------------------------------+----------------------
  经过 nginx(worker_connections 64) | 45   | 31      | {'ConnectionClosedError': 11, 'InvalidMessage': 3} | 1                    
  直连上游                            | 45   | 45      | {}                                                 | -                    

每个长连接在网关上占两条连接。

分步演示

WebSocket 与 SSE 穿过网关
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。nginx 是 Homebrew 的 1.31.6;WebSocket 服务与 client 是 websockets 17.2 的同步 API(装在 ttmp_venv 里);SSE 上游是原始 TCP 服务;第四节把 worker_connections 改成 64;各个场景只测了一次(第二节的心跳间隔只取了 4 个值)
    • 没有验证:HTTP/2 上的 WebSocket(RFC 8441);WebSocket 的压缩扩展(permessage-deflate)穿过 nginx;连接升级之后的 proxy_send_timeout;大量 SSE 连接时 nginx 的内存;proxy_ignore_client_abort 在上游长时间不发数据时的表现(这里的上游每 0.2 s 都在写)
    京ICP备2026067502号-1京公网安备11010502063487号