文档目录

6.8 配套代码:结论模板与证据打包

对应小节:6.8 结论的写法 两件事:① 结论模板;② 把证据文件自动打包成可交付的报告。

一、结论模板

<!-- tools/conclusion-template.md —— 复制到 docs/experiments/<EXP_ID>/CONCLUSION.md -->

# 根因结论:<一句话标题>

> 分析人 / 日期 / 关联实验编号

## 1. 现象

| 维度 | 观测值 |
| --- | --- |
| 接口 | |
| 指标变化 | P50: →  P95: →  P99: → |
| 错误率 | |
| 发现时间 | |
| 影响范围 | 全部实例 / 部分实例 / 特定请求类型 |
| 与变更的关系 | 与 YYYY-MM-DD HH:MM 的部署吻合 / 无关联 |

## 2. 延迟分解

| 环节 | P99 | 占比 | 测量方式 |
| --- | --- | --- | --- |
| 网络 + 客户端排队 | | | 客户端 − 网关 |
| 网关处理 + 服务排队 | | | 网关 − 应用内 |
| 应用自身 | | | 应用内 − 依赖之和 |
| └ 依赖 1 | | | 依赖级指标 |
| └ 依赖 2 | | | 依赖级指标 |

## 3. 根因

**<一句话根因>**

具体位置:`<文件:行号>` 或 `<配置项>`

## 4. 证据链

### 证据 1:<说明>

<命令> <输出>


### 证据 2:<说明>

<命令> <输出>


### 证据 3:<相关性/时间对齐验证>

<命令或对比方式> <结果>


## 5. 贡献占比

**约 ___%**

测量方法:☐ 逐个关闭(最可信)☐ 延迟分解(最常用)☐ 相关性(最弱)

<测量过程> 关闭前:___ ms 关闭后:___ ms 差值:___ ms,占总延迟 ___%


## 6. 被排除的假设

| 假设 | 排除依据 |
| --- | --- |
| GC 停顿 | GC 日志停顿时间戳与 P99 尖刺不对齐(差值 > 2s) |
| 慢查询 | pg_stat_statements 的 mean/total 与基线一致 |
| 连接池 | pending 全程为 0 |
| CPU 饱和 | CPU 45%,且关闭瓶颈后 CPU 未变 |
| 锁竞争 | 线程 dump 无 BLOCKED,lock 火焰图为空 |
| 下游服务 | JFR 的 SocketRead 无异常 |

## 7. 复现方式

```bash
# 最小复现命令集(三条以内)
1. <命令>
2. <命令>
3. 预期:<现象>

8. 修复建议

短期(立即)

  1. <措施>

长期(根治)

  1. <措施>

验证方法

<修复后如何确认有效>

9. 未覆盖的场景

  • 冷启动路径(缓存为空)
  • 数据量增长 10 倍后的表现
  • 多副本部署下的负载分配
  • <其他>

10. 结论强度自评

  • 跑了几轮?≥ 3 吗?
  • 报了中位数与波动范围吗?
  • 差异大于噪声底线与 MDD 吗?
  • 做了统计检验吗?(p 值:___)
  • 异常值有成因解释吗?
  • 观测配置前后一致吗?

## 二、证据自动打包

```python
# tools/package-evidence.py <EXP_ID>
"""
把实验的证据文件打包成一个可交付的报告。

产出:
  docs/experiments/<EXP_ID>/EVIDENCE.md   —— 汇总所有证据
  docs/experiments/<EXP_ID>/evidence.tar.gz —— 原始文件打包
"""
import json
import pathlib
import sys
from datetime import datetime

EVIDENCE_TYPES = {
    "env-check.txt": ("环境检查", "环境是否干净"),
    "env.txt": ("环境元数据", "可复现性"),
    "conclusions.md": ("根因结论", "结论全文"),
    "hypotheses.json": ("假设台账", "假设状态与依据"),
    "threads/analysis.txt": ("线程快照分析", "阻塞点"),
    "flamegraph/cpu.collapsed": ("CPU 火焰图数据", "热点"),
    "flamegraph/wall.collapsed": ("wall 火焰图数据", "等待点"),
    "flamegraph/alloc.collapsed": ("分配火焰图数据", "分配热点"),
    "k6-summary.json": ("压测结果", "延迟与错误率"),
    "noise-floor.json": ("噪声底线", "判断差异是否可信"),
    "pg-stats.txt": ("数据库统计", "慢查询与 N+1"),
    "metrics.txt": ("指标快照", "四层指标"),
}


