Files
im_server/loadtest/im-pressure
..

IM 双机压力测试工具

该工具按配置指定的企业租户运行:两台压测机各承载 1000 个 WebSocket 用户,共 2000 个用户、1000 对好友和一个 2000 人群。当前测试环境使用 allrich,本轮实时参数见 MACHINE-B.md 和 allrich 双机压测执行记录。

只使用一台物理压测机时,请使用 单机 60% 容量压测方案 和 config.single-machine.example.json。单机依然运行 A/B 两个逻辑 agent,用于保留好友对、跨逻辑分片投递和合并报告的完整契约。 首轮实测数据与瓶颈分析见 单机 60% 容量压测结果。

机器 B 上的 AI 或操作人请直接阅读 MACHINE-B.md。该文件不重复讲方案,只给出机器 B 从核验到启动、观察和回传结果的完整命令。

两台机器的固定分工

当前仓库所在机器固定作为 机器 A。两台机器不要交换 -machine 参数,否则账号分片和结果归属会混乱。

项目 机器 A(当前机器,主控) 机器 B(另一台机器,发压)
模拟账号 lta0001 至 lta1000 ltb0001 至 ltb1000
WS 连接数 1000 1000
固定参数 -machine a -machine b
数据准备 执行 prepare、verify 不执行
双机协调 运行 coordinator :8090 连接机器 A 的 :8090
场景发压 运行 agent A 运行 agent B
指标 agent :9105,运行中央 collect agent :9105,供机器 A 抓取
结果 保存 A 结果、接收 B 结果、执行 merge 保存 B 结果并传回机器 A

机器 A 至少准备三个终端:协调器、agent A、指标采集。机器 B 至少准备一个终端运行 agent B;需要单独采集本机指标时再开第二个终端。

机器 B 最短操作清单

机器 B 的完整命令见 MACHINE-B.md。机器 B 操作人只需完成以下事项:

  1. 检出与机器 A 相同的 Git 提交并构建 im-pressure。
  2. 接收机器 A 生成的 config.json 和 manifest.json,不要自行运行 prepare。
  3. 设置与机器 A 相同的 IM_API_KEY、IM_LOAD_PASSWORD,执行一次 verify。
  4. 从机器 A 获取本轮 RUN_ID 和机器 A 内网 IP。
  5. 等机器 A 启动 coordinator 后,固定使用 -machine b 启动 agent B。
  6. agent B 退出后,将 agent-b-summary.json 传回机器 A。

机器 B 不负责建账号、加好友、建群、选择场景或合并报告。

网络要求

  • 机器 B 必须能访问 http://机器A内网IP:8090。
  • 如果由机器 A 集中采集 agent 指标,机器 A 必须能访问 http://机器B内网IP:9105/metrics。
  • 两台机器都必须能访问配置中的 HTTP API 和各自的 WS 地址。
  • 8090 是无鉴权的压测协调端口,只允许压测内网访问,不要暴露到公网。
  • 两台机器必须使用 NTP/chrony 同步时间。报告会分别输出同机时钟延迟和跨机器单向延迟;跨机器出现负延迟样本时,后者仅用于诊断,不参与门禁。
  • agent 默认每 20 秒发送 WebSocket Ping,覆盖等待另一台机器、发压、drain 和历史核验阶段;ws_ping_interval 必须小于公网入口的空闲连接超时。
  • 正式开始前 agent 会持续检查实际存活连接;连接断开后自动重新登录并订阅 P2P、群 Topic。初次连接不齐或等待另一台机器期间掉线不会退出。
  • coordinator 使用两阶段屏障:两边实时连接数都达到配置值后进入验证倒计时;倒计时内任一方掉线就撤销屏障。两边再次确认满连接后才提交最终开始时间。
  • 压测机如保持 OpenVPN 等全局 TUN,仅在另一套代理中设置 DIRECT 无法绕过它。起压前必须确认 http_base 和 WS 域名的每个当前 IP 都走物理网卡;需保留 VPN 访问内网时,可为这些 IP 配置经局域网关的 /32 主机路由。VPN 重连、DNS 变更或机器重启后需重新核验。

