# 实战：只调一个已证实的瓶颈

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

---

本节从第 26 章继续，不新造一个“参数一改、TPS 翻倍”的玩具例子。

第 26 章已经证明：

```text
c8 server work ratio   about 84%
c8 client work ratio   below 30%
throughput vs c1       about 1.9x
p95 vs c1              about 4.4x
temp bytes             0
failures/late          0
exact knee             unknown
production TPS         null
```

可确认的是：

- c8 进入较高 server CPU/queue 区域；
- load generator 没有先饱和；
- 并发提高的 tail 代价很大。

不可确认的是：

- CPU 花在 planning 还是 execution；
- I/O、WAL 或 lock 是否已经限制 throughput；
- 哪个 GUC 能改善；
- c8 是否是 knee。

严格说，第 26 章只证实了 **service-center pressure**，没有证实
**parameter-specific root cause**。所以本节的正确目标不是“必须调成”，而是只允许
一个可逆参数假设进入 A/B，并接受拒绝。

## 27.7.1 从 ch26 证据提出参数假设 {#item-27-7-1}

### 候选矩阵

| candidate | ch26 支持 | 缺口 | 决定 |
|---|---|---|---|
| raise `work_mem` | 无 | 六个 cell temp=0 | 不测试 |
| raise `shared_buffers` | L 有 block read | iowait 低、OS/device 未归因 | 不测试 |
| raise `max_connections` | 无 | 只测 8 clients，knee 未知 | 拒绝 |
| `synchronous_commit=off` | 写产生 WAL | WAL flush 未证实 | 拒绝 durability change |
| change `wal_compression` | 有 WAL/tx | WAL I/O 未证实、CPU 已高 | 不测试 |
| force generic plan | prepared + CPU pressure | planning time未测，auto 可能已 generic | **允许证伪** |

这张表的关键不是挑中了 `plan_cache_mode`，而是拒绝了五项不能归因的变化。

### hypothesis

```yaml
question: >
  Does forcing generic plans materially improve the prepared OLTP mix
  without changing representative plan shapes or tail latency?
parameter: plan_cache_mode
baseline: auto
candidate: force_generic_plan
mechanism: avoid repeated custom planning
scope: benchmark session only
apply: PGOPTIONS
rollback: session end
```

预测若成立：

```text
auto repeatedly custom-plans
  -> visible planning CPU
force generic
  -> same plan shapes
  -> lower CPU/service demand
  -> stable TPS gain
  -> no tail regression
```

可证伪点：

```text
auto already changes to generic
planning is too small
generic shape differs by key
TPS gain is noise
tail latency regresses
```

### 为什么不用 `pg_stat_statements` total planning time 直接判

它可以作为证据，但：

- `track_planning` policy/overhead；
- shared cumulative state；
- prepared custom/generic lifecycle；
- queryid aggregation；
- workload 重叠；
- planning 减少不一定转成 end-to-end SLO。

所以本实验直接测 client outcome，并用 prepared counters/plan probe 解释机制。

### 作用域

```bash
PGOPTIONS="-c plan_cache_mode=auto" pgbench ...
PGOPTIONS="-c plan_cache_mode=force_generic_plan" pgbench ...
```

不使用：

```text
ALTER SYSTEM
pg_parameters
Patroni DCS
reload/restart
ALTER ROLE
```

因为本轮只回答 hypothesis，不创建持久 desired state。

### safety target

```text
environment  pg36-l2-vagrant teaching sandbox
client       pg-meta-1
server       pg-test-1 primary
database     pg36_tuning
role         dbuser_pg36tune
data         synthetic only
risk         L2 bounded
```

`exercise` 会真实制造 CPU/I/O/WAL/replication load，不能在 production 运行。

### 一次性连接边界

临时 database 创建为：

```text
ALLOW_CONNECTIONS false
  -> REVOKE CONNECT FROM PUBLIC
      -> GRANT CONNECT only to disposable benchmark role
          -> ALLOW_CONNECTIONS true
```

为什么多这一步？

Pigsty exporter 可自动发现 database 并建立观测连接。若先开放 PUBLIC，再跑数分钟，
exporter 的 idle connection 会让普通 `DROP DATABASE` 安全拒绝。runner 不应：