def scan(exp_dir):
    found = {}
    for rel, (label, purpose) in EVIDENCE_TYPES.items():
        p = exp_dir / "results" / rel
        if p.exists():
            size = p.stat().st_size
            found[rel] = (label, purpose, size, p)
    return found


def summarize_threads(path):
    """从线程分析里提取关键结论"""
    try:
        text = path.read_text(encoding="utf-8", errors="ignore")
    except Exception:
        return None
    lines = []
    for line in text.splitlines():
        if any(k in line for k in ("BLOCKED", "持续", "线程数趋势", "⚠️", "❌")):
            lines.append(line.strip())
    return lines[:10]


def summarize_hypotheses(path):
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return None
    out = {"已验证成立": [], "已排除": [], "待验证": []}
    for h in data.get("hypotheses", []):
        out.setdefault(h["status"], []).append(
            f"{h['id']}: {h['hypothesis']}"
            + (f" —— 证据: {h['evidence']}" if h.get("evidence") else ""))
    return out


def main(exp_id):
    exp = pathlib.Path("docs/experiments") / exp_id
    if not exp.exists():
        print(f"❌ 找不到实验目录 {exp}")
        return

    found = scan(exp)
    out = exp / "EVIDENCE.md"

    lines = [
        f"# 证据汇总:{exp_id}",
        "",
        f"> 生成时间:{datetime.now().isoformat(timespec='seconds')}",
        "",
        "本文件由 `tools/package-evidence.py` 自动生成,汇总了本次实验的全部证据。",
        "",
        "---",
        "",
        "## 一、证据清单",
        "",
        "| 文件 | 类型 | 用途 | 大小 |",
        "| --- | --- | --- | --- |",
    ]

    for rel, (label, purpose, size, _) in found.items():
        lines.append(f"| `{rel}` | {label} | {purpose} | {size:,} B |")

    if not found:
        lines.append("| (无) | | | |")

    lines += ["", "---", ""]

    # 线程分析摘要
    if "threads/analysis.txt" in found:
        lines += ["## 二、线程快照关键结论", ""]
        summary = summarize_threads(found["threads/analysis.txt"][3])
        if summary:
            for s in summary:
                lines.append(f"- {s}")
        else:
            lines.append("(未能提取关键行,请查看原始文件)")
        lines += ["", "---", ""]

    # 假设台账
    if "hypotheses.json" in found:
        lines += ["## 三、假设台账", ""]
        hs = summarize_hypotheses(found["hypotheses.json"][3])
        if hs:
            for status in ("已验证成立", "已排除", "待验证"):
                items = hs.get(status, [])
                if items:
                    lines.append(f"### {status}({len(items)} 条)")
                    lines.append("")
                    for it in items:
                        lines.append(f"- {it}")
                    lines.append("")
        lines += ["---", ""]

    # 结论强度检查
    lines += ["## 四、结论强度自检", ""]
    checks = [
        ("有噪声底线", "noise-floor.json" in found),
        ("有压测结果", "k6-summary.json" in found),
        ("有环境元数据", "env.txt" in found or "env-check.txt" in found),
        ("有根因结论", (exp / "CONCLUSION.md").exists()),
        ("有假设台账", "hypotheses.json" in found),
        ("有线程快照分析", "threads/analysis.txt" in found),
    ]
    for label, ok in checks:
        lines.append(f"- [{'x' if ok else ' '}] {label}")

    missing = [label for label, ok in checks if not ok]
    if missing:
        lines += ["", f"⚠️ 缺少 {len(missing)} 项:{', '.join(missing)}"]
    else:
        lines += ["", "✅ 要素齐全"]

    lines += ["", "---", "", "## 五、如何复现", ""]
    lines += [
        "```bash",
        f"# 1. 查看环境",
        f"cat docs/experiments/{exp_id}/results/env-check.txt",
        "",
        "# 2. 查看噪声底线(判断差异是否可信)",
        f"cat docs/experiments/{exp_id}/results/noise-floor.json",
        "",
        "# 3. 查看压测结果",
        f"python3 tools/robust_stats.py docs/experiments/{exp_id}/results/",
        "",
        "# 4. 查看假设台账",
        f"python3 tools/hypotheses.py report {exp_id}",
        "```",
    ]

    out.write_text("\n".join(lines), encoding="utf-8")
    print(f"✅ 证据汇总 → {out}")
    print()
    print(f"共 {len(found)} 类证据,{sum(1 for _, ok in checks if ok)}/{len(checks)} 项检查通过")
    if missing:
        print(f"⚠️  缺少:{', '.join(missing)}")


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("usage: package-evidence.py <EXP_ID>")
        sys.exit(1)
    main(sys.argv[1])