统计口径

  • 发送成功以 {ctrl code=2xx, params.seq>0} 为准,形成两台机器共享的 ACK 台账。
  • 实时到达只统计同时匹配本轮 run_id 和 x-im-client-mid 的 {data}。
  • 群消息设置 noecho=false,每条服务端接受的群消息期望 2000 个连接各收到一次。
  • 实时 drain 结束后,两机先汇总 ACK 台账和“已实时观测但未收到 ACK”的消息台账;等待 history_settle(默认 5 分钟)让 Kafka 自然排空,再通过 {get what=data} 按 Topic/Seq 回查两类消息。
  • 每轮建立 WS 后、正式计时前,以 32 个 worker 对本机 1000 个账号做好友关系、P2P Topic 和发布权限预检;任一异常会终止本轮。
  • 历史回查默认每机最多使用 12 个 worker、每页 10 条、同一 Topic 两页之间至少间隔 500ms,并在每页请求后等待目标消息全部返回(最长 3 秒)再继续。同一 Topic 严格串行,不同 Topic 才并行;因此群聊只会有 1 个活跃回查 worker,单聊可并行核验不同 Topic。总超时默认 3 分钟,并在超时内持续重试缺失区间。该限速用于避免正确性核验本身制造 Topic/Proxy 洪峰;历史读取吞吐应使用独立场景测试。
  • agent 完成后默认在 10 秒内均匀关闭本机 WebSocket,避免 1000 个群订阅同时离线冲击 Topic 队列。两机退出时间可能相差数秒,协调器完成以两份 summary 都已提交为准。
  • publish 错误完整分类为 403、502、503、ctrl_timeout、socket_error 和 other。publish_rejected 只统计明确拒绝,超时和 socket 错误单独计入 publish_outcome_unknown。
  • delivery_local_p95_ms 使用发送方本机收到 echo 的同机时钟样本,是投递延迟门禁口径;兼容字段 delivery_p95_ms 与它保持一致。
  • delivery_cross_machine_p95_ms 是跨机器单向观测值,只有两机时钟同步达标时才有意义;cross_machine_negative_latency_samples > 0 明确表示存在时钟偏差。
  • manifest.json 含登录 token,文件权限为 0600,不得提交到 Git 或放入结果包。

两台机器初始化

两台机器先检出完全相同的 Git 提交,并分别构建本机二进制:

git rev-parse HEAD
go build -o loadtest/im-pressure/im-pressure ./loadtest/im-pressure
ulimit -n 100000

只在机器 A 创建配置:

cp loadtest/im-pressure/config.example.json loadtest/im-pressure/config.json
export IM_API_KEY='...'
export IM_LOAD_PASSWORD='...'

修改 config.json:http_base 指向 IM HTTP API;endpoints.a/b 分别填写两台机器应直连的 WS Pod/LB 地址,多个地址时用户会轮询分配。正式测试保持 insecure_tls=false。

测试环境要求短信验证注册时,将 registration_phone_prefix 配置为 7 位数字。工具为机器 A 追加后缀 0001 起的序号,为机器 B 追加后缀 5001 起的序号,生成互不冲突的 11 位测试手机号;验证码从 verification_code_env 指定的环境变量读取。正式环境不得使用固定测试验证码。

endpoints.a 是机器 A 的 1000 个用户使用的 WS Pod/LB 地址,endpoints.b 是机器 B 的 1000 个用户使用的地址。要验证跨 Pod 投递时,应让 A、B 连接到不同 Pod 或不同的定向入口;如果都填同一个普通 LB,连接落在哪个 Pod 将由 LB 决定。

机器 B 同样需要设置 IM_API_KEY 和 IM_LOAD_PASSWORD。密码用于 Token 失效时自动回退 Basic 登录,不能只在机器 A 设置。

机器 A:一次性准备

以下命令只在当前机器 A 执行。命令可重复运行,会复用已有账号、好友和群:

./loadtest/im-pressure/im-pressure prepare \
  -config loadtest/im-pressure/config.json \
  -manifest loadtest/im-pressure/manifest.json

./loadtest/im-pressure/im-pressure verify \
  -config loadtest/im-pressure/config.json \
  -manifest loadtest/im-pressure/manifest.json

