跳转至

08 测试与容量计划

状态:可实施(容量表待压测回填) 更新日期:2026-09-01 上游契约:docs/PLAN.md §25、§26、附录 B.7 实施决策:ADR-0006(Rust + Redpanda,受 ADR-0012 覆盖)、ADR-0007(一期简化形态)

0. 文档边界

本文定义:测试用例的组织与工具、压测脚本、混沌注入、容量参数实测回填流程。

本文不得放宽:§26 的通过判据数值(只能加严,不能放宽)、 §2.4 的 SLO 目标值。


1. 测试分层

L0 契约断言    编译期常量与不可变项(§27.2.4)—— 见 §2
L1 单元        codec、序号编码、事件组切分、未读三态判定
L2 组件        单个二进制 + 内存桩,覆盖各文档的验收用例
L3 集成        全链路 + 真实 standalone Redis/Scylla/Redpanda(docker compose)
L4 混沌        §26.8 场景清单
L5 压测        容量回填(§4)

各专题文档的验收用例编号前缀:

前缀 来源 数量
26.x PLAN.md §26 主干用例
P-x docs/01 协议层 7
S-x docs/02 存储层 7
M-x docs/03 邮箱与 fanout 7
J-x docs/04 会话列表 7
C-x docs/05 聊天室与控制消息 6
D-x docs/06 部署 5
R-x docs/07 可靠性安全 6
N-x docs/09 推送与角标 7
X-x docs/10 保留与合规 7

标注【发布阻断】的用例失败即不得发布,不接受"已知问题"豁免。


2. L0:契约断言(编译期)

不可变项必须是编译期常量并加静态断言,防止实现期被改:

// qim-proto/src/consts.rs
pub const VIRTUAL_BUCKET_COUNT: u32 = 65536;
pub const LANE_COUNT: u32 = 64;
pub const MESSAGE_SEQ_BUCKET_WIDTH: u64 = 4096;
pub const MAILBOX_EVENT_GROUP_MAX_ITEMS: usize = 8;

const _: () = assert!(VIRTUAL_BUCKET_COUNT.is_power_of_two());
const _: () = assert!(LANE_COUNT.is_power_of_two() && LANE_COUNT <= 256);
const _: () = assert!(MESSAGE_SEQ_BUCKET_WIDTH.is_power_of_two());

CI 额外的 grep 断言:

禁止别名     MessageNode / "Mailbox Node" / "Message Store" / connection_epoch
             (命中禁止别名表与 CI 正则自身的行除外)
