文档目录

8.8 配套代码:评审清单、周报与业务语言

对应小节:8.8 性能评审与组织落地 三部分:① PR 模板(关键 5 项);② 性能周报生成器;③ 业务语言翻译器。

一、PR 模板(只有关键 5 项)

<!-- .github/pull_request_template.md -->

## 变更说明

<!-- 简要描述这个 PR 做了什么 -->

## 性能影响

> ⚠️ 只需回答**与本次改动相关**的项。无关的项请勾选「无性能影响」。
>
> 详细清单见 `docs/performance-review-checklist.md`(按需查阅)。

- [ ] **无性能影响**(纯文案 / 纯样式 / 纯配置注释等,可跳过下面所有项)

### 如果改动涉及数据访问、外部调用或热路径:

- [ ] **IO / RPC 次数**:这个改动增加了多少次数据库查询、外部调用或序列化?
      → 如果是循环里的调用(N+1),请说明为什么不能批处理
      → 增加了 ____ 次

- [ ] **资源边界**:引入了新的缓存 / 队列 / 线程池 / 连接池吗?
      → 如果有,它们**有容量上限吗**?(无界的缓存/队列会导致 OOM)
      → 上限是 ____

- [ ] **超时与失败**:新增的下游调用有超时吗?
      → 超时值必须**小于上游给的预算**(逐层递减)
      → 超时是 ____ ms,上游预算是 ____ ms
      → 失败时会快速失败还是排队?

- [ ] **可观测性**:新增的接口/依赖有指标吗?
      → 至少要有:延迟(直方图)、错误率
      → 出问题时能否回答「这个接口的 P99 里,数据库占多少毫秒」?

- [ ] **性能验证**:关键路径做了 before/after 对比吗?
      → 附实验编号(如 E08-xxx),或说明为什么不需要
      → 实验编号:____

## 需要同步更新的文档

- [ ] 容量表(`docs/capacity.md`)——如果改变了资源用量
- [ ] SLO 定义(`slo/slos.yaml`)——如果新增了接口或改变了目标
- [ ] 告警规则(`observability/alerts.yml`)——如果新增了关键路径
- [ ] 不需要更新

## 其他

- [ ] 已运行单元测试
- [ ] 已运行 lint / format

这个模板的设计原则:

设计 理由
只有 5 项 长了会被机械全勾(第 8.8 节)
「无性能影响」放在第一项 允许快速跳过(避免形式主义)
每项都有追问(“增加了多少次”) 逼出具体答案,而不是打勾了事
明确列出连带更新 防止漏掉容量表/SLO/告警

二、性能周报生成器

# tools/weekly-perf-report.py <PROMETHEUS_URL> [--output report.md]
"""
从 Prometheus 生成性能周报。

设计意图:让性能「可见」——每周在团队频道发一次,
        而不是等到出问题时才谈性能。
"""
import argparse
import json
import sys
import urllib.parse
import urllib.request
from datetime import datetime, timedelta


def query(prom, expr):
    url = f"{prom}/api/v1/query?" + urllib.parse.urlencode({"query": expr})
    try:
        with urllib.request.urlopen(url, timeout=10) as r:
            data = json.load(r)
        result = data["data"]["result"]
        return float(result[0]["value"][1]) if result else None
    except Exception:
        return None


def fmt(v, unit="", precision=1):
    if v is None:
        return "n/a"
    return f"{v:.{precision}f}{unit}"


