Nginx 反向代理 WebSocket 频繁断连:一次 400 与 1006 的定位过程
背景
最近把一个内部消息推送服务从 HTTP 轮询改成了 WebSocket。服务在本地用 wscat 连一切正常,一挂到 Nginx 后面就出现两种现象:一部分握手直接返回 400,另一部分能连上,但十几秒后无故断开,客户端报 1006。折腾了一下午,问题都出在代理层的三个配置细节上,记录如下。
一、400 的根因:Upgrade 头没有被透传
Nginx 默认以 HTTP/1.0 向后端转发,并且不会自动带上 Connection 与 Upgrade 这两个逐跳(hop-by-hop)头。后端收到的是一个普通 GET 请求,自然拒绝升级,直接回 400。
正确的写法是三个条件同时成立:
location /ws/ {
proxy_pass http://127.0.0.1:9000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
proxy_http_version 必须显式写成 1.1;Upgrade 要透传客户端带来的值;Connection 必须改写成 upgrade。少任何一个,要么 400,要么握手成功后立刻被关闭。
二、连上就断:读超时与心跳
第二个现象更隐蔽。抓包能看到服务端每 60 秒发一次 ping,但 Nginx 恰好在 60 秒整断开连接。原因是 proxy_read_timeout 默认 60 秒,而它计算的是两次读操作之间的间隔——如果业务长时间没有下行消息,即使 TCP 连接还活着,也会被判定为超时。
两种处理方式:
- 把 proxy_read_timeout 提高到 300 秒以上;
- 更稳妥的是让应用层带心跳,间隔明显小于超时时间,例如 25 秒一次 ping/pong。
我选了第二种,同时把超时设成 120 秒兜底。心跳还有一个额外好处:能更早发现中间链路被 NAT 或负载均衡回收的情况,而不是等用户反馈消息收不到。
三、容易被忽略的 buffering
proxy_buffering 默认开启,Nginx 会先把后端响应攒进缓冲区再发给客户端。对 WebSocket 这种长连接、小包、低延迟的场景,缓冲会带来可感知的延迟,个别版本上还会让帧被合并,前端解析出错。
proxy_buffering off;
proxy_cache off;
这两项建议一起关掉。顺带一提,如果前面还有一层 CDN 或 SLB,也要确认它支持 Upgrade 并且没有开启压缩缓冲,否则同样会在中间层断掉。
四、上线后的验证清单
- 用 curl 带上 Connection: Upgrade 与 Upgrade: websocket 请求一次,确认返回 101;
- 观察 error.log 中是否还有 upstream prematurely closed connection;
- 客户端记录 close code:1006 多为链路被断,1011 多为服务端异常;
- 用 ss -tnp 看连接是否稳定处于 ESTAB,而不是反复重建。
小结
WebSocket 走 Nginx,本质是把一条长连接交给一个为短连接设计的代理。协议版本、逐跳头、超时与缓冲这三件事必须显式声明,不能依赖默认值。改完这三处之后,之前偶发的 400 和 1006 都没有再出现。
- 点赞
- 收藏
- 关注作者
评论(0)