27.7 实战:只调一个已证实的瓶颈
本节从第 26 章继续,不新造一个“参数一改、TPS 翻倍”的玩具例子。
第 26 章已经证明:
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 证据提出参数假设
候选矩阵
| 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
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预测若成立:
auto repeatedly custom-plans
-> visible planning CPU
force generic
-> same plan shapes
-> lower CPU/service demand
-> stable TPS gain
-> no tail regression可证伪点:
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_planningpolicy/overhead;- shared cumulative state;
- prepared custom/generic lifecycle;
- queryid aggregation;
- workload 重叠;
- planning 减少不一定转成 end-to-end SLO。
所以本实验直接测 client outcome,并用 prepared counters/plan probe 解释机制。
作用域
PGOPTIONS="-c plan_cache_mode=auto" pgbench ...
PGOPTIONS="-c plan_cache_mode=force_generic_plan" pgbench ...不使用:
ALTER SYSTEM
pg_parameters
Patroni DCS
reload/restart
ALTER ROLE因为本轮只回答 hypothesis,不创建持久 desired state。
safety target
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 boundedexercise 会真实制造 CPU/I/O/WAL/replication load,不能在 production 运行。
一次性连接边界
临时 database 创建为:
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 不应:
DROP DATABASE ... WITH (FORCE)
terminate unrelated monitoring sessions先收紧 connect privilege,从源头避免 observer race。
database 与 role 都写:
pg36-ch27-disposable-tuning-fixture-v1:<run-id>清理精确比对 marker;只允许等待 autovacuum 自然退出,不强杀。
数据与 workload
M scale:
customers 80,000
products 16,000
historical orders 800,000mix:
| 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:
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
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 的必要条件:
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最后一条同时表达:
gain is material
gain is supported under measured repetition uncertainty通过也只代表 “candidate-worthy-for-larger-canary”,不是 production apply。
27.7.2 比较收益、副作用和故障恢复
运行
静态检查:
static/labs/ch27/task.sh lint完整实验:
export PG36_EVIDENCE_DIR=/absolute/private/new-empty/ch27-run
static/labs/ch27/task.sh allPG36_EVIDENCE_DIR 必须是不存在或为空的私密绝对目录。all:
capture L0 preflight
-> exercise L2
-> verify L0
-> review L0正式 run identity
run id efb7efec-beb2-4237-8535-6864de2a2e4e
preflight d06c94b2-efb0-4688-86bf-b4e6ca92b90f
measured runs 10
transactions 357,685
raw tx logs 20所有 run:
failures 0
late 0
skipped 0
deadlocks 0
temp bytes 0arm 聚合
| 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 的:
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 测:
low key
high keyplan shape 只保留:
Node Type
Relation/Index
Join Type/Strategy
child topology再计算 SHA-256。auto/candidate 四个 probe shape 相同。
这支持:
candidate did not alter these representative plan shapes不支持:
generic is safe for every tenant/key/query机制解释
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:
small tenant -> selective index plan
large tenant -> broad scan/hash plan
MCV key -> different selectivity
rare key -> different selectivityglobal/role 强制会禁止 planner 对具体 parameter 自适应。收益证据弱,潜在 plan blast radius 大,所以即便 p95 没退化也不应接受。
global settings 无变化
实验前后逐项比较:
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:
session ends
-> candidate gone
server restarts/fails over
-> no persistent candidate to carry
benchmark aborts
-> ephemeral connection closes这使 hypothesis test 的 rollback 简单,但不代表 persistent rollout 也简单。
如果实验通过并要配置 role:
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 证明:
database absent
role absent
marker matched
unrelated sessions terminated 0
DROP ... WITH FORCE used false
remote /tmp absent清理证据是实验的一部分。不能只因 fixture “名字看起来像测试库”就 drop。
27.7.3 输出参数 ADR、回退条件与拒绝修改项
ADR
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
rejected because:
all ch26 temp_bytes = 0
no spill mechanism
current 64MB already has concurrency risk如果未来 report spill,按 report role/query 单独实验。
shared_buffers increase
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
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
rejected because:
WAL flush not established as bottleneck
changes durability semantics它需要产品/RPO 决策,不是本章 performance shortcut。
change wal_compression
rejected because:
WAL/tx measured
WAL I/O not established limiting
CPU already pressured
replay cost untestedunknown backlog
- planning 与 execution CPU 的直接分解;
- c2/c4/c12/c16… 的 precise knee;
- open-loop offered-load SLO;
- realistic application/PgBouncer path;
- parameter-sensitive tenant skew;
- long-duration background overlap;
- production hardware;
- N+1/failover envelope。
这些 unknown 不阻止本次“拒绝”,因为 candidate 必须证明自己;没有足够收益证据时 baseline 胜出。
28 个反例
validator 必须拒绝:
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 fabricatedpositive evidence 通过而 negative evidence 也通过的 validator 没有保护力。
evidence bundle
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:
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:
独立练习
练习一:把结论做错。
只用两个 arm 的 median TPS,写出“提升 2.35%,接受”。再用 paired ratio 重算,解释 为什么结论翻转。
练习二:设计 skew probe。
构造一个 tenant-size 极不均匀的 prepared query,比较:
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,不执行。包括:
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。
当最终结果是“不改”,而团队能清楚说明为什么,这套调优流程才真正成熟。
上一节:模板参数与集群变更 · 返回本章目录 · 下一章:除旧布新:VACUUM、冻结与膨胀治理 · 查看全书目录 · 查看索引中心