# 设计受控实验

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

---

诊断实验的目标不是“让一次执行变快”，而是区分竞争解释。一次同时 `ANALYZE`、建索引、改 SQL、增大内存又重启实例的操作即使奏效，也不知道哪项有效、哪项多余、哪项埋下副作用。

## 8.5.1 每次只改变一个解释变量 {#item-8-5-1}

把假设写成实验合同：

```text
现象：
  hot tenant 的同一 query family 资源量异常。

假设：
  generic plan 使用全体 tenant 平均选择率，严重低估 hot tenant。

保持不变：
  PostgreSQL 版本、数据快照、SQL 语义、参数、连接、cache 条件、
  并发、session settings（除 plan_cache_mode）。

唯一改变：
  force_generic_plan → force_custom_plan。

预测：
  estimate/actual error 从 >=100x 降到 <=2x；
  无 Lock/Client wait；可能改变路径和 buffers。

判错：
  custom estimate 仍严重偏离，或主要时间其实属于 wait/client。

回退：
  session 结束即恢复 plan_cache_mode；不修改持久对象。
```

“每次只改变一个变量”不等于一次只能执行一个命令。重建同一 deterministic fixture、采集 before/after、运行 analyzer 可以是一项完整操作；关键是两组之间只有被检验机制不同。

实验级别应逐级提升：

1. **只读观察**：activity、wait、统计、日志、已有计划；
2. **session-local probe**：同一 session 改一个 setting，事务结束恢复；
3. **隔离 fixture/副本**：重放代表数据与并发；
4. **受控 canary**：少量真实流量、明确 SLO 与自动退出；
5. **生产变更**：审批、回退、监控和审计齐全。

不要跳级只是为了快。特别是：

- 不在生产对未知写 SQL 随意 `EXPLAIN ANALYZE`；
- 不为对比清空全局 query statistics；
- 不用 `pg_terminate_backend` 代替理解事务；
- 不把 planner debug setting 留在连接池 session；
- 不在没有磁盘/写放大评估时直接创建大索引；
- 不把生产 traffic 同时承担探索、验证和上线三个阶段。

若无法控制某个重要变量，应把它记录为混杂因素，而不是从报告中删掉。例如 A/B 两次恰好跨 checkpoint、流量 mix 不同，则结论降级为“支持但未确认”，需要新的对照。

## 8.5.2 冷热缓存、参数与数据规模控制 {#item-8-5-2}

数据库性能至少受四种实验条件影响：

### 缓存

“冷缓存”可能指：

```text
application cache
PgBouncer/prepared state
PostgreSQL shared buffers
kernel page cache
storage controller/device cache
```

`DISCARD ALL` 不会清空 shared buffers 或 OS page cache；重连也不等于冷缓存。生产执行 `echo 3 > /proc/sys/vm/drop_caches` 或重启实例会影响全机 workload，不是普通诊断动作。若真的需要 cold/warm 对照，应在隔离、可重建环境明确 cache 层并记录方法。

更常见也更安全的策略是：

- 先跑 warm-up，不计入测量；
- A/B 交替或随机顺序，减少随时间漂移；
- 重复多次，报告分布和原始值，不只选最快一次；
- 同时保存 buffers、I/O timing 与 OS 指标；
- 若无法制造 cold，明确结论只适用于 warm steady state。

### 参数

均匀随机参数会掩盖业务分布。先按机制分层：

```text
hot/cold tenant
existent/missing key
small/large time range
few/many result rows
common/rare status combination
first/subsequent page
```

每层使用脱敏、可重现的代表值，并按真实 traffic weight 汇总。一个只让 cold tenant 快 2 ms、却让占 90% 流量的 hot tenant 慢 100 ms 的索引/计划不是总体改善。

### 数据规模与分布

一万行测试库上的 Index Scan 不能证明十亿行生产行为。fixture 至少应保持：

- relation/partition 数量级；
- row count、row width、null fraction；
- distinct count、MCV、列间相关；
- index 与 heap physical correlation；
- dead tuple/bloat 与 statistics 状态；
- 参数访问倾斜。

