8.7 实战:三种“慢”只修真正瓶颈
三个 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 估算错误、锁等待与客户端慢消费
确认 service 指向可写、可演练的 L1:
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 做受控重建。之后顺序为:
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一次实测输出:
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=f8a7bfae59c6d16cd323abecfefe1014case A:estimate/plan
estimate-case.sh 复用 ch07 tenant skew,用同一 SQL、同一 hot parameter 分别捕获:
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 调用第 5 章的确定性编排:
blocker 在事务内更新 order 1002 并进入受控 hold
→ 普通 reader 仍看见旧 committed version
→ waiter 尝试更新同一行
→ observer 捕获 waiter → blocker 精确边
→ 只取消 exact blocker
→ blocker rollback
→ waiter 前进并 rollback
→ order fingerprint 不变中性 signals:
{
"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 用 generate_series 生成大结果,由 slow-reader.py 每次只读少量字节并等待。它不读业务表,也不创建对象。
observer 必须同时看到:
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 随机隐藏一种根因,先独立诊断再揭晓
为避免“知道脚本名再写结论”,运行 seeded mystery:
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 写到:
$PG36_EVIDENCE_DIR/.sealed/answer.json其 mode 为 0600。这防止正常步骤误读,不是同一 OS 用户之间的加密安全边界;知道源代码和 seed 的人可以作弊。盲测的纪律是先不读取 .sealed,独立填写诊断记录模板:
观察到的 state/wait/blocker/plan 事实
最可能分类
两个被排除的替代解释
还缺什么证据
最小修复实验与回退再运行答案盲诊断器:
# diagnose 只读 public/signals.json;离线运行,不需要数据库连接
env -u PGSERVICEFILE ./task.sh diagnose
python3 -m json.tool "$PG36_EVIDENCE_DIR/public/diagnosis.json"diagnose.py 的三条规则是:
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它的输出固定声明:
"answer_artifact_read": false最后揭晓:
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:
{
"diagnosis": "lock-wait"
}然后指定:
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 输出证据包、假设树、修复前后对照与复位结果
task.sh all 的 evidence 结构按用途分层:
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
把本章证据追加到 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 的成功条件:
estimate session ends
lock transactions rollback
client worker exactly cancelled
all lab workers disappear
ch07 fixture remains valid
ch04-v1 business checksum unchanged手动复核:
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 执行;本章不会替读者擅自删除。
完成实验后,读者应能在看到“请求卡住”时先问:
它尚未进入数据库、正在执行、正在等谁,还是等客户端?
哪条原生证据能区分?
最小动作怎样让不同假设产生不同结果?
怎样证明动作没有留下新问题?下一章只有在证据指向访问路径时才进入索引设计,避免把索引当所有慢请求的通用药方。
上一节:从可观测面板回到原生证据 · 返回本章目录 · 下一章:巧夺天工:索引设计与效果验证 · 查看全书目录 · 查看索引中心