网站 WebSocket 连接总在空闲后断开?心跳、代理超时与 CDN 限制排查

WebSocket 建立连接后,为什么一段时间没有消息就断开?本文从最后收发时间和关闭码入手,说明 Nginx 上游读取超时、协议 Ping/Pong 与浏览器应用层心跳的区别,给出限定路径的代理配置,并核对 CDN 空闲限制、页面后台计时器、重连退避和业务状态恢复,帮助区分超时清理与其他连接故障。
网站 WebSocket 连接总在空闲后断开?心跳、代理超时与 CDN 限制排查

聊天窗口放着不动,一分钟后就提示断线;行情页面一直有数据时正常,停止推送后连接却消失。遇到这类 WebSocket 故障,应先记录“最后一次收发消息到断开”的间隔,再核对每一层的空闲超时和心跳方向。

Nginx 官方文档说明,代理服务器在默认情况下,如果 60 秒没有从上游收到数据,会关闭连接;可以通过 proxy_read_timeout 调整,也可以让上游定期发送 WebSocket Ping 帧。[1] 这与“连接只能存活 60 秒”有区别:只要满足读取条件,连接可以继续保持。CDN、负载均衡和应用还可能有各自的限制,修改 Nginx 一处不一定解决全链路问题。

下面以浏览器经 CDN、Nginx 访问业务 WebSocket 的常见结构为例,涉及 Nginx 的配置采用传统 HTTP/1.1 Upgrade 代理方式。示例用于定位与配置参考,不代表已经在读者的生产环境中实测。

先确认:升级失败,还是已经建立后断开

打开浏览器开发者工具,在 Network 中找到对应 WebSocket 请求。传统 HTTP/1.1 握手成功通常会出现 101 Switching Protocols,随后可以查看消息记录;若使用其他协议承载或框架回退传输,应结合实际传输方式判断,不要仅凭一个状态码套用结论。

如果根本没有完成升级,常见问题包括路径错误、鉴权失败、代理没有传递 Upgrade 头,或 WAF 拦截。这些问题应从握手请求排查。

如果已经收发过消息,随后才断开,记录以下信息更有帮助:连接建立时间、最后一次客户端发送、最后一次服务端发送、断开时间,以及浏览器的 code、reason 和 wasClean。服务端和代理日志尽量使用一致时区,并用不含敏感信息的连接标识关联。

浏览器显示 1006,只能说明本端观察到异常关闭,不能据此认定 CDN 或 Nginx 就是责任方。RFC 6455 将 1006 定义为保留值,端点不能把它放入 Close 控制帧发送。[3] 网络中断、进程退出、代理超时都可能表现为这一结果。

可以按现象缩小范围:

观察结果 优先检查
空闲时间接近固定值,持续推送时正常 代理、CDN 或负载均衡的空闲计时
从建立起经过固定时长就断,即使持续有消息 最大连接时长、凭据到期、应用主动关闭
手机锁屏、浏览器后台后明显增加 页面计时器节流、系统休眠和移动网络切换
发布或重启时一批连接同时断开 应用重启、连接排空、网关或边缘节点维护

这些是排查方向,需要日志与对照测试确认。

Nginx 的读取超时看的是上游方向

proxy_read_timeout 限定的是两次相邻上游读取操作之间的等待时间,并非整条连接的总寿命。[2] 对 WebSocket 而言,要留意业务服务器有没有向 Nginx 发送数据。

假设客户端每隔一段时间向服务端发送一条消息,服务端收到后完全不响应。客户端确实在发送,但 Nginx 仍可能长时间读不到上游数据。这样的单向心跳不能可靠解决上游读取超时。

可采用两种方式:

  • 服务端 WebSocket 库周期性发送协议 Ping,浏览器在协议层自动响应 Pong。
  • 前后端约定应用层心跳消息,服务端收到客户端心跳后明确回复,让双向路径都产生流量。

proxy_connect_timeout 主要涉及连接上游阶段;proxy_send_timeout 涉及向上游写入时的等待。它们不能直接代替 proxy_read_timeout。普通 HTTP 连接复用的 keepalive 设置、TCP keepalive 也不能一概当作 WebSocket 应用心跳:不同中间层可能按不同层次的流量重置计时。

一份限定在 WebSocket 路径的配置示例

以下片段需要合并到现有配置,不能直接覆盖完整的 nginx.conf。map 放在 http 上下文,location 放入原有 server;TLS、证书、访问控制和其他站点配置继续保留。

# 放在 http 上下文
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

# 放在原有 server 内
location /ws/ {
    proxy_pass http://127.0.0.1:9000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_set_header Host $host;

    proxy_read_timeout 120s;
    proxy_send_timeout 120s;
}

这里显式使用 HTTP/1.1,便于兼容仍需手动指定的部署。proxy_pass 没有附带 URI,示例保留 /ws/ 请求路径;若后端只接受其他路径,需要按项目路由调整,不能随手增加或删除尾部斜杠。

120s 是示例值,不是推荐给所有网站的标准答案。假设服务端每 25 秒发送一次 Ping,应给调度延迟和短暂抖动留出余量;如果 CDN 的空闲限制更短,还需结合那一层重新设计。将超时统一拉到几小时,可能让已失联连接长期占用文件描述符、内存和连接配额。

变更前备份实际站点文件,核查是否存在上层或更精确 location 的覆盖。在常见 systemd 部署中,可先检查再重载:

