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

Node.js Express SSE服务器同一设备建立大量连接时连接失败的问题排查

Node.js Express SSE服务器同一设备建立大量连接时连接失败的问题排查

嘿,我来帮你拆解下你碰到的这两个核心问题,以及对应的落地解决思路:

一、先搞定客户端脚本的「Resource temporarily unavailable」错误

你在压测时遇到的这个shell报错,本质是系统进程/资源配额不够导致的:

  • bash默认给每个用户的进程数设了上限(一般是1024左右),你要开4000个客户端,每个客户端fork一个进程肯定直接触发限制
  • 另外,每个进程会占用文件描述符,系统默认的用户文件描述符上限也撑不住这么多连接

客户端侧优化方案:

  1. 临时提升系统资源限制(当前终端会话生效,重启后恢复):
    先在运行压测脚本的终端执行这两条命令,拉高资源配额:

    ulimit -u 8192  # 把单用户最大进程数调到8192
    ulimit -n 65535 # 把单进程文件描述符上限调到65535
    

    如果要永久生效,Linux可以修改/etc/security/limits.conf,macOS则调整/etc/sysctl.conf,具体配置可以查对应系统的文档。

  2. 换用更高效的压测工具
    用bash脚本每个客户端开一个进程太浪费资源了,建议用Node.js写一个单进程的压测脚本,用原生http模块模拟多个SSE连接,资源占用会大幅降低。举个简单的例子:

    // sse-stress-test.js
    const http = require('http');
    const totalClients = 4000;
    const targetUrl = 'http://localhost:3000/api/v1/discover-stations';
    
    let connectedCount = 0;
    let errorCount = 0;
    
    function createSseClient() {
      const req = http.get(targetUrl, {
        headers: {
          'Accept': 'text/event-stream',
          'Cache-Control': 'no-cache'
        }
      });
    
      req.on('response', (res) => {
        connectedCount++;
        console.log(`已连接客户端数: ${connectedCount}`);
        // 只维持连接,忽略接收的消息
        res.on('data', () => {});
        res.on('error', (err) => {
          errorCount++;
          console.error(`客户端错误: ${err.message}`);
        });
        res.on('close', () => {
          connectedCount--;
          // 连接断开后自动重连,维持总连接数
          setTimeout(createSseClient, 1000);
        });
      });
    
      req.on('error', (err) => {
        errorCount++;
        console.error(`请求错误: ${err.message}`);
        setTimeout(createSseClient, 1000);
      });
    }
    
    // 分批创建客户端,避免瞬间打满服务器
    for (let i = 0; i < totalClients; i++) {
      setTimeout(createSseClient, i * 10);
    }
    
    // 定时输出状态
    setInterval(() => {
      console.log(`当前状态: 已连接=${connectedCount}, 错误数=${errorCount}`);
    }, 5000);
    

    运行这个Node.js脚本,单进程就能轻松模拟4000个SSE连接,资源占用比bash脚本低一个数量级。

二、解决服务器端的「aborted」连接中断问题

服务器端的连接失败,主要和Node.js/Express默认配置限制、代码连接管理bug有关,逐个解决:

1. 提升Node.js的系统资源限制

和客户端一样,Node.js进程的文件描述符上限默认也不高(一般1024),要支持4000个长连接,必须调整:
启动Node.js服务前,先在终端执行ulimit -n 65535,或者在启动命令前直接加:

ulimit -n 65535 && node your-server-file.js

2. 修复代码中连接管理的致命bug

你当前用forEach遍历connectedClients数组时,直接用splice(index, 1)删除无效连接,会导致数组索引错乱,后面的客户端会被跳过遍历,最终出现大量无效连接堆积,引发aborted错误。

修复后的broadcastToAllClients函数

const broadcastToAllClients = (data) => {
  const message = `data: ${JSON.stringify(data)}\n\n`;
  // 先过滤掉已断开的连接,用filter生成新数组,避免遍历中修改原数组
  connectedClients = connectedClients.filter(client => !client.res.writableEnded);
  
  // 再遍历有效连接发送消息
  connectedClients.forEach((client) => {
    try {
      client.res.write(message);
      // 处理背压:如果返回false,说明缓冲区满了,等待drain事件
      if (!client.res.writable) {
        client.res.once('drain', () => {
          // 缓冲区有空位时自动恢复,SSE场景下一般不需要额外处理
        });
      }
    } catch (error) {
      console.error(`[BROADCAST ERROR] 发送给客户端 ${client.id} 失败:`, error.message);
      // 错误连接后续会被filter清理
    }
  });
};

3. 优化Express的长连接配置

Express默认的一些配置不适合长连接场景,调整如下:

const express = require('express');
const app = express();

// 关闭etag,减少资源消耗
app.set('etag', false);
// 禁用x-powered-by,避免暴露技术栈
app.disable('x-powered-by');

const server = app.listen(3000, () => {
  console.log('服务器运行在端口3000');
});

// 调整长连接超时时间,避免服务器主动关闭连接
server.keepAliveTimeout = 60 * 1000; // 60秒
server.headersTimeout = 65 * 1000; // 必须比keepAliveTimeout大一点

4. 用Map替代数组存储客户端,提升性能

当客户端数量达到4000时,数组的查找、删除性能不如Map,建议把connectedClients改成Map

// 初始化时改成Map
const connectedClients = new Map();

// 添加客户端时
const clientId = Date.now() + Math.random();
const client = { id: clientId, res: res };
connectedClients.set(clientId, client);

// 清理无效连接时
connectedClients.forEach((client, id) => {
  if (client.res.writableEnded) {
    connectedClients.delete(id);
  }
});

Map的遍历、删除操作性能比数组splice好很多,尤其是当客户端数量较多时。

三、最终排查验证步骤

  1. 先运行客户端压测脚本的优化版本,解决客户端资源不足的问题
  2. 调整服务器和客户端的系统资源限制(进程数、文件描述符)
  3. 修复代码中数组遍历修改的bug,用Map存储客户端
  4. 调整Express和Node.js的长连接配置
  5. 启动服务器后,运行压测脚本,观察服务器日志和客户端状态,确认连接稳定

按照这些步骤调整后,你应该能稳定支持4000个SSE连接了。如果还有问题,可以用process.resourceUsage()在服务器端打印资源使用情况,或者查看系统dmesg日志,排查是否有内核层面的TCP连接限制。

火山引擎 最新活动