7.9 配套代码:Lab 7 优化编排与验证
对应小节:7.9 Lab 7 把「优化 → 测 → 记录 → 否决」串成可执行流程。
一、Lab 7 的目录结构
docs/experiments/E07-optimization/
├── README.md # 实验档案
├── OPTIMIZATION.md # 优化收益报告(本 Lab 的核心产出)
├── optimizations.json # 结构化的优化数据(用于生成报告)
├── rejections.json # 被否决的方案
├── results/
│ ├── before/ # 优化前的数据
│ │ ├── round-1.json …
│ ├── opt1-batch/ # 优化 1 之后
│ ├── opt2-regex/ # 优化 2 之后
│ └── opt3-dispatcher/ # 优化 3 之后
└── hypotheses.json # 假设台账(来自 Lab 6)
二、单个优化的验证编排
#!/usr/bin/env bash
# tools/verify-optimization.sh <BASE_EXP> <VARIANT_NAME> [ROUNDS]
#
# 对一个优化做完整的 before/after 验证。
#
# 前置:代码已经改成优化后的版本,服务已重启并预热。
set -uo pipefail
BASE_EXP="${1:?usage: verify-optimization.sh <base_exp> <variant_name> [rounds]}"
VARIANT="${2:?}"
ROUNDS="${3:-3}"
NOISE_JSON="${NOISE_JSON:-docs/experiments/E05-noise/results/noise-floor.json}"
DIR="docs/experiments/${BASE_EXP}/results/${VARIANT}"
mkdir -p "$DIR"
echo "═══════════════════════════════════════════════════════════════"
echo "优化验证:$VARIANT"
echo " 基线数据 : docs/experiments/${BASE_EXP}/results/before/"
echo " 本变体 : $DIR"
echo " 轮数 : $ROUNDS"
echo "═══════════════════════════════════════════════════════════════"
echo
# 记录当前代码状态
{
echo "commit=$(git rev-parse HEAD)"
echo "dirty=$(git status --porcelain | wc -l | tr -d ' ')"
echo "timestamp=$(date -Iseconds)"
} > "$DIR/code-state.txt"
for i in $(seq 1 "$ROUNDS"); do
echo "───────── 第 $i / $ROUNDS 轮 ─────────"
# 重启服务(确保同一初始状态)
scripts/restart-app.sh "$DIR/gc-$i.log"
sleep 5
# 预热(不计入统计)
BASE_URL=http://127.0.0.1:8080 k6 run --quiet --vus 20 --duration 60s \
loadtest/profile-constant.js > /dev/null 2>&1
# 测量
BASE_URL=http://127.0.0.1:8080 RATE=500 DURATION=3m \
k6 run --summary-export="$DIR/round-$i.json" \
loadtest/profile-constant.js > "$DIR/round-$i-stdout.txt" 2>&1
# 如果是慢接口相关的优化,还要测"受影响的接口"
if [ -n "${AFFECTED_ENDPOINT:-}" ]; then
BASE_URL=http://127.0.0.1:8080 ENDPOINT="$AFFECTED_ENDPOINT" RATE=50 DURATION=1m \
k6 run --summary-export="$DIR/affected-round-$i.json" \
loadtest/single-endpoint.js > /dev/null 2>&1
echo " (同时测了受影响接口:$AFFECTED_ENDPOINT)"
fi
python3 - "$DIR/round-$i.json" <<'PY'
import json, sys
m = json.load(open(sys.argv[1]))["metrics"]
d = m["http_req_duration"]
print(f" QPS={m['http_reqs']['rate']:.1f} P50={d['med']:.1f} "
f"P99={d['p(99)']:.1f} 错误率={m['http_req_failed']['rate']:.2%}")
PY
done
echo
echo "═══ 对比基线 ═══"
tools/compare-with-noise.sh "$BASE_EXP" "$BASE_EXP" "$NOISE_JSON" 2>/dev/null || true
# 手动指定两个目录做对比
python3 - "docs/experiments/${BASE_EXP}/results/before" "$DIR" "$NOISE_JSON" <<'PY'
import json, pathlib, statistics as st, sys
before_dir, after_dir, noise_path = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2]), pathlib.Path(sys.argv[3])
noise = json.loads(noise_path.read_text()) if noise_path.exists() else {}
def med(d, key):
vals = []
for f in sorted(d.glob("round-*.json")):
try:
m = json.loads(f.read_text())["metrics"]
dr = m["http_req_duration"]
vals.append({"qps": m["http_reqs"]["rate"], "p50": dr["med"],
"p95": dr["p(95)"], "p99": dr["p(99)"]}[key])
except Exception:
pass
return st.median(vals) if vals else None
print("═" * 74)
print("优化效果对比")
print("═" * 74)
print()
print(f"{'指标':<8}{'优化前':>12}{'优化后':>12}{'变化':>12}{'噪声底线':>12}{'判定'}")
print("-" * 74)
for key, label in [("p50", "P50"), ("p95", "P95"), ("p99", "P99"), ("qps", "QPS")]:
b, a = med(before_dir, key), med(after_dir, key)
if b is None or a is None:
continue
delta = (a - b) / b * 100
n = noise.get(key, 0)
mdd = n * 2
if key == "qps":
# QPS 上升是好事
if delta > mdd: verdict = "✅ 显著提升"
elif delta > n: verdict = "⚠️ 超过噪声"
else: verdict = "❌ 在噪声内"
else:
if abs(delta) < n: verdict = "❌ 在噪声内"
elif abs(delta) < mdd: verdict = "⚠️ 未达 MDD"
else: verdict = "✅ 超过 MDD"
print(f"{label:<8}{b:>12.2f}{a:>12.2f}{delta:>11.1f}%{n:>11.1f}% {verdict}")
print()
print("═" * 74)
PY
echo
echo "✅ 验证完成 → $DIR"
三、Lab 7 三个优化的具体操作
优化 1:消除 N+1
# ① 改成批量查询(代码改动见下文)
# ② 验证(注意:这个优化要同时看 DB 查询次数)
BASE_EXP=E06-bottlenecks VARIANT=opt1-batch \
AFFECTED_ENDPOINT=normal \
tools/verify-optimization.sh E06-bottlenecks opt1-batch 3
# ③ 验证 DB 查询次数(用 pg_stat_statements)
psql -c "SELECT calls, mean_exec_time, left(query,50) FROM pg_stat_statements
WHERE query LIKE '%url from orders%' ORDER BY calls DESC LIMIT 3;" \
> docs/experiments/E06-bottlenecks/results/opt1-batch/pg-stats-after.txt
代码改动:
// before:100 次查询
val urls = ids.map { id ->
ds.connection.use { c ->
c.prepareStatement("select url from orders where id = ?").use { ps ->
ps.setLong(1, id)
ps.executeQuery().use { rs -> if (rs.next()) rs.getString(1) else null }
}
}
}
// after:1 次查询
val urlMap = ds.connection.use { c ->
c.prepareStatement("select id, url from orders where id = any(?)").use { ps ->
ps.setArray(1, c.createArrayOf("bigint", ids.toTypedArray()))
ps.executeQuery().use { rs ->
buildMap { while (rs.next()) put(rs.getLong(1), rs.getString(2)) }
}
}
}
val urls = ids.mapNotNull { urlMap[it] }
预期:DB 查询次数从 100/请求 降到 1/请求;P99 下降一个数量级。
优化 2:正则提到顶层
VARIANT=opt2-regex tools/verify-optimization.sh E06-bottlenecks opt2-regex 3
代码改动:
// before
repeat(200) {
val re = Regex("""\d{3}-\d{4}""") // ❌ 每次编译
hits += re.findAll(text).count()
}
// after
private val PHONE_RE = Regex("""\d{3}-\d{4}""") // ✅ 只编译一次
repeat(200) {
hits += PHONE_RE.findAll(text).count()
}
预期:CPU 利用率下降,P99 下降(幅度取决于编译成本占比)。
优化 3:阻塞调用切到 IO ⭐ 最容易测错的一个
# ⚠️ 关键:这个优化的效果要测"受影响的接口",不是它自己
VARIANT=opt3-dispatcher \
AFFECTED_ENDPOINT=normal \
tools/verify-optimization.sh E06-bottlenecks opt3-dispatcher 3
代码改动:
// before:阻塞调用在 Default 上(占满 8 个 worker)
get("/slow-io") {
val n = ds.connection.use { /* JDBC */ }
call.respondText("n=$n")
}
// after:切到 IO
get("/slow-io") {
val n = withContext(Dispatchers.IO) {
ds.connection.use { /* JDBC */ }
}
call.respondText("n=$n")
}
验证的关键:
# 对比"受影响接口"(/normal)在优化前后的表现
python3 - <<'PY'
import json, pathlib, statistics as st
def affected_p99(d):
vals = []
for f in sorted(d.glob("affected-round-*.json")):
try:
vals.append(json.loads(f.read_text())["metrics"]["http_req_duration"]["p(99)"])
except Exception:
pass
return st.median(vals) if vals else None
base = pathlib.Path("docs/experiments/E06-bottlenecks/results/slow-io")
opt = pathlib.Path("docs/experiments/E06-bottlenecks/results/opt3-dispatcher")
b, a = affected_p99(base), affected_p99(opt)
if b and a:
print(f"/normal 的 P99(压 /slow-io 期间):")
print(f" 优化前: {b:.1f} ms")
print(f" 优化后: {a:.1f} ms")
print(f" 改善 : {(b-a)/b*100:.0f}%")
print()
print("这就是优化 3 的真实效果 —— 它体现在【其他接口】上,")
print("而不是 /slow-io 自己(后者可能变化不大)。")
else:
print("(需要先采集受影响接口的数据)")
PY
四、optimizations.json(报告数据)
{
"title": "Lab 6 三个瓶颈的优化",
"root_cause": "三个独立瓶颈:重复正则编译(CPU)、Default 上阻塞调用(排队)、N+1 查询(数据访问)",
"experiments": {"before": "E06-bottlenecks", "after": "E07-optimization"},
"noise_floor_pct": 13.2,
"mdd_pct": 26.4,
"optimizations": [
{
"name": "消除 N+1(批量查询)",
"level": 2,
"level_name": "② 算法与数据访问",
"changes": "select url from orders where id = ? × 100 → where id = any(?) × 1",
"rationale": "把 100 次往返合并成 1 次,消除 99% 的数据库调用",
"metrics": {
"before": {"p50": 182.3, "p99": 420.5, "qps": 45, "error_rate": 0.0},
"after": {"p50": 18.1, "p99": 52.3, "qps": 480, "error_rate": 0.0}
},
"p_value": 0.008,
"costs": [
"几乎没有代价 —— 这是「减少工作」类优化的特点",
"唯一的权衡:单次查询的 SQL 略复杂(any 数组),但执行计划仍走索引"
],
"rollback": "改回循环单查",
"regressions": {
"functional": "返回的 url 列表与 before 逐条比对一致",
"performance": "其他接口 P99 变化 < 4%(在噪声范围内)",
"soak": "30 分钟浸泡,内存与连接数稳定"
}
},
{
"name": "正则提到顶层",
"level": 2,
"level_name": "② 算法与数据访问",
"changes": "把 Regex(...) 从循环内提到文件顶层常量",
"rationale": "正则编译是昂贵操作,每次请求编译 200 次浪费 CPU",
"metrics": {
"before": {"p50": 45.2, "p99": 180.3, "qps": 120, "error_rate": 0.0},
"after": {"p50": 12.1, "p99": 38.5, "qps": 420, "error_rate": 0.0}
},
"p_value": 0.008,
"costs": ["无"],
"rollback": "把常量改回循环内的 Regex(...)",
"regressions": {
"functional": "匹配结果与 before 一致(同一份测试文本)",
"performance": "其他接口无变化",
"soak": "纯计算改动,未做浸泡"
}
},
{
"name": "阻塞调用切到 Dispatchers.IO",
"level": 3,
"level_name": "③ 架构与并发",
"changes": "JDBC 调用包进 withContext(Dispatchers.IO) + Semaphore(64)",
"rationale": "Default 的并行度 = 核数(8),被阻塞调用占满后所有协程排队",
"metrics": {
"before": {"p50": 25.1, "p99": 480.2, "qps": 80, "error_rate": 0.0},
"after": {"p50": 22.3, "p99": 95.4, "qps": 95, "error_rate": 0.0}
},
"p_value": 0.008,
"costs": [
"引入一个并行度参数需要调优(Semaphore 的 permits)",
"极端情况下会因限流返回 429(已有明确状态码与指标)"
],
"rollback": "去掉 withContext(Dispatchers.IO)",
"regressions": {
"functional": "查询结果一致",
"performance": "⚠️ 关键:受影响的 /normal 接口 P99 从 420ms 降到 45ms(这才是主要改善)",
"soak": "30 分钟浸泡,协程数稳定"
}
}
],
"rejected": [
{
"name": "给 /slow-db 加 Redis 缓存",
"expected_benefit": "P50 -70%",
"reason": "批量查询已消除 N+1(P99 从 420ms 降到 52ms),缓存边际收益有限;key 分散导致命中率预估低,P99 不会改善;引入一致性与运维成本",
"reconsider_when": "命中率能到 90% 以上,或数据量增长导致批量查询也变慢"
},
{
"name": "换 ZGC",
"expected_benefit": "P99 -5%",
"reason": "收益小于噪声底线(13.2%);三个瓶颈的根因都与 GC 无关;验证成本高、可能降低吞吐",
"reconsider_when": "确认 GC 停顿与 P99 尖刺时间对齐,且停顿超过 100ms"
},
{
"name": "把慢接口改成异步驱动(R2DBC)",
"expected_benefit": "不确定",
"reason": "withContext(IO) 已解决主要问题(Default 不再被占满);引入新依赖与学习成本的收益不明确",
"reconsider_when": "IO 池的并行度(默认 64)成为新瓶颈时"
}
],
"not_covered": [
"冷启动路径(缓存为空、JIT 未预热)",
"数据量增长 10 倍后的表现",
"多副本部署下的负载分配与共享数据库争用",
"长时间运行(> 24 小时)的内存稳定性"
],
"next_steps": [
"监控 /slow-db 的 P99 一周,确认稳定",
"把 Semaphore 的 permits 参数做成可配置",
"评估是否需要给 /slow-db 加索引(当前走主键,暂无必要)"
]
}
五、Lab 7 验收脚本
#!/usr/bin/env bash
# tools/verify-lab7.sh <EXP_ID>
set -uo pipefail
EXP_ID="${1:?usage: verify-lab7.sh <exp_id>}"
BASE="docs/experiments/${EXP_ID}"
PASS=0; FAIL=0
ok() { echo " ✅ $1"; PASS=$((PASS+1)); }
bad() { echo " ❌ $1"; FAIL=$((FAIL+1)); }
echo "═══ Lab 7 验收:$EXP_ID ═══"
echo
echo "① 三个优化的验证数据"
for OPT in opt1-batch opt2-regex opt3-dispatcher; do
D="$BASE/results/$OPT"
if [ -d "$D" ]; then
N=$(ls "$D"/round-*.json 2>/dev/null | wc -l | tr -d ' ')
if [ "$N" -ge 3 ]; then
ok "$OPT:$N 轮数据"
else
bad "$OPT:只有 $N 轮(需 ≥3)"
fi
else
bad "$OPT:目录不存在"
fi
done
echo
echo "② 优化 3 的「受影响接口」数据(关键)"
if ls "$BASE/results/opt3-dispatcher"/affected-round-*.json > /dev/null 2>&1; then
ok "有受影响接口的数据"
python3 - "$BASE" <<'PY'
import json, pathlib, statistics as st, sys
base = pathlib.Path(sys.argv[1])
def p99(d, pattern):
vals = []
for f in sorted(d.glob(pattern)):
try:
vals.append(json.loads(f.read_text())["metrics"]["http_req_duration"]["p(99)"])
except Exception:
pass
return st.median(vals) if vals else None
b = p99(base / "results/slow-io", "affected-round-*.json")
a = p99(base / "results/opt3-dispatcher", "affected-round-*.json")
if b and a:
print(f" /normal 的 P99:{b:.1f}ms → {a:.1f}ms(改善 {(b-a)/b*100:.0f}%)")
if a < b * 0.5:
print(f" ✅ 改善显著 —— 说明优化 3 生效(效果体现在其他接口上)")
else:
print(f" ⚠️ 改善不明显 —— 检查是否真的切到了 IO 调度器")
PY
else
bad "缺受影响接口的数据 —— 优化 3 的效果必须这样验证(见 7.9 节)"
fi
echo
echo "③ 三问验证完整性"
for OPT in opt1-batch opt2-regex opt3-dispatcher; do
D="$BASE/results/$OPT"
[ -f "$D/code-state.txt" ] && ok "$OPT:有代码状态记录" || bad "$OPT:缺 code-state.txt"
done
echo
echo "④ 优化收益报告"
if [ -f "$BASE/OPTIMIZATION.md" ]; then
ok "OPTIMIZATION.md 存在"
for KW in "提升多少" "代价" "回归验证" "被否决" "未覆盖"; do
grep -q "$KW" "$BASE/OPTIMIZATION.md" && ok "含「$KW」" || bad "缺「$KW」"
done
else
bad "缺 OPTIMIZATION.md(本 Lab 的核心产出)"
fi
echo
echo "⑤ 否决记录(至少 1 项)"
if [ -f "$BASE/rejections.json" ]; then
N=$(python3 -c "import json;print(len(json.load(open('$BASE/rejections.json'))['rejections']))" 2>/dev/null || echo 0)
[ "$N" -ge 1 ] && ok "有 $N 项否决记录" || bad "否决记录为空"
# 检查是否写了「重新评估条件」
python3 - "$BASE/rejections.json" <<'PY'
import json, sys
data = json.load(open(sys.argv[1]))
missing = [r["id"] for r in data["rejections"] if not r.get("reconsider_when")]
if missing:
print(f" ⚠️ {', '.join(missing)} 缺少「重新评估条件」")
else:
print(f" ✅ 每项否决都写了重新评估条件")
PY
else
bad "缺 rejections.json —— 至少否决一项优化"
fi
echo
echo "⑥ 噪声底线对比"
if [ -f "$BASE/OPTIMIZATION.md" ]; then
grep -qE "噪声底线|MDD" "$BASE/OPTIMIZATION.md" && ok "引用了噪声底线" || bad "未引用噪声底线(无法判断差异真假)"
fi
echo
echo "═══════════════════════════════"
echo "通过 $PASS 项,失败 $FAIL 项"
[ "$FAIL" -eq 0 ] && echo "✅ Lab 7 完成" || echo "❌ 还有 $FAIL 项待补"
六、动手改造
| 改动 | 观察什么 |
|---|---|
| 只做优化 1、跳过 2、3 | 验收脚本会报缺失——体会"完整的优化流程"需要什么 |
优化 3 只测 /slow-io 自己(不测 /normal) |
会看到"几乎没有改善"——这就是测错指标的后果 |
| 故意不记录否决 | 验收报错——否决是流程的一部分 |
把 optimizations.json 的某个 p_value 改成 0.3 |
报告会标记「不显著」 |
用 verify-optimization.sh 给你的真实优化做验证 |
体会完整的验证流程 |
七、这段代码的局限
verify-optimization.sh需要手动改代码后再跑:它只做验证编排,不做代码改动。- 每个优化都要重启服务 + 预热:3 轮 × 3 个优化 ≈ 30 分钟,这是必要的时间成本(否则无法保证可比性)。
AFFECTED_ENDPOINT的验证设计是本节的关键:像"调度器污染"这类优化,效果体现在其他接口上——如果只测被改动的接口,会得出"没效果"的错误结论。- JSON 格式的
optimizations.json需要手工填写:数字要从 k6 summary 里提取(可以写脚本自动化,但本 Lab 保留手工以确保你理解每个数字的来源)。