def main(prom, output):
    now = datetime.now()
    week_ago = now - timedelta(days=7)

    lines = [
        f"## 性能周报({week_ago.strftime('%m-%d')} ~ {now.strftime('%m-%d')})",
        "",
    ]

    # ── 核心指标 ────────────────────────────────────────────
    metrics = [
        ("P99 延迟", 'histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[7d])))', "ms", 1000),
        ("P50 延迟", 'histogram_quantile(0.50, sum by (le) (rate(http_request_duration_seconds_bucket[7d])))', "ms", 1000),
        ("错误率", 'sum(rate(http_requests_total{status=~"5.."}[7d])) / sum(rate(http_requests_total[7d]))', "%", 100),
        ("峰值 QPS", 'max_over_time(sum(rate(http_requests_total[5m]))[7d:5m])', "", 1),
    ]

    lines += ["| 指标 | 本周 | 上周 | 变化 |", "| --- | --- | --- | --- |"]

    for label, expr, unit, scale in metrics:
        # 本周
        cur = query(prom, expr)
        # 上周(用 offset)
        last = query(prom, expr.replace("[7d]", "[7d] offset 7d"))
        if cur is not None:
            cur *= scale
        if last is not None:
            last *= scale
        if cur is not None and last is not None and last != 0:
            delta = (cur - last) / last * 100
            flag = "⚠️" if abs(delta) > 10 else ""
            delta_str = f"{delta:+.1f}% {flag}"
        else:
            delta_str = "—"
        lines.append(f"| {label} | {fmt(cur, unit)} | {fmt(last, unit)} | {delta_str} |")

    lines.append("")

    # ── 错误预算 ────────────────────────────────────────────
    budget = query(prom, "slo:orders_availability:budget_remaining")
    if budget is not None:
        lines += [
            "### 错误预算",
            "",
            f"- 剩余:**{budget * 100:.0f}%**(30 天窗口)",
        ]
        if budget < 0.3:
            lines.append("- ⚠️ **预算不足 30% —— 建议暂停非必要的功能发布**")
        elif budget < 0.5:
            lines.append("- ⚠️ 预算不足 50% —— 建议谨慎发布")
        else:
            lines.append("- ✅ 预算充足")
        lines.append("")

    # ── 饱和度(领先指标)──────────────────────────────────
    lines += [
        "### 饱和度(领先指标)",
        "",
        "| 指标 | 本周峰值 | 状态 |",
        "| --- | --- | --- |",
    ]

    sat_metrics = [
        ("连接池 pending", 'max_over_time(db_pool_pending[7d])', 0, "= 0"),
        ("队列深度峰值", 'max_over_time(executor_queue_depth[7d])', 100, "< 100"),
        ("CPU 节流峰值", 'max_over_time(rate(container_cpu_cfs_throttled_periods_total[5m]) / rate(container_cpu_cfs_periods_total[5m])[7d:5m])', 0.25, "< 25%"),
    ]

    for label, expr, threshold, good_desc in sat_metrics:
        v = query(prom, expr)
        if v is None:
            lines.append(f"| {label} | n/a | — |")
            continue
        ok = v <= threshold
        lines.append(f"| {label} | {v:.2f} | {'✅' if ok else '❌'} 应 {good_desc} |")
    lines.append("")

    # ── 需要关注的事 ────────────────────────────────────────
    lines += ["### 需要关注", ""]
    concerns = []

    p99 = query(prom, metrics[0][1])
    if p99 and p99 * 1000 > 150:
        concerns.append(f"P99 = {p99*1000:.0f}ms,已接近 SLO(200ms)")

    pending = query(prom, 'max_over_time(db_pool_pending[7d])')
    if pending and pending > 0:
        concerns.append(f"本周出现过连接池排队(峰值 {pending:.0f})")

    throttled = query(prom, sat_metrics[2][1])
    if throttled and throttled > 0.1:
        concerns.append(f"容器 CPU 节流峰值 {throttled*100:.0f}%")

    if concerns:
        for c in concerns:
            lines.append(f"- ⚠️ {c}")
    else:
        lines.append("- ✅ 本周无异常")
    lines.append("")

    lines += [
        "---",
        "",
        f"<sub>自动生成于 {now.strftime('%Y-%m-%d %H:%M')} · "
        f"[Grafana 看板](http://grafana/d/slo) · [容量表](docs/capacity.md)</sub>",
    ]

    report = "\n".join(lines)
    print(report)

    if output:
        import pathlib
        pathlib.Path(output).write_text(report, encoding="utf-8")
        print(f"\n✅ 已写入 {output}", file=sys.stderr)


if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("prometheus", nargs="?", default="http://localhost:9090")
    ap.add_argument("--output")
    args = ap.parse_args()
    main(args.prometheus, args.output)

预期输出:

## 性能周报(01-08 ~ 01-15)

