记一次长连接断开排查过程
问题
WebSocket 的网络链路是 浏览器 <-> Nginx <-> 后端服务,心跳时间是 60 s,出现了有心跳发送但长连接中断的问题。
过程
-
查看后端服务日志,发现是被动断开,不是空闲检测主动断开的,再抓服务器的网络包,确认后端服务是被断开的,排除后端服务的问题。
-
使用其他语言建立 WebSocket 长连接,维持 10 秒一次心跳,经过 45 小时测试没有断开连接,那么大概率是浏览器 或 JavaScript 哪里不对。
-
抓浏览器的包,这次比较诡异,发现浏览器的长连接也是被动断开的,那问题就只能是 Nginx 的了。
-
抓 Nginx 的包,发现每次都是 Nginx 先把后端服务的连接断开,之后再断开浏览器的 WebSocket,查看与后端服务的通信,没有问题,接着查看浏览器发送的心跳包,发现这种场景的最后一个心跳包都是 60 s 前发送的,而正常的心跳是 10 s 一次,那么问题就确定了,是浏览器的心跳包不是 10 s 一次,而是 1 分钟一次导致的,但 1 分钟没有心跳导致断开连接是应该由后端服务的空闲检测触发的,不应该由 Nginx 触发,将该问题扔给 ChatGPT 后得到了一个 Nginx 的参数 proxy_read_timeout,这个参数默认 60 s,即 60 s 内没有通信就关闭连接,这样就解释了为什么 Nginx 会主动断开与后端服务的连接,将 proxy_read_timeout 设置为更长时间,经过测试,主动断开连接的发起者就变成了后端服务的空闲检测机制。
下面的图片描述了这个过程:
如果图中 1 分钟后的心跳是心跳 1,那么就不会断开连接,如果慢了一会,比如 100 ms,即心跳 2,则会导致连接断开。
-
上面 Nginx 的配置只解决了断开连接一定由后端服务的空闲检测发起,但问题的关键在于浏览器的心跳包发送时间间隔不对,刚开始是 10 s 一次,过了一段时间就变成了 1 分钟一次。首先如果在当前页面的话不会发生这种问题,但如果在其他页面就会触发这个问题,接着查看开发者工具中的网络,发现过一段时间定时器变成了 1 分钟一次(也可能是 1 分钟内的不固定的时间点,但断开连接的最后一个心跳一定是 1 分钟),在查阅资料后确定浏览器会优化非当前页面的定时器,将所有定时器合并为 1 分钟触发一次已降低系统资源占用。
-
如何解决浏览器的定时器被优化的问题?使用 WebWorker。使用 WebWorker 后定时任务心跳发送的时间间隔不再出现 1 分钟执行一次的问题,也就没有出现过 WebSocket 被断开的问题。看了下友商的前端实现,发现也是 WebWorker。
总结
开始排查时思维有点凌乱,没有确定思路,实际应该先确定心跳(客户端)是否有问题,如果心跳没问题再排查服务端。
资料
https://developer.chrome.com/blog/timer-throttling-in-chrome-88/