三、结论的「同行评审」检查

#!/usr/bin/env bash
# tools/review-conclusion.sh <CONCLUSION.md>
#
# 模拟同行评审:检查结论是否包含必需要素。
set -uo pipefail

F="${1:?usage: review-conclusion.sh <conclusion.md>}"

echo "═══════════════════════════════════════════════════════════════"
echo "结论评审:$F"
echo "═══════════════════════════════════════════════════════════════"
echo

check() {
  local label="$1" pattern="$2" hint="$3"
  if grep -qE "$pattern" "$F"; then
    echo "  ✅ $label"
  else
    echo "  ❌ $label"
    echo "     → $hint"
  fi
}

echo "【必需要素】"
check "现象量化" "现象|P99.*→|影响范围" "补充:谁慢、多慢、何时开始、影响范围"
check "延迟分解" "延迟分解|环节.*占比|分解表" "补充:把总延迟拆成几段,说明哪段最大"
check "根因陈述" "根因" "补充:一句话说清根因,并给出代码位置/配置项"
check "证据链" "证据|命令|输出" "补充:每条证据的命令与实际输出"
check "贡献占比" "贡献占比|占总延迟|约.*%" "补充:该根因解释了总延迟的多少"
check "被排除的假设" "被排除|排除依据|为什么不是" "补充:列出并说明为什么排除了其他可能"
check "复现方式" "复现" "补充:别人照着能重现的最小命令集"
check "未覆盖场景" "未覆盖|未验证|局限" "补充:诚实列出本次没覆盖的情况"
echo

echo "【质量信号】"
if grep -qE '```' "$F"; then echo "  ✅ 含代码/命令块"; else echo "  ⚠️  没有代码块 —— 证据可能是描述性的而非可执行的"; fi
if grep -qE 'p\s*[<=]\s*0\.|置信区间|Mann' "$F"; then
  echo "  ✅ 含统计检验"
else
  echo "  ⚠️  没有统计检验 —— 差异的显著性缺少证据(第 5 章)"
fi
if grep -qE '噪声底线|MDD' "$F"; then
  echo "  ✅ 引用了噪声底线"
else
  echo "  ⚠️  没有引用噪声底线 —— 无法判断差异是否可信(第 5 章 5.7)"
fi
if grep -qiE '可能|也许|应该是' "$F"; then
  echo "  ⚠️  出现「可能/也许/应该是」—— 检查是否在陈述未经证实的推测"
fi
echo

echo "【反模式检测】"
grep -qiE '最好的一次|最快的一轮' "$F" && echo "  ❌ 出现 Cherry-picking 信号" || echo "  ✅ 无 Cherry-picking"
grep -qE '平均延迟|平均 P99' "$F" && echo "  ⚠️  用了平均值 —— 建议改用中位数 + 百分位" || echo "  ✅ 未用平均值"
echo

echo "═══════════════════════════════════════════════════════════════"

四、动手改造

改动 观察什么
用 review-conclusion.sh 检查你过去写的性能报告 会检出多少缺失要素?
把「被排除的假设」这一节删掉再检查 会报 ❌——体会它在结构上的必要性
把 package-evidence.py 接进 Lab 6 的收尾步骤 每次实验自动产出 EVIDENCE.md
给 EVIDENCE_TYPES 加上你项目的特有文件 适配你的目录结构
用 review-conclusion.sh 检查一份结论,然后按提示补全 体会「结构约束」如何提升结论质量

五、这段代码的局限

  • 模板是约束,不是保证:填满所有章节不等于结论正确——内容的质量取决于分析本身。
  • package-evidence.py 依赖目录结构约定(results/ 下放特定文件名):如果你的实验目录不同,需要调整 EVIDENCE_TYPES。
  • review-conclusion.sh 是关键词匹配:可能误报(比如"我们没有用平均值"会被判为用平均值)或漏报。
  • 它不能替代人的评审:一份结论的真正质量,取决于「证据链是否成立」「贡献占比是否可信」「被排除的假设是否合理」——这些需要人来判断。