基于该 WebSocket 脚本开展专业的压力测试

一、压测核心目标 & 关键指标

先明确压测要验证的核心问题,避免盲目加压:

核心目标 关键监控指标
服务端最大并发连接数 成功建立的 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();
  1. 环境隔离

    • 压测客户端和服务端尽量部署在局域网(避免公网带宽成为瓶颈);
    • 服务端关闭非核心功能(日志、监控、数据库同步等),只保留 WS 核心逻辑;
    • 客户端机器:建议多核 CPU、足够内存(比如 8 核 16G,可承载 1000 + 并发连接)。
压测配置调整

根据你的服务端能力,调整pressureConfig中的核心参数:

参数 调整建议
pressureSteps 从低并发开始(如 100→200→500),逐步增加,避免直接上高并发打垮服务端;
interval 单连接发送间隔(100ms=10QPS / 连接,50ms=20QPS / 连接),根据需求调整;
baseMessages 模拟业务真实消息大小(用实际 Protobuf 生成 Base64,避免测试消息和真实场景差异);
startOffset 压测时设为 20-50ms(快速建立连接),但不要设为 0(避免瞬间 SYN 洪水);
执行压测 & 实时监控
  1. 启动服务端,确保无报错;

  2. 启动客户端压测脚本: bash

    运行

    复制代码
    node ws-pressure-test.js
  3. 实时监控关键指标:

    • 客户端:关注日志中的「连接成功率」「发送失败数」「平均发送耗时」;
    • 服务端
      • 系统层面:用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 + 连接),可采用:
  1. 多机分布式压测

    • 多台客户端机器同时执行压测脚本,每台负责一部分并发数(比如 5 台机器,每台 2000 连接);
    • 统一记录每台机器的压测报告,最后汇总。
  2. 连接复用 / 长连接优化:若业务场景是「长连接 + 低频消息」,重点测试「最大并发连接数」;若为「高频消息」,重点测试「消息吞吐量」。

  3. 突发流量模拟:在梯度中加入「短时间高并发」(比如 500 连接,间隔 10ms),验证服务端的限流 / 熔断能力。

相关推荐
小宇的天下2 小时前
Synopsys Skipper 核心功能与使用指南(结构化总结)
运维·服务器
丁丁丁梦涛2 小时前
nginx在多层服务器代理接口地址的应用
运维·服务器·nginx
@淡 定2 小时前
MVCC(多版本并发控制)实现机制详解
java·服务器·数据库
GISer_CV攻城狮2 小时前
MapLibre/Martin 地图服务器docker化安装部署
运维·服务器·docker
代码总长两年半2 小时前
Linux---配置编程环境VSCode
linux·运维·服务器
Tipriest_2 小时前
Linux 桌面(Desktop)图标的生成原理/执行流程/自己编写桌面图标的方法
linux·运维·服务器
小尧嵌入式2 小时前
CANOpen协议
服务器·网络·c++·windows
_OP_CHEN2 小时前
【Linux系统编程】(十七)揭秘 Linux 进程创建与终止:从 fork 到 exit 的底层逻辑全解析
linux·运维·服务器·操作系统·shell·进程·进程创建与终止
爱学大树锯2 小时前
【快刷面试-高并发锁篇】- 基于票务系统在不同服务器,分布式场景中该如何解决
服务器·分布式·面试