压测客户端才是瓶颈:鸿蒙设备模拟从 1 万拉到 2 万,附云手机真机基准鸿蒙HarmonyOS压测
系列第 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.js | 20.20.2 |
| Redis | 7.0.15(主从 + 三哨兵) |
| Nginx | 1.24.0 |
| ws 库 | 8.18.0 |
| 端侧 | 鸿蒙云手机 ALN-AL00 / HarmonyOS 5.0.1 / API 17 |
| 压测工具 | 自研 Node 长连接压测客户端(k6 不支持长连接保活统计) |
一、一个反常的现象
压测鸿蒙任务接续后端时,数据是这样的:
| 连接数 | RTT P50 | P95 | 服务端 CPU |
|---|---|---|---|
| 20 | 2ms | 5ms | ≈0 |
| 5,000 | 108ms | 142ms | <2% |
| 10,000 | 202ms | 239ms | 1.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,000 | 20,000 / 20,000 |
| 建链总耗时 | 1.6s | 2.1s |
| 消息总数 | 55,000 | 110,000 |
| 消息丢失 | 0 | 0 |
| RTT P50 | 202ms | 143ms |
| RTT P95 | 239ms | 305ms |
| RTT P99 | 253ms | 346ms |
| RTT 最大值 | 258ms | 373ms |
单看每个 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。
这也说明诚实的压测必须带真机基准:光看压测数字,会漏掉端侧协议层的缺陷。
六、复盘:三条压测纪律
这次踩的坑可以归纳成三条,以后做长连接压测都用得上:
- 先看资源,再下结论。 延迟上升时第一件事是查服务端 CPU、内存、连接数。资源没打满而延迟飙升,优先怀疑客户端。
- 客户端要先自证清白。 单进程压测客户端的承载能力本身就是上限,设备数上万后应该默认拆多进程,必要时分多台压测机。
- 分位数合并原始样本,不要平均。 多进程、多机压测时,这是最容易悄悄出错的地方。
下一篇回到代码本身:给设备影子加 TTL,让离线设备自动清理;给消息加序号,解决重连之后的重复和乱序。
环境:4 × 华为云 x3e.8u.16g(8C16G,Ubuntu 24.04.4),Node 20.20.2,Redis 7.0.15(主从 + 三哨兵),Nginx 1.24.0,2026-11 实测。
更多推荐




所有评论(0)