sudo nginx -t
sudo systemctl reload nginx

只有校验通过后才执行重载。容器、Ingress 和托管面板应使用各自的配置发布方式,避免直接修改随后会被控制器覆盖的文件。回滚时恢复备份,再次校验并重载。不要为了排查断线顺手关闭鉴权、WAF 或 TLS 校验。

浏览器的 send(“ping”) 不等于协议 Ping

RFC 6455 定义了 Ping、Pong 控制帧,收到 Ping 的端点通常需要回复 Pong。[3] 但浏览器原生 WebSocket JavaScript API 没有直接发送协议 Ping 的方法。调用 ws.send("ping") 发出的是普通文本消息,服务端必须按双方约定处理。

如果业务框架已有保活机制,应先使用其配置,避免再叠加一套心跳造成状态冲突。原生实现可按下面的思路约定应用层消息:

{"type":"heartbeat","id":"hb-001"}

服务端识别后返回对应响应:

{"type":"heartbeat_ack","id":"hb-001"}

这些字段只是协议示例,不是发给任意 WebSocket 服务都有效的通用命令。实现时要记录最近一次有效响应,连续超出允许等待窗口后再判定失联;断开时清理计时器,避免每次重连都新增一个心跳循环。

页面进入后台或设备休眠后,JavaScript 计时器可能延迟执行。要求连接更稳定时,可优先让服务端发送协议层 Ping,同时保留客户端恢复前台后的状态检查。无论哪种心跳,都不能保证设备离线时仍保持连接。

经过 CDN:核对当前产品限制,不套用旧数字

CDN 支持 WebSocket,不代表连接可以无限空闲,也不代表边缘维护永远不会中断连接。

以 Cloudflare 为例,其官方文档说明 WebSocket 连接在双向均无数据一段时间后可能被关闭,并建议实现保活;边缘网络发布新代码、重启服务器也可能终止现有连接。[4] 当前该页面没有给出适用于所有场景的统一空闲秒数,不能把网上流传的某个 HTTP 请求超时值直接当成 WebSocket 的限制。

排查时核对实际服务商、套餐、代理开关以及负载均衡、Worker 等参与链路的功能,区分:

  • 建连或握手超时;
  • 已建立连接的空闲超时;
  • 持续有流量时仍然生效的最大连接时长。

心跳对空闲超时可能有效,对最大时长限制、网络切换和节点重启则不能提供同样保证。

需要做对照时,可在受控测试环境比较“应用直连、经过 Nginx、经过 CDN”三条路径。保留等效的 Host、路径、鉴权和 TLS 条件;不要为了测试把源站临时全面开放到公网,也不要把一次绕过 CDN 后成功当成充分证据。多次重复相同空闲和心跳条件,结果才便于比较。

验证要覆盖无消息、心跳和业务恢复

配置调整后,至少保留三组观察:

  1. 静默连接:关闭应用心跳和业务推送,仅在受控测试中观察空闲多久断开,确认基线。
  2. 正常心跳连接:启用选定机制,检查服务端是否实际发送或响应,观察是否跨过原来的断线窗口。
  3. 故障恢复:在测试环境重启应用或切换客户端网络,确认能识别断线、重连,并恢复订阅或会话。

只发送客户端心跳、没有服务端响应的条件可以额外保留,用于验证上游读取方向。测试不要在正式业务连接上随意关闭心跳,更不要用高频重连向生产入口施压。

Nginx 中出现上游读取超时,能提示某一跳长时间未收到数据,但仍要与应用日志对应。WebSocket 的访问日志可能到长连接结束才写入,日志中的 101 也只代表握手阶段成功,不能证明后续消息全程正常。

浏览器消息面板不一定完整显示协议级控制帧。心跳验证应结合服务端库提供的 Ping/Pong 事件、最近收发时间等指标。日志不要记录完整业务消息、Cookie 或带令牌的连接 URL。

连接总会断,重连后不能丢掉业务状态

即使超时与心跳配置合理,发布、边缘维护和网络切换仍可能中断 WebSocket。客户端应采用带随机抖动的指数退避重连,限制重试频率;鉴权失效时走正常续期或重新登录流程,不要无限重连制造额外请求。

重连成功后,需要重新订阅、恢复最后已确认的消息位置,并按业务规则去重。跨多个应用实例部署时,还要确认共享状态或路由策略:新连接可能落到另一台实例,原实例内存中的会话未必存在。

最终验收应包含两件事:空闲时不再被不符合预期的规则提前清理;连接确实中断后,用户仍能恢复所需业务。只看到连接一直处于 OPEN,无法证明消息没有积压、订阅没有失效。

参考资料

资料核验日期:2026 年 11 月 6 日。配置值与消息格式均为示例,请结合实际版本和产品限制使用。

  1. Nginx:WebSocket proxying
  2. Nginx HTTP proxy module:proxy_read_timeout
  3. RFC 6455:The WebSocket Protocol
  4. Cloudflare:WebSockets;官方文档源码
  5. MDN:Writing WebSocket client applications
Web服务与建站

Nginx 日志轮转后不再写入?文件句柄、属主与 reopen 排查

2026-11-5 10:01:29

知识库

服务器成本优化的“不可能三角”:在性能、稳定与预算间的真实博弈

2026-1-6 15:38:14