禁止调用     subscribe(  —— 日志消费必须用 assign()(§5.3.7)
禁止模式     SESSION_DELTA / REACTION_UPDATE 处理函数中出现 "+=" 型计数更新
禁止 DELETE  MessageStore 与 MailboxStore 的 CQL 中不得出现 DELETE
禁止重写     服务端不得对客户端上行 Protobuf 结构做"解析后重写"(docs/01 §3.3)

3. 恒零指标:所有测试的公共断言

每一个 L2 以上的用例结束时都必须断言这五个指标为零:

mailbox_seq_regression_count
cursor_advanced_by_push_total
event_id_nondeterminism_total
孤儿 dispatch 计数
covered_through_seq 跨越未物化区间的计数

它们非零不是性能退化,是某条不变量已被破坏。把它做成测试框架的 after_each 钩子,而不是逐个用例手写。


4. 容量回填流程

4.1 待实测参数(附录 B.7)

entry_logical_bytes        约 110 B(估算)
entry_ondisk_bytes         含键前缀压缩、索引、WAL、压缩后真实结果
lsm_write_amp              **只用于设备寿命**,不用于存储容量
lsm_space_amp              用于存储容量,leveled 约 1.1~1.3
mailbox_replicas           默认 3
per_node_entry_budget      单 MailboxNode 可持续的 entry/s
checkpoint_bytes / checkpoint_interval / replay_rate / checkpoint_load_time_budget
b_row                      消息正文行均值(docs/02 用 600 B 估算)
conversation_active_member_ratio   α,§10.2.3 判据的输入量

4.2 回填纪律

1. 回填前,任何一格容量数字都**不得用于采购、合同或容量评审结论**(§25.0.3)
2. 回填后,凡引用该参数的推导必须重算:
   §25.0.3 三档容量表、§25.6 分片数下界、ADR-0001 的四条切换判据、
   ADR-0007 的 standalone Redis 资源边界(R_avg <= 25、30 天日桶驻留的实测上限)
3. 量纲检查:驻留容量用 space_amp,设备寿命用 write_amp —— v1 曾把两者混用

4.3 一期压测目标(ADR-0007 起步档)

DAU 100 万 / 峰值连接 20 万 / 日消息 2000 万 / R_avg ≈ 22(千人群)

需要验证的四条:
  A. 峰值邮箱写入 ≈ 8.5 万 entry/s 下 standalone Redis 的 P99 与内存占用
  B. 30 天邮箱日桶驻留是否满足当前 standalone Redis 的资源边界;不满足时优先评估 ScyllaDB
  C. rust-rdkafka 幂等 producer + acks=all 的 durable delivery 吞吐
       (ADR-0006 经 ADR-0012 修订后的复评条件 1)
  D. “全部 dispatch durable 后才提交 source offset”的故障注入是否无遗漏,
       且 GroupDispatch.fencing_epoch 的消费侧过滤是否 100% 拒收旧 owner 记录
       (ADR-0006/0012 复评条件 2;两项分别验证提交顺序与 §19.2.1 校验点 2)

C 与 D 是 ADR-0006 标为"倾向性决策"的解锁条件,优先级高于容量本身。

4.4 开发机实测(2026-09-26,非目标硬件,不得用于采购或容量结论)

同一台开发机(E5-2680 v4 14C/28T、62 GB、同机 Docker、三星 PM9A1 消费级 NVMe)上的 180 秒门禁结果,只用于回归对照与瓶颈归因:

10,000 msg/s,4+4 Redis:到达率 ≥ 9,999.9/s ×3;SEND_ACK P99 16.4~19.5 ms;
                          端到端 P99 103~108 ms;T-HEAVY 首屏 P99 ≤ 0.8 ms
15,000 msg/s,8+8 Redis,fanout 批 1024:到达率 14,930~14,938/s ×3;
                          SEND_ACK P99 53~124 ms;端到端 P99 163~250 ms;整机 87~88% 忙
10,000 msg/s,历史分层(Redis 热层 + ScyllaDB 归档,ADR-0018):SEND_ACK P99 16.8 ms、
                          端到端 P99 107.8 ms;归档 1,800,040/1,800,040,失败 0
Redis 成本:每条消息约 0.71 ms CPU(core 0.34 + mailbox 0.37);实例数是容量参数(ADR-0014)
Redis 驻留:邮箱约 410 B/条(30 天)、热层历史约 360 B/条(7 天)、提交辅助约 2.2 KB/条(2 h)
Redis DispatchProgress:稀疏 manifest 后 2,173 → 472 B/条(2k msg/s×60 s,邮箱 Redis 总内存 −52%)
Redis 提交辅助:COMMITTED 后压缩为 ACK 坐标 1,917 → 552 B/条;分层写入方不再登记 msghistorygc;
                          两台 Redis 合计每条 4.86 → 2.21 KB(hybrid 2k msg/s×60 s),10k 时延无回归
群扇出(R_avg 22.6,3,800 msg/s ≈ 8.2 万 entry/s,60 s):提交达标(ACK P99 21~24 ms);投递未达标——
                          Redis 邮箱 8+8 仅 2.04 万 entry/s,Scylla v2(8 核)3.86 万 entry/s;
                          根因是 64 shard 下每条消息约 18.7 个 dispatch 的固定成本(ADR-0023 同日)
ScyllaDB 邮箱 v2(ADR-0022,单节点 4 核、tmpfs、60 s 各一次):10,000 msg/s 投递 100%,
                          SEND_ACK/端到端 P99 26.6/131.4 ms,Scylla 2.6 核(含归档),LWT 0;
                          页上限 1 对照端到端 P99 904.9 ms;Redis 邮箱对照 23.0/110.5 ms

对 §4.3 四条的影响:A/B 已有开发机数据但目标硬件未测;C 已由真实 Redpanda 门禁验证 durable delivery;D 的 fencing_epoch 消费侧过滤仍未实现。


5. 压测场景

5.1 稳态

S-STEADY   峰均比 1,持续 4 h,观察漂移类指标(unread_drift_ratio、projection_lag)
S-PEAK     峰均比 3,持续 30 min
S-MIX      按 §25.0.2 的 f_mix 构造会话规模分布(单聊 74% / 中群 24% / 大群 2%)

默认 qim-loadgen --loadtest 的 steady 是确定性相位错峰:起跑门在全部 连接 ready 后只发布同一个未来 start_at 与固定 end_at;随后按全局客户端 ordinal 分配 floor(interval * ordinal / total_clients) 的 [0, interval) 相位。 SDK 泳道使用 ordinal 0..sdk_clients,帧层紧随其后,因此两条泳道不会各自从 零相位重叠。没有随机数、无界 jitter 或按连接完成顺序的隐式扰动;同一输入必须 得到同一排程,且完整窗口内每客户端的计划发送数不因相位而减少。

旧式“所有客户端在同一时刻发包”的同步 burst 不是 steady,只能作为定位 最坏情况的诊断工件,不能冒充稳态容量或 SLO 证据。未来的峰值与 fanout-storm 须定义独立场景和独立门禁。

5.2 尖峰

K-LOGIN    百万连接 60 秒内重连(接管风暴)
           判据:takeover_admit_rate 生效、无用户 > 60 s 无法登录
K-FANOUT   大群消息风暴(1000 人群 20 msg/s 打满)
           判据:单聊投递 P99 不受影响(lane 隔离验证)
K-ROOM     聊天室洪峰(room_msg_rate 打满)
           判据:共享 ConnectionNode 上的单聊投递 P99 不受影响

当前预期(2026-09-01):现行 finish_dispatch 在整条 record 完成后统一推进 64 lane,尚未实现目标 lane 独立推进,因此 K-FANOUT 的 lane 隔离子判据应失败并保留 为发布阻断;不得以 packed 64-lane 格式存在为通过依据。

5.3 长尾

T-BACKLOG  1 万条积压登录,判据 online_ready_latency_p99 <= 10 s
T-HEAVY    5000 会话重度用户首屏,判据 <= 500 ms
T-COLD     冷用户(30 天未登录)登录,验证 CURSOR_EXPIRED -> REBUILD 全流程

T-HEAVY 已固化为判据(qim-loadgen --heavy,由隔离 scripts/acceptance.sh 门禁 6 或 scripts/perf-test.sh full 运行)。此前它是占位——跑的是普通 e2e,并不真的造重度用户, 于是 §2.4 的 P0 SLO 从来没被判定过。没被判定过的判据等于没有判据。

首次实测(2026-08-19,旧一期 Redis 架构、特定单机硬件):5000 会话,首屏 P50 5.0 / P95 5.3 / P99 6.1 ms,判据 500 ms 有近 100 倍余量。 测量点与 §2.4 P0 逐字一致:客户端发出 PULL_SESSION_LIST → 第一页 SESSION_LIST_BATCH 收到,含网关转发与网络往返。该历史结果不能作为当前 Redpanda 日志驱动实现的发布门禁或容量结论;当前门禁必须在对应实现、隔离依赖和目标硬件上重跑。

该数字对应会话列表改为服务端分页之后的形态(docs/04 §2.2): ZREVRANGE convs:{user} 0 199 直接取首页,成本与用户会话总数无关; 此前是"全量载入 5000 行 + 内存排序 + 截断",成本随会话数线性增长。

用例本身的两条纪律,都是被实测打脸后才写下的:

  • 会话必须经真实写路径造出来(5000 个对端各发一条 → 服务端分配序号 → 投影)。直接往 Redis 里种数据也能造出 5000 个会话,但那会把键布局抄进 测试代码:布局一改,测试照样绿,而线上已经读不出东西。
  • client_message_id 必须每轮不同。第一版用了固定值,库里没清干净时 服务端按幂等回放首次结果且不再 fanout(ADR-0008),5000 个 SEND_ACK 照收,消息却全进了上一轮那个会话——用例报的是"投影没追上", 真相是它压根没发出去。测试必须能在非空库上重复跑。

6. 混沌注入工具

qim-chaos kill      <service> [--signal 9|STOP]
qim-chaos partition <from> <to> --duration
qim-chaos diskfull  <service>
qim-chaos clockskew <service> --ms N        跨越/不跨越 clock_regression_reject_ms 各一次
qim-chaos redis-cluster-reject               三节点 Cluster 配置必须启动拒绝,readiness 不得变绿
qim-chaos logdown   --partition P --duration 10m

--signal STOP 是替代"注入 GC 停顿"的标准手段:Rust 无 GC、Go 的 STW 是 亚毫秒级,都无法按字面制造 30 秒停顿。SIGSTOP/SIGCONT 是语言无关的等价注入 (§19.2.3 的验收用例已改用此方式)。

统一通过判据:

五个恒零指标保持为零
持久消息丢失数 == 0(以邮箱层三元组 (mailbox_seq, event_ordinal, event_id) 集合比对)

7. 环境

L3 集成    docker compose:Redis 7.0.0+ standalone(AOF + noeviction)+ scylla(3) + redpanda(1) + minio(1)
L4 混沌    同上 + 网络注入 sidecar
L5 压测    独立集群,规格按 §25.0.3 起步档;客户端用 qim-loadgen 模拟

qim-loadgen 必须驱动完整客户端状态机(§6.8 的游标语义、去重、REBUILD),
否则压测出的到达率没有意义 —— 一个不推进游标的假客户端永远不会暴露丢消息。
状态机**用 qim-sdk 的那一份**:loadgen 曾自带副本,两边都在演进就必然漂移,
而漂移的那一侧永远不会被压测执行到,也就永远不会被发现。

8. 发布门禁

门禁 1  L0 契约断言全绿(编译期 + CI grep)
门禁 2  全部【发布阻断】用例通过
门禁 3  五个恒零指标在 L4 混沌全场景下保持为零
门禁 4  §2.4 的 P0 SLO 在 L5 压测中达标
门禁 5  ADR-0006/0012 的复评条件已验证(acks=all durable delivery、source checkpoint
        顺序、GroupDispatch epoch 过滤);不要求 Kafka transaction / ProducerFenced
门禁 6  Redis Cluster 三节点负向启动门禁通过:相关服务非零退出且不得 ready

任一门禁不过即不得发布。门禁 5 未过时,语言选型仍处"倾向性决策"状态, 不得对外宣称技术栈已锁定。

scripts/acceptance.sh 的九道门禁是上述架构门禁的可执行编排:格式/lint/契约、 测试(含历史分层真实门禁)、release + 六服务启动(含 qim-archiver)、15 项功能、 180 秒实际到达率/SLO、T-HEAVY、Redis 最终工件、不变量证据、MessageRecord ↔ dispatch 独立 orphan 审计(qimctl audit dispatch:Outbox 提交事实与 dispatch 展开回执逐条比对,孤儿与 不一致计数须为 0);完整门禁认证 hybrid 形态(tiered MessageStore + Redis 邮箱)。后者所需的 durable CommitAuditFact、持久 FanoutAuditReceipt 与独立 coverage cursor 尚未实现;receipt 必须遵守“dispatch 全部 durable 后、source offset 提交前”的顺序, 因此完整 acceptance 当前按设计失败闭合;smoke 明确不执行该发布证明。它也不替代 尚未接入自动脚本的 L4 混沌与 ADR-0006 复评证据。


9. 待办

[x] 客户端状态机(游标语义、去重、REBUILD)—— 用 `qim-sdk` 的实现,loadgen 不再自带副本
[x] SDK 回归泳道:压测 5% 连接由真实 SDK 驱动并断言三条不变量(`--sdk-clients`,0 = 关闭作容量对照)
[x] 功能验收 15 项自动化(`--e2e`,每项为断言而非打印)
[x] 持续速率压测与分位数(`--loadtest`,`--rate`/`--duration` 生效,含 §2.4 SLO 判定)
[x] standalone Redis 真实集成(`qim-store/tests/`、`qim-common/tests/`,由隔离门禁统一运行)
[x] 编解码与路由基准(`cargo bench`,防退化用)
[x] 一键门禁 `scripts/acceptance.sh`(格式/lint/契约 → 测试 → release/五服务 → 15 项功能 → 180 秒 → T-HEAVY → 恒零指标)
[ ] qim-chaos 的注入 sidecar
[x] `compose.integration.yml` 隔离 standalone Redis / Redpanda / Scylla 环境与安全包装器(`gate scylla` 固定三节点 RF3;`gate hybrid` 默认单节点 RF=1,`QIM_INTEGRATION_SCYLLA_NODES` 可改 3)
[ ] 将隔离门禁接入 CI,并提供满足三节点 Scylla 的 runner 资源
[ ] 容量回填后重算全部推导值(§4.2 第 2 条清单)