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. 修复建议
短期(立即)
- <措施>
长期(根治)
- <措施>
验证方法
<修复后如何确认有效>
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是关键词匹配:可能误报(比如"我们没有用平均值"会被判为用平均值)或漏报。- 它不能替代人的评审:一份结论的真正质量,取决于「证据链是否成立」「贡献占比是否可信」「被排除的假设是否合理」——这些需要人来判断。