```text
DROP DATABASE ... WITH (FORCE)
terminate unrelated monitoring sessions
```

先收紧 connect privilege，从源头避免 observer race。

database 与 role 都写：

```text
pg36-ch27-disposable-tuning-fixture-v1:<run-id>
```

清理精确比对 marker；只允许等待 autovacuum 自然退出，不强杀。

### 数据与 workload

M scale：

```text
customers          80,000
products           16,000
historical orders  800,000
```

mix：

| operation | weight | distribution |
|---|---:|---|
| product read | 50% | product Zipf 1.15 |
| order read | 30% | customer Zipf 1.08 |
| place order | 20% | customer uniform、product Zipf 1.10 |

每个 arm：

```text
8 clients / 2 jobs
prepared protocol
closed-loop / zero think time
5-second natural warm-up
12-second measured run
250ms latency limit
max-tries=1
```

这组短 run 仍是教学证据，不是 production capacity characterization。

### 配对和 counterbalance

```text
r1 auto  -> force, same seed
r2 force -> auto,  same seed
r3 auto  -> force
r4 force -> auto
r5 auto  -> force
```

每次 measured run 前：

- truncate live orders；
- reset inventory data；
- 不 reset shared statistics；
- 不 drop cache；
- 不 force checkpoint；
- 不 restart；
- 不暂停 autovacuum/replication/archive。

每对相同 seed 降低 workload sampling noise；顺序交错降低时间趋势偏差。

### predeclared rule

candidate 进入更大 canary 的必要条件：

```text
failure/late/skipped = 0
representative plan shapes equal
global settings unchanged
candidate pooled p95 / auto pooled p95 <= 1.05
paired TPS ratio bootstrap 95% lower >= 1.02
```

最后一条同时表达：

```text
gain is material
gain is supported under measured repetition uncertainty
```

通过也只代表 “candidate-worthy-for-larger-canary”，不是 production apply。

## 27.7.2 比较收益、副作用和故障恢复 {#item-27-7-2}

### 运行

静态检查：

```bash
static/labs/ch27/task.sh lint
```

完整实验：

```bash
export PG36_EVIDENCE_DIR=/absolute/private/new-empty/ch27-run
static/labs/ch27/task.sh all
```

`PG36_EVIDENCE_DIR` 必须是不存在或为空的私密绝对目录。`all`：

```text
capture L0 preflight
  -> exercise L2
      -> verify L0
          -> review L0
```

### 正式 run identity

```text
run id         efb7efec-beb2-4237-8535-6864de2a2e4e
preflight      d06c94b2-efb0-4688-86bf-b4e6ca92b90f
measured runs  10
transactions   357,685
raw tx logs    20
```

所有 run：

```text
failures    0
late        0
skipped     0
deadlocks   0
temp bytes  0
```

### arm 聚合

| arm | runs | tx | median TPS | pooled p50 | pooled p95 | pooled p99 | max |
|---|---:|---:|---:|---:|---:|---:|---:|
| `auto` | 5 | 178,823 | 2,981.72 | 1.279 | 9.217 | 13.127 | 73.540 |
| `force_generic_plan` | 5 | 178,862 | 3,051.72 | 1.285 | 9.135 | 12.933 | 77.943 |

latency 单位 ms。p50/p95/p99 从每个 arm 的 raw transaction sample 合并后重算。

### 为什么不能用 median TPS 除法

直接：

$$
\frac{3051.72}{2981.72}
\approx
1.0235
$$

看起来正好超过 2%。但运行是配对设计，正确 effect：

$$
r_i
=
\frac{TPS_{\text{force},i}}
{TPS_{\text{auto},i}}
$$

五个 ratio 的：

```text
median               1.0073527
bootstrap 95%        [0.9808162, 1.0277460]
required lower       1.02
```

区间既包含负收益，又没有达到预设下界。candidate pooled p95 ratio：

$$
\frac{9.135}{9.217}
\approx
0.9911
$$

tail gate 通过，但 material-gain gate 失败。

### plan probe

对 product/order 两个 read prepared statement，各执行 10 次：

| mode | query | custom | generic |
|---|---|---:|---:|
| auto | product | 5 | 5 |
| auto | order | 5 | 5 |
| force generic | product | 0 | 10 |
| force generic | order | 0 | 10 |

