用 Nginx 反代 WebSocket,连接总是 60 秒断一次:一次按图索骥的排查
前阵子把一个小工具改成前端配合后端的长连接,本地跑得好好的,部署上去以后换了个毛病:页面不动的时候正常,一到要推消息就断,每次都是连上差不多一分钟,控制台飘一行 WebSocket connection closed: code 1006。
1006 是个很讨厌的错误码。按 WebSocket 协议的规定,1006 表示"异常关闭",而且浏览器被明确要求不能把这个原因的细节告诉 JS——它怕你拿它去探测内网端口。所以你打开 onclose、打印 event.reason,拿到的永远是一个空字符串。真正的原因只能从别处找。
一、先确认断的是哪一层
我的链路是:浏览器 → Nginx → 后端。三层里任何一层都可能先走。别急着改配置,先分层卡点:
1. 在服务器上直接连后端端口(绕过 Nginx),观察同样闲着一分钟会不会断;
2. 在服务器上直接 curl Nginx 的 WebSocket 路径,用 --http1.1 加 Upgrade 头握手,看能不能进去;
3. 用 tcpdump 或浏览器网络面板看断开那一刻是谁先发的 FIN。
我这一步就直接定位到了:绕开 Nginx 直连后端,闲一分钟不断。那问题就在中间这一层。
二、Nginx 的默认读超时是 60 秒
这是我这次真正的坑。Nginx 做反向代理时,有一个参数叫 proxy_read_timeout,它的默认值是 60s,含义是"从被代理方两次读操作之间最多允许空闲多久"。WebSocket 握手成功之后,TCP 连接就进入长时间空闲的常态——没人说话的时候,连接上是一个字节都不流动的。Nginx 看不到任何数据,60 秒一到就认为对端没响应,主动把连接关掉。
这个数字比"随机断线"好认得多,因为它是固定的。如果每次断开的时间点都差不多,先怀疑超时而不是网络抖动。
配上这几个:
location /ws/ {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 900s;
proxy_send_timeout 900s;
}
这里有三处容易被漏掉的:
- proxy_http_version 1.1 不写,默认走 1.0,握手根本升不上去,表现是连不上而不是断线;
- Upgrade 和 Connection 两个头必须成对出现,只设一个等于没设;
- 只调大超时没有替代心跳。超时调大只是把问题推到 15 分钟后,TCP 连接经过家用路由、运营商 NAT、云厂商负载均衡时,任何一环的空闲回收都可能把它干掉,而且这一环你控制不了。
三、加了一层 CDN 之后,又断在 100 秒
改完 Nginx 以为收工,结果第二天变成 100 秒断。这次连服务器都没动,变量是前面挂了一层 CDN——它自己的空闲超时是 100 秒,而且这个值不在我的配置文件里。
这是排查这类问题最容易踩的误区:你以为的链路只有你配过的那几段。 公网上的长连接经过的每一跳(负载均衡、WAF、CDN、运营商)都有自己的空闲策略,挨个去改是不现实的,有些根本不给你改。
正确做法是把"保持连接"的责任交回应用层,协议里本来就有现成的机制:
- 服务端或客户端每 20~30 秒发一次 ping / 应用层心跳;
- 心跳间隔要小于链路上最短的那个超时,否则心跳本身就被超时切了;
- 心跳里带个自增序号或时间戳,断线重连后能判断"我错过了多少条"。
我的心跳设成 25 秒,之后再也没有固定时间的断开。Nginx 那边的 proxy_read_timeout 保持 900s 只是兜底,真正让连接活下来的是每 25 秒一个字节。
四、顺手记下来的几个小结论
1. 1006 看不到原因,不要在这上面浪费时间,直接去分层排查。
2. 固定周期断开 = 超时;随机断开 = 网络或进程崩溃。这个区分能省掉一大半排查。
3. proxy_read_timeout 管的是无数据的时长,不是整个连接的总时长,所以调大了不会导致连接被无限拖住。
4. 心跳从客户端发比从服务端发更可靠。有讨论区的作者实测过:客户端每 5 秒 ping 一次能稳定保活,而服务端单向推的方案在某些中间设备上仍然会被丢。
5. 断线不要当作异常处理,当作正常路径处理:留重连、留退避(1s、2s、4s…),否则服务器一抖动会被所有客户端同时重连打垮。
最后一句:这套排查思路不限于 WebSocket。任何"连上了、过一会儿就没了"的长连接——SSH 会话、数据库连接池的空闲连接、gRPC 流——背后都是同一件事:某个中间层的空闲计时器先到点了。找那个计时器,比换库换框架有用。
参考:nginx proxy_read_timeout 默认 60s 官方文档;Stack Overflow 上关于 close code 1006 的多条实证讨论;Cloudflare 默认 WebSocket 空闲 100 秒的已知行为。
- 点赞
- 收藏
- 关注作者
评论(0)