| 指标 | 本周 | 上周 | 变化 |
| --- | --- | --- | --- |
| P99 延迟 | 98.3ms | 95.2ms | +3.3% |
| P50 延迟 | 12.4ms | 12.3ms | +0.8% |
| 错误率 | 0.04% | 0.05% | -20.0% ⚠️ |
| 峰值 QPS | 3120 | 2980 | +4.7% |

### 错误预算
- 剩余:**78%**(30 天窗口)
- ✅ 预算充足

### 饱和度(领先指标)

| 指标 | 本周峰值 | 状态 |
| --- | --- | --- |
| 连接池 pending | 0.00 | ✅ 应 = 0 |
| 队列深度峰值 | 12.00 | ✅ 应 < 100 |
| CPU 节流峰值 | 0.08 | ✅ 应 < 25% |

### 需要关注
- ⚠️ P99 = 98ms,已接近 SLO(200ms)

注意「P99 +3.3%」这个变化——它在噪声范围内,所以周报里不加"⚠️"(脚本只在 > 10% 时标记)。但它连续几周上升就是趋势,值得在周报正文里提一句。

三、业务语言翻译器

# tools/translate-for-business.py <INPUT.json>
"""
把技术指标翻译成业务语言。

输入:
{
  "service": "订单服务",
  "daily_requests": 50000,
  "current": {"p50_ms": 12, "p99_ms": 400, "error_rate": 0.008},
  "target": {"p50_ms": 12, "p99_ms": 200, "error_rate": 0.005},
  "monthly_revenue_per_order": 50,
  "avg_order_value": 200
}
"""
import json
import sys


def translate(d):
    cur, tgt = d["current"], d["target"]
    daily = d["daily_requests"]

    # 计算"受影响的人数"
    slow_pct = 0.01      # P99 意味着 1% 的请求
    slow_users = int(daily * slow_pct)
    failed_users = int(daily * cur["error_rate"])
    target_failed = int(daily * tgt["error_rate"])

    # 计算"感知阈值"(200ms 是人类可感知卡顿的门槛,400ms 是明显卡顿)
    p99 = cur["p99_ms"]
    if p99 < 200:
        experience = "流畅"
    elif p99 < 500:
        experience = "偶尔有轻微卡顿"
    elif p99 < 1000:
        experience = "明显卡顿"
    else:
        experience = "经常卡住"

    lines = [
        f"# {d['service']} 性能现状(给业务方)",
        "",
        "## 用户现在的体验",
        "",
        f"- **典型体验**:{cur['p50_ms']} 毫秒({daily:,} 个请求中的大多数)",
        f"- **最慢的 1% 用户**:要等 **{p99} 毫秒** —— {experience}",
        f"- 按每天 {daily:,} 次请求计算,**每天约 {slow_users:,} 个用户**遇到这种情况",
        "",
        f"- **成功率**:{(1 - cur['error_rate']) * 100:.2f}%"
        f"(每天约 **{failed_users:,} 次失败**)",
        "",
        "## 与我们的承诺相比",
        "",
        "| 项目 | 承诺 | 现状 | 是否达标 |",
        "| --- | --- | --- | --- |",
        f"| 典型响应时间 | {tgt['p50_ms']} ms | {cur['p50_ms']} ms | "
        f"{'✅' if cur['p50_ms'] <= tgt['p50_ms'] else '❌'} |",
        f"| 最慢 1% 的响应时间 | {tgt['p99_ms']} ms | {cur['p99_ms']} ms | "
        f"{'✅' if cur['p99_ms'] <= tgt['p99_ms'] else '❌'} |",
        f"| 成功率 | {(1 - tgt['error_rate']) * 100:.2f}% | "
        f"{(1 - cur['error_rate']) * 100:.2f}% | "
        f"{'✅' if cur['error_rate'] <= tgt['error_rate'] else '❌'} |",
        "",
    ]

    # 影响估算
    if cur["error_rate"] > tgt["error_rate"]:
        gap = failed_users - target_failed
        revenue_loss = gap * d.get("avg_order_value", 0)
        lines += [
            "## 可能的影响",
            "",
            f"- 每天比承诺多 **{gap:,} 次失败**",
            f"- 按客单价 {d.get('avg_order_value', 0)} 元估算,"
            f"每月约影响 **{revenue_loss * 30:,.0f} 元**营收",
            "",
        ]

    # 趋势(如果提供了历史数据)
    trend = d.get("trend")
    if trend:
        lines += [
            "## 趋势",
            "",
            f"- {trend['description']}",
            f"- {trend['impact']}",
            "",
        ]

    # 建议
    lines += [
        "## 建议",
        "",
    ]
    for i, s in enumerate(d.get("recommendations", []), 1):
        lines.append(f"{i}. **{s['action']}**({s['effort']})")
        lines.append(f"   → 预期:{s['expected']}")
    lines.append("")

    # 不做的后果
    lines += [
        "## 如果不做",
        "",
        d.get("consequence", "问题可能继续恶化(需要评估具体影响)"),
        "",
    ]

    return "\n".join(lines)


