文档目录

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:可以,但要明确说明跳过的代价:你会少一个「框架开销(拦截器、事务、序列化)」的观测点。如果三层结果里「集成 − 组件」的差额很大,那说明框架开销值得优化——跳过这一层你就看不到这个信息。