8.9 配套代码:Lab 8 编排与零误报验证
对应小节:8.9 Lab 8 把「建门禁 → 测噪声 → 定阈值 → 验证零误报」串成可执行流程。
一、Lab 8 一键编排
#!/usr/bin/env bash
# tools/run-lab8.sh <EXP_ID>
set -euo pipefail
EXP_ID="${1:?usage: run-lab8.sh <exp_id>}"
DIR="docs/experiments/${EXP_ID}"
mkdir -p "$DIR/results" perf
echo "═══════════════════════════════════════════════════════════════"
echo "Lab 8:做一个不会误报的性能门禁"
echo " 实验编号: $EXP_ID"
echo "═══════════════════════════════════════════════════════════════"
echo
# ── 步骤 1:确认代码干净(噪声测量必须在固定代码上做)──
if [ "$(git status --porcelain | wc -l | tr -d ' ')" -gt 0 ]; then
echo "❌ 工作区有未提交改动 —— 噪声测量必须在固定代码上做"
echo " 请先 commit 或 stash"
exit 1
fi
echo "✅ 代码干净(commit: $(git rev-parse --short HEAD))"
echo
# ── 步骤 2:跑基准,确认能正常工作 ──────────────────────
echo "【步骤 2】跑一次基准(确认环境正常)"
./gradlew benchmark -q 2>&1 | tail -3
if [ ! -f build/reports/benchmarks/main.json ]; then
echo "❌ 未生成基准结果 —— 检查 kotlinx-benchmark 配置"
exit 1
fi
echo "✅ 基准结果已生成"
echo
# ── 步骤 3:测 CI 环境的噪声底线(关键)────────────────
echo "【步骤 3】测 CI 环境噪声底线(10 轮)"
echo " ⚠️ 这一步必须在【真实的 CI 环境】跑"
echo " 如果现在在本地,结果只能作为参考"
read -r -p " 在 CI 环境吗?[y/N] " ans
if [[ "${ans,,}" == "y" ]]; then
tools/ci-noise-floor.sh benchmark 10 | tee "$DIR/results/noise-floor.txt"
else
echo " ⚠️ 在本地跑(结果仅供参考,阈值需要放宽)"
tools/ci-noise-floor.sh benchmark 5 | tee "$DIR/results/noise-floor.txt"
fi
echo
# ── 步骤 4:分析噪声,得到阈值建议 ──────────────────────
echo "【步骤 4】分析噪声"
python3 tools/analyze-ci-noise.py perf/ci-noise | tee "$DIR/results/noise-analysis.txt"
echo
echo " ⚠️ 请把上面输出的 PER_BENCHMARK_THRESHOLD 复制到 tools/compare_benchmark.py"
read -r -p " 完成后按回车继续..." _
echo
# ── 步骤 5:建立基线 ────────────────────────────────────
echo "【步骤 5】建立基线"
if [ -f perf/baseline.json ]; then
echo " 基线已存在,跳过(如需重建,先删除 perf/baseline.json)"
else
./gradlew benchmark -q
cp build/reports/benchmarks/main.json perf/baseline.json
echo " ✅ 基线已建立:perf/baseline.json"
echo " ⚠️ 请人工确认结果无误,并提交:"
echo " git add perf/baseline.json"
echo " git commit -m 'perf: establish baseline'"
fi
echo
# ── 步骤 6:验证零误报(核心验收)──────────────────────
echo "【步骤 6】验证零误报(5 轮)"
THRESHOLD="${THRESHOLD:-0.25}" tools/verify-gate-no-false-positive.sh 5 \
| tee "$DIR/results/false-positive-verification.txt"
echo
# ── 步骤 7:生成告警规则草案 ────────────────────────────
echo "【步骤 7】生成告警规则草案"
mkdir -p observability
if [ ! -f observability/alerts.yml ]; then
cat > observability/alerts.yml <<'YAML'
# 告警规则草案(需按你的实际指标名调整)
# 详见 docs/performance-evaluation/08-continuous-performance/05-alerting.md
groups:
- name: slo-burn-rate
rules:
- alert: ErrorBudgetBurnFast
expr: |
slo:orders_availability:burn_rate_1h > 14.4
and
slo:orders_availability:burn_rate_5m > 14.4
for: 2m
labels: { severity: page }
annotations:
summary: "错误预算燃烧过快(双窗口同时超标)"
runbook: "见 runbooks/ErrorBudgetBurnFast.md"
- name: leading-indicators
rules:
- alert: ConnectionPoolSaturated
expr: max(db_pool_pending) > 5
for: 10m
labels: { severity: ticket }
annotations:
summary: "连接池排队"
runbook: "⚠️ 先查慢查询,不要先加池(第 6.5 节)"
- alert: ContainerCpuThrottled
expr: |
rate(container_cpu_cfs_throttled_periods_total[5m])
/ rate(container_cpu_cfs_periods_total[5m]) > 0.25
for: 10m
labels: { severity: ticket }
annotations:
summary: "容器 CPU 节流 > 25%"
runbook: "第 4.8 节"
- name: observe-only
rules:
- alert: GcPauseHigh
expr: |
histogram_quantile(0.99,
sum by (le) (rate(jvm_gc_pause_seconds_bucket[5m]))) > 0.2
for: 10m
labels: { severity: log }
annotations:
summary: "GC 停顿 P99 > 200ms(先观察,确认与 P99 尖刺是否对齐)"
YAML
echo " ✅ 已生成 observability/alerts.yml"
else
echo " ⚠️ observability/alerts.yml 已存在,跳过"
fi
echo
# ── 步骤 8:验收 ────────────────────────────────────────
echo "【步骤 8】验收"
tools/verify-lab8.sh "$EXP_ID"
二、Lab 8 验收脚本
#!/usr/bin/env bash
# tools/verify-lab8.sh <EXP_ID>
set -uo pipefail
EXP_ID="${1:?usage: verify-lab8.sh <exp_id>}"
DIR="docs/experiments/${EXP_ID}"
PASS=0; FAIL=0
ok() { echo " ✅ $1"; PASS=$((PASS+1)); }
bad() { echo " ❌ $1"; FAIL=$((FAIL+1)); }
echo "═══ Lab 8 验收:$EXP_ID ═══"
echo
echo "① 基准集合(3–5 个,适合 CI)"
if [ -f build/reports/benchmarks/main.json ]; then
N=$(python3 -c "
import json
d = json.load(open('build/reports/benchmarks/main.json'))
print(len(d.get('benchmarks', [])))
" 2>/dev/null || echo 0)
if [ "$N" -ge 3 ] && [ "$N" -le 8 ]; then
ok "$N 个基准"
else
bad "$N 个基准(建议 3–5 个)"
fi
else
bad "未找到基准结果"
fi
echo
echo "② CI 环境噪声数据(至少 5 轮)"
if [ -d perf/ci-noise ]; then
N=$(ls perf/ci-noise/round-*.json 2>/dev/null | wc -l | tr -d ' ')
[ "$N" -ge 5 ] && ok "$N 轮数据" || bad "只有 $N 轮(需 ≥5)"
else
bad "缺 perf/ci-noise/ —— 未测噪声底线"
fi
echo
echo "③ 噪声分析结果"
if [ -f "$DIR/results/noise-analysis.txt" ]; then
ok "已生成分析"
grep -A2 "环境质量评估" "$DIR/results/noise-analysis.txt" | tail -1 | sed 's/^/ /'
else
bad "缺噪声分析(跑 tools/analyze-ci-noise.py)"
fi
echo
echo "④ 基线文件"
if [ -f perf/baseline.json ]; then
ok "perf/baseline.json 存在"
# 检查是否在 git 里(应该被版本控制)
git ls-files --error-unmatch perf/baseline.json > /dev/null 2>&1 \
&& ok "基线已纳入版本控制" \
|| bad "基线未提交到 git(无法 review 变更)"
else
bad "缺 perf/baseline.json"
fi
echo
echo "⑤ 门禁脚本"
if [ -f tools/compare_benchmark.py ]; then
ok "比较脚本存在"
grep -q "report-only" tools/compare_benchmark.py \
&& ok "支持 --report-only(先报告后失败)" \
|| bad "缺 --report-only(不便于渐进启用)"
grep -q "PER_BENCHMARK_THRESHOLD" tools/compare_benchmark.py \
&& ok "支持逐基准阈值" \
|| echo " ℹ️ 建议加 PER_BENCHMARK_THRESHOLD(不同基准噪声不同)"
else
bad "缺 tools/compare_benchmark.py"
fi
echo
echo "⑥ 零误报验证(核心验收项)"
F="$DIR/results/false-positive-verification.txt"
if [ -f "$F" ]; then
FP=$(grep -oE "误报次数 *: *[0-9]+" "$F" | grep -oE "[0-9]+" | head -1)
if [ "${FP:-1}" = "0" ]; then
ok "零误报 ✅(这是最关键的一项)"
else
bad "有 ${FP} 次误报 —— 阈值太紧,需要提高"
fi
grep -E "误报率" "$F" | sed 's/^/ /'
else
bad "缺零误报验证记录(跑 tools/verify-gate-no-false-positive.sh)"
fi
echo
echo "⑦ CI workflow"
if [ -f .github/workflows/perf-gate.yml ]; then
ok "workflow 存在"
grep -q "branches: \[main\]" .github/workflows/perf-gate.yml \
&& ok "只在主干跑(不在每个 PR)" \
|| bad "建议只在主干跑(PR 上噪声大、成本高)"
grep -q "GITHUB_STEP_SUMMARY" .github/workflows/perf-gate.yml \
&& ok "结果写入 CI 摘要" \
|| echo " ℹ️ 建议写入 \$GITHUB_STEP_SUMMARY(便于查看)"
else
bad "缺 .github/workflows/perf-gate.yml"
fi
echo
echo "⑧ 告警规则"
if [ -f observability/alerts.yml ]; then
ok "alerts.yml 存在"
# 检查是否有分级
grep -q "severity: page" observability/alerts.yml && ok "有 page 级告警" || bad "缺 page 级"
grep -q "severity: ticket" observability/alerts.yml && ok "有 ticket 级告警" || bad "缺 ticket 级"
# 检查是否有 runbook
RB=$(grep -c "runbook" observability/alerts.yml || echo 0)
TOTAL=$(grep -c "alert:" observability/alerts.yml || echo 0)
if [ "$RB" -ge "$TOTAL" ] && [ "$TOTAL" -gt 0 ]; then
ok "$TOTAL 条告警都有 runbook"
else
bad "有 $((TOTAL - RB)) 条告警缺 runbook"
fi
# 检查领先指标
grep -q "db_pool_pending" observability/alerts.yml && ok "含连接池饱和度告警" || bad "缺饱和度告警"
else
bad "缺 observability/alerts.yml"
fi
echo
echo "⑨ 误报日志"
if [ -f perf/false-positives.md ]; then
ok "误报日志存在(用于校准阈值)"
else
echo " ℹ️ 建议创建 perf/false-positives.md(记录每次触发与处理)"
fi
echo
echo "═══════════════════════════════"
echo "通过 $PASS 项,失败 $FAIL 项"
if [ "$FAIL" -eq 0 ]; then
echo "✅ Lab 8 完成"
else
echo "❌ 还有 $FAIL 项待补"
echo
echo "⚠️ 最重要的一项:零误报验证(⑥)"
echo " 如果做不到零误报,就提高阈值 —— 宁可漏报,不可误报。"
fi
三、Lab 8 实验档案模板
<!-- docs/experiments/E08-perf-gate/README.md -->
---
id: E08
title: 性能门禁建设与零误报验证
date: 2025-xx-xx
chapter: 8
kind: gate
status: done
hypothesis: "在本项目的 CI 环境里,阈值设为噪声底线的 1.5~2 倍可以实现零误报"
variable: "无(门禁建设)"
control: "代码不变时的多次运行"
commit: <hash>
tags: [gate, ci, lab8]
---
## 1. 目标与预期
- 建立 3–5 个适合 CI 的微基准门禁
- 在真实 CI 环境测出噪声底线
- 阈值设为噪声 × 1.5~2
- **验收:代码不变时连续 5 次零误报**
## 2. CI 环境信息
(粘 perf/ci-noise/env.txt)
| 项 | 值 |
| --- | --- |
| Runner | |
| CPU 核数 | |
| 内存 | |
| JDK | |
## 3. 噪声底线测量结果
| 基准 | 中位数 | 波动范围 | CV | 建议阈值 |
| --- | --- | --- | --- | --- |
| | | | | |
**环境质量**:最大 CV = ___%
→ 结论:☐ 可以做精细对比 ☐ 只能粗粒度 ☐ 噪声过大,建议只报告不失败
## 4. 门禁设计
| 配置项 | 值 |
| --- | --- |
| 触发时机 | push 到 main |
| 阈值 | 见 PER_BENCHMARK_THRESHOLD |
| 模式 | ☐ report-only ☐ 启用失败 |
| 基线位置 | perf/baseline.json |
## 5. 零误报验证
| 轮次 | 结果 | 备注 |
| --- | --- | --- |
| 1 | | |
| 2 | | |
| 3 | | |
| 4 | | |
| 5 | | |
**误报次数**:___
**误报率**:___%
## 6. 观察与解释
| 我以为 | 实际是 | 结论 |
| --- | --- | --- |
| | | |
### 意外发现
- 例如:CI 环境的噪声(11%–16%)远大于本地(3%–5%)
## 7. 结论
## 8. 被推翻的假设
## 9. 遗留问题
- 全链路门禁(需要专用环境)
- 告警规则的实际效果(需要生产数据)
- 阈值是否需要按季度重新校准
四、动手改造
| 改动 | 观察什么 |
|---|---|
完整跑一遍 run-lab8.sh |
得到你自己的门禁配置与阈值 |
| 故意把阈值调低一半 | 零误报验证会失败——理解"宁可漏报" |
| 在 CI 环境与本地分别测噪声 | 对比两者的差距(通常 CI 噪声大 2–3 倍) |
| 给某条告警去掉 runbook | 验收会报错——理解 runbook 的必要性 |
| 用验收脚本检查你的仓库 | 看还缺哪些要素 |
五、这段代码的局限
run-lab8.sh需要在 CI 环境跑才能真正有效:本地跑的结果只能作为参考(脚本会提示)。verify-gate-no-false-positive.sh需要 5 次完整的基准运行:如果基准较慢(每次 5 分钟),总共要 25 分钟。- 验收脚本检查的是「要素是否齐全」,不是「门禁是否真的有用」——后者要靠时间验证(观察几个月的误报率)。
- 告警规则草案需要按实际指标名调整:生成的是模板,不是可直接用的配置。
- 门禁的价值需要几个月才能体现(拦住渐进退化)——不要因为"暂时没拦住什么问题"就放弃它。