Node.js + Cloud Run环境下WebSocket出现1006无原因连接关闭但消息投递正常的问题排查
遇到这种情况确实挺头疼的——明明消息都能正常送达,却频繁收到1006的异常关闭日志。先给你理清楚1006错误的本质:它是WebSocket的异常关闭码,代表连接没有通过标准的WebSocket close帧完成关闭,而是底层TCP连接被强制断开(比如FIN/RST包触发的断开)。结合你的代码和Cloud Run的环境特性,我整理了几个可能的原因和对应的解决思路:
一、核心原因分析
1. WebSocket关闭流程的时序问题
你的代码里,在Promise.all(sendPromises)完成后立刻调用ws.close(1000),但这里有个容易忽略的点:ws.send的回调只表示数据已写入本地套接字缓冲区,不代表Cloud Run上的WS服务器已经实际收到并处理了消息。
此时立刻发起关闭请求,可能会导致TCP连接在数据传输途中被强制中断,或者服务器还没来得及回复close帧,客户端就因为超时触发1006错误。
2. Cloud Run负载均衡的TCP连接管理
Cloud Run前端是Google的全球负载均衡(GLB),即使你在Cloud Run控制台设置了1小时的WebSocket超时,GLB底层对TCP连接还有一些隐性的管理逻辑:
- 当连接处于半关闭状态(比如客户端发了close帧,服务器还没回复),GLB可能因为网络波动或自身超时逻辑提前断开TCP连接;
- 频繁创建/销毁短连接的场景下,GLB的连接复用机制可能会干扰正常的关闭流程。
3. ws库的默认关闭超时行为
ws库的close()方法会发送close帧,然后等待服务器回复close帧,默认超时时间是30秒。如果服务器因为Cloud Run的实例调度、网络延迟等原因没有及时回复,客户端会主动断开TCP连接,触发1006错误。
二、针对性解决办法
1. 调整代码逻辑:消息已送达则忽略异常关闭
既然你确认所有消息都能正常投递,那可以修改onclose的处理逻辑——只要所有消息已经发送完成,即使连接以1006关闭,也视为操作成功,不再抛出错误。
修改后的核心代码片段:
const sendMessageCore = async (messages) => { return new Promise((resolve, reject) => { const callId = Math.random().toString(36).substring(2, 15); winstonLogger.debug(`Apertura connessione WebSocket (CallId: ${callId})`); let allMessagesSent = false; // 标记所有消息是否已成功发送 const ws = new WebSocket(WS_SERVER); ws.onopen = async () => { winstonLogger.debug(`Connessione WebSocket aperta (CallId: ${callId})`); try { const sendPromises = messages.map(({ toClientId, messageToSend, other }) => { // ... 原消息构造逻辑不变 }); await Promise.all(sendPromises); allMessagesSent = true; // 标记所有消息已发送完成 winstonLogger.debug(`Chiusura connessione WebSocket (CallId: ${callId})`); ws.close(1000, "Normal closure"); } catch (err) { // ... 原错误处理逻辑不变 } }; ws.onerror = (error) => { // ... 原错误处理逻辑不变 }; ws.onclose = (event) => { if (allMessagesSent) { // 所有消息已送达,即使连接异常关闭也视为成功 winstonLogger.debug( `WebSocket chiuso dopo invio messaggi (CallId: ${callId}) - Codice: ${event.code}, Motivo: ${event.reason}` ); resolve(); } else if (event.code !== 1000) { // 消息未发送完成时的异常关闭才抛出错误 const error = new Error(`WebSocket chiuso inaspettatamente. Codice: ${event.code}, motivo: ${event.reason}`); winstonLogger.error(`Errore di chiusura WebSocket (CallId: ${callId}): ${error.message}`, error); reject(error); } else { winstonLogger.debug(`WebSocket chiuso correttamente (CallId: ${callId})`); resolve(); } }; }); };
2. 增加关闭超时的兜底处理
为了避免客户端无限等待服务器的close回复,可以在调用ws.close()后设置一个超时,超时后主动终止连接并标记成功:
// 在allMessagesSent = true之后添加: const closeTimeout = setTimeout(() => { winstonLogger.debug(`Timeout attesa chiusura WebSocket, terminazione forzata (CallId: ${callId})`); ws.terminate(); // 强制断开TCP连接 resolve(); }, 5000); // 5秒超时,可根据实际情况调整 // 在ws.onclose里清除超时: ws.onclose = (event) => { clearTimeout(closeTimeout); // ... 后续逻辑不变 };
3. 采用连接复用减少短连接频率
你的代码每次发送消息都新建一个WebSocket连接,这种短连接模式在Cloud Run环境下很容易触发网络层的异常。可以改成复用长连接的模式,比如维护一个全局的WebSocket实例:
// 全局维护一个复用的WebSocket连接 let globalWs = null; let isConnecting = false; const getReusableWs = async () => { if (globalWs && globalWs.readyState === WebSocket.OPEN) { return globalWs; } if (isConnecting) { // 避免重复创建连接 await new Promise(resolve => setTimeout(resolve, 100)); return getReusableWs(); } isConnecting = true; return new Promise((resolve, reject) => { const ws = new WebSocket(WS_SERVER); ws.onopen = () => { globalWs = ws; isConnecting = false; resolve(ws); }; ws.onerror = (err) => { isConnecting = false; reject(err); }; ws.onclose = () => { globalWs = null; isConnecting = false; }; }); }; // 修改sendMessageCore使用复用连接 const sendMessageCore = async (messages) => { const ws = await getReusableWs(); // ... 原消息发送逻辑不变,不再需要手动关闭连接 };
4. 排查Cloud Run服务器端的关闭逻辑
如果你的WS服务器也是部署在Cloud Run上,需要检查服务器端是否正确处理了客户端的close帧:
- 确保服务器收到close帧后,会回复一个对应的close帧;
- 避免服务器在处理消息后直接强制断开连接(比如用
ws.terminate()而不是ws.close())。
三、其他可选方案
- 更换WebSocket库:可以试试
socket.io替代ws,它自带完善的重连、心跳和连接管理机制,对Cloud Run这类Serverless环境的适配性更好; - 抓包排查细节:如果想彻底定位问题,可以在本地测试环境用Wireshark抓包,或者通过Cloud Run的VPC peering功能抓取生产环境的WebSocket帧交互,看close帧在哪个环节丢失了;
- 调整Cloud Run实例配置:确保实例的CPU分配是总是分配(而不是按需分配),避免实例休眠导致的连接异常。
总结
这类1006错误大多是Cloud Run网络层和WebSocket关闭流程的适配问题,结合你消息已正常送达的现状,优先调整代码逻辑(忽略消息发送后的异常关闭)和采用连接复用,应该能大幅减少错误日志的产生。如果还想深究根源,抓包分析WebSocket的帧交互是最直接的方式。




