You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Node.js + Cloud Run环境下WebSocket出现1006无原因连接关闭但消息投递正常的问题排查

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())。

三、其他可选方案

  1. 更换WebSocket库:可以试试socket.io替代ws,它自带完善的重连、心跳和连接管理机制,对Cloud Run这类Serverless环境的适配性更好;
  2. 抓包排查细节:如果想彻底定位问题,可以在本地测试环境用Wireshark抓包,或者通过Cloud Run的VPC peering功能抓取生产环境的WebSocket帧交互,看close帧在哪个环节丢失了;
  3. 调整Cloud Run实例配置:确保实例的CPU分配是总是分配(而不是按需分配),避免实例休眠导致的连接异常。

总结

这类1006错误大多是Cloud Run网络层和WebSocket关闭流程的适配问题,结合你消息已正常送达的现状,优先调整代码逻辑(忽略消息发送后的异常关闭)和采用连接复用,应该能大幅减少错误日志的产生。如果还想深究根源,抓包分析WebSocket的帧交互是最直接的方式。

火山引擎 最新活动