无法复制完整规模时，目标是复制决策边界，而不是复制全部数据。例如第 7/8 章用 90000/10 的 tenant skew 让 generic/custom plan 面临清晰选择率差异。

### 并发

单 session 提速不保证系统吞吐改善。并发会改变：

- buffer/cache 命中与 I/O queue；
- CPU run queue；
- lock contention；
- 每 query 可用内存与 spill；
- connection pool queue；
- background vacuum/checkpoint 干扰。

至少分别回答：

```text
single-query latency 是否改善？
固定并发下 throughput/p95/p99 是否改善？
达到 SLO 的最大可持续吞吐是否改善？
错误、超时、WAL、CPU/I/O、内存副作用怎样？
```

不要用无限并发压到崩溃后，只比较“谁最后倒下”。容量实验应有 ramp、steady state、
abort threshold 与恢复验证，[第 26 章](/capacity-benchmarking/)会完整展开。

## 8.5.3 反证、回退与副作用观察 {#item-8-5-3}

一个强实验既设计正结果，也预先声明什么会证明自己错。示例：

| 假设 | 支持结果 | 反证/降级 |
|---|---|---|
| 锁是直接原因 | blocker 释放后 waiter 立即前进，其他变量不变 | 无 blocker edge；释放后仍慢 |
| generic estimate 是原因 | custom estimate/资源显著改善 | 两者 estimate 与资源相近 |
| ClientWrite 是原因 | 无 blocker，慢 reader 恢复后 query 完成 | server 内仍有主要 I/O/Lock wait |
| `work_mem` 太小 | 单 session 增大后 spill 消失且端到端改善 | spill 消失但延迟不变/内存压力恶化 |
| 缺索引 | 代表参数与并发下 blocks/延迟改善 | 只冷门参数改善，写放大/SLO 变差 |

“未能反证”不等于“证明”。尽量加入负对照：

- 同一 SQL 的 unaffected parameter；
- 同时间的 unaffected instance/route；
- 同 seed 重跑；
- 故意错误的诊断必须被 reveal 拒绝；
- 恢复原设置后现象按预测返回。

效果报告应同时包含：

```text
correctness/result equivalence
latency distribution + sample count
throughput/concurrency/errors
calls/rows/buffers/temp/WAL
CPU/I/O/memory/locks
plan/estimate/settings
change and rollback duration
known confounders
```

副作用经常决定一个“快方案”不可上线：

- 新索引增加写 latency、WAL、磁盘、vacuum 工作；
- 提高 `work_mem` 增加并发 OOM 风险；
- 缩短 timeout 降低资源占用，却提高错误/重试风暴；
- 增大 pool 提高 backend 并发，却让数据库饱和；
- 缓存结果改善读取，却引入失效与一致性问题；
- denormalization 减少 join，却增加写路径和校验复杂度。

回退不是文档最后一行“必要时回滚”。实验前就应验证：

```text
谁有权限回退
回退触发阈值
是否真的可逆
回退耗时和锁影响
回退后怎样证明状态恢复
外部副作用如何补偿
```

本章三个 case 都把复位写进执行路径：

- estimate 只改变 session-local plan mode；
- lock 的 blocker 与 waiter 最终 rollback；
- client 只生成派生结果并精确取消本实验 backend；
- final `verify.sql` 检查 worker=0、ch07 fixture 和 ch04-v1 checksum。

运行全量实验：

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

./task.sh all
cat "$PG36_EVIDENCE_DIR/review.json"
```

这里 `all` 是 L1 教学环境动作。它会在必要时受控重建 ch07 专属 fixture、制造短暂锁与 socket backpressure，并精确取消自己的 worker；不会成为生产诊断脚本。

下一节把同一方法映射到 Pigsty：用面板快速发现范围，用原生证据确认，而不依赖某个版本的按钮位置。

---

[上一节：建立而不是猜测假设](../04/) · [返回本章目录](../) · [下一节：从可观测面板回到原生证据](../06/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