if __name__ == "__main__":
    if len(sys.argv) < 2:
        demo = {
            "service": "订单服务",
            "daily_requests": 50000,
            "current": {"p50_ms": 12, "p99_ms": 400, "error_rate": 0.008},
            "target": {"p50_ms": 15, "p99_ms": 200, "error_rate": 0.005},
            "avg_order_value": 200,
            "trend": {
                "description": "P99 在过去 3 个月从 210ms 上升到 400ms",
                "impact": "按当前趋势,2 个月后最慢的 1% 用户将等待超过 1 秒",
            },
            "recommendations": [
                {"action": "修复下单失败的主要原因", "effort": "1 名工程师 × 1 周",
                 "expected": "成功率回到 99.4%"},
                {"action": "优化订单查询的索引与批量查询", "effort": "1 名工程师 × 2 周",
                 "expected": "P99 从 400ms 降到 120ms"},
            ],
            "consequence": (
                "P99 正在以每月约 15% 的速度恶化。如果不处理,两个月后会有约 5% 的用户"
                "(每天 2500 人次)遇到明显卡顿,届时修复成本是现在的 3–5 倍。"
            ),
        }
        print(translate(demo))
        sys.exit(0)

    data = json.loads(open(sys.argv[1], encoding="utf-8").read())
    print(translate(data))

预期输出:

# 订单服务 性能现状(给业务方)

## 用户现在的体验

- **典型体验**:12 毫秒(50,000 个请求中的大多数)
- **最慢的 1% 用户**:要等 **400 毫秒** —— 偶尔有轻微卡顿
- 按每天 50,000 次请求计算,**每天约 500 个用户**遇到这种情况

- **成功率**:99.20%(每天约 **400 次失败**)

## 与我们的承诺相比

| 项目 | 承诺 | 现状 | 是否达标 |
| --- | --- | --- | --- |
| 典型响应时间 | 15 ms | 12 ms | ✅ |
| 最慢 1% 的响应时间 | 200 ms | 400 ms | ❌ |
| 成功率 | 99.50% | 99.20% | ❌ |

## 可能的影响

- 每天比承诺多 **150 次失败**
- 按客单价 200 元估算,每月约影响 **900,000 元**营收

...

注意最后那个数字:「每月影响 90 万元营收」——即使这个估算很粗糙,它也比「P99 超标 100%」更能推动决策。

四、动手改造

改动 观察什么
用 weekly-perf-report.py 生成一份周报 看它能否自动标出需要关注的点
把周报发到团队频道(每周) 观察"性能可见"带来的讨论变化
用 translate-for-business.py 翻译你项目的指标 看业务方能否理解
把 PR 模板里的 5 项加进你的仓库 观察有多少 PR 会认真回答
故意在 PR 里加一个无界缓存 模板会强制你回答"有容量上限吗"

五、这段代码的局限

  • weekly-perf-report.py 依赖 Prometheus 的指标名:需要按你的项目调整。
  • 周报的"趋势判断"很粗糙(只看 10% 阈值):真正的趋势分析需要看多周数据(可以用 predict_linear 或简单移动平均)。
  • translate-for-business.py 的营收估算很粗略:它假设"失败 = 损失一单",实际转化率损失更复杂——但它足以把技术问题变成业务问题。
  • PR 模板依赖人的诚实:可以被敷衍(全部打勾)——关键是 code review 时由 reviewer 追问(第 8.8 节)。
  • 业务语言的翻译需要业务知识:脚本只能提供结构,具体的营收估算、用户影响需要和业务方确认。