3.8 配套代码:Lab 3 的实验编排
对应小节:3.8 Lab 3 这份文件把前面的代码组织成一条流水线,并给出记录模板。
一、Lab 3 的目录与代码来源
docs/experiments/E03-two-level-test/
├── README.md # 实验档案(模板见下方)
├── scripts/ # 冻结的脚本快照
│ ├── component-bench.txt # 组件基准输出(见 02)
│ ├── integration-bench.txt # 集成基准输出(见 03)
│ └── e2e-baseline.js # k6 脚本(见 03)
└── results/
├── env.txt # 环境元数据(自动采集)
├── component-bench.txt
├── integration-bench.txt
├── k6-summary.json
└── k6-raw.json
| 步骤 | 用哪份代码 |
|---|---|
| 组件基准 | 02-component-benchmark.md |
| 集成基准 | 03-integration-and-e2e.md 第一节 |
| 全链路压测 | 03-integration-and-e2e.md 第二、三节 |
| 结果对比 | 本文件第三节 |
二、一键跑完三层(编排脚本)
#!/usr/bin/env bash
# tools/run-lab3.sh <EXP_ID>
set -euo pipefail
EXP_ID="${1:?usage: run-lab3.sh <EXP_ID>}"
DIR="docs/experiments/${EXP_ID}"
mkdir -p "$DIR/results" "$DIR/scripts"
echo "═══ 步骤 0:环境元数据 ═══"
tools/collect-env.sh "$EXP_ID"
echo
echo "═══ 步骤 1:组件基准(Testcontainers,约 5 分钟)═══"
./gradlew test --tests 'bench.OrderRepositoryBenchmark' --info 2>&1 \
| tee "$DIR/results/component-bench.txt"
echo
echo "═══ 步骤 2:集成基准(单服务完整启动,约 3 分钟)═══"
./gradlew -PmainClass=bench.IntegrationBenchmark run 2>&1 \
| tee "$DIR/results/integration-bench.txt"
echo
echo "═══ 步骤 3:全链路压测(k6,约 12 分钟)═══"
# 前置:服务以生产同构配置启动
scripts/restart-app.sh "$DIR/results/gc.log"
sleep 5
# 预热(不计入统计)
BASE_URL=http://127.0.0.1:8080 k6 run --quiet --vus 20 --duration 60s loadtest/e2e-baseline.js > /dev/null
# 正式测量
BASE_URL=http://127.0.0.1:8080 RATE=500 DURATION=10m \
k6 run --out json="$DIR/results/k6-raw.json" \
--summary-export="$DIR/results/k6-summary.json" \
loadtest/e2e-baseline.js | tee "$DIR/results/k6-stdout.txt"
echo
echo "═══ 步骤 4:验证数据可用性 ═══"
tools/check-k6-result.sh "$DIR/results/k6-summary.json" 500 || {
echo "⚠️ 全链路数据不可用 —— 请先修压测侧(preAllocatedVUs / 压测机资源)"
}
echo
echo "✅ Lab 3 完成 → $DIR"
echo "下一步:填写 $DIR/README.md(模板见 docs/experiments/_TEMPLATE/README.md)"
三、三层对比的自动汇总
把三份输出汇总成一张表,方便写结论:
# tools/summarize-lab3.py <EXP_DIR>
import json, pathlib, re, sys
exp = pathlib.Path(sys.argv[1])
def parse_component(path):
"""从组件基准输出里提取各操作的 P50/P95/P99"""
out = {}
if not path.exists():
return out
for line in path.read_text(encoding="utf-8", errors="ignore").splitlines():
m = re.search(r"([①②③④][ab]?\s*[^\s].*?)\s+P50\s+([\d.]+) us\s+P95\s+([\d.]+) us\s+P99\s+([\d.]+) us", line)
if m:
out[m.group(1).strip()] = (float(m.group(2)), float(m.group(3)), float(m.group(4)))
return out
def parse_integration(path):
out = {}
if not path.exists():
return out
text = path.read_text(encoding="utf-8", errors="ignore")
for key in ("P50", "P95", "P99"):
m = re.search(rf"{key}\s*=\s*([\d.]+)\s*us", text)
if m:
out[key] = float(m.group(1)) / 1000.0 # 转 ms
return out
def parse_e2e(path):
if not path.exists():
return {}
m = json.load(open(path))["metrics"]
d = m["http_req_duration"]
return {
"P50": d["med"], "P95": d["p(95)"], "P99": d["p(99)"],
"arrival_rate": m["http_reqs"]["rate"],
"error_rate": m["http_req_failed"]["rate"],
}
comp = parse_component(exp / "results" / "component-bench.txt")
integ = parse_integration(exp / "results" / "integration-bench.txt")
e2e = parse_e2e(exp / "results" / "k6-summary.json")
print("═" * 78)
print("Lab 3 三层结果汇总")
print("═" * 78)
print()
print("【第 1 层:组件基准】")
for name, (p50, p95, p99) in comp.items():
print(f" {name:<42} P50 {p50:8.1f} P95 {p95:8.1f} P99 {p99:8.1f} (us)")
if not comp:
print(" (未找到结果)")
print()
print("【第 2 层:集成基准】(单服务完整链路,闭环)")
if integ:
print(f" P50 {integ.get('P50', 0):8.2f} ms P95 {integ.get('P95', 0):8.2f} ms P99 {integ.get('P99', 0):8.2f} ms")
else:
print(" (未找到结果)")
print()
print("【第 3 层:全链路】(客户端观测,开环)")
if e2e:
print(f" P50 {e2e['P50']:8.2f} ms P95 {e2e['P95']:8.2f} ms P99 {e2e['P99']:8.2f} ms")
print(f" 到达率 {e2e['arrival_rate']:.1f} req/s 错误率 {e2e['error_rate']:.2%}")
else:
print(" (未找到结果)")
print()
print("【差额归因】")
if e2e and integ:
print(f" 全链路 − 集成 = {e2e['P99'] - integ.get('P99', 0):8.2f} ms ← 网络 + 网关 + 客户端排队")
if integ and comp:
# 用组件基准里最有代表性的那个(通常是首个操作)
worst = max(comp.values(), key=lambda v: v[2])[2] / 1000.0
print(f" 集成 − 组件 = {integ.get('P99', 0) - worst:8.2f} ms ← 框架开销 + 服务内排队(参考值)")
print()
print("═" * 78)
print("必答三个问题:")
print(" 1. 哪个问题只有组件基准能发现?(提示:无索引列的 P99、批量 vs 循环)")
print(" 2. 哪个问题只有集成基准能发现?(提示:连接池 pending、事务开销)")
print(" 3. 哪个问题只有全链路能发现?(提示:网络、排队、跨进程放大)")
四、实验档案模板
<!-- docs/experiments/E03-two-level-test/README.md -->
---
id: E03
title: 同一接口的两层(三层)测试对比
date: 2025-xx-xx
chapter: 3
kind: baseline
status: done
hypothesis: "组件基准能发现慢 SQL;全链路能发现网络与排队;两者的差额应能被解释"
variable: "无(层级对比)"
control: "故意留了一个无索引列作为阳性对照"
commit: <hash>
distribution: "zipf(前 1% 用户占约 35% 请求)"
data_rows: 100000
measurement_point: "组件=Repository / 集成=服务端 / 全链路=客户端"
tags: [pyramid, testcontainers, k6, lab3]
---
## 1. 假设与预期
- 组件基准:findByStatus(无索引)的 P99 应显著高于 findById
- 集成基准:P99 应比组件基准高,差额来自框架开销与连接池
- 全链路:P99 应比集成基准更高,差额来自网络与网关
## 2. 环境元数据
(粘 results/env.txt)
## 3. 原始结果
(粘 summarize-lab3.py 的输出)
## 4. 观察与解释
| 我以为 | 实际是 | 结论 |
| --- | --- | --- |
| | | |
### 阳性对照是否被发现
- 无索引列的 P99:组件基准 ______ us,全链路 P99 ______ ms
- 结论:组件基准 ☐ 能 / ☐ 不能 发现这个已知瓶颈
## 5. 三个必答问题
1. 只有组件基准能发现的:
2. 只有集成基准能发现的:
3. 只有全链路能发现的:
## 6. 结论
## 7. 被推翻的假设
## 8. 遗留问题与下一步
五、验收自检
# ① 组件基准用的是真 PostgreSQL 吗?
grep -c "PostgreSQLContainer" src/test/kotlin/bench/*.kt
# ② 数据量与分布对了吗?
grep -E "rows = |zipf" docs/experiments/E03-two-level-test/README.md
# ③ 组件基准报的是百分位而不是均值吗?
grep -E "P50|P95|P99" docs/experiments/E03-two-level-test/results/component-bench.txt | head -5
# ④ 集成基准有独立预热吗?
grep -i "预热" docs/experiments/E03-two-level-test/results/integration-bench.txt
# ⑤ 全链路验证了实际到达率吗?
tools/check-k6-result.sh docs/experiments/E03-two-level-test/results/k6-summary.json 500
# ⑥ 三个必答问题都写了吗?
grep -A3 "三个必答问题" docs/experiments/E03-two-level-test/README.md
六、常见问题
Q:三层跑完发现 P99 差异很小(都差不多),白做了吗? A:差异小本身是有价值的结论——说明框架开销、网络开销、排队都不显著。但要先排除三个可能:① 数据量太小(瓶颈被掩盖);② 负载太低(排队还没出现);③ 集成基准和全链路用的是不同数据量,导致不可比。如果这三项都确认过,那就是可信的好结论。
Q:组件基准跑得很慢(几十秒以上一次操作)怎么办?
A:检查三件事:① 是否预热不足(连接池还没填满,前几次都在建连);② 是否在 Sampler 里每次都从池里借还连接(可以复用一个连接测"纯 SQL");③ 数据量是否过大导致建表/灌数据占了主要时间(把灌数据放到 @BeforeAll 且只做一次,或用 Testcontainers 的 withReuse)。
Q:没有 Docker 怎么办?
A:连一个本地安装的 PostgreSQL,但必须在 env.txt 里记录它的版本、配置(max_connections、shared_buffers)。绝不要退回 H2 —— 那会让整个 Lab 失去意义(第 3.2 节)。
Q:我可以只做两层(组件 + 全链路),跳过集成基准吗? A:可以,但要明确说明跳过的代价:你会少一个「框架开销(拦截器、事务、序列化)」的观测点。如果三层结果里「集成 − 组件」的差额很大,那说明框架开销值得优化——跳过这一层你就看不到这个信息。