准备成功后,机器 A 的 manifest 中包含 2000 个账号 Token、1000 个 P2P Topic 和群 Topic。将配置和 manifest 安全复制到机器 B:

export MACHINE_B_SSH='user@机器B内网IP'
export MACHINE_B_REPO='/home/user/im_server'

scp loadtest/im-pressure/config.json \
  "${MACHINE_B_SSH}:${MACHINE_B_REPO}/loadtest/im-pressure/config.json"
scp loadtest/im-pressure/manifest.json \
  "${MACHINE_B_SSH}:${MACHINE_B_REPO}/loadtest/im-pressure/manifest.json"

机器 B 收到后执行:

cd /home/user/im_server
chmod 600 loadtest/im-pressure/manifest.json
export IM_API_KEY='...'
export IM_LOAD_PASSWORD='...'
./loadtest/im-pressure/im-pressure verify \
  -config loadtest/im-pressure/config.json \
  -manifest loadtest/im-pressure/manifest.json

MACHINE_B_REPO 必须填写机器 B 上仓库的绝对路径。每次机器 A 重新执行 prepare 并改变了 manifest,都必须重新同步到机器 B。

每轮双机运行

以下示例执行单聊场景。机器 A 设置本轮场景;两台机器使用同一个、每轮唯一且不超过 24 个字符的 RUN_ID,保证结果目录一致:

export RUN_ID='p2p-20260927-01'
export SCENARIO='p2p'
export MACHINE_A_IP='机器A内网IP'
export MACHINE_B_IP='机器B内网IP'
mkdir -p "loadtest/im-pressure/results/${RUN_ID}"

机器 A 应在正式起压前启动终端 A3 的指标采集,然后按 A1、A2、B1 的顺序启动协调器和两个 agent。

第一步:机器 A 启动协调器

机器 A 终端 A1:

./loadtest/im-pressure/im-pressure coordinator \
  -listen :8090 \
  -run-id "${RUN_ID}" \
  -scenario "${SCENARIO}" \
  -output "loadtest/im-pressure/results/${RUN_ID}/coordinator.json"

协调器会等待两个 agent 都报告 1000 条连接 READY,再统一下发至少 30 秒后的开始时间。不要跳过协调器直接启动 agent。

第二步:机器 A 启动 agent A

机器 A 终端 A2:

./loadtest/im-pressure/im-pressure agent \
  -config loadtest/im-pressure/config.json \
  -manifest loadtest/im-pressure/manifest.json \
  -machine a \
  -coordinator "http://${MACHINE_A_IP}:8090" \
  -metrics-listen :9105 \
  -output "loadtest/im-pressure/results/${RUN_ID}/agent-a-summary.json"

第三步:机器 B 启动 agent B

机器 B 终端 B1 需要先设置与机器 A 相同的值:

cd /home/user/im_server
export IM_API_KEY='...'
export IM_LOAD_PASSWORD='...'
export RUN_ID='p2p-20260927-01'
export MACHINE_A_IP='机器A内网IP'
mkdir -p "loadtest/im-pressure/results/${RUN_ID}"

./loadtest/im-pressure/im-pressure agent \
  -config loadtest/im-pressure/config.json \
  -manifest loadtest/im-pressure/manifest.json \
  -machine b \
  -coordinator "http://${MACHINE_A_IP}:8090" \
  -metrics-listen :9105 \
  -output "loadtest/im-pressure/results/${RUN_ID}/agent-b-summary.json"

机器 B 不需要传 SCENARIO,场景由机器 A 的 coordinator 下发。两个 agent 可以任意先后启动,但 coordinator 必须最先启动。

两个 agent 都建立 1000 条连接后,协调器下发统一的 UTC start_at。场景可替换为:

  • p2p:1000 对用户两侧同时发送,默认每人 10 条、间隔 1 秒。
  • group:users_per_machine=1000 时每个 agent 有 500 个发送者,每人 2 条;全局每 100ms 一条,共 2000 条。其他容量按每个 agent 用户数的 50% 选发送者。
  • mixed:users_per_machine=1000 时使用 300 对私聊用户和每个 agent 250 个群发送者;其他容量分别按 30% 和 25% 同比计算。

机器 A:回收并合并报告