`auto` 不是“每次都 custom”；它在前五次收集 custom cost 后，已经为这两条稳定 point/
range lookup 选 generic。

对每个 query 测：

```text
low key
high key
```

plan shape 只保留：

```text
Node Type
Relation/Index
Join Type/Strategy
child topology
```

再计算 SHA-256。auto/candidate 四个 probe shape 相同。

这支持：

```text
candidate did not alter these representative plan shapes
```

不支持：

```text
generic is safe for every tenant/key/query
```

### 机制解释

```text
auto:
  pays five early custom plans per prepared statement/session
  then reuses generic

force:
  avoids those early custom plans
  but sustained run mostly compares generic vs generic
```

在 12 秒、高 transaction count 的 run 中，早期差异被摊薄，因此很难产生稳定 2%
收益。这与实验结果一致。

### 副作用

强制 generic 的风险不是本轮四个 shape，而是 parameter-sensitive workload：

```text
small tenant  -> selective index plan
large tenant  -> broad scan/hash plan
MCV key       -> different selectivity
rare key      -> different selectivity
```

global/role 强制会禁止 planner 对具体 parameter 自适应。收益证据弱，潜在 plan blast
radius 大，所以即便 p95 没退化也不应接受。

### global settings 无变化

实验前后逐项比较：

```text
setting
unit
context
source
sourcefile (private only)
pending_restart
file error set
```

结果完全一致。参考 baseline：

| parameter | value | context | source |
|---|---|---|---|
| `plan_cache_mode` | auto | user | default |
| `work_mem` | 65536 kB | user | config file |
| `shared_buffers` | 62592 × 8kB | postmaster | config file |
| `max_connections` | 500 | postmaster | command line |
| `synchronous_commit` | on | user | default |
| `wal_compression` | lz4 | superuser | config file |

raw sourcefile path 只在私密 evidence，不进入公共 JSON。

### 故障与恢复边界

因为 candidate 只在 session：

```text
session ends
  -> candidate gone

server restarts/fails over
  -> no persistent candidate to carry

benchmark aborts
  -> ephemeral connection closes
```

这使 hypothesis test 的 rollback 简单，但不代表 persistent rollout 也简单。

如果实验通过并要配置 role：

```sql
ALTER ROLE ... SET plan_cache_mode = force_generic_plan;
```

还需：

- 新 session/pool recycle；
- workload-wide parameter skew probes；
- failover 后 catalog replication；
- canary identity；
- revert；
- observation window。

本轮没有进入这一步。

### cleanup

正式 run 证明：

```text
database absent
role absent
marker matched
unrelated sessions terminated 0
DROP ... WITH FORCE used       false
remote /tmp absent
```

清理证据是实验的一部分。不能只因 fixture “名字看起来像测试库”就 drop。

## 27.7.3 输出参数 ADR、回退条件与拒绝修改项 {#item-27-7-3}

### ADR

```yaml
id: ch27-plan-cache-mode-falsification
status: rejected
date: 2026-07-30

context:
  upstream_run: 6c44ebdb-2206-48c3-8089-d90fdff45204
  observation:
    c8_server_work: about 84%
    prepared_protocol: true
    exact_knee_known: false
    production_tps: null

hypothesis:
  parameter: plan_cache_mode
  baseline: auto
  candidate: force_generic_plan
  mechanism: reduce custom planning CPU

experiment:
  scope: benchmark session
  paired_repetitions: 5
  transactions: 357685
  plan_probe: low/high keys

result:
  paired_tps_ratio_median: 1.00735
  bootstrap_95: [0.98082, 1.02775]
  candidate_p95_ratio: 0.99110
  plan_shapes_equal: true

decision:
  persistent_change: rejected
  reason: material-gain lower bound 1.02 did not pass
  production_gate: pending

rollback:
  needed: false
  reason: candidate was session-local and no persistent change was applied
```

### 拒绝项

**`work_mem` increase**

```text
rejected because:
  all ch26 temp_bytes = 0
  no spill mechanism
  current 64MB already has concurrency risk
```

如果未来 report spill，按 report role/query 单独实验。

**`shared_buffers` increase**

```text
rejected because:
  L block reads observed
  but iowait low
  OS cache/device attribution missing
  restart + memory budget required
```

