文档目录

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 分钟。
  • 验收脚本检查的是「要素是否齐全」,不是「门禁是否真的有用」——后者要靠时间验证(观察几个月的误报率)。
  • 告警规则草案需要按实际指标名调整:生成的是模板,不是可直接用的配置。
  • 门禁的价值需要几个月才能体现(拦住渐进退化)——不要因为"暂时没拦住什么问题"就放弃它。