两个 agent 完成历史回查并退出后,在机器 A 拉取机器 B 的 summary:

scp "${MACHINE_B_SSH}:${MACHINE_B_REPO}/loadtest/im-pressure/results/${RUN_ID}/agent-b-summary.json" \
  "loadtest/im-pressure/results/${RUN_ID}/agent-b-summary.json"

然后仍在机器 A 合并报告:

./loadtest/im-pressure/im-pressure merge \
  -a "loadtest/im-pressure/results/${RUN_ID}/agent-a-summary.json" \
  -b "loadtest/im-pressure/results/${RUN_ID}/agent-b-summary.json" \
  -output "loadtest/im-pressure/results/${RUN_ID}/report.json"

report.json 给出 ACK、实时投递、重复、乱序、断连和历史持久化核验结果。断连会额外输出 disconnect_reasons 分类计数和最多 100 条 disconnect_samples 原始错误样本。当前正确性门禁要求上述错误计数全部为 0。

每个 agent 在 :9105/metrics 暴露连接、发送、接收、重复、乱序、断连以及 Go/process 指标;端口冲突时可通过 -metrics-listen 修改,传空字符串可关闭。

机器 A:集中采集指标

collect 可并行抓取 Prometheus、Redis exporter、Kafka exporter、mysqld exporter、kube-state-metrics/cAdvisor 和压测机 node-exporter,原始响应按时间写入 JSONL:

./loadtest/im-pressure/im-pressure collect \
  -duration 20m -interval 5s \
  -coordinator "http://${MACHINE_A_IP}:8090" \
  -coordinator-output "loadtest/im-pressure/results/${RUN_ID}/coordinator.json" \
  -url agent-a=http://127.0.0.1:9105/metrics \
  -url agent-b=http://${MACHINE_B_IP}:9105/metrics \
  -url im=http://PROMETHEUS_HOST/api/v1/query?query=up \
  -url redis=http://REDIS_EXPORTER/metrics \
  -url kafka=http://KAFKA_EXPORTER/metrics \
  -url mysql=http://MYSQLD_EXPORTER/metrics \
  -url node-a=http://NODE_A_EXPORTER/metrics \
  -url node-b=http://NODE_B_EXPORTER/metrics \
  -output "loadtest/im-pressure/results/${RUN_ID}/metrics.jsonl"

机器 A 终端 A3 运行采集器。每条 JSONL 快照会自动标记 phase=before|during|after:coordinator 下发开始时间后进入 during,coordinator 结果文件生成后进入 after。URL 含 & 时需用单引号包住整个 name=url 参数。建议起压前 2 分钟开始采集,结束后继续 5 分钟,以观察 Kafka lag、数据库落库追平时间及资源恢复。如果机器 A 无法访问机器 B 的 9105,就在机器 B 单独运行一份 collect,结束后把 JSONL 一并传回机器 A。

MySQL 独立落库核验

合并报告后在机器 A 使用只读 DSN 对 ACK ledger 做独立校验。工具会根据租户和消息时间自动查询 im_NNN_messages 及涉及的群消息月表,精确比较 client_mid + topic + seq,并统计缺失、错配、重复和非 ACK 消息:

export MYSQL_DSN='readonly:password@tcp(mysql-host:3306)/meeting_im?parseTime=true'
./loadtest/im-pressure/im-pressure verify-db \
  -coordinator "loadtest/im-pressure/results/${RUN_ID}/coordinator.json" \
  -tenant allrich \
  -dsn-env MYSQL_DSN \
  -registry-db hotgo \
  -output "loadtest/im-pressure/results/${RUN_ID}/db-verification.json"

DSN 只能通过环境变量传入,不要写入配置、命令脚本或结果包。命令在 missing、mismatch、duplicate 或 unexpected 任一计数非零时返回失败。

运行前检查

ulimit -n 100000
curl -fsS "http://${MACHINE_A_IP}:8090/v1/status"
curl -fsS http://127.0.0.1:9105/metrics | head

两台压测机应完成时钟同步,并确认压测出口、Ingress、Pod 与 Redis/MySQL/Kafka 连接数限制可以容纳本轮负载。协调器没有鉴权,只能监听在受控压测网络中。