一、压测核心目标 & 关键指标
先明确压测要验证的核心问题,避免盲目加压:
核心目标 关键监控指标 服务端最大并发连接数 成功建立的 WS 连接数 / 总尝试连接数(连接成功率)、服务端 TCP 连接数 消息吞吐量 所有客户端总发送 QPS(每秒消息数)、服务端接收 QPS、消息平均延迟 服务端稳定性 错误率(连接失败 / 消息发送失败 / 超时占比)、服务端 CPU / 内存 / 网络占用、是否崩溃 极限恢复能力 高压力下停止发送后,服务端是否能恢复、连接是否稳定
const WebSocket = require('ws'); const os = require('os'); const { spawn } = require('child_process'); // ====================== 压测核心配置 ====================== const pressureConfig = { // 基础WS配置 wsUrl: "ws://localhost:18888", // 压测梯度(核心:逐步增加并发数,避免瞬间打垮服务端) pressureSteps: [ { clientCount: 100, duration: 60000, interval: 100 }, // 第1阶段:100连接,发送间隔100ms(10QPS/连接),持续60秒 { clientCount: 200, duration: 60000, interval: 100 }, // 第2阶段:200连接,持续60秒 { clientCount: 500, duration: 120000, interval: 100 }, // 第3阶段:500连接,持续120秒 { clientCount: 500, duration: 60000, interval: 50 }, // 第4阶段:500连接,间隔50ms(20QPS/连接),突发流量 ], // 消息配置(模拟不同大小的Protobuf消息,压测服务端处理能力) messageConfig: { baseMessages: [ // 不同大小的Base64消息(可通过Protobuf生成不同大小的二进制后转Base64) // "CwAzdQoAEgdhbmRyb2lk", "DgA1dQgBEMXGCBiJlQY=", "DgA1dQgBEMXGCBiJlQY=", "DgA1dQgBEMXGCBiJlQY=", "DgA1dQgBEMXGCBiJlQY=", "EgA2dQgBEMXGCBgBIJz5BygB", "EgA2dQgBEMXGCBgBIJz5BygB", "EgA2dQgBEMXGCBgBIJz5BygB", "DAA3dQgBEMXGCBgF", "DAA3dQgBEMXGCBgF", "DAA3dQgBEMXGCBgF", "EQA4dQgBEPXPrjYYtfgHIAE=", "EQA4dQgBEPXPrjYYtfgHIAE=", ], randomMessage: true, // 每个连接随机选择消息发送(模拟真实业务) }, // 实例启动配置(压测专用) startOffset: 20, // 实例启动间隔(毫秒):压测时缩小间隔,快速建立连接(但避免瞬间打满) reconnectMaxTimes: 3, // 压测时减少重连次数(重连会干扰压测数据) reconnectInterval: 1000, // 监控配置(可选:监控服务端资源,需提前配置服务端IP和ssh) serverMonitor: { enable: false, serverIp: "127.0.0.1", // 服务端IP sshUser: "root", monitorInterval: 2000, // 监控间隔(ms) }, }; // ====================== 全局压测统计 ====================== const pressureStats = { totalClients: 0, // 总启动客户端数 connectedClients: 0, // 成功连接数 totalSendAttempts: 0, // 总发送尝试数 totalSendSuccess: 0, // 发送成功数 totalSendFail: 0, // 发送失败数 sendLatency: [], // 消息发送耗时(ms) startTime: null, // 压测开始时间 endTime: null, // 压测结束时间 serverStats: [], // 服务端资源监控数据 }; // ====================== 工具函数 ====================== // 带实例ID的日志 function log(clientId, type, msg) { function coverTimeStr2 (timestamp) { const date = new Date(timestamp * 1000); // 根据时间戳创建Date对象 const year = date.getFullYear(); // 获取年份 let month = date.getMonth() + 1; // 获取月份,需要加1 let day = date.getDate(); // 获取日期 let hour = date.getHours(); // 获取小时 let minute = date.getMinutes(); // 获取分钟 let second = date.getSeconds(); // 获取秒钟 month = month<10 ? ("0"+month) : (month) day = day<10 ? ("0"+day) : (day) hour = hour<10 ? ("0"+hour) : (hour) minute = minute<10 ? ("0"+minute) : (minute) second = second<10 ? ("0"+second) : (second) return `${year}/${month}/${day} ${hour}:${minute}:${second}`; // 拼接成格式化后的日期字符串 } const now = (new Date() ).getTime()/1000 const timestamp1 = coverTimeStr2(now) console.log(`[${timestamp1}] [客户端${clientId}] [${type}] ${msg}`); } // 生成压测报告 function generateReport() { pressureStats.endTime = new Date(); const totalDuration = (pressureStats.endTime - pressureStats.startTime) / 1000; // 总时长(秒) const totalQPS = pressureStats.totalSendSuccess / totalDuration; // 总成功QPS const connectSuccessRate = (pressureStats.connectedClients / pressureStats.totalClients) * 100; // 连接成功率 const sendSuccessRate = (pressureStats.totalSendSuccess / pressureStats.totalSendAttempts) * 100; // 发送成功率 const avgSendLatency = pressureStats.sendLatency.length > 0 ? (pressureStats.sendLatency.reduce((a, b) => a + b) / pressureStats.sendLatency.length).toFixed(2) : 0; // 输出报告 console.log("\n====================== 压测报告 ======================"); console.log(`压测总时长:${totalDuration.toFixed(2)} 秒`); console.log(`总启动客户端数:${pressureStats.totalClients}`); console.log(`成功连接数:${pressureStats.connectedClients}(成功率:${connectSuccessRate.toFixed(2)}%)`); console.log(`总发送尝试数:${pressureStats.totalSendAttempts}`); console.log(`发送成功数:${pressureStats.totalSendSuccess}(成功率:${sendSuccessRate.toFixed(2)}%)`); console.log(`总成功QPS:${totalQPS.toFixed(2)} 条/秒`); console.log(`消息平均发送耗时:${avgSendLatency} ms`); console.log(`发送失败数:${pressureStats.totalSendFail}`); console.log("======================================================\n"); } // 监控服务端资源(可选:需服务端开启ssh,安装top/iftop) function startServerMonitor() { if (!pressureConfig.serverMonitor.enable) return; const { serverIp, sshUser, monitorInterval } = pressureConfig.serverMonitor; const monitorIntervalId = setInterval(() => { // 执行ssh命令获取服务端CPU/内存/网络(示例:Linux服务端) const cmd = `ssh ${sshUser}@${serverIp} "top -b -n 1 | grep '%Cpu' && free -m | grep Mem && iftop -t -s 1 -n | grep 'Total send'"`; spawn(cmd, { shell: true }) .stdout.on('data', (data) => { const stats = { time: new Date().toISOString(), data: data.toString().trim(), }; pressureStats.serverStats.push(stats); log('MONITOR', 'INFO', `服务端资源:${stats.data}`); }) .stderr.on('data', (err) => { log('MONITOR', 'ERROR', `服务端监控失败:${err.toString()}`); }); }, monitorInterval); // 压测结束停止监控 process.on('SIGINT', () => clearInterval(monitorIntervalId)); } // ====================== 压测客户端类 ====================== class WsPressureClient { constructor(clientId) { this.clientId = clientId; this.socket = null; this.intervalId = null; this.timeoutId = null; this.reconnectCount = 0; this.isConnected = false; // 标记是否成功连接 } // 清理资源 cleanResources() { if (this.intervalId) clearInterval(this.intervalId); if (this.timeoutId) clearTimeout(this.timeoutId); if (this.socket && this.socket.readyState === WebSocket.OPEN) { this.socket.close(1000, `客户端${this.clientId}压测结束`); } this.socket = null; this.isConnected = false; } // 选择要发送的消息(随机选择不同大小) getRandomMessage() { const { baseMessages, randomMessage } = pressureConfig.messageConfig; if (!randomMessage) return baseMessages[0]; return baseMessages[Math.floor(Math.random() * baseMessages.length)]; } // 消息发送逻辑(压测专用:统计发送耗时/成功率) startSending(duration, interval) { // 压测时长到了自动停止 this.timeoutId = setTimeout(() => { log(this.clientId, 'INFO', "⏰ 本阶段压测时长到,停止发送"); this.cleanResources(); }, duration); // 高频发送消息 this.intervalId = setInterval(() => { if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { pressureStats.totalSendAttempts++; pressureStats.totalSendFail++; log(this.clientId, 'WARN', "❌ 连接未打开,发送失败"); return; } try { const startTime = Date.now(); const msgBase64 = this.getRandomMessage(); const buffer = Buffer.from(msgBase64, 'base64'); // 发送并统计耗时 this.socket.send(buffer, (err) => { const latency = Date.now() - startTime; pressureStats.sendLatency.push(latency); if (err) { pressureStats.totalSendAttempts++; pressureStats.totalSendFail++; log(this.clientId, 'ERROR', `❌ 发送失败:${err.message}(耗时:${latency}ms)`); } else { pressureStats.totalSendAttempts++; pressureStats.totalSendSuccess++; log(this.clientId, 'DEBUG', `📤 发送成功(大小:${buffer.length}B,耗时:${latency}ms)`); } }); } catch (error) { pressureStats.totalSendAttempts++; pressureStats.totalSendFail++; log(this.clientId, 'ERROR', `❌ 发送异常:${error.message}`); } }, interval); } // 连接逻辑 connect(duration, interval) { this.cleanResources(); this.socket = new WebSocket(pressureConfig.wsUrl); // 连接成功 this.socket.onopen = () => { this.isConnected = true; pressureStats.connectedClients++; log(this.clientId, 'INFO', "✅ 连接成功"); this.startSending(duration, interval); // 启动发送 }; // 连接关闭 this.socket.onclose = (code, reason) => { this.isConnected = false; log(this.clientId, 'WARN', `🔌 连接断开(code: ${code}, reason: ${reason})`); // 压测时仅少量重连,避免干扰统计 if (this.reconnectCount < pressureConfig.reconnectMaxTimes) { this.reconnectCount++; setTimeout(() => this.connect(duration, interval), pressureConfig.reconnectInterval); } }; // 连接错误 this.socket.onerror = (error) => { this.isConnected = false; pressureStats.totalSendAttempts++; pressureStats.totalSendFail++; log(this.clientId, 'ERROR', `❌ 连接错误:${error.message}`); }; } // 启动单个压测客户端 start(duration, interval) { pressureStats.totalClients++; log(this.clientId, 'INFO', "🚀 启动压测客户端"); this.connect(duration, interval); } } // ====================== 梯度加压逻辑 ====================== async function runPressureTest() { pressureStats.startTime = new Date(); log('GLOBAL', 'INFO', "🚀 开始WebSocket压力测试"); // startServerMonitor(); // 启动服务端监控 // 遍历梯度,逐阶段压测 for (const step of pressureConfig.pressureSteps) { const { clientCount, duration, interval } = step; log('GLOBAL', 'INFO', `\n====== 开始压测阶段:并发${clientCount}连接,发送间隔${interval}ms,持续${duration/1000}秒 ======`); // 启动当前阶段的所有客户端 const clients = []; for (let i = 0; i < clientCount; i++) { const clientId = `${pressureStats.totalClients + i + 1}`; // 全局唯一客户端ID const client = new WsPressureClient(clientId); clients.push(client); // 快速启动客户端(缩小启动间隔) setTimeout(() => client.start(duration, interval), i * pressureConfig.startOffset); } // 等待当前阶段结束 await new Promise(resolve => setTimeout(resolve, duration + (clientCount * pressureConfig.startOffset) + 5000)); // 清理当前阶段客户端 clients.forEach(client => client.cleanResources()); log('GLOBAL', 'INFO', `====== 结束压测阶段:并发${clientCount}连接 ======\n`); } // 压测结束,生成报告 generateReport(); process.exit(0); } // ====================== 异常处理 ====================== process.on('uncaughtException', (err) => { log('GLOBAL', 'ERROR', `未捕获异常:${err.message}`); generateReport(); process.exit(1); }); process.on('unhandledRejection', (err) => { log('GLOBAL', 'ERROR', `未处理Promise拒绝:${err.message}`); generateReport(); process.exit(1); }); process.on('SIGINT', () => { log('GLOBAL', 'INFO', "🛑 手动终止压测"); generateReport(); process.exit(0); }); // 启动压测 runPressureTest();
环境隔离:
- 压测客户端和服务端尽量部署在局域网(避免公网带宽成为瓶颈);
- 服务端关闭非核心功能(日志、监控、数据库同步等),只保留 WS 核心逻辑;
- 客户端机器:建议多核 CPU、足够内存(比如 8 核 16G,可承载 1000 + 并发连接)。
压测配置调整
根据你的服务端能力,调整
pressureConfig中的核心参数:
参数 调整建议 pressureSteps从低并发开始(如 100→200→500),逐步增加,避免直接上高并发打垮服务端; interval单连接发送间隔(100ms=10QPS / 连接,50ms=20QPS / 连接),根据需求调整; baseMessages模拟业务真实消息大小(用实际 Protobuf 生成 Base64,避免测试消息和真实场景差异); startOffset压测时设为 20-50ms(快速建立连接),但不要设为 0(避免瞬间 SYN 洪水); 执行压测 & 实时监控
启动服务端,确保无报错;
启动客户端压测脚本: bash
运行
node ws-pressure-test.js实时监控关键指标:
- 客户端:关注日志中的「连接成功率」「发送失败数」「平均发送耗时」;
- 服务端 :
- 系统层面:用
top(CPU / 内存)、iftop(网络带宽)、netstat -an | grep 18888 | wc -l(WS 连接数);- 应用层面:打印服务端的「每秒接收消息数」「连接数」「内存占用」。
压测结果分析
重点关注以下维度,判断服务端是否达标:
7. 连接成功率:理想值≥99%,若低于 90%,说明服务端无法承载该并发数;
8. 发送成功率:理想值≥99%,若失败率高,可能是服务端处理能力不足 / 网络丢包;
9. QPS:记录服务端稳定支撑的最大总 QPS(比如 5000 条 / 秒);
10. 服务端资源:CPU 占用≥80%、内存持续上涨→服务端达到瓶颈;
11. 异常场景:服务端是否崩溃、是否出现消息堆积、是否有连接超时。
12.
#### 四、进阶压测优化(突破单机客户端限制) 如果单机客户端无法模拟足够的并发(比如需要 10000 + 连接),可采用:
多机分布式压测:
- 多台客户端机器同时执行压测脚本,每台负责一部分并发数(比如 5 台机器,每台 2000 连接);
- 统一记录每台机器的压测报告,最后汇总。
连接复用 / 长连接优化:若业务场景是「长连接 + 低频消息」,重点测试「最大并发连接数」;若为「高频消息」,重点测试「消息吞吐量」。
突发流量模拟:在梯度中加入「短时间高并发」(比如 500 连接,间隔 10ms),验证服务端的限流 / 熔断能力。