系列第 3 篇,一篇压测复盘笔记。前面给鸿蒙多设备任务接续服务做压测,1 万连接时 RTT P50 到了 202ms,但三台服务器 CPU 加起来不到 10%。这篇记录怎么确认瓶颈在压测客户端自己,以及拆成多进程之后 2 万连接的真实数据。

环境与版本

项目版本 / 规格
服务端3 台华为云 ECS,8 vCPU / 16 GiB,Ubuntu 24.04.4
压测机1 台华为云 ECS,8 vCPU / 16 GiB
Node.js20.20.2
Redis7.0.15(主从 + 三哨兵)
Nginx1.24.0
ws 库8.18.0
端侧鸿蒙云手机 ALN-AL00 / HarmonyOS 5.0.1 / API 17
压测工具自研 Node 长连接压测客户端(k6 不支持长连接保活统计)

一、一个反常的现象

压测鸿蒙任务接续后端时,数据是这样的:

连接数RTT P50P95服务端 CPU
202ms5ms≈0
5,000108ms142ms<2%
10,000202ms239ms1.6%~3.6%

连接数翻倍,延迟几乎线性翻倍,但 CPU 纹丝不动。如果是服务端到瓶颈,应该先看到 CPU 或者内存打满。"延迟涨、资源不涨"基本可以断定瓶颈不在被测系统。

我的压测客户端是单个 Node 进程:1 万个 WebSocket 连接、5 千个定时器、每 5 秒一轮消息收发,全部挤在一个事件循环里。Node 是单线程的,1 万个连接的回调排队,消息自然要多等一百多毫秒——这个延迟被算进了 RTT,却是客户端自己造成的。

二、改造:一个进程变四个

思路很直接:把设备分摊到多个进程,每个进程的事件循环只负责自己那一份。用 child_process.fork 起 4 个 worker,每个 5,000 连接,总计 20,000。

两个必须处理的细节:

设备名要加进程前缀。 设备影子按 deviceId 索引,不同 worker 如果都叫 phone-0,后连的会把先连的顶掉,消息就乱了:

// 每个 worker 启动时带不同 prefix
const device = PREFIX + (isPhone ? 'phone-' : 'tablet-') + idx;
const target = PREFIX + (isPhone ? 'tablet-' : 'phone-') + pairId;

配对关系保持在同一个 worker 内部(phone-i 配 tablet-i),避免跨进程依赖。

分位数不能各算各的再平均。 P95 的平均值不等于整体的 P95。正确做法是每个 worker 把原始 RTT 样本写进结果文件,主进程合并后再算分位:

// worker:保留原始样本
fs.writeFileSync(`result-${tag}.json`, JSON.stringify({ ...out, rttSamples: rtts }));

// 主进程:合并样本后统一计算
const all = [];
for (let i = 0; i < PROCS; i++) {
  const r = JSON.parse(fs.readFileSync(`result-${tag}-w${i}.json`));
  all.push(...r.rttSamples);
}
const p95 = pct(all, 0.95);

这是压测里最容易算错的指标,尤其 worker 之间负载不均时,平均出来的 P95 会明显偏乐观。

三、结果:2 万连接,11 万条消息零丢失

4 进程 × 5,000 连接,同样每 5 秒一轮任务接续消息,跑 60 秒:

指标单进程 10,000四进程 20,000
建链成功10,000 / 10,00020,000 / 20,000
建链总耗时1.6s2.1s
消息总数55,000110,000
消息丢失00
RTT P50202ms143ms
RTT P95239ms305ms
RTT P99253ms346ms
RTT 最大值258ms373ms

单看每个 worker:P50 分别在 110~180ms,比单进程扛 1 万连接时的 202ms 都低。

三节点连接分布依然均匀:6667 / 6665 / 6668,单节点 6,666 个长连接,是之前 1 万总量时的两倍。

四、这组数据说明了什么

1. 设备翻倍,P50 反而降了 29%。 2 万连接的 P50(143ms)低于单进程 1 万连接(202ms),直接坐实了之前的判断:那 200ms 里有一大半是客户端事件循环排队,不是服务端处理。拆分之后每个进程只负责 5,000 连接,排队时间立刻下来。

2. P95/P99 开始随总量上升。 P50 降了,但 P95 从 239ms 涨到 305ms,P99 从 253ms 涨到 346ms。这说明尾部延迟的主导因素变了:客户端瓶颈被削弱后,服务端的跨节点转发(Redis Pub/Sub)和 Nginx 汇聚开始显现。2 万设备意味着每 5 秒约 1 万条消息打进同一个 Redis,尾部的几百毫秒是真实的服务端排队。

