# 实战：三种“慢”只修真正瓶颈

LLMS 索引： [llms.txt](/llms.txt)

---

三个 case 都制造“请求没有及时返回”，但修复方向互斥：

| case | 决定性证据 | 被排除的直接解释 | 正确方向 |
|---|---|---|---|
| estimate/plan | generic error 900x，custom 1x，无 Lock/ClientWrite | 当前锁链、慢客户端 | 参数/统计/计划与访问路径 |
| lock wait | `active + Lock/transactionid`，blocker=1 | ClientWrite、正在推进的 plan | root blocker 与事务边界 |
| client slow consumer | `active + Client/ClientWrite`，blocker=0 | 数据库锁链 | 返回规模、客户端/网络/读取方式 |

实验不以单次 elapsed time 或节点名作为 golden。它保存机器可读 signal，再由一个不能读取答案文件的分类器只按证据关系判断。

## 8.7.1 估算错误、锁等待与客户端慢消费 {#item-8-7-1}

确认 service 指向可写、可演练的 L1：

```bash
export PGSERVICEFILE=/absolute/private/path/pg_service.conf
export PGSERVICE=pg36-admin
cd static/labs/ch08
export PG36_EVIDENCE_DIR="$PWD/evidence/ch08/all-$(date -u +%Y%m%dT%H%M%SZ)"

./task.sh all
```

若 ch07 fixture 已通过，`all` 直接复用；若缺失，则调用 ch07 marker guard 做受控重建。之后顺序为：

```text
ch04/ch05 preflight
  → estimate case → normalize signals → diagnose
  → lock case     → normalize signals → diagnose
  → client case   → normalize signals → diagnose
  → seeded mystery
  → deliberately wrong diagnosis → reveal must fail
  → answer-blind diagnosis → reveal must pass
  → final worker/model/fixture verify
  → semantic review
```

一次实测输出：

```text
status=ok
classifications=estimate-plan,lock-wait,client-slow-consumer
mystery=client-slow-consumer/matched=true/wrong-guess-rejected=true
same-seed-reproducible=true/answer-mode=0600
state-restored=true/remaining-workers=0/
relation-checksum=f8a7bfae59c6d16cd323abecfefe1014
```

### case A：estimate/plan

[`estimate-case.sh`](/labs/ch08/estimate-case.sh) 复用 ch07 tenant skew，用同一 SQL、同一 hot parameter 分别捕获：

```text
force_generic_plan:
  estimate=100
  actual=90000
  cardinality error=900x

force_custom_plan:
  estimate=90000
  actual=90000
  cardinality error=1x
```

本次机器上 generic 是 Index Scan、custom 是 Seq Scan，但分类器不以节点名判定；不同硬件、cost setting 或 PostgreSQL 版本可能选择不同 shape。稳定事实是 generic 用总体平均选择率严重低估 hot parameter，custom 能看见具体参数并改善估算。

这也不自动宣布永久修复应是 `force_custom_plan`。生产还要比较：

- hot/cold traffic weight；
- planning 与 execution 成本；
- prepared statement/driver/pool 行为；
- statistics、SQL/index 是否有更好解；
- 并发下延迟、blocks 与写副作用。

### case B：lock wait

[`lock-case.sh`](/labs/ch08/lock-case.sh) 调用第 5 章的确定性编排：

```text
blocker 在事务内更新 order 1002 并进入受控 hold
  → 普通 reader 仍看见旧 committed version
  → waiter 尝试更新同一行
  → observer 捕获 waiter → blocker 精确边
  → 只取消 exact blocker
  → blocker rollback
  → waiter 前进并 rollback
  → order fingerprint 不变
```

中性 signals：

```json
{
  "state": "active",
  "wait_event_type": "Lock",
  "wait_event": "transactionid",
  "blocking_pid_count": 1,
  "plan": null,
  "state_restored": true,
  "remaining_workers": 0
}
```

直接原因是锁等待，不是 waiter 的执行计划。长期修复应回到 blocker 所属事务：为何持锁、是否在事务内睡眠/调用外部服务、锁顺序是否一致、更新集合是否过宽。取消是止血且只允许精确命中实验身份。

### case C：client slow consumer

[`client-write-lab.sh`](/labs/ch08/client-write-lab.sh) 用 `generate_series` 生成大结果，由 [`slow-reader.py`](/labs/ch08/slow-reader.py) 每次只读少量字节并等待。它不读业务表，也不创建对象。

observer 必须同时看到：

```text
state=active
wait_event_type=Client
wait_event=ClientWrite
blocking_pid_count=0
```

随后用 PID + backend_start epoch + database + unique application name 精确执行 `pg_cancel_backend`，等待 pipeline 退出并确认 worker=0。

这个 case 特意证明 planner 的 elapsed/cost 边界：server 可能很快产生数据，却因客户端不读而长时间不返回。修复应查返回规模、分页/流式协议、driver fetch、客户端线程与网络；取消任意 PID或加索引都没有解释证据。

## 8.7.2 随机隐藏一种根因，先独立诊断再揭晓 {#item-8-7-2}

为避免“知道脚本名再写结论”，运行 seeded mystery：

```bash
export PGSERVICEFILE=/absolute/private/path/pg_service.conf
export PGSERVICE=pg36-admin
export PG36_EVIDENCE_DIR="$PWD/evidence/ch08/mystery-$(date -u +%Y%m%dT%H%M%SZ)"

# 可选：同一 seed 会选择同一 case；不设置则自动生成
export PG36_CASE_SEED='team-drill-2026-07-29'

./task.sh mystery
python3 -m json.tool "$PG36_EVIDENCE_DIR/public/signals.json"
```

