用 Nginx 反代 WebSocket,连接总是 60 秒断一次:一次按图索骥的排查

举报
yd_276750186 发表于 2026/11/03 15:51:53 2026/11/03
【摘要】 前阵子把一个小工具改成前端配合后端的长连接,本地跑得好好的,部署上去以后换了个毛病:页面不动的时候正常,一到要推消息就断,每次都是连上差不多一分钟,控制台飘一行 WebSocket connection closed: code 1006。

前阵子把一个小工具改成前端配合后端的长连接,本地跑得好好的,部署上去以后换了个毛病:页面不动的时候正常,一到要推消息就断,每次都是连上差不多一分钟,控制台飘一行 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 秒的已知行为。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。