Nginx 反向代理下 WebSocket 连接频繁断开的定位与修复
一、先分清是哪一层断开
WebSocket 断连的 close code 是最直接的分层依据:
1000:正常关闭,业务主动调用 close,属于预期行为。1001:对端离开,通常是浏览器刷新、页面跳转或进程重启。1006:非正常关闭,连 close 帧都没收到,绝大多数是中间代理把连接掐断了。
如果日志里 1006 密集出现且间隔接近某个固定秒数(比如 60 秒),那基本可以锁定是 Nginx 的 proxy_read_timeout 在起作用,先不用去查业务心跳逻辑。
二、必须配对的三个头
WebSocket 的握手依赖 HTTP/1.1 的协议升级机制。Nginx 要正确转发,下面两行缺一不可:
proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";
最常见的问题是只配了 Upgrade 忘了 Connection,或者 proxy_http_version 仍是默认的 1.0。此时握手会以普通 HTTP 请求的形式转发给后端,后端返回 200 但连接无法升级,表现为「连上立刻断」。
另一个高频坑是 Connection 写死成 "upgrade",导致普通 HTTP 请求也被加上 upgrade 头。正确写法是用 map 变量按请求动态取值:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
proxy_set_header Connection $connection_upgrade;
三、超时参数怎么设
代理层有两类超时会杀掉长连接:
proxy_read_timeout:后端在设定时间内没有向 Nginx 发送任何数据,Nginx 主动断开。默认 60 秒,这是 1006 断连的头号原因。proxy_send_timeout:Nginx 向后端写数据的超时,一般不容易触发。
如果业务本身没有心跳,要和后端确认心跳间隔,代理超时必须大于心跳间隔,通常按 2~3 倍留余量。比如心跳 30 秒,proxy_read_timeout 至少设 90s,长连接网关场景常见设置为 3600s。
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
}
注意 proxy_buffering off:开启缓冲会让实时消息被攒着再发,对聊天、行情推送这类场景会造成明显延迟。
四、容易被忽略的另外三处
- 上游负载均衡的空闲超时。ELB/CLB 一般默认 60 秒空闲断开,且它和 Nginx 的超时是串联关系,两边都要改。若用的是云上弹性负载均衡,需在监听器里显式调大空闲超时时间。
- 多节点下的会话粘滞。WebSocket 握手后连接固定落在某个后端节点,如果中间层做了轮询转发而后端又用本地内存存会话状态,重连时会出现「状态丢失」。要么开会话保持,要么把状态外置到 Redis。
- 客户端重连策略。代理层的断连无法完全避免,客户端必须实现带指数退避 + 抖动的重连,避免服务端重启瞬间所有客户端同时重连造成雪崩。
五、验证方法
改完不要只看「好像没断」,要量化验证:
- 用
wscat -c ws://host/ws保持连接,静置超过原超时时间(比如 90 秒)后仍可收发,说明超时已生效。 - 抓包确认握手响应是
101 Switching Protocols,而不是 200。 - 压测时统计 1006 的出现频率,目标是降到 0;残留的少量断连多为网络抖动,应由客户端重连兜底。
小结
WebSocket 频繁断连的排查顺序是:先看 close code 分层 → 检查 Upgrade/Connection 三个头是否配对 → 把代理与负载均衡的空闲超时调到大于心跳间隔 → 最后补客户端退避重连。绝大多数线上问题在前两步就能解决,参数照抄但没理解心跳与超时的关系,才是这类故障反复出现的根源。
- 点赞
- 收藏
- 关注作者
评论(0)