Node.js Express SSE服务器同一设备建立大量连接时连接失败的问题排查
嘿,我来帮你拆解下你碰到的这两个核心问题,以及对应的落地解决思路:
一、先搞定客户端脚本的「Resource temporarily unavailable」错误
你在压测时遇到的这个shell报错,本质是系统进程/资源配额不够导致的:
- bash默认给每个用户的进程数设了上限(一般是1024左右),你要开4000个客户端,每个客户端fork一个进程肯定直接触发限制
- 另外,每个进程会占用文件描述符,系统默认的用户文件描述符上限也撑不住这么多连接
客户端侧优化方案:
临时提升系统资源限制(当前终端会话生效,重启后恢复):
先在运行压测脚本的终端执行这两条命令,拉高资源配额:ulimit -u 8192 # 把单用户最大进程数调到8192 ulimit -n 65535 # 把单进程文件描述符上限调到65535如果要永久生效,Linux可以修改
/etc/security/limits.conf,macOS则调整/etc/sysctl.conf,具体配置可以查对应系统的文档。换用更高效的压测工具:
用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好很多,尤其是当客户端数量较多时。
三、最终排查验证步骤
- 先运行客户端压测脚本的优化版本,解决客户端资源不足的问题
- 调整服务器和客户端的系统资源限制(进程数、文件描述符)
- 修复代码中数组遍历修改的bug,用Map存储客户端
- 调整Express和Node.js的长连接配置
- 启动服务器后,运行压测脚本,观察服务器日志和客户端状态,确认连接稳定
按照这些步骤调整后,你应该能稳定支持4000个SSE连接了。如果还有问题,可以用process.resourceUsage()在服务器端打印资源使用情况,或者查看系统dmesg日志,排查是否有内核层面的TCP连接限制。