下一证据是 longer/XL working set 与 `pg_stat_io`/device latency，不是立即 restart。

**`max_connections` increase**

```text
rejected because:
  eight clients only
  exact knee unknown
  slots do not add CPU
  500 already exceeds credible active memory envelope
```

下一动作是 pool/admission 与 c2–c32 sweep。

**`synchronous_commit=off`**

```text
rejected because:
  WAL flush not established as bottleneck
  changes durability semantics
```

它需要产品/RPO 决策，不是本章 performance shortcut。

**change `wal_compression`**

```text
rejected because:
  WAL/tx measured
  WAL I/O not established limiting
  CPU already pressured
  replay cost untested
```

### unknown backlog

1. planning 与 execution CPU 的直接分解；
2. c2/c4/c12/c16... 的 precise knee；
3. open-loop offered-load SLO；
4. realistic application/PgBouncer path；
5. parameter-sensitive tenant skew；
6. long-duration background overlap；
7. production hardware；
8. N+1/failover envelope。

这些 unknown 不阻止本次“拒绝”，因为 candidate 必须证明自己；没有足够收益证据时
baseline 胜出。

### 28 个反例

validator 必须拒绝：

```text
wrong/recovery target
pre-existing fixture
upstream run/claim changed
two parameters
ALTER SYSTEM / Patroni edit / restart
durability change
statistics reset / cache drop
unpaired seed / fixed order
missing raw log / averaged p95
hidden failure / plan change / global drift
weak benefit or tail regression accepted
marker mismatch / force drop
secret/query leaked
production approval fabricated
```

positive evidence 通过而 negative evidence 也通过的 validator 没有保护力。

### evidence bundle

```text
preflight-evidence.json
remote-experiment.log
remote/
  tuning-evidence.json
  runs/<run-id>/
    pgbench.stdout
    pgbench.stderr
    transactions.*
    stats-before.json
    stats-after.json
remote-cleanup.json
validation-report.json
negative-report.json
public-summary.json
review.txt
```

正式 review：

```text
source hashes                 matched
measured runs                 10
raw files verified            60
raw transaction logs          20
private bytes scanned         11,307,205
counterexamples               28 rejected
fixture/remote cleanup        verified
public raw/secret/query        absent
```

公共 allowlist：

[`tuning-run.json`](/labs/ch27/tuning-run.json)

### 独立练习

**练习一：把结论做错。**

只用两个 arm 的 median TPS，写出“提升 2.35%，接受”。再用 paired ratio 重算，解释
为什么结论翻转。

**练习二：设计 skew probe。**

构造一个 tenant-size 极不均匀的 prepared query，比较：

```text
auto
force_custom_plan
force_generic_plan
```

先写 correctness/plan/tail/memory gate，不要先预设 generic 更好。

**练习三：为 `work_mem` 写拒绝 ADR。**

只允许使用 ch26 evidence。说明为什么 `temp_bytes=0` 足以拒绝 increase，却不足以批准
global decrease。

**练习四：把 session candidate 变成 role canary。**

只写 runbook，不执行。包括：

```text
new canary role
pool route
new session proof
plan skew catalog
rollback
failover
observation window
```

### 完成检查表

- [ ] objective 与 non-regression 在实验前声明。
- [ ] observed service center 与 parameter mechanism 有证据连接。
- [ ] 一次只测试一个机制。
- [ ] 使用最小可逆 scope。
- [ ] A/B 配对、seed 和顺序可复现。
- [ ] p95 从 raw sample 重算。
- [ ] effect 报区间，不只报两个 median。
- [ ] correctness、plan、failure 与 resource 同时验证。
- [ ] desired/configured/effective 没有漂移。
- [ ] cleanup 不使用 force 或误杀。
- [ ] 未测试参数明确拒绝。
- [ ] candidate 不通过时没有“为了有成果”落盘。
- [ ] production gate 在 production evidence 不足时保持 pending。

当最终结果是“不改”，而团队能清楚说明为什么，这套调优流程才真正成熟。

---

[上一节：模板参数与集群变更](../06/) · [返回本章目录](../) · [下一章：除旧布新：VACUUM、冻结与膨胀治理](/vacuum-freeze-bloat/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
