3.8 Lab 3:同一个接口的两层测试
上一节:3.7 层级选择与成本 | 下一节:第 4 章 工具链 配套代码:03-test-pyramid/08-lab3 预计时长:90 分钟
一、这个 Lab 的目标
亲手验证本章的核心论断:不同层级会发现不同的问题,而且有些问题只有某一层能发现。
做完之后你应该能对任何性能问题回答:「这个该在哪一层测,为什么不是另一层。」
二、任务清单
| # | 任务 | 产出 | 时长 |
|---|---|---|---|
| 1 | 搭组件基准(Testcontainers + 采样循环) | ComponentBench.kt + 结果表 |
35 min |
| 2 | 搭集成基准(Ktor 完整启动) | IntegrationBench.kt + 结果表 |
20 min |
| 3 | 全链路压测(k6 从另一进程) | k6 报告 | 20 min |
| 4 | 对比三层结果,写出「只有它能发现」的清单 | 实验档案 | 15 min |
三、任务 1:组件基准
准备:
- 用 Testcontainers 起 PostgreSQL(不要用 H2)。
- 建表并灌 10 万行数据,用幂律分布(前 1% 的用户占 30%–40% 的查询)。
- 建必要的索引,故意留一个没有索引的列作为对照。
要测的四件事(分开测):
| # | 测什么 | 预期发现 |
|---|---|---|
| 1 | 主键查询 findById |
走索引,P99 应该很低 |
| 2 | 二级索引查询 findByUserId |
走索引 + 回表 |
| 3 | 无索引列查询 findByStatus |
⚠️ 全表扫描,P99 随数据量线性恶化 |
| 4 | 批量查询 vs 循环单查 | 量化「消除 N+1」的收益 |
方法要求:
- 预热 5000 次(让连接池填满、PG 缓存热起来)。
- 采样 20000–50000 次,每次单独计时。
- 排序后报告 P50 / P95 / P99(不要只报平均)。
- 统计查询次数(用来发现 N+1)。
四、任务 2:集成基准
做法:把 Ktor 服务完整启动(真实端口,不加反向代理),用一个简单的并发客户端压。
必须先做:
- 独立预热阶段(至少 60 秒或直到 CV 收敛,第 2 章 2.4 节)。
- 明确并发模型(闭环还是开环,写进记录)。
要观察:
| 观察点 | 为什么 |
|---|---|
| 端到端 handler 耗时 | 与组件基准的差额 = 框架开销(拦截器、事务、序列化) |
连接池 active/pending |
单服务并发下池子够不够 |
| 启动预热曲线 | 从启动到稳态需要多久 |
已知局限(写进记录):
- 闭环模型会低估尾延迟(第 0 章 0.6 节)。
- 不含网络与网关。
- 所以它的定位是「与组件基准做相对比较」,不是「验收 SLO」。
五、任务 3:全链路压测
做法:用 k6 从另一个进程(最好另一台机器)压。
必须做:
- 用
constant-arrival-rateexecutor(开环)。 - 验证实际 QPS 是否等于设定值——不相等就说明压测机是瓶颈,数据作废。
- 记录客户端的 P50/P95/P99 与错误率。
六、任务 4:对比与结论(本 Lab 的核心)
填出这张表:
| 指标 | 组件基准 | 集成基准 | 全链路 | 差额归因 |
|---|---|---|---|---|
| P50 (ms) | ||||
| P95 (ms) | ||||
| P99 (ms) | ||||
| 错误率 | ||||
| 测量点 | Repository 方法 | 服务端 handler | 压测客户端 | — |
| 并发模型 | 单线程循环 | 闭环 | 开环 | — |
然后回答这三个问题(这是验收的关键):
问题 1:哪个问题只有组件基准能发现?
(提示:无索引列的 P99、批量 vs 循环的差异)
问题 2:哪个问题只有集成基准能发现?
(提示:连接池 pending、事务开销、框架拦截器成本、启动预热)
问题 3:哪个问题只有全链路能发现?
(提示:网络延迟、排队、网关开销、跨进程的尾延迟放大)
如果三个问题的答案都是「差不多」,说明你的实验设计有问题——最常见的原因是:三层用了不同的数据量或不同的负载,导致不可比。
七、验收标准
- 组件基准用真实 PostgreSQL(Testcontainers),不是 H2。
- 数据集是 10 万行 + 幂律分布,并写明在记录里。
- 组件基准把「SQL 执行 / 映射 / 业务逻辑 / 序列化」至少分开测了两项。
- 组件基准报告的是 P50/P95/P99,不是平均值。
- 集成基准有独立预热阶段,并写明了并发模型。
- 全链路压测验证了实际 QPS == 设定 QPS。
- 三层结果填在同一张表里,差额有归因。
- 明确说出三项「只有某一层能发现」的结论。
- 实验档案有「预期 vs 实际」表和假设台账条目。
八、常见问题
Q:我没有第二台机器做全链路压测怎么办? A:两个降级方案:① 压测客户端与服务分容器部署,用 Docker 的 CPU 限制把两边隔开(比同机好,但仍不理想);② 明确记录「本次全链路数据受同机干扰」,只用于相对比较(比如优化前后),不用于容量规划。不要假装它是可信的端到端数据。
Q:组件基准的 P99 波动很大(±40%)怎么办? A:先排查三件事:① 是否预热不足(用滚动 CV 判定,第 2 章 2.4 节);② 后台任务干扰(autovacuum、日志轮转);③ 采样次数太少(P99 需要足够样本才有意义)。如果排除后仍波动大,说明数据库本身有周期性抖动(checkpoint、autovacuum)——这本身是一个重要发现,应该记进档案。
Q:为什么建议故意留一个没索引的列? A:因为你需要一个「已知的、确凿的瓶颈」作为参照。如果所有查询都很快,你无法判断「组件基准能不能发现瓶颈」——有了这个对照组,你才能确认这个层级真的有效。这在方法论上叫「阳性对照」。
Q:三层测完发现差异很小,是不是白做了? A:差异小本身就是一个有价值的结论——说明你的服务没有显著的框架开销、网络开销和排队。但要检查:① 数据量是否太小(差异会被掩盖);② 负载是否太低(排队还没出现);③ 是否真的用了开环模型。如果这三项都确认了,那么「差异小」就是可信的、值得高兴的结论。