3. 零丢失在 2 万规模下依然成立。 11 万条消息全部收到回执,没有重连。这套"设备影子 + 精确投递"的结构在 2 万长连接下是稳的。

4. 还没摸到服务端上限。 服务端 CPU 依旧很低,真正的天花板还要继续加进程、加客户端机器才能找到。但这次的目标不是刷数字,而是把测量方法修正——在客户端瓶颈没排除之前,任何"服务端最多扛多少"的结论都是错的。

五、压测客户端模拟的到底是什么

这一节补一个容易被忽略的前提:上面 2 万个「连接」不是抽象数字,它们模拟的是鸿蒙设备。

压测客户端不是随便发心跳,而是完全复刻 ArkTS 端的建链参数与消息格式。这是我从云手机抓包对照着写的。

5.1 建链参数与鸿蒙端一致

ArkTS 端:

this.ws.connect(`ws://1.92.107.89/ws?device=${this.deviceId}&type=phone`);

压测客户端:

const ws = new WebSocket(`ws://${HOST}/ws?device=load-${i}&type=phone`);

device 与 type 两个参数不能省——服务端要靠它们做设备注册和路由。如果压测脚本漏掉,服务端不会为这些连接建影子,测出来的数字就是虚的。

5.2 消息体结构与 ArkTS 端一字不差

从 Index.ets 抄下来的结构:

const msg: TaskMsg = {
  type: 'task_update',
  seq: this.seq,
  rid: rid,
  t0: Date.now(),
  taskId: 'task-' + this.seq,
  target: this.peerId,
  payload: this.draft
};

压测端:

ws.send(JSON.stringify({
  type: 'task_update',
  seq: n,
  rid: rid,
  t0: Date.now(),
  taskId: 'load-task-' + n,
  target: peerOf(i),
  payload: '压测消息 ' + n
}));

字段少一个,服务端的分支就不一样。比如不发 rid,就不会有回执,零丢失 这个指标根本没法统计。

5.3 真机基准:云手机跑出来的真实数据

压测是为了模拟,但必须有真机对照,否则不知道模拟得像不像。

我在一台鸿蒙云手机(ALN-AL00,HarmonyOS 5.0.1,API 17)上跑同一套 ArkTS 客户端:

云手机连续发送任务态

发送 3  接收 9  断线 0  RTT 0ms

12:17:09 发送 seq=1 -> hm-tablet-01
12:17:12 发送 seq=2 -> hm-tablet-01
12:17:15 发送 seq=3 -> hm-tablet-01

对照结果:

维度云手机(真机)压测客户端(模拟)
建链耗时点连接后 5 秒内收到首包1.6 秒建 1 万条(并发摊薄)
消息格式task_update + seq/rid/t0完全一致
收发计数发送 3 / 接收 9发送 5.5 万 / 接收 5.5 万
断线检测close + error 同秒触发同
RTT 统计恒为 0(服务端无 ACK)无 ACK 时同样为 0

最后一行是这个对照最有价值的地方:压测脚本里我看到 P50 一直有值,是因为脚本自己算的是 t0 到收到消息的时间差,不依赖服务端 ACK。而 ArkTS 端依赖 ACK,所以恒为 0。

同一个服务端,两种客户端得出了不同的 RTT——问题不在压测,在协议缺了 ACK。

这也说明诚实的压测必须带真机基准:光看压测数字,会漏掉端侧协议层的缺陷。

六、复盘:三条压测纪律

这次踩的坑可以归纳成三条,以后做长连接压测都用得上:

  1. 先看资源,再下结论。 延迟上升时第一件事是查服务端 CPU、内存、连接数。资源没打满而延迟飙升,优先怀疑客户端。
  2. 客户端要先自证清白。 单进程压测客户端的承载能力本身就是上限,设备数上万后应该默认拆多进程,必要时分多台压测机。
  3. 分位数合并原始样本,不要平均。 多进程、多机压测时,这是最容易悄悄出错的地方。

下一篇回到代码本身:给设备影子加 TTL,让离线设备自动清理;给消息加序号,解决重连之后的重复和乱序。


环境:4 × 华为云 x3e.8u.16g(8C16G,Ubuntu 24.04.4),Node 20.20.2,Redis 7.0.15(主从 + 三哨兵),Nginx 1.24.0,2026-11 实测。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