feat: add dual-host IM pressure test tool
This commit is contained in:
@@ -0,0 +1,792 @@
|
||||
# IM 006 商户双机消息压力测试方案
|
||||
|
||||
日期:2026-09-25
|
||||
|
||||
## 1. 测试目标
|
||||
|
||||
本次测试使用 006 商户的 2000 个账号,在两台压测机上各运行 1000 个在线 WebSocket 用户,验证多 Pod 场景下:
|
||||
|
||||
1. 1000 对 P2P 会话同时收发时,消息是否被接受、实时投递并最终持久化。
|
||||
2. 2000 人大群中 1000 个成员轮流发言时,跨 Pod 群广播的丢失率、重复率、乱序率和延迟。
|
||||
3. 300 对 P2P 与 500 个群发送者同时工作时,混合负载是否相互影响。
|
||||
4. Tinode 原生 Cluster、Kafka 消息持久化、数据库分表和 WebSocket 下行是否出现容量瓶颈。
|
||||
|
||||
本轮不在正式发压窗口内注册账号、加好友或建群。所有数据准备完成并核验后,再开始消息压力测试。
|
||||
|
||||
## 2. 本方案对需求的精确定义
|
||||
|
||||
### 2.1 账号与机器分片
|
||||
|
||||
账号使用只包含小写字母和数字、且不超过 10 个字符的用户名,以满足当前注册校验:
|
||||
|
||||
| 压测机 | 账号 | 数量 | 用户名示例 |
|
||||
| --- | --- | ---: | --- |
|
||||
| Machine A | A0001-A1000 | 1000 | `lta0001`、`lta1000` |
|
||||
| Machine B | B0001-B1000 | 1000 | `ltb0001`、`ltb1000` |
|
||||
|
||||
固定配对关系为 `A0001 <-> B0001`,依此类推到 `A1000 <-> B1000`。“号码相同”指账号序号相同,不使用相同手机号。
|
||||
|
||||
每个账号最终记录:`username`、`user_id`、`token`、`token_expires`、`machine`、`pair_no`、`p2p_topic`、`group_topic`。密码只从环境变量读取,不写入结果文件或日志。
|
||||
|
||||
### 2.2 群聊需求解释
|
||||
|
||||
“1000 个人间隔 0.1 秒发消息,2 轮合计 2000 条”按全局调度解释:
|
||||
|
||||
- 每 100ms 只安排一个发送者发送一条消息。
|
||||
- 每轮 1000 个发送者各发送 1 条,共 1000 条,持续约 100 秒。
|
||||
- 连续执行 2 轮,共 2000 条,正式发压约 200 秒。
|
||||
- 不是每个发送者每 100ms 发一条;后一种解释会产生 10000 条/秒和 100 万条/100 秒,与“合计 2000 条”矛盾。
|
||||
|
||||
### 2.3 P2P 场景默认强度
|
||||
|
||||
原需求未指定每对消息数。本方案默认每对双方各发 10 条:
|
||||
|
||||
- 1000 对同时开始。
|
||||
- 每个用户每秒发送 1 条,共 10 秒;每次加 0-200ms 确定性抖动,避免所有请求落在同一毫秒。
|
||||
- 总发布量:`1000 × 2 × 10 = 20000` 条。
|
||||
- 主要期望投递量:20000 次对端投递;发送方自身回显单独统计,不混入对端丢失率。
|
||||
|
||||
消息数、间隔和抖动必须可通过命令行参数修改,但每轮结果必须记录实际参数。
|
||||
|
||||
## 3. 协议与接口契约
|
||||
|
||||
### 3.1 WebSocket 建连与账号注册
|
||||
|
||||
WebSocket 地址为 `/v0/channels`,Upgrade 携带普通 API Key。每个连接严格执行:
|
||||
|
||||
```text
|
||||
Upgrade
|
||||
-> hi(tenant="006")
|
||||
-> acc 或 login
|
||||
-> sub(me)
|
||||
-> sub(目标 P2P/群 Topic)
|
||||
-> READY
|
||||
```
|
||||
|
||||
006 是企业商户,注册使用 `registration_type: 0` 和 `tenant_code: "006"`:
|
||||
|
||||
```json
|
||||
{
|
||||
"acc": {
|
||||
"id": "prep-acc-lta0001",
|
||||
"user": "new",
|
||||
"scheme": "basic",
|
||||
"username": "lta0001",
|
||||
"registration_type": 0,
|
||||
"tenant_code": "006",
|
||||
"secret": "base64(lta0001:password)",
|
||||
"login": true,
|
||||
"desc": {"public": {"fn": "LT A0001"}}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
当前 006 环境必须确认未强制手机号或邀请码注册。准备程序应先尝试 basic 登录:已存在且密码正确则复用;仅在账号不存在时注册。账号存在但密码不匹配必须停止,禁止覆盖真实账号。
|
||||
|
||||
### 3.2 HTTP 公共请求头
|
||||
|
||||
好友、隐私和群接口使用登录返回的同一 Token:
|
||||
|
||||
```text
|
||||
X-MeetingIM-APIKey: <ordinary api key>
|
||||
X-IM-Schema: 1
|
||||
X-Request-ID: <unique request id>
|
||||
Authorization: Bearer <token>
|
||||
Idempotency-Key: <unique key for write requests>
|
||||
```
|
||||
|
||||
所有密钥和 Token 通过环境变量或权限受控文件传入,日志只记录账号索引和脱敏 ID。
|
||||
|
||||
### 3.3 好友准备
|
||||
|
||||
对每个序号执行:
|
||||
|
||||
1. A 调用 `POST /v1/im/friend-requests`,Body 为 `{"target_user_id":"usr_b","message":"loadtest","source":"search","source_topic":""}`。
|
||||
2. 若返回 `pending`,B 调用 `POST /v1/im/friend-requests/{request_id}/accept`。
|
||||
3. 若已经是好友,读取好友列表或会话 context 并复用关系。
|
||||
4. 保存响应中的 P2P `topic`;必要时调用 `POST /v1/im/conversations/p2p`,Body 为 `{"peer_user_id":"usr_b"}`。
|
||||
5. 最终核验 1000 对关系均为 `friend`,且双方能订阅同一个 P2P Topic。
|
||||
|
||||
好友创建会产生 `friend_added` 系统消息,因此正式统计只识别本轮 `run_id` 对应的 `x-im-client-mid`,不能把 Topic 的初始 `seq` 当成 0。
|
||||
|
||||
### 3.4 2000 人群准备
|
||||
|
||||
1. 使用 A0001 调用 `POST /v1/im/groups` 创建群。创建接口最多附带 99 个成员,建议只创建群主。
|
||||
2. 先确认所有账号的 `group_invite_verification=false`,避免进入当前尚未完整开放的邀请确认链路。
|
||||
3. 调用 `POST /v1/im/groups/{topic}/invitations` 分批邀请,每批不超过 100 人,每批使用独立 `Idempotency-Key`。
|
||||
4. 邀请完成后分页调用 `GET /v1/im/groups/{topic}/members`,核验去重后的有效成员恰好为 2000。
|
||||
5. 2000 个 WebSocket 全部订阅该群,并完成屏障消息验证后才允许发压。
|
||||
|
||||
直接邀请会产生群业务事件和系统消息。准备结束后至少静默 30 秒,并记录正式测试前的群 `baseline_seq`;统计时仍以 `run_id/client_mid` 为准。
|
||||
|
||||
## 4. 环境硬门槛
|
||||
|
||||
任一门槛不满足,本轮标记为 `INVALID`,不能把结果当作服务端性能结论:
|
||||
|
||||
1. `max_subscriber_count >= 2500`。仓库配置默认是 128,必须在所有 Pod 上统一调高并滚动重启。
|
||||
2. 006 商户 `max_group_members` 为 0(不限)或不小于 2000;目标群 `max_member_count` 同样满足要求。
|
||||
3. IM Server 所有 Pod 版本和配置一致,原生 Cluster 成员数与 Running Pod 数一致。
|
||||
4. Kafka 的 P2P、群消息持久化 Topic 可写,消费组无持续积压,死信在基线时为 0。
|
||||
5. 两台压测机到目标入口网络稳定,文件描述符上限、临时端口和 NAT 容量足够。
|
||||
6. 两台压测机使用 chrony/NTP,同一时刻的时钟偏差绝对值小于 5ms,否则跨机器单向延迟无效。
|
||||
7. 每台压测机至少预留 2 CPU、4GB 内存和稳定的 100Mbps 网络;测试进程自身 CPU 不应长期超过 70%。
|
||||
8. 2000 个连接全部 READY;正式阶段不允许把连接失败用户从分母中静默删除。
|
||||
|
||||
## 5. 多 Pod 拓扑
|
||||
|
||||
仅通过同一个负载均衡入口连接,不能证明消息确实跨 Pod。推荐提供仅供压测使用的三个直连入口,并保持业务数据路径不变:
|
||||
|
||||
```text
|
||||
Machine A: A0001-A0500 -> Pod 0
|
||||
A0501-A1000 -> Pod 1
|
||||
Machine B: B0001-B1000 -> Pod 2
|
||||
```
|
||||
|
||||
这样 1000 对 P2P 的双方一定不在同一个 Pod;群成员也覆盖全部 3 个 Pod。若只能提供两个直连入口,则 Machine A 固定 Pod 0、Machine B 固定 Pod 1,并在另一轮把 Machine B 切到 Pod 2 重跑。
|
||||
|
||||
入口必须能明确确认实际 Pod,例如握手诊断日志或反向代理访问日志能关联 `session -> pod`。不要依赖负载均衡概率分布。
|
||||
|
||||
Topic 消息在启用 Tinode 原生 Cluster 时由 Cluster 路由;此时 Redis realtime 指标中的 `publish/skip_native_cluster` 是预期状态,不代表丢失。Redis 仍需观察用户级业务事件和 Session 控制类 fanout。
|
||||
|
||||
## 6. 统一消息格式
|
||||
|
||||
所有正式测试消息都使用 JSON 文本消息,并设置 `noecho:false`:
|
||||
|
||||
```json
|
||||
{
|
||||
"pub": {
|
||||
"id": "s1-a0001-00001",
|
||||
"topic": "p2p_or_grp",
|
||||
"noecho": false,
|
||||
"head": {
|
||||
"mime": "application/json",
|
||||
"x-im-schema": 1,
|
||||
"x-im-type": "text",
|
||||
"x-im-client-mid": "lt-20260925-s1-a0001-00001"
|
||||
},
|
||||
"content": {
|
||||
"text": "loadtest",
|
||||
"run_id": "lt-20260925-s1",
|
||||
"scenario": "p2p",
|
||||
"sender_index": 1,
|
||||
"message_index": 1,
|
||||
"sent_at_unix_nano": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`x-im-client-mid` 在 `tenant + topic + sender` 范围内必须唯一,重试时必须复用原值。发送程序用 `{ctrl.id}` 关联请求,用 `ctrl.params.seq` 记录服务端分配的 Topic 序号。
|
||||
|
||||
## 7. 执行阶段
|
||||
|
||||
### 7.1 阶段 P0:预检与热身
|
||||
|
||||
1. 两台机器各以 50 连接/秒建立 1000 条连接,总建连时间约 20 秒。
|
||||
2. 每个用户订阅 `me`、自己的 P2P Topic 和群 Topic。
|
||||
3. 群主发送 1 条不计入正式结果的群屏障消息;必须在 10 秒内被 2000 个连接全部收到。
|
||||
4. 每个 P2P Topic 发送 1 条不计入正式结果的屏障消息;双方必须收到,且对端投递为 1000/1000。
|
||||
5. 等待 30 秒,确认错误计数、Kafka lag、CPU、内存和连接数回到稳定状态。
|
||||
|
||||
### 7.2 阶段 S1:1000 对 P2P 同时聊天
|
||||
|
||||
参与者:全部 2000 人,1000 个 P2P Topic。
|
||||
|
||||
默认调度:
|
||||
|
||||
- A、B 双方各发送 10 条,每用户 1 条/秒,持续 10 秒。
|
||||
- 每个用户使用由账号索引计算的 0-200ms 固定抖动,确保可复现。
|
||||
- 总发布量 20000,平均发布速率约 2000 条/秒。
|
||||
- 发布完成后保留连接并等待 30 秒排空,再进行历史核对。
|
||||
|
||||
主要期望:每条被服务端接受的消息恰好被对应的另一方实时收到一次。发送方回显也应收到一次,但作为独立指标。
|
||||
|
||||
### 7.3 阶段 S2:2000 人群、1000 人发言
|
||||
|
||||
参与者:2000 个在线订阅者。发送者选择 A0001-A0500 与 B0001-B0500,共 1000 人,避免所有发布压力只落在一台压测机上。
|
||||
|
||||
默认调度:
|
||||
|
||||
- Machine A 每 200ms 发 1 条,Machine B 在其后错开 100ms 发 1 条,合并后全局间隔为 100ms。
|
||||
- 每轮每个发送者恰好发送 1 条,共 1000 条、约 100 秒。
|
||||
- 连续 2 轮,共 2000 条、约 200 秒。
|
||||
- `noecho:false`,因此每条被接受的消息期望投递到 2000 个在线连接。
|
||||
- 理论实时投递总量:`2000 × 2000 = 4000000` 次,平均约 20000 次/秒。
|
||||
- 发布结束后等待 60 秒排空,然后由多个校验账号分页拉取历史。
|
||||
|
||||
### 7.4 阶段 S3:P2P 与群聊混合
|
||||
|
||||
正式窗口为 100 秒:
|
||||
|
||||
- P2P:A0001-A0300 与 B0001-B0300,共 300 对;双方各发 5 条,总发布量 `300 × 2 × 5 = 3000`。在第 0、20、40、60、80 秒附近发送,并加入固定抖动。
|
||||
- 群聊:A0301-A0550 与 B0301-B0550,共 500 个发送者;每人发 2 条。两台机器错峰形成全局 100ms 间隔,两轮共 1000 条,持续约 100 秒。
|
||||
- 其余用户保持在线并接收群消息。
|
||||
- 群实时理论投递总量:`1000 × 2000 = 2000000` 次。
|
||||
- P2P 主要理论对端投递总量:3000 次;发送方回显另计 3000 次。
|
||||
- 发布结束后等待 60 秒排空,再做 P2P 和群历史核对。
|
||||
|
||||
混合场景的群发送者与 P2P 发送者故意分开,便于判断瓶颈来自连接写队列、群广播还是同一连接并发写。通过后可增加一轮“发送者重叠”增强测试。
|
||||
|
||||
## 8. 丢失率与正确性口径
|
||||
|
||||
不能只统计压测端成功调用 `WriteJSON` 的数量。每轮必须同时输出以下四层结果。
|
||||
|
||||
### 8.1 发送接受率
|
||||
|
||||
```text
|
||||
publish_accept_rate = ctrl_202_with_seq / publish_attempted
|
||||
publish_reject_rate = (ctrl_4xx + ctrl_5xx + ctrl_timeout + write_error) / publish_attempted
|
||||
```
|
||||
|
||||
只有收到 `ctrl.code=202` 且包含有效 `params.seq` 的消息,才进入后续“期望投递”分母。
|
||||
|
||||
### 8.2 实时投递丢失率
|
||||
|
||||
实时到达率必须由 2000 个 WebSocket 连接实际监听得到,不能使用发送 ACK、Kafka offset 或数据库行数代替。每个连接建立后启动一个常驻 read loop,全量读取服务端下行,并把 `{ctrl}`、`{data}`、`{meta}`、`{pres}`、`{info}` 分流处理。
|
||||
|
||||
监听器要求:
|
||||
|
||||
1. 每个 WebSocket 只能有一个 goroutine 调用 `ReadMessage/ReadJSON`,避免多个读取者争抢数据;`ctrl` 通过 `id` 分发给等待中的发布请求。
|
||||
2. 只统计 `head.x-im-client-mid` 前缀和 `content.run_id` 都匹配本轮的 `{data}`,忽略准备阶段系统消息、其他业务消息和历史噪声。
|
||||
3. 记录 `receiver_index + client_mid + seq + first_received_at`。相同接收者再次收到同一 `client_mid` 计为重复,不增加到达数。
|
||||
4. 服务端允许 `{data}` 早于对应 `{ctrl 202}` 到达,因此监听器先保存观测事实,最终再与 ACK ledger 对账,不能因为 ACK 尚未到达而丢弃该 `{data}`。
|
||||
5. P2P 以配对的另一方为主要期望接收者;发送者自身回显单独统计。
|
||||
6. 群消息以发压开始时的 2000 个 READY 成员为固定分母。因为 `noecho:false`,发送者本人也属于期望接收者。
|
||||
7. 正式窗口内连接断开时不得从分母中删除。记录断开时间、所属机器和 Pod;服务端导致的断开计入结果,压测机自身饱和导致的断开则将整轮标记为无效。
|
||||
8. P2P 发布结束后继续监听 30 秒,群聊和混合场景继续监听 60 秒。窗口内未收到计为实时丢失;窗口后到达另记 `late_delivery`,不能把它改写为正常实时到达。
|
||||
|
||||
期望投递账本由成功 ACK 生成:
|
||||
|
||||
```text
|
||||
P2P expected key = run_id + client_mid + peer_receiver_index
|
||||
Group expected key = run_id + client_mid + each_ready_receiver_index
|
||||
```
|
||||
|
||||
群场景 2000 条消息对应 400 万个期望键。实现时为每个接收者维护按消息序号索引的 bitset,不保存 400 万份完整 JSON;延迟使用 HDR Histogram 或固定桶,缺失和重复才输出逐条明细。
|
||||
|
||||
P2P 主要口径:
|
||||
|
||||
```text
|
||||
p2p_realtime_loss =
|
||||
(accepted_messages - unique_peer_data_received_within_drain_window)
|
||||
/ accepted_messages
|
||||
```
|
||||
|
||||
群聊口径:
|
||||
|
||||
```text
|
||||
group_expected_deliveries = accepted_group_messages × ready_group_subscribers
|
||||
group_realtime_loss =
|
||||
(group_expected_deliveries - unique_group_deliveries_within_drain_window)
|
||||
/ group_expected_deliveries
|
||||
```
|
||||
|
||||
每个接收者按 `run_id + x-im-client-mid` 去重。重复到达不用于抵消其他消息的缺失。
|
||||
|
||||
同时输出:
|
||||
|
||||
```text
|
||||
arrival_rate = unique_deliveries_within_drain_window / expected_deliveries
|
||||
loss_rate = 1 - arrival_rate
|
||||
late_rate = unique_late_deliveries / expected_deliveries
|
||||
duplicate_rate = duplicate_deliveries / expected_deliveries
|
||||
```
|
||||
|
||||
实时监听结束后继续执行历史回查,将缺失分为:
|
||||
|
||||
- `realtime_lost_but_persisted`:WS 窗口内没收到,但历史中存在,说明实时投递链路丢失,客户端可通过重连补偿。
|
||||
- `persisted_but_late`:超过实时排空窗口才由 WS 收到,同时历史中存在。
|
||||
- `durable_lost`:WS 未收到且最长等待后历史也不存在,属于真正的持久化消息丢失。
|
||||
- `receiver_disconnected`:期望接收者在消息窗口内断线,单独统计并保留在实时丢失分母中。
|
||||
|
||||
### 8.3 持久化丢失率
|
||||
|
||||
Kafka 持久化是异步链路,`ctrl 202` 不等于数据库已经可查。排空后通过 `{get what:"data"}` 分页回查:
|
||||
|
||||
```text
|
||||
persistence_loss =
|
||||
(accepted_unique_messages - unique_messages_found_in_history)
|
||||
/ accepted_unique_messages
|
||||
```
|
||||
|
||||
- P2P:1000 个 Topic 各由一侧账号回查。
|
||||
- 群聊:至少使用分别连接不同 Pod 的 3 个成员回查,三份结果应完全一致。
|
||||
- 最长等待 120 秒,每 5 秒重试一次;超时仍不存在才记为持久化丢失。
|
||||
|
||||
### 8.4 重复与乱序
|
||||
|
||||
```text
|
||||
duplicate_rate = duplicate_deliveries / expected_deliveries
|
||||
out_of_order_rate = receivers_observing_seq_regression / ready_receivers
|
||||
```
|
||||
|
||||
同时记录:同一 `client_mid` 对应多个 `seq`、同一 `seq` 对应多个测试消息、接收端 Topic `seq` 回退、ACK `seq` 与下行 `seq` 不一致。这些都必须单独报错,不能归入普通丢失。
|
||||
|
||||
## 9. 延迟口径
|
||||
|
||||
记录以下时间点:
|
||||
|
||||
- `t_write`:压测端开始写 `{pub}`。
|
||||
- `t_ack`:收到对应 `{ctrl 202}`。
|
||||
- `t_data`:接收者收到并完成 JSON 解析。
|
||||
|
||||
输出:
|
||||
|
||||
```text
|
||||
ack_latency = t_ack - t_write
|
||||
e2e_realtime_latency = t_data - sent_at
|
||||
```
|
||||
|
||||
每个场景输出 p50、p95、p99、p99.9 和 max。跨机器延迟仅在 NTP 偏差达标时有效;否则仍可统计 ACK 延迟,但端到端单向延迟标记为无效。
|
||||
|
||||
群聊 400 万次投递不应把所有延迟样本常驻内存。使用固定桶直方图或 HDR Histogram,并按接收端机器和 Pod 分组。
|
||||
|
||||
## 10. 建议通过标准
|
||||
|
||||
第一轮基线建议使用:
|
||||
|
||||
| 指标 | 标准 |
|
||||
| --- | --- |
|
||||
| READY 连接 | 2000/2000 |
|
||||
| 发布接受率 | 100% |
|
||||
| 持久化丢失率 | 0 |
|
||||
| 同一 `client_mid` 多 `seq` | 0 |
|
||||
| P2P 对端实时丢失率 | <= 0.001%,且任何丢失都要输出明细 |
|
||||
| 群实时投递丢失率 | <= 0.001%,发布版本验收目标为 0 |
|
||||
| 重复投递率 | 0 |
|
||||
| Topic `seq` 回退 | 0 |
|
||||
| ACK p95 / p99 | < 100ms / < 300ms |
|
||||
| 实时投递 p95 / p99 | < 200ms / < 500ms |
|
||||
| `deliver/hub_full`、WS 写队列满 | 0 |
|
||||
| Kafka producer/commit 错误 | 0 |
|
||||
| Kafka 消费重试、单条降级、死信新增 | 0 |
|
||||
| Kafka 排空 | 发布结束后 60 秒内 lag 回到基线/0 |
|
||||
| Kafka 到 MySQL 落库 p95 / p99 | < 200ms / < 500ms |
|
||||
| MySQL 实际落库数 | 与 `ctrl 202` 唯一消息数完全一致 |
|
||||
| MySQL deadlock、lock timeout、连接池耗尽 | 0 |
|
||||
| Redis 操作错误、淘汰、拒绝连接 | 0 |
|
||||
| Redis command p95 / p99 | < 5ms / < 10ms |
|
||||
| Pod CPU throttling ratio | < 10% |
|
||||
| Pod CPU、内存峰值 | < limit 的 80% |
|
||||
| 容器/节点网络 drop、error | 0 新增或不高于基线噪声 |
|
||||
| 压测机 CPU、内存、网络 | < 70%,无 read-loop backlog |
|
||||
| Pod 重启、OOM、持续 GC/内存增长 | 0 |
|
||||
|
||||
如果实时有缺失但历史回查完整,结论应写“实时链路丢失、可由历史恢复”,不能写成“消息未丢失”。如果历史也缺失,则属于持久化消息丢失,直接判失败。
|
||||
|
||||
## 11. 全链路观测与资源采集
|
||||
|
||||
### 11.1 观测目标与时间窗口
|
||||
|
||||
监控必须能把同一轮测试拆成以下链路:
|
||||
|
||||
```text
|
||||
压测机 WriteJSON
|
||||
-> IM Pod 收到 pub
|
||||
-> Redis 分配消息 seq / 幂等 claim
|
||||
-> Kafka producer 写入消息持久化 Topic
|
||||
-> IM Pod 返回 ctrl 202 并实时广播
|
||||
-> Kafka consumer 拉取与组批
|
||||
-> MySQL 批量提交
|
||||
-> Kafka offset commit
|
||||
-> WebSocket 历史查询可见
|
||||
```
|
||||
|
||||
每轮采集窗口为:正式 `start_at` 前 10 分钟、正式测试全程、排空与历史核对结束后 10 分钟。Prometheus 在压测期间建议使用 5 秒抓取周期,重要 counter 以 `increase` 计算,不使用瞬时值推算总量。
|
||||
|
||||
每轮在 Grafana 或结果文件中记录四个时间点:
|
||||
|
||||
- `baseline_start`:基线开始。
|
||||
- `load_start`:正式发送开始。
|
||||
- `load_end`:最后一次发布结束。
|
||||
- `reconcile_end`:历史核对完成。
|
||||
|
||||
所有图表使用相同 UTC 时间轴,并标注 `run_id`、scenario、Git commit、Pod 镜像版本和压测参数。
|
||||
|
||||
### 11.2 采集组件
|
||||
|
||||
推荐最小采集栈:
|
||||
|
||||
| 对象 | 采集方式 | 主要用途 |
|
||||
| --- | --- | --- |
|
||||
| IM Server | `/metrics`、`/debug/vars` | WS、HTTP、DB、fanout、Go runtime、连接池 |
|
||||
| Kubernetes Pod/Container | kube-state-metrics + kubelet/cAdvisor | CPU、内存、磁盘 IO、网络、重启、限流 |
|
||||
| Kubernetes Node | node-exporter | 节点 CPU、内存、磁盘、网络、conntrack |
|
||||
| Kafka | kafka-exporter + Broker JMX Exporter | consumer lag、吞吐、ISR、请求队列、磁盘 |
|
||||
| Redis | redis-exporter | OPS、延迟、内存、连接、命中率、淘汰、网络 |
|
||||
| MySQL | mysqld-exporter + performance_schema | QPS、事务、锁、Buffer Pool、慢 SQL、IO |
|
||||
| 两台压测机 | node-exporter + 压测 agent `/metrics` | 生成端 CPU/网络/连接和客户端统计 |
|
||||
| 链路探测 | blackbox-exporter 或压测 agent 主动探测 | DNS、TCP、HTTP、依赖 RTT |
|
||||
|
||||
如果环境没有完整 Prometheus 栈,压测前至少以 5 秒周期抓取上述 HTTP 指标,并同步保存 `kubectl top`、Pod 日志、Kafka consumer group、Redis INFO 和 MySQL status。手工快照只能作为兜底,不能替代连续时间序列。
|
||||
|
||||
### 11.3 当前已有与必须补充的应用指标
|
||||
|
||||
当前 IM Server 已经提供:
|
||||
|
||||
| 指标 | 状态 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `ws_connections_active` | 已有 | 当前 Pod/实例的活跃 WS 数 |
|
||||
| `http_requests_total` | 已有 | HTTP 请求量和错误码 |
|
||||
| `http_request_duration_seconds` | 已有 | HTTP 延迟 |
|
||||
| `db_query_duration_seconds` | 已有 | MySQL query/exec/commit 等通用耗时 |
|
||||
| `realtime_fanout_events_total` | 已有 | Redis realtime 发布、接收和本地投递结果 |
|
||||
| `realtime_fanout_delivery_duration_seconds` | 已有 | Redis realtime 消息年龄 |
|
||||
| `/debug/vars` 中 `DbStats` | 已有 | Go SQL 连接池状态 |
|
||||
| `/debug/vars` 中 Cluster/Session/Topic 计数 | 已有 | Cluster 节点、会话和活跃 Topic |
|
||||
|
||||
为准确测量 Kafka 落库速度,压测版本必须再补以下低基数指标。严禁把 `topic`、`user_id`、`client_mid`、`run_id` 或 tenant ID 放入 Prometheus label:
|
||||
|
||||
```text
|
||||
message_persist_events_total{stage,kind,status}
|
||||
message_persist_duration_seconds{stage,kind,status}
|
||||
message_persist_end_to_end_seconds{kind,status}
|
||||
message_persist_batch_size{kind}
|
||||
message_persist_inflight{kind}
|
||||
kafka_consumer_commit_total{topic_kind,status}
|
||||
redis_operation_duration_seconds{operation,status}
|
||||
redis_operation_errors_total{operation}
|
||||
mysql_pool_connections{state}
|
||||
mysql_pool_wait_total
|
||||
mysql_pool_wait_duration_seconds_total
|
||||
cluster_route_events_total{operation,status}
|
||||
cluster_route_duration_seconds{operation,status}
|
||||
ws_outbound_events_total{status}
|
||||
ws_outbound_queue_depth
|
||||
```
|
||||
|
||||
标签约定:
|
||||
|
||||
- `kind`:仅允许 `p2p`、`group`。
|
||||
- `stage`:`producer_write`、`consumer_fetch`、`db_batch`、`consume_finish`。
|
||||
- `status`:`ok`、`error`、`duplicate`、`retry`、`dead`。
|
||||
- `operation`:Redis 使用 `seq_reserve`、`unique_claim`、`hot_cache`、`fanout_publish`、`fanout_receive`;Cluster 使用 `route`、`proxy_join`、`proxy_leave`。
|
||||
|
||||
`message_persist_end_to_end_seconds` 从 Kafka envelope 的 `occurred_at` 计算到 MySQL 提交并标记消费成功的时刻。它是回答“消息从服务端接受到真正落库用了多久”的核心指标。
|
||||
|
||||
另外应在自定义 Prometheus Registry 中注册 Go collector 和 process collector,提供标准 `go_*`、`process_*` 指标;在完成前从 `/debug/vars` 采集对应 runtime 数据。
|
||||
|
||||
### 11.4 Kafka 处理速度与积压
|
||||
|
||||
Kafka 需要同时观察应用层和 Broker 层,至少包含:
|
||||
|
||||
1. P2P Topic `im.message.persist` 的生产/消费条数与字节数。
|
||||
2. 群 Topic `im.group.message.persist` 的生产/消费条数与字节数。
|
||||
3. 对应 consumer group 每个 partition 的 lag、总 lag 和 lag 恢复到 0 的时间。
|
||||
4. producer 写入延迟、错误、重试和 timeout。
|
||||
5. consumer fetch 速率、批量大小、DB 批次耗时、offset commit 延迟和失败。
|
||||
6. 批量落库失败后降级为单条写入的次数。
|
||||
7. 消费重试、Redis 幂等 claim 失败和死信新增数。
|
||||
8. Broker request queue、network processor idle、request handler idle、ISR shrink、under-replicated/offline partition 和磁盘使用率。
|
||||
|
||||
不同 exporter 的 lag 指标名可能是 `kafka_consumergroup_lag` 或 `kafka_consumergroup_group_lag`,部署时必须以实际 `/metrics` 为准建立 dashboard,不允许直接照抄不存在的指标名。
|
||||
|
||||
典型查询:
|
||||
|
||||
```promql
|
||||
sum by (kind, stage, status) (
|
||||
rate(message_persist_events_total[1m])
|
||||
)
|
||||
```
|
||||
|
||||
```promql
|
||||
histogram_quantile(
|
||||
0.99,
|
||||
sum by (le, kind) (
|
||||
rate(message_persist_end_to_end_seconds_bucket[5m])
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
```promql
|
||||
sum by (consumergroup, topic) (
|
||||
kafka_consumergroup_lag{
|
||||
topic=~"im.message.persist|im.group.message.persist"
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
每轮报告必须给出:峰值生产速度、峰值消费速度、峰值 lag、lag 清零耗时、平均/最大 batch size、DB batch p95/p99、端到端落库 p95/p99/max、重试数和死信数。
|
||||
|
||||
### 11.5 数据库落库正确性与速度
|
||||
|
||||
006 商户使用默认库 `meeting_im`,但消息表名按数据库中的数值 `tenant_id` 生成,不能直接把企业码字符串当表后缀。准备阶段先从租户注册信息解析并记录 006 的真实 `tenant_id`,再生成只读核对 SQL。
|
||||
|
||||
当前消息存储位置:
|
||||
|
||||
- P2P:`im_<tenant_id三位补零>_messages`,例如真实 tenant ID 为 6 时是 `im_006_messages`。
|
||||
- 群聊:`im_<tenant_id三位补零>_group_messages_YYYYMM`。
|
||||
- 群消息分片注册:`im_group_message_shards`。
|
||||
- Kafka 消费幂等状态:Redis `kafka_consume` namespace,TTL 7 天。
|
||||
- Kafka 死信:MySQL `im_kafka_dead_letters`。
|
||||
|
||||
正式核对优先使用 WebSocket 历史接口,SQL 作为服务端审计证据。SQL 由核对程序使用白名单表名生成,不允许把外部输入直接拼入表名。
|
||||
|
||||
P2P 落库数量示例:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
COUNT(*) AS rows_written,
|
||||
COUNT(DISTINCT client_msg_id) AS unique_client_mid,
|
||||
MIN(createdat) AS first_persisted_at,
|
||||
MAX(createdat) AS last_persisted_at
|
||||
FROM im_006_messages
|
||||
WHERE client_msg_id LIKE 'lt-20260925-s1-%';
|
||||
```
|
||||
|
||||
群消息落库数量示例:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
COUNT(*) AS rows_written,
|
||||
COUNT(DISTINCT client_msg_id) AS unique_client_mid,
|
||||
MIN(created_at) AS first_persisted_at,
|
||||
MAX(created_at) AS last_persisted_at
|
||||
FROM im_006_group_messages_202609
|
||||
WHERE client_msg_id LIKE 'lt-20260925-s2-%';
|
||||
```
|
||||
|
||||
重复检查:
|
||||
|
||||
```sql
|
||||
SELECT client_msg_id, COUNT(*) AS cnt, MIN(seqid), MAX(seqid)
|
||||
FROM im_006_messages
|
||||
WHERE client_msg_id LIKE 'lt-20260925-s1-%'
|
||||
GROUP BY client_msg_id
|
||||
HAVING COUNT(*) <> 1;
|
||||
```
|
||||
|
||||
群表执行同样检查。P2P 有 1000 个 Topic,不能用整张表的 `MAX(seq)-MIN(seq)+1` 判断丢失;必须以压测端保存的 `client_mid -> topic -> ack_seq` manifest 与数据库逐条比对。
|
||||
|
||||
MySQL 性能至少采集:
|
||||
|
||||
- `Threads_connected`、`Threads_running`、连接使用率和连接池 wait count/duration。
|
||||
- QPS、Com_insert、事务提交/回滚、行写入速度。
|
||||
- InnoDB Buffer Pool 命中率、dirty pages、checkpoint age、log waits。
|
||||
- row lock waits/time、deadlock、慢 SQL、临时磁盘表。
|
||||
- data/log fsync、读写 IOPS、吞吐、磁盘 await、util 和剩余空间。
|
||||
- P2P 批量 INSERT、群月表批量 INSERT 的 p95/p99。
|
||||
- 主从架构下的 replication lag;压测历史回查必须明确读主还是读从。
|
||||
|
||||
数据库落库速度报告包含两个值:
|
||||
|
||||
```text
|
||||
db_write_throughput = 测试消息成功落库数 / 首次到最后一次 DB commit 时间
|
||||
drain_time = 最后一条 ctrl 202 到 Kafka lag=0 且全部历史可见的时间
|
||||
```
|
||||
|
||||
### 11.6 Redis 观测
|
||||
|
||||
Redis 在当前消息链路中承担 seq 分配、`client_mid` 幂等 claim、群热消息缓存、Kafka 消费幂等状态和用户级 realtime fanout。需要采集:
|
||||
|
||||
- `instantaneous_ops_per_sec`、命令总量与各命令耗时。
|
||||
- connected/blocked clients、连接拒绝和连接创建速度。
|
||||
- used memory、RSS、碎片率、maxmemory 使用率。
|
||||
- evicted/expired keys、keyspace hit/miss。
|
||||
- network input/output bytes、Pub/Sub channel/pattern/subscriber 数。
|
||||
- Redis 主线程 CPU、事件循环延迟、slowlog 数量。
|
||||
- Sentinel/主从环境的 role、replication offset、replication lag、failover。
|
||||
- `seq_reserve`、`unique_claim`、`hot_cache`、`kafka_consume` 操作错误和 p95/p99。
|
||||
|
||||
压测期间禁止使用无界 `KEYS` 或全库 `SCAN` 做统计,这会污染结果。Kafka 消费状态只通过应用指标抽样统计,不逐条扫描 Redis key。
|
||||
|
||||
常见异常判定:
|
||||
|
||||
- evicted keys 或 rejected connections 增加:直接判失败。
|
||||
- used memory 持续增长且排空后不回落:检查 hot message、幂等 key TTL 和消费记录泄漏。
|
||||
- p99 command latency 超过 10ms:标记风险;超过 50ms 且与消息延迟尖刺相关时判失败。
|
||||
- replication lag 持续扩大或发生 failover:本轮单独标记,不与无故障基线混合。
|
||||
|
||||
### 11.7 Pod、容器与节点资源
|
||||
|
||||
每个 IM Pod 单独展示,不只看 namespace 汇总:
|
||||
|
||||
```promql
|
||||
sum by (pod) (
|
||||
rate(container_cpu_usage_seconds_total{namespace="im-server-system",container="im-server"}[1m])
|
||||
)
|
||||
```
|
||||
|
||||
```promql
|
||||
sum by (pod) (
|
||||
container_memory_working_set_bytes{namespace="im-server-system",container="im-server"}
|
||||
)
|
||||
```
|
||||
|
||||
```promql
|
||||
sum by (pod) (
|
||||
rate(container_network_receive_bytes_total{namespace="im-server-system",pod=~"im-server-.*"}[1m])
|
||||
)
|
||||
```
|
||||
|
||||
```promql
|
||||
sum by (pod) (
|
||||
rate(container_network_transmit_bytes_total{namespace="im-server-system",pod=~"im-server-.*"}[1m])
|
||||
)
|
||||
```
|
||||
|
||||
必须采集:
|
||||
|
||||
- CPU 使用核数、CPU limit 使用率、CFS throttled seconds/periods。
|
||||
- memory working set、RSS、cache、limit 使用率、OOM 和 page fault。
|
||||
- 容器文件系统 read/write bytes、IOPS、IO wait;若根文件系统不承载业务数据,也要观察日志写入压力。
|
||||
- 网络接收/发送 bytes、packets、errors、drops、TCP retransmit。
|
||||
- Pod restart、readiness、liveness、Pending/NotReady 时间。
|
||||
- Go goroutine、heap、stack、GC 次数、GC pause、进程文件描述符。
|
||||
- `LiveSessions`、`LiveTopics`、`LiveClusterNodes`、Cluster leader 和代理 Topic 数。
|
||||
- Node CPU steal/iowait、内存压力、磁盘压力、网络错误、conntrack 使用率。
|
||||
|
||||
资源报告既要给峰值,也要给排空后是否回落。单个 Pod 明显高于其他 Pod 时,结合连接分布、Topic master 位置和网络流量判断是否发生热点。
|
||||
|
||||
建议风险线:
|
||||
|
||||
- Pod CPU 持续超过 limit 的 80%,或 throttling ratio 超过 10%。
|
||||
- memory working set 超过 limit 的 80%,或排空 10 分钟后仍持续增长。
|
||||
- 节点磁盘 util 持续超过 80%,await 持续超过 20ms。
|
||||
- 网络 drop/error 或 TCP retransmit 明显增长。
|
||||
- 任一 Pod restart/OOM/NotReady,直接判本轮失败。
|
||||
|
||||
### 11.8 网络时延与压测机资源
|
||||
|
||||
两台压测机分别持续测量:
|
||||
|
||||
- 到各 Pod 直连入口和负载均衡入口的 DNS、TCP connect、TLS handshake、HTTP `/healthz` RTT。
|
||||
- WebSocket Ping/Pong RTT p50/p95/p99/max,每 5 秒一次,不能用业务 `note kp` 代替协议 Ping。
|
||||
- 到 Kafka、Redis、MySQL 服务地址的 TCP connect RTT;只做低频只读探测,不绕过业务链路写数据。
|
||||
- 两台压测机之间的 RTT、丢包和 NTP offset。
|
||||
- 压测进程 CPU、RSS、GC、goroutine、FD、socket、send/receive bytes、read-loop queue depth。
|
||||
|
||||
压力生成端出现 CPU > 70%、网卡 > 70%、FD 接近上限、read-loop backlog 或 GC 尖刺时,本轮标记 `LOAD_GENERATOR_SATURATED`,不能把客户端处理不过来算成服务端丢消息。
|
||||
|
||||
### 11.9 现有 fanout 与 Cluster 查询
|
||||
|
||||
每轮开始前记录 counter 快照,结束后记录增量:
|
||||
|
||||
```promql
|
||||
sum by (stage, kind, target_type, status) (
|
||||
increase(realtime_fanout_events_total[10m])
|
||||
)
|
||||
```
|
||||
|
||||
```promql
|
||||
histogram_quantile(
|
||||
0.99,
|
||||
sum by (le, kind, target_type) (
|
||||
rate(realtime_fanout_delivery_duration_seconds_bucket[5m])
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
Topic 消息使用原生 Cluster 时,`publish/skip_native_cluster` 增加属于正常现象。重点检查 Cluster 连接数、route/proxy 错误、`node for topic not found`、gob 编解码错误、Hub 队列满和 WS 写失败。
|
||||
|
||||
### 11.10 日志、快照与关联规则
|
||||
|
||||
压测日志统一带 `run_id`、scenario、machine、account_index 和脱敏 Topic hash。服务端日志不新增用户 ID 或 `client_mid` 的高基数 Prometheus label;需要定位时通过压测端保存的 manifest 关联。
|
||||
|
||||
每轮保存:
|
||||
|
||||
- Prometheus 查询结果或 Grafana dashboard snapshot。
|
||||
- IM Pod、Kafka、Redis、MySQL 在测试窗口内的错误日志。
|
||||
- Kafka consumer group 压测前、峰值、结束、排空后的 offset/lag 快照。
|
||||
- Redis INFO、SLOWLOG 摘要和 exporter 指标快照。
|
||||
- MySQL GLOBAL STATUS、InnoDB status 摘要、慢 SQL 摘要和表行数核对。
|
||||
- `kubectl top pod/node`、Pod restart/condition/event 和容器日志摘要。
|
||||
- 两台压测机 node/process 指标及 agent summary。
|
||||
|
||||
日志采集必须过滤 Token、密码、API Key、Kafka/Redis/MySQL 凭证和完整消息正文。
|
||||
|
||||
### 11.11 建议仪表盘布局
|
||||
|
||||
一张压测 dashboard 分为六行:
|
||||
|
||||
1. 业务结果:attempt、ACK、实时接收、历史可见、丢失、重复、乱序、p95/p99。
|
||||
2. IM Pod:连接数、发布/下行速率、Cluster route、CPU、内存、GC、goroutine。
|
||||
3. Kafka:producer/consumer rate、lag、batch、commit、retry、dead letter、broker health。
|
||||
4. Redis:OPS、command latency、memory、clients、network、eviction、replication。
|
||||
5. MySQL:insert/transaction rate、DB batch latency、pool wait、locks、Buffer Pool、IO。
|
||||
6. 基础设施:Pod/Node CPU、memory、disk IO、network、压测机资源和网络 RTT。
|
||||
|
||||
所有面板至少按 Pod、Kafka Topic/consumer group、Redis instance、MySQL instance 和压测机分组;不得只展示总和。
|
||||
|
||||
## 12. 压测程序结构
|
||||
|
||||
现有 `loadtest/realtime_fanout_probe.go` 只支持单群和单进程,不能直接用于本方案。新工具使用 Go 实现并提供六个子命令:
|
||||
|
||||
```text
|
||||
im-pressure prepare 注册、好友、群和 manifest
|
||||
im-pressure verify 只读核验 2000 账号、1000 对关系和 2000 群成员
|
||||
im-pressure coordinator 协调双机 READY、统一起跑时间、ACK 台账与结果上报
|
||||
im-pressure agent 在每台压测机运行连接与场景
|
||||
im-pressure collect 抓取 Prometheus、K8s 和依赖组件快照
|
||||
im-pressure merge 合并两台机器结果并生成报告
|
||||
```
|
||||
|
||||
推荐文件:
|
||||
|
||||
```text
|
||||
loadtest/im-pressure/config.example.yaml
|
||||
loadtest/im-pressure/cmd/im-pressure/
|
||||
loadtest/im-pressure/internal/protocol/
|
||||
loadtest/im-pressure/internal/prepare/
|
||||
loadtest/im-pressure/internal/runner/
|
||||
loadtest/im-pressure/internal/collector/
|
||||
loadtest/im-pressure/internal/report/
|
||||
```
|
||||
|
||||
协调方式:Machine A 启动 coordinator,等待 A/B 两个 agent 都上报 1000 个 READY,然后发布一个至少晚于当前时间 30 秒的 UTC `start_at`。两台机器按相同 `run_id` 和确定性调度执行。任何 agent 失联都将本轮标记为 `INVALID`。
|
||||
|
||||
`agent` 自身暴露 `/metrics`,提供连接状态、发布、接收、重复、乱序、断连及 Go/process 指标;ACK 和投递延迟分布写入最终 summary。`collect` 只执行只读查询,并把各数据源的原始快照与查询时间一起保存;连接凭证通过环境变量传入,不写入结果目录。
|
||||
|
||||
## 13. 结果文件
|
||||
|
||||
每轮保留以下文件:
|
||||
|
||||
```text
|
||||
results/<run_id>/config.redacted.json
|
||||
results/<run_id>/manifest.sha256
|
||||
results/<run_id>/agent-a-summary.json
|
||||
results/<run_id>/agent-b-summary.json
|
||||
results/<run_id>/publish.jsonl
|
||||
results/<run_id>/missing.jsonl
|
||||
results/<run_id>/duplicate.jsonl
|
||||
results/<run_id>/history-reconcile.json
|
||||
results/<run_id>/db-reconcile-p2p.json
|
||||
results/<run_id>/db-reconcile-group.json
|
||||
results/<run_id>/timeline.json
|
||||
results/<run_id>/metrics/im-server.prom
|
||||
results/<run_id>/metrics/load-agent.prom
|
||||
results/<run_id>/metrics/kafka.json
|
||||
results/<run_id>/metrics/redis.json
|
||||
results/<run_id>/metrics/mysql.json
|
||||
results/<run_id>/metrics/kubernetes.json
|
||||
results/<run_id>/metrics/network.json
|
||||
results/<run_id>/logs/im-server-errors.log
|
||||
results/<run_id>/logs/kafka-errors.log
|
||||
results/<run_id>/logs/mysql-slow-summary.log
|
||||
results/<run_id>/grafana-snapshot.json
|
||||
results/<run_id>/report.md
|
||||
```
|
||||
|
||||
缺失明细至少包含 `run_id`、场景、Topic 脱敏哈希、发送者索引、接收者索引、`client_mid`、ACK `seq`、发送时间、最后等待时间、发送 Pod 和接收 Pod。不得写入 Token、密码、API Key 或完整手机号。
|
||||
|
||||
最终 `report.md` 必须分别给出:
|
||||
|
||||
- 压测业务量和消息正确性结论。
|
||||
- 实时投递与历史恢复结论。
|
||||
- Kafka 生产、消费、lag、排空和死信结论。
|
||||
- MySQL 落库数量、吞吐、耗时、锁与 IO 结论。
|
||||
- Redis OPS、耗时、内存、幂等/seq 错误和 fanout 结论。
|
||||
- 每个 Pod/Node 以及两台压测机的资源峰值和瓶颈判断。
|
||||
- 测试是否有效、是否通过,以及所有未通过项的原始证据路径。
|
||||
|
||||
## 14. 推荐执行顺序
|
||||
|
||||
1. 在测试窗口前完成环境门槛检查和 2000 账号准备。
|
||||
2. 完成 1000 对好友关系与 2000 人群,运行 `verify` 直到全部通过。
|
||||
3. 先用 20 对 P2P、100 人群做冒烟测试,验证统计口径。
|
||||
4. 执行 P0 热身。
|
||||
5. 执行 S1,等待服务端和 Kafka 恢复基线至少 5 分钟。
|
||||
6. 执行 S2,等待恢复基线至少 10 分钟。
|
||||
7. 执行 S3。
|
||||
8. 合并结果,按实时链路与持久化链路分别给出结论。
|
||||
9. 全部稳定后再单独进行 Pod 重启、断网或 Kafka/Redis 故障注入,不与本轮无故障基线混跑。
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
日期:2026-09-13
|
||||
|
||||
> 006 商户、2000 账号、两台压测机的可执行场景与统计口径见
|
||||
> [IM-006商户双机消息压力测试方案](./IM-006商户双机消息压力测试方案.md)。本文保留为通用阶梯与指标说明。
|
||||
|
||||
## 1. 目标
|
||||
|
||||
验证 IM Server 使用 Redis realtime fanout 后,在多 Pod、多端登录、群消息和群业务事件场景下的实时投递能力。
|
||||
|
||||
Reference in New Issue
Block a user