文档目录

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 保留手工以确保你理解每个数字的来源)。