WebSocket 频繁断连?Nginx 超时与心跳排查实战

WebSocket 长连接在生产环境频繁断开,十有八九不是业务代码的 bug,而是 Nginx 反向代理的超时配置与缺失心跳共同导致的。本文用一套 5 步排查法,从 proxy_read_timeout 到应用层 ping/pong,帮你彻底终结 WebSocket 断连问题。

一、先看清断连的特征

排查前先分类,避免南辕北辙:

  • 代理层掐断:连接在固定时间(如 60s、75s)被干净关闭,客户端收到 1006 异常码,服务端日志无业务报错。这是 Nginx / K8s Ingress 超时的典型表现。
  • 业务层掉线:连接随机中断,伴随心跳超时日志,通常与代码、GC 停顿、下游依赖有关。

如果你的断连时间高度规律,几乎可以锁定是代理层超时。这也是本文重点。基础环境可回顾 Nginx 反向代理完整配置 与 WebSocket 实时通信实战 两篇,这里不重复展开。

二、根因一:Nginx proxy_read_timeout 默认只有 60s

Nginx 的 proxy_read_timeout 默认值是 60 秒。它代表”两次从上游读取数据之间允许的最长空闲时间”。WebSocket 是长连接,客户端可能几十秒不发任何消息,于是 Nginx 认为连接已死,直接断开——而此时业务层毫无感知。

修复:为 WebSocket 单独拉高超时

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    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;

        # 关键:把空闲超时拉到 1 小时以上
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}

注意:不要全局把 proxy_read_timeout 调大,那只会影响普通 HTTP,应仅对 /ws/ 这类 WebSocket 路径单独设置,否则会拖慢故障连接的回收。

三、根因二:缺少应用层心跳

即便 Nginx 超时已经放开,TCP 层面仍然可能静默丢包,或者中间网络设备回收空闲连接。最稳妥的做法是应用层自己发心跳,让连接”始终有数据流动”。

浏览器端定时 ping

const ws = new WebSocket('wss://example.com/ws/');
let heartbeat = setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: 'ping' }));
  }
}, 25000); // 每 25s 一次,远小于代理超时

ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  if (msg.type === 'pong') {/* 收到服务端回应,连接健康 */}
};
ws.onclose = () => clearInterval(heartbeat);

Node.js 服务端回应 pong

wss.on('connection', (ws) => {
  ws.on('message', (raw) => {
    const msg = JSON.parse(raw);
    if (msg.type === 'ping') {
      ws.send(JSON.stringify({ type: 'pong' }));
    }
  });
  // 服务端主动探测:30s 内无消息则判定掉线
  ws.isAlive = true;
  ws.on('pong', () => { ws.isAlive = true; });
});

// 每 30s 巡检一次
setInterval(() => {
  wss.clients.forEach((ws) => {
    if (!ws.isAlive) return ws.terminate();
    ws.isAlive = false;
    ws.ping();
  });
}, 30000);

心跳间隔建议取代理超时的 1/3 到 1/2。例如 Nginx 设 3600s,心跳 25s 绰绰有余;若代理超时只有 75s,则心跳应 ≤ 30s。更多超时相关排查思路可参考 Nginx 502/504 排查实录。

四、根因三:K8s Ingress / 负载均衡的 idle timeout

如果 WebSocket 跑在 K8s 后面,Ingress NGINX 默认也有 proxy-read-timeout,需要在 Ingress 上用注解单独放开:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ws-ingress
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/configuration-snippet: |
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
spec:
  rules:
  - host: example.com
    http:
      paths:
      - path: /ws/
        pathType: Prefix
        backend:
          service:
            name: backend
            port:
              number: 80

云厂商 CLB / ALB 同样有空闲超时(如阿里云 CLB 默认 60s、900s 不等),务必同步检查,否则上游全改了仍会断。

五、5 步排查清单

按下面顺序逐项确认,基本能定位 90% 的 WebSocket 断连:

步骤检查项正常表现
1断连是否呈固定时间间隔规律断连→代理超时;随机→业务层
2Nginx proxy_read / send_timeoutWebSocket 路径 ≥ 业务空闲时长
3应用层心跳客户端定期 ping,服务端回 pong
4Ingress / CLB idle timeout与 Nginx 一致,均拉高
5客户端重连策略指数退避 + 断线自动恢复

六、前端重连:别让用户看到断线

再稳的长连接也可能短暂抖动,健壮的重连机制能掩盖绝大多数瞬时故障:

function connect() {
  const ws = new WebSocket('wss://example.com/ws/');
  let retries = 0;
  ws.onclose = () => {
    retries++;
    const delay = Math.min(1000 * 2 ** retries, 30000); // 指数退避,上限 30s
    setTimeout(connect, delay);
  };
}
connect();

七、一个常见误区

有人以为开了 TCP keepalive 就万事大吉,但 keepalive 默认探测间隔长达 2 小时,且很多 NAT / 防火墙根本不转发 keepalive 包。它只能兜底死连接回收,无法替代应用层心跳。两者应同时使用。

八、小结

WebSocket 断连的本质,是”长连接”撞上了”为短请求设计的超时”。记住三件事:代理层超时单独拉高、应用层心跳持续保活、全链路(Nginx + Ingress + CLB)超时一致。再加上前端指数退避重连,生产环境的实时通信就能稳如磐石。

上一篇 atuin 实战:Rust 打造的 Shell 历史管理工具
下一篇 Go 并发实战:goroutine 与 channel
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]