`PG36_CASE_SEED` 经 SHA-256 后按三类取模。public 目录只给出 metadata、raw evidence 与中性 `signals.json`；ground truth 写到：

```text
$PG36_EVIDENCE_DIR/.sealed/answer.json
```

其 mode 为 `0600`。这防止正常步骤误读，不是同一 OS 用户之间的加密安全边界；知道源代码和 seed 的人可以作弊。盲测的纪律是先不读取 `.sealed`，独立填写[诊断记录模板](/labs/ch08/hypothesis-template.md)：

```text
观察到的 state/wait/blocker/plan 事实
最可能分类
两个被排除的替代解释
还缺什么证据
最小修复实验与回退
```

再运行答案盲诊断器：

```bash
# diagnose 只读 public/signals.json；离线运行，不需要数据库连接
env -u PGSERVICEFILE ./task.sh diagnose
python3 -m json.tool "$PG36_EVIDENCE_DIR/public/diagnosis.json"
```

`diagnose.py` 的三条规则是：

```text
active + Lock + blockers>0
  → lock-wait

active + Client/ClientWrite + blockers=0
  → client-slow-consumer

no Lock/ClientWrite + generic error>=100x + custom error<=2x
  → estimate-plan
```

它的输出固定声明：

```json
"answer_artifact_read": false
```

最后揭晓：

```bash
env -u PGSERVICEFILE ./task.sh reveal
python3 -m json.tool "$PG36_EVIDENCE_DIR/public/reveal.json"
```

若 diagnosis 与 sealed ground truth 不一致，`reveal` 写 `matched=false` 并返回非零，不能把错误猜测包装成通过。`task.sh all` 内置一个故意错误的负对照，稳定验收要求该负对照失败。

想提交人工分类时，可在 public 目录创建最小 JSON：

```json
{
  "diagnosis": "lock-wait"
}
```

然后指定：

```bash
export PG36_DIAGNOSIS_FILE="$PG36_EVIDENCE_DIR/public/my-diagnosis.json"
export PG36_REVEAL_FILE="$PG36_EVIDENCE_DIR/public/my-reveal.json"
./mystery.sh reveal
```

先写假设再 reveal 的顺序比“猜错后改答案”更接近真实事件复盘。

## 8.7.3 输出证据包、假设树、修复前后对照与复位结果 {#item-8-7-3}

`task.sh all` 的 evidence 结构按用途分层：

```text
manifest.txt                  versions + target facts + source hashes
preflight.txt                 ch04/ch05 state before
ch07-fixture.txt              reused or controlled-rebuild
estimate/
  generic-hot.json
  custom-hot.json
  signals.json
  diagnosis.json
lock/
  raw/activity.csv
  raw/locks.csv
  raw/summary.txt
  signals.json
  diagnosis.json
client/
  raw/activity.csv
  raw/summary.txt
  signals.json
  diagnosis.json
mystery/
  public/...
  .sealed/answer.json
verify.txt
review.json
```

其中 raw artifact 负责可复核，中性 signals 负责教学分类，diagnosis 负责解释和排除项，review 只断言稳定关系。不要删 raw 只保留 `status=ok`。

[`baseline-v0.3-proposal.json`](/labs/ch08/baseline-v0.3-proposal.json)
把本章证据追加到 `DEFAULT-EVID-009`：慢请求证据必须包含影响范围、
UTC 时间窗、wait/blocking edge、query/parameter identity、竞争假设和
复位结果；盲测诊断不得读答案，错误负对照必须失败。它绑定不可变
v0.1 checksum，并绑定 ch07 v0.2 proposal 的 canonical checksum；在 v0.2
尚未晋升前，它仍只是有依赖的 v0.3 candidate，不冒充已发布规约。

一份生产级证据包还应补齐：

- 用户 SLI 时间窗、样本数、吞吐、并发、错误；
- request/trace/application release identity；
- service/cluster/instance/database/user/application；
- PID + backend_start（若做实时会话动作）；
- query family、参数分桶与数据敏感处理；
- Prometheus expression、Grafana variables/step/datasource；
- 日志 session、SQLSTATE、duration 与采样率；
- before/after 计划、statistics、settings、版本；
- 假设排序、反证、审批动作与回退阈值；
- 修复前后 correctness/SLO/resource 副作用；
- artifact hash、访问权限与保留期限。

本章没有持久 ch08 对象，所以没有“删库式 reset”。复位是每个 case 的成功条件：

```text
estimate session ends
lock transactions rollback
client worker exactly cancelled
all lab workers disappear
ch07 fixture remains valid
ch04-v1 business checksum unchanged
```

手动复核：

```bash
export PG36_EVIDENCE_DIR="$PWD/evidence/ch08/verify-$(date -u +%Y%m%dT%H%M%SZ)"
./task.sh verify
cat "$PG36_EVIDENCE_DIR/verify.txt"
```

若 `all` 为本章自动重建了 ch07 fixture，它会保留供后续计划/索引实验使用。清理 ch07 属于另一个 R2 动作，只能按第 7 章的双 token reset 执行；本章不会替读者擅自删除。

完成实验后，读者应能在看到“请求卡住”时先问：

```text
它尚未进入数据库、正在执行、正在等谁，还是等客户端？
哪条原生证据能区分？
最小动作怎样让不同假设产生不同结果？
怎样证明动作没有留下新问题？
```

下一章只有在证据指向访问路径时才进入索引设计，避免把索引当所有慢请求的通用药方。

---

[上一节：从可观测面板回到原生证据](../06/) · [返回本章目录](../) · [下一章：巧夺天工：索引设计与效果验证](/index-design/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
