文档目录

1.9 配套代码:Lab 1 的填写模板与算术校验

对应小节:1.9 Lab 1 这里给的是能直接复制粘贴的模板,以及一个帮你做预算加法的校验器。

一、SLO 表模板(复制到 docs/slo.md)

# SLO 表

> 最后更新:YYYY-MM-DD | 对应版本:<commit>
> 填写说明:四要素缺一不可。写不出来的项标「待确认」,并注明打算怎么获取。

## 接口 1:<GET /orders/{id}>

| 字段 | 值 |
| --- | --- |
| 接口 | |
| 关键路径 | 客户端 → ? → ? → 数据库 |
| 目标 QPS(**峰值**) | |
| P50 / P95 / P99 / P999 | |
| 错误率阈值 | |
| 超时率阈值 | |
| 负载前提:数据量 | |
| 负载前提:**数据分布** | (uniform / zipf(alpha) / 真实流量回放——必填) |
| 负载前提:缓存状态 | (cold / warm,预热多久) |
| **测量点** | 客户端 / 网关(不能填「应用内」) |
| 预热时长 | |
| 稳态时长 | |
| 验收轮次 | 至少 3 轮,取中位数 |
| 环境 | 实例规格、副本数、容器 CPU limit |
| 崩溃线 | (压力测试终止条件) |
| 测量日期与版本 | |

**待确认项**:
- [ ] 峰值 QPS 的真实数据(向业务方确认 / 从监控取值)
- [ ] 数据分布(从生产 SQL 日志统计热点比例)

## 接口 2:<POST /orders>
(同上表)

二、延迟预算表模板

## 延迟预算:<GET /orders/{id}>

SLO 目标:P99 ≤ ______ ms

| 环节 | 预算(P99) | 实测 | 状态 | 备注 |
| --- | --- | --- | --- | --- |
| 客户端 → 网关 | | 待测 | | |
| 网关处理 | | 待测 | | TLS / 鉴权 / 限流 |
| **应用排队** | | 待测 | | ⚠️ 没有对应代码,靠饱和度指标验证 |
| 应用逻辑 | | 待测 | | |
| ├ PostgreSQL | | 待测 | | |
| ├ Redis | | 待测 | | |
| └ 下游依赖 | | 待测 | | |
| 响应传输 | | 待测 | | |
| **余量** | | — | | 必须 > 0,建议占 10%~20% |

**合计检查**:所有预算 + 余量 = ______ ms,与 SLO ______ ms 相比:☐ 通过 ☐ 超出
(超出时必须砍某一跳,或放宽 SLO)

**并行标注**:哪些依赖是并行调用的?并行组:__________(并行段取最大值,不相加)

## 超时预算

| 层 | 上游给的时间 | 本层超时 | 留给下游 | 检查 |
| --- | --- | --- | --- | --- |
| 客户端 | — | | | |
| 网关 | | | | 必须 < 上游 |
| 应用 | | | | 必须 < 网关 |
| → PostgreSQL | | | — | 必须 < 应用给的总时间 |
| → Redis | | | — | 必须 < 应用给的总时间 |
| → 下游服务 | | | — | 必须 < 应用给的总时间 |

**重试预留**:如果允许重试,单次超时必须更短。重试次数:____,退避策略:____

三、预算算术校验器

手算容易错,用脚本核对:

# tools/check_budget.py
"""
读取延迟预算 CSV,检查:
  1. 合计是否超过 SLO
  2. 余量是否为正
  3. 超时链是否逐层递减

CSV 格式(表头固定):
  name,p99_ms,parallel_group,timeout_upstream_ms,timeout_self_ms
"""
import csv, sys

