跳至内容
27.7 实战:只调一个已证实的瓶颈

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 证据提出参数假设

候选矩阵

candidatech26 支持缺口决定
raise work_mem六个 cell temp=0不测试
raise shared_buffersL 有 block readiowait 低、OS/device 未归因不测试
raise max_connections只测 8 clients,knee 未知拒绝
synchronous_commit=off写产生 WALWAL flush 未证实拒绝 durability change
change wal_compression有 WAL/txWAL I/O 未证实、CPU 已高不测试
force generic planprepared + CPU pressureplanning 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_planning policy/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 bounded

exercise 会真实制造 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,000

mix:

operationweightdistribution
product read50%product Zipf 1.15
order read30%customer Zipf 1.08
place order20%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 all

PG36_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  0

arm 聚合

armrunstxmedian TPSpooled p50pooled p95pooled p99max
auto5178,8232,981.721.2799.21713.12773.540
force_generic_plan5178,8623,051.721.2859.13512.93377.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 次:

modequerycustomgeneric
autoproduct55
autoorder55
force genericproduct010
force genericorder010

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

对每个 query 测:

low key
high key

plan 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 selectivity

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

global settings 无变化

实验前后逐项比较:

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

结果完全一致。参考 baseline:

parametervaluecontextsource
plan_cache_modeautouserdefault
work_mem65536 kBuserconfig file
shared_buffers62592 × 8kBpostmasterconfig file
max_connections500postmastercommand line
synchronous_commitonuserdefault
wal_compressionlz4superuserconfig 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 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 必须拒绝:

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

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:

tuning-run.json

独立练习

练习一:把结论做错。

只用两个 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、冻结与膨胀治理 · 查看全书目录 · 查看索引中心

最后更新于