websocket-disconnect

WebSocket 连接在固定时间后被断开,客户端反复重连,close code 多为 1006。

现象

连接在空闲约 30/60/350 秒后断开,浏览器或客户端自动重连;DevTools 显示 close code 1006(异常关闭,未收到 close frame)。Nginx 日志出现 upstream prematurely closed connection,后端可能完全没有收到关闭原因。

快速定位

先确认断开时间是否固定,以及哪一层先发 FIN/RST:

websocat -v --ping-interval 20 wss://app.example.com/ws
tcpdump -i any -nn 'tcp port 443 and (tcp[tcpflags] & (tcp-fin|tcp-rst) != 0)'
ss -tnp '( dport = :443 or sport = :443 )' | grep -i estab
conntrack -L -p tcp --dport 443 | head

再看 Nginx 的超时与 Upgrade 配置:

grep -E 'upstream prematurely closed|client prematurely closed|timed out' /var/log/nginx/error.log | tail
nginx -T | grep -E 'proxy_read_timeout|proxy_send_timeout|keepalive_timeout|proxy_http_version|Upgrade'

正确的代理配置至少包含:

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;

Kubernetes Ingress 需要同步注解:

nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"

云 LB 的 idle timeout 常是元凶:ALB 默认 60 秒、NLB 与 NAT 网关常见 350 秒。可用 aws elbv2 modify-load-balancer-attributes --load-balancer-arn <arn> --attributes Key=idle_timeout.timeout_seconds,Value=3600 调整。

易误判:TCP keepalive 不能保活 HTTP/WebSocket 连接 —— LB 的空闲计时看应用数据,keepalive 探测常常不被转发。close code 1006 只表示“没收到 close frame”,不能据此判断是客户端还是服务端主动断开;要看 FIN 方向和中间层日志。固定 60 秒断开不一定是 Nginx,也可能是 ALB 默认 idle timeout;固定 350 秒更像 NLB/NAT。

常见根因

  1. LB/Nginx 的 idle timeout 或 proxy_read_timeout 小于心跳间隔,空闲连接被关闭。
  2. 应用没有 WebSocket ping/pong 心跳,或心跳间隔大于中间层空闲超时。
  3. 云 NAT/conntrack 空闲超时静默丢流,两端 socket 仍显示 ESTABLISHED。
  4. Nginx 缺少 Upgrade/Connection 头或 proxy_http_version 1.1,升级失败或行为异常。
  5. 后端发布、重启或 Pod 驱逐,未发送 close frame,客户端看到 1006。
  6. 服务端事件循环阻塞或写缓冲满,心跳没有按时发出。
  7. 客户端网络切换(Wi-Fi/移动网络)、代理或防火墙的空闲策略。
  8. 服务端 read deadline/idle timeout 设置过短,或 TLS 会话异常。

修复

相关命令

读完自测

  1. 为什么设置 TCP keepalive 不能阻止 LB 断开空闲 WebSocket?
  2. close code 1006 能说明是哪一端主动断开吗?
  3. 固定 60 秒和固定 350 秒断开分别更像哪类超时?
答案
  1. HTTP 感知型 LB 的空闲计时通常只认应用数据,TCP keepalive 探测往往不被计为活跃流量,也不一定被转发。
  2. 不能;1006 表示未收到 close frame,只能结合 FIN/RST 方向和中间层日志判断。
  3. 60 秒更像 ALB 默认 idle timeout 或 Nginx proxy_read_timeout;350 秒更像 NLB/NAT 的空闲超时。