def check(path):
    rows = list(csv.DictReader(open(path)))
    problems, notes = [], []

    serial = [float(r["p99_ms"]) for r in rows if not r.get("parallel_group")]
    groups = {}
    for r in rows:
        g = r.get("parallel_group")
        if g:
            groups.setdefault(g, []).append(float(r["p99_ms"]))

    parallel_total = sum(max(v) for v in groups.values())
    total = sum(serial) + parallel_total

    slo = float(input("请输入 SLO 的 P99 (ms): "))
    reserve_row = next((r for r in rows if "余量" in r["name"]), None)
    reserve = float(reserve_row["p99_ms"]) if reserve_row else 0.0

    print(f"\n串行合计      : {sum(serial):8.1f} ms")
    for g, v in groups.items():
        print(f"并行组 {g:<8}: {max(v):8.1f} ms(组内 {len(v)} 项取最大值)")
    print(f"余量          : {reserve:8.1f} ms")
    print(f"总计          : {total:8.1f} ms")
    print(f"SLO           : {slo:8.1f} ms")

    if total > slo:
        problems.append(f"❌ 预算合计超出 SLO {total - slo:.1f} ms —— 必须砍某一跳或放宽 SLO")
    if reserve <= 0:
        problems.append("❌ 余量必须为正数")
    elif reserve / slo < 0.10:
        problems.append(f"⚠️  余量只占 {reserve/slo*100:.1f}%,建议 10%~20%")
    if not any("排队" in r["name"] for r in rows):
        problems.append("⚠️  预算表缺少「应用排队」这一项(它没有代码,但必须计入预算)")

    # 超时链检查
    chain = [r for r in rows if r.get("timeout_self_ms")]
    prev = float("inf")
    for r in chain:
        self_to = float(r["timeout_self_ms"])
        up = float(r["timeout_upstream_ms"]) if r.get("timeout_upstream_ms") else float("inf")
        if up != float("inf") and self_to > up:
            problems.append(f"❌ {r['name']}:本层超时 {self_to}ms > 上游给的 {up}ms(会级联堆积)")
        prev = self_to

    print()
    if problems:
        print("\n".join(problems))
        sys.exit(1)
    print("✅ 预算自洽,超时链逐层递减")

if __name__ == "__main__":
    check(sys.argv[1])

示例 CSV:

name,p99_ms,parallel_group,timeout_upstream_ms,timeout_self_ms
客户端 → 网关,25,,,
网关处理,15,,300,235
应用排队,20,,,
应用逻辑,45,,,
PostgreSQL,40,,200,115
Redis,5,,200,30
下游 HTTP,25,,200,80
响应传输,5,,,
余量,20,,,

四、错误埋点检查清单

做任务 3 时,按这四类逐个排查现有代码:

### 检查 1:分母

搜索关键词:`timer.record`、`histogram`、`requests_total`、`increment`
问题:是否只在成功路径上记录?
修正:把记录放在 `finally` 里,或确保失败/超时也走同一条统计路径。

### 检查 2:测量范围

问题:计时是否漏掉了序列化 / 响应写出?
验证方法:对比 `http_server_requests`(框架自动埋的全量计时)
          与你自己埋的 `app_handler_duration`,
          如果框架的数字明显更大,说明你的计时范围偏小。

### 检查 3:测量点声明

问题:这个指标是客户端、网关还是应用内测的?
修正:在指标名或 description 里写清楚,例如
     `app.handler.duration` → "handler 内部耗时,不含网络与排队"。

### 检查 4:依赖级埋点是否存在

自检问题:你能回答「这个接口的 P99 里,数据库占多少毫秒」吗?
如果不能,说明缺少依赖级 Timer(见 03 节)。

五、完成 Lab 1 后的自检

# 1. 模板文件已填
ls -la docs/slo.md

# 2. 预算校验通过
python3 tools/check_budget.py docs/budget.csv

# 3. 实验档案已建
ls -la docs/experiments/E01-slo-baseline/

# 4. 索引已更新
grep -n "E01" docs/experiments/README.md

四项都通过,Lab 1 就完成了。

六、常见问题

Q:预算表里的「实测」列现在填不了怎么办? A:留空是对的。这一列要等第 4 章搭好观测回路后回来填,那时这张表就变成了你的定位工具。现在硬填数字反而是自欺。

Q:CSV 里的并行组怎么标? A:给并行调用的条目填同一个组名(例如都填 fanout),串行的留空。校验器会对同组取最大值。

Q:我算出来的合计严重超过 SLO,说明什么? A:两种情况之一:① 你的 SLO 定得太严(用户其实能接受更慢);② 当前架构确实达不到(需要砍功能或加资源)。这两种结论都比「不知道」有价值得多——这正是做预算的意义。