跳转到主要内容

26 胸有成竹:容量规划与压测基线

一张写着“12 万 TPS”的截图,没有回答容量问题。

它可能来自:

select-only
  + 全部命中缓存
  + fsync 关闭
  + 与 server 同机的 load generator
  + 一次 10 秒运行
  + 没有约束、索引维护、WAL、复制、备份和故障余量

也可能来自一个完全严谨的实验。数字本身无法告诉你是哪一种。

容量规划真正需要的是一条可审计推理链:

业务到达过程
  -> operation mix 与数据形状
      -> 延迟、错误和正确性目标
          -> 可复现实验
              -> throughput / latency / failure distribution
                  -> CPU / memory / I/O / WAL / lock evidence
                      -> 单位业务资源需求
                          -> 增长、保留、维护和故障模型
                              -> 扩容触发线与提前期

本章把 PostgreSQL 的原生证据与 Pigsty 的观察面放到同一条链上。目标不是教你 “跑一个 pgbench 命令”,而是让你能判断:

  • workload 是否代表业务;
  • 实验是否控制了足够多的变量;
  • load generator、连接路径或缓存是否在替 server 背锅;
  • throughput 上升是否以 tail latency、错误或排队为代价;
  • 一次测量能支持什么结论,不能支持什么结论;
  • 怎样把测量变成 CPU、WAL、存储和提前期模型;
  • 为什么生产容量仍需要故障、维护、备份和 open-loop 场景。

本章实验先给出一个反直觉结论

第 26 章参考运行不是“性能榜单”,而是一组教学用、有界的证据链:

  • Pigsty v4.5.0 教学沙箱;
  • PostgreSQL 18.6
  • 独立 pg-meta-1 load generator:2 vCPU、约 3.8 GiB RAM;
  • pg-test-1 primary:1 vCPU、约 1.9 GiB RAM;
  • shared_buffers:512,753,664 bytes;
  • 50% product read、30% order read、20% place-order;
  • S/M/L 三档数据,1/8 两档 client;
  • 每个 cell 五次重复,共 30 次 measured run;
  • 511,709 笔事务,失败、skipped、deadlock 和超过 250 ms 的事务均为零;
  • raw transaction、OS、SQL snapshot 与 wait evidence 全部留在私密 evidence;
  • 专用 database、role 和远端临时目录全部清理。

聚合结果:

cell schema size clients median TPS pooled p95 server work client work
S-c1 28.3 MiB 1 1,508.5 2.152 ms 48.9% 17.7%
S-c8 28.3 MiB 8 2,920.5 9.448 ms 84.2% 29.5%
M-c1 224.3 MiB 1 1,555.8 2.123 ms 50.1% 17.8%
M-c8 224.3 MiB 8 2,911.5 9.398 ms 84.0% 29.1%
L-c1 898.6 MiB 1 1,423.6 2.259 ms 46.3% 16.2%
L-c8 898.6 MiB 8 2,774.3 10.087 ms 84.2% 27.8%

从 c1 到 c8:

throughput gain       1.87x ~ 1.95x
pooled p95 multiplier 4.39x ~ 4.47x

这说明八个 client 获得了更多吞吐,却付出了约四倍多的 p95。它没有说明:

  • knee 就在八个 client;
  • 2,774 TPS 是 L 档生产容量;
  • 65% CPU 对应的线性投影可以直接采购;
  • 0 deadlock 代表业务没有锁风险;
  • Pigsty 全窗口 median 就是某个 cell 的资源消耗;
  • 虚拟磁盘代表生产存储。

两点只能 bracket,不能定位 knee;closed-loop 只能观察固定 client population, 不能重现上游 offered load;8 秒运行也远短于生产基线所需的时间。PostgreSQL 官方 pgbench good practices 明确警告,不应相信只跑几秒的测试,可靠数字可能需要几分钟、几次乃至数小时。 因此本章参考 run 用来证明实验管线与推理方法,不批准生产数字。

公共 allowlist 结果见 capacity-run.json,完整边界见 lab-contract.md

本章学习成果

完成本章后,你应该能独立完成:

  1. 把业务 forecast 转成 operation-class arrival model,而不是只写“读写比 8:2”;
  2. 区分 request、SQL statement、database transaction、connection 与并发;
  3. 用 Little’s Law 检查到达率、响应时间和在途量是否自洽;
  4. 选择内置 pgbench 作为 engine calibration,或写业务自定义脚本;
  5. 声明 key distribution、mix、think time、arrival model、protocol 和连接路径;
  6. 固定版本、配置、数据、随机 seed、预热、顺序和重复;
  7. 正确处理 pooled percentile、run-level estimate、置信区间与异常样本;
  8. pg_stat_databasepg_stat_iopg_stat_wal、wait event 和 OS 证据解释曲线;
  9. 判断 load generator 是否成为瓶颈;
  10. 把 CPU seconds/transaction、WAL bytes/transaction 和 bytes/order 写进容量模型;
  11. 计算增长、保留、maintenance workspace、failure headroom 与 lead time;
  12. 明确保留 unknown,并拒绝把 sandbox 结果升级为生产承诺。

本章目录

26.1 从需求建立容量模型

26.2 设计代表性工作负载

26.3 建立可信实验

26.4 找到饱和点与瓶颈

26.5 从测量推导容量与成本

26.6 实战:pg36_shop 容量基线

阅读与实践路线

如果你负责应用:

26.1 -> 26.2 -> 26.3 -> 26.6

重点是 operation contract、arrival model、idempotency/retry、connection path 和 load generator。

如果你负责平台:

26.1 -> 26.3 -> 26.4 -> 26.5 -> 26.6

重点是实验控制、native counters、Pigsty time series、headroom、failure model 和 provisioning lead time。

两条路线最终必须合流。平台无法从数据库指标猜出业务 mix;应用也不能从一张 TPS 表判断 WAL、backup、replica 和 maintenance 是否还有余量。

实验文件

static/labs/ch26/
├── requirements.json
├── workload-contract.json
├── experiment-matrix.json
├── capacity-model.json
├── negative-cases.json
├── topology.mmd
├── lab-contract.md
├── setup.sql
├── reset-cell.sql
├── read-product.sql
├── read-order.sql
├── place-order.sql
├── stat-snapshot.sql
├── wait-sampler.sql
├── system_sampler.py
├── capture.py
├── exercise.py
├── remote_benchmark.py
├── validate.py
├── review.py
├── task.sh
└── capacity-run.json

它们分别固定:

文件 责任
requirements target、风险、支持/不支持的 claim、gate
workload contract mix、分布、arrival、protocol、seed、cache policy
matrix 三规模、两并发、五重复与 counterbalanced order
model 业务输入、单位需求、空间与 lead-time 方程
SQL / pgbench scripts synthetic schema、reset 与三类事务
samplers OS measured window、PostgreSQL wait 与 counter snapshot
capture L0 目标、上游与 clean-start gate
exercise 远端隔离、完整矩阵与精确清理
validate / review 正向合同、26 个反例、hash、mode、secret 与 claim
public run 聚合 allowlist;不含 raw evidence

参考资料


上一章:望闻问切:监控体系与可观测诊断 · 返回下卷导读 · 下一章:精益求精:参数调优与资源治理 · 查看全书目录 · 查看索引中心

26.1 从需求建立容量模型

容量规划的第一份输入不是 CPU 核数,而是业务在什么时间、以什么方式要求系统 完成什么工作。

一句“峰值 5,000 QPS,读写比 8:2”至少缺少:

  • QPS 是 HTTP request、SQL statement 还是 transaction;
  • 读是 point lookup、范围扫描、聚合还是 cache miss 后回源;
  • 写是单行更新、订单事务、批量导入还是索引构建;
  • 一次 request 打开几个 transaction、执行几条 SQL;
  • key 是均匀分布还是集中在少数 tenant/product;
  • peak 持续 10 秒、10 分钟还是 10 小时;
  • 失败后谁重试、最多几次、是否产生重复写;
  • batch、backup、vacuum、DDL 和 failover 是否同时发生;
  • 延迟、正确性、恢复和成本目标是什么。

所以容量模型从一份 workload contract 开始:

operation
  arrival process
  transaction boundary
  SQL and data shape
  consistency and durability
  retry and timeout
  latency/error/correctness objective
  growth and retention
  overlap and failure state

第 24 章把服务承诺写成 SLO;第 25 章把承诺落实为观察合同;本节把同一套语义 变成容量输入。三章必须使用同一个 operation name 和 eligible event 定义。

26.1.1 事务类型、读写比、并发和数据增长

先分清五个容易混用的量

符号 例子 不能替代
到达率 $\lambda$ 2,000 eligible order request/s database TPS
在途量 $L$ 300 个尚未完成的 request connection count
响应时间 $W$ p95 120 ms SQL execution time
database transaction rate $X$ 2,400 commit/s request rate
statement rate $Q$ 18,000 statement/s transaction rate

一次 place-order 可能:

HTTP request
  -> auth/cache reads
  -> BEGIN
      SELECT product
      UPDATE inventory
      INSERT order
      INSERT order_item
     COMMIT
  -> publish outbox later

于是:

1 request
!= 1 SQL
may equal 1 or more database transactions
may cause background transactions after response

如果把 5,000 HTTP RPS 直接填成 pgbench --rate=5000,脚本却只执行一条 SELECT 1,得到的不是业务容量,只是另一种 workload 的数字。

用 operation class,而不是“读/写”两个桶

一个最小 operation catalog:

operation request/s DB tx/request statements/tx data shape consistency
read-product 4,000 1.0 1–2 Zipf point read current primary
read-order 1,500 1.0 1 customer recent-N 5 s freshness
place-order 800 1.0 4–8 hot inventory + append durable commit
reconcile 20 1.0 range + aggregate time window correctness control
expire-order 100 1.0 scan + update batch eventual

同为“读”:

indexed point lookup
index-only recent-N
wide range scan
hash aggregate spilling to temp
JSON path evaluation
vector nearest-neighbor

CPU、buffer、I/O、work_mem、parallel worker 和 lock footprint 完全不同。

同为“写”:

HOT-eligible update
indexed-column update
append-only insert
upsert on a hot unique key
multi-table transaction with foreign keys
bulk COPY

WAL、index maintenance、dead tuple、checkpoint、replication 和 vacuum 成本也不 一样。

给每种 operation 建资源需求向量

对 operation $i$,定义:

$$ \mathbf{D_i}

(D_{cpu}, D_{read}, D_{write}, D_{wal}, D_{lock}, D_{temp}, D_{net}) $$

混合 workload 的单位需求不是简单“平均 SQL”:

$$ \mathbf{D_{mix}}

\sum_i w_i \mathbf{D_i} $$

其中 $w_i$ 是 transaction mix 权重。若流量增长只发生在 place-order,不能继续使用旧的全局 $D_{mix}$:

$$ \text{resource rate}

\sum_i \lambda_i \mathbf{D_i} $$

因此容量报告至少同时保留:

  • operation-class rate;
  • mix;
  • 每类 service demand;
  • mixed demand;
  • 预测期内 mix 是否变化。

读写比为什么经常误导

“80% 读、20% 写”没有说明计数单位:

by request?
by transaction?
by statement?
by tuple?
by byte?
by CPU second?
by WAL byte?

参考实验按 pgbench transaction 选择脚本:

read-product 50
read-order   30
place-order  20

这是 transaction selection weight。它不保证:

  • 80% CPU 消耗来自读;
  • 20% statement 是写;
  • 20% WAL-producing operation;
  • 20% wall time 在写事务;
  • 每次运行精确出现 50/30/20。

raw transaction log 必须核对实际 script count。

用 Little’s Law 做第一轮自洽检查

稳定系统中,平均在途量满足:

L=λW L = \lambda W

若:

arrival rate   2,000 request/s
mean latency   80 ms = 0.08 s

则平均在途 request:

L=2000×0.08=160 L = 2000 \times 0.08 = 160

这不是说“必须给 PostgreSQL 160 个连接”。应用中的在途量可能分布在:

edge queue
application worker
pool wait
database execution
external call
response serialization

如果 transaction 只占 15 ms:

Ldb=2000×0.015=30 L_{db} = 2000 \times 0.015 = 30

一个 30 左右的 active DB concurrency 可能足够;160 个 backend 反而增加上下文 切换和内存风险。

Little’s Law 是平均量关系,不描述 tail,也不自动证明系统稳定。若 offered load 超过 service capacity,queue 持续增长,就不存在可长期使用的稳态平均。

并发不是连接数

把连接状态拆开:

状态 是否占 client connection 是否占 server backend 是否消耗执行资源
application queue 否/可能 应用资源
PgBouncer waiting client pool queue
server connection idle backend memory,少量管理成本
active on CPU CPU
active waiting lock/I/O queue + held resources
idle in transaction snapshot/lock/vacuum horizon 风险

容量模型关心 active concurrency、queue、transaction duration 和 backend footprint,而不是只看 max_connections

数据增长必须进入 workload

数据量改变:

  • B-tree 高度和 cache working set;
  • statistics 与 selectivity;
  • index/table correlation;
  • vacuum 和 analyze 时间;
  • checkpoint、base backup、restore、upgrade 时间;
  • retained WAL、replication catch-up 和 archive volume;
  • partition 数量、catalog 开销与 planning time;
  • maintenance workspace。

至少分开:

logical rows/day
logical bytes/day
table bytes/day
index bytes/day
TOAST bytes/day
WAL bytes/day
archive bytes/day
backup repository growth/day

参考实验三档 shopbench schema:

scale history rows schema bytes
S 100,000 29,704,192
M 800,000 235,175,936
L 3,200,000 942,268,416

L 已大于 512,753,664-byte shared_buffers,但小于 server RAM。它能观察 PostgreSQL buffer miss,却不能区分 page 是否真正来自物理磁盘还是 OS page cache。PostgreSQL 的 pg_stat_io 文档 也明确指出,数据库 I/O 统计无法区分内核调用最终命中 page cache 还是访问 storage,必须结合 OS 证据。

本目产物:业务容量输入表

forecast_window: 12 months
operations:
  place-order:
    eligible_peak_tps: 800
    peak_duration: 20m
    db_transactions_per_request: 1
    retry_amplification_p95: 1.04
    data_growth:
      orders_per_success: 1
      line_items_per_order_p95: 6
    objective:
      p95_ms: 250
      error_ratio: 0.001
      correctness: no unexplained duplicate

不要在这个阶段填“8 vCPU”。先把问题写完整。

26.1.2 平均值、峰值、突发与批处理叠加

日平均掩盖容量风险

一天 86,400 秒。日订单 8,640,000:

daily average=100/s \text{daily average} = 100/s

但若 40% 发生在两小时促销窗:

$$ \text{promotion average}

\frac{8{,}640{,}000 \times 0.4}{7200} = 480/s $$

再叠加一分钟抢购峰值、支付回调、重试和 batch,瞬时 offered load 可能超过 1,000/s。用 100/s 采购必然低估。

至少保留四种时间尺度

时间尺度 回答问题 常见误用
1–10 s 突发、queue、admission scrape 太慢完全看不见
1–5 min 用户影响、autoscaling 响应 被小时均值摊平
1 h 班次、batch、业务时段 当成 peak
day/week/season 容量增长、节日、结算 无法解释短时 saturation

一个 forecast 需要:

baseline
peak factor
burst factor and duration
seasonality
growth trend
event calendar
retry amplification
batch overlap
maintenance overlap

区分 arrival burst 与 backlog drain

两种看起来都像“TPS 突然上升”:

new user demand
  arrival rises

backlog drain
  arrival may have fallen
  workers consume queued work faster

后者常在依赖恢复后发生:

payment provider recovers
  -> retries released
  -> queue drain
  -> database write burst
  -> replicas/archive/backup lag

如果模型只用前台 request,遗漏 backlog,恢复本身就可能触发第二次事故。

重试是负载放大器

每个原始 operation 平均尝试次数:

$$ A

1 + r_1 + r_2 + \cdots $$

更实用地:

$$ \lambda_{\text{database}}

\lambda_{\text{eligible}} \times \text{attempts per eligible operation} $$

例如:

eligible arrival          1,000/s
5% requests retry once
1% requests retry twice

平均尝试:

1+0.05+2×0.01=1.07 1 + 0.05 + 2 \times 0.01 = 1.07

数据库看到约 1,070 attempt/s。若超时发生在 commit outcome unknown 区域,盲目 重试还可能制造 duplicate 与 reconciliation workload;不能只算 TPS,不算正确性。

batch 不能用“夜间”一笔带过

列出每个 batch:

job schedule duration read/write temp WAL lock retry
settlement 00:05 18 min heavy read/update possible high row yes
expire-order every 5 min 40 s scan/update low medium row yes
analytics extract 01:00 45 min range read spill risk low AccessShare restart
backup 02:00 60 min storage/network n/a archive coupling n/a resume

“平均业务低谷”不代表有余量。batch、autovacuum、checkpoint、archive、 replication catch-up 和 backup 可能恰好在低谷争用 I/O。

把 overlap 写成场景

N0 normal peak
  interactive peak + routine background

N1 campaign peak
  interactive 2.5x + retries 1.1x

M1 maintenance overlap
  normal peak + vacuum/index build/backup

F1 one replica unavailable
  normal peak + catch-up/rebuild or read traffic reroute

F2 primary failover
  reconnect storm + retry burst + cold-ish cache + reduced topology

容量批准必须说明要满足哪个场景。只在 N0 通过,不等于生产通过。

平均、quantile 与最大值各有用途

mean
  resource accounting, Little's Law, long-run throughput

p50
  typical transaction

p95/p99
  user tail and queue onset

max
  evidence lead, but sample-size sensitive

不能把每分钟 p99 再做平均并称为“全天 p99”。若需要跨 window 或 instance 聚合,保留原始 transaction sample 或可聚合 histogram。Prometheus 的 histogram 指南 解释了为什么直接平均预计算 quantile 在统计上没有意义。

26.1.3 延迟目标、错误预算与安全余量

容量是带约束的可行域

把 capacity 定义为:

maxλsubject to{P(WT)Serror ratioEcorrectness invariant holdsresource and recovery limits hold \max \lambda \quad \text{subject to} \quad \begin{cases} P(W \le T) \ge S \\ \text{error ratio} \le E \\ \text{correctness invariant holds} \\ \text{resource and recovery limits hold} \end{cases}

例如:

place-order p95 <= 250 ms
availability >= 99.9%
no unexplained duplicate order
replica/archive remain within declared recovery bounds
primary disk < 70%
one declared failure still has headroom

没有约束的“最大 TPS”只是最大努力点。

延迟目标要定义 measurement boundary

这些延迟不相等:

client schedule -> response
application admission -> response
pool checkout -> release
transaction BEGIN -> COMMIT response
one SQL execute
server execution excluding network

参考实验的 latency:

origin     pgbench client
boundary   one chosen script
protocol   prepared
connection persistent
path       pg-meta-1 -> pg-test-1:5432

它不包含 application edge、HAProxy、PgBouncer 和 WAN,所以不能与第 24 章 place-order 用户 SLO 直接画等号。

错误不能从分母消失

报告至少包含:

attempted
processed successful
failed
retried transactions
total retries
late
skipped
client aborted
server disconnect

若只用:

$$ \text{TPS}

\frac{\text{successful}}{\text{elapsed}} $$

系统可以通过拒绝慢请求让成功样本看起来更快。failed、skipped 和 admission rejection 必须与 latency 并列。

参考实验固定 --max-tries=1,不让 pgbench 把 serialization/deadlock retry 隐藏到成功事务里;250 ms 只计 late。正式运行:

failed       0
skipped      0
late         0
deadlock     0

这只适用于 511,709 个合成事务。零次观察不是“真实概率为零”的证明。

错误预算不是容量余量

SLO 允许 0.1% bad event,不代表正常运行可以把资源推到 99.9%:

resource headroom
  absorbs burst, forecast error, maintenance and failure

error budget
  governs reliability trade-offs and release policy

若平时已经在 knee 右侧,任何轻微 burst 都会使 queue 与 tail 非线性上升,错误 预算会被快速消耗。

拆分安全余量

不要只写“预留 30%”。说明它覆盖什么:

headroom 覆盖
statistical workload 与测量波动
forecast 增长和 mix 误差
burst 短时 arrival
maintenance vacuum、backup、DDL、reindex
failure 节点/副本/路径损失
operational 扩容、验证、回滚提前期

这些余量不能总是简单相加,也不能互相冒充。failure headroom 可能要求一整台 node,而不是 10% CPU。

target utilization 是政策,不是自然常数

参考模型使用 65% CPU 作教学投影点

λ650.65Dcpu \lambda_{65} \approx \frac{0.65}{D_{cpu}}

得到 S/M/L 约 2,237 / 2,223 / 2,117 TPS。但这是假设:

  • mix 不变;
  • CPU demand 近似线性;
  • cache、I/O、lock、client 和 background 不先成为瓶颈;
  • 仍是同一个 sandbox。

所以公共结果将字段命名为 tps_at_65_percent_cpu_if_linear,并把 production_sustainable_tps 保持为 null

生产 target utilization 应由:

failure model
burst duration
autoscaling/provisioning time
workload convexity
cost objective
operational experience

共同决定,而不是复制 65%。

本节验收:一页容量问题陈述

在开始压测前,评审以下问题:

  • operation catalog 有稳定名称与 owner;
  • request/transaction/statement 的换算明确;
  • arrival、peak、burst、seasonality 和 retry 已量化;
  • data size、growth、retention 与 access skew 已量化;
  • batch、maintenance 和 failure overlap 已列场景;
  • latency measurement boundary 与 percentile 明确;
  • error、late、skip、retry 和 correctness 都在验收条件;
  • headroom 分解,而不是一个来历不明的百分比;
  • production gate 的未知项仍然可见。

如果这页写不出来,更多 pgbench client 只会更快地产生无意义数字。


返回本章目录 · 下一节:设计代表性工作负载 · 查看全书目录 · 查看索引中心

26.2 设计代表性工作负载

benchmark 首先是一个模型,然后才是一条命令。

业务现实
  -> workload model
      -> executable scripts
          -> measurements
              -> claims

模型遗漏 hot key,脚本跑得再稳定也只能稳定地回答错误问题;连接路径从 PgBouncer 换成 direct primary,数字仍然精确,但已经是另一个实验。

本节用 pgbench 作为 workload driver。它的优点是与 PostgreSQL 同源、部署 简单、支持自定义事务、权重、随机分布、rate、transaction log 和失败统计。 它不是应用模拟器,也不会自动知道你的业务语义。

26.2.1 内置 pgbench 与业务自定义脚本

内置场景适合 calibration,不是业务证明

PostgreSQL 18 提供:

pgbench --builtin=list
tpcb-like
simple-update
select-only

用途:

built-in 适合回答 不适合直接回答
select-only 简单 PK read 与 client/path sanity 业务 read latency
simple-update 一种 update/WAL/commit calibration 订单写容量
tpcb-like 固定 schema 的混合 engine baseline TPC-B 认证或通用 TPS

名字明确写着 TPC-B (sort of)。它不是经过 TPC 审计的 TPC-B 结果。

内置 tpcb-like 有少量 branch/teller 热行。若 scale 小于 client 数,结果会 主要测量这些行的 contention。PostgreSQL pgbench good practices 明确要求默认场景的 scale 至少不小于最大 client,并提醒 dead tuple、vacuum 时机和 client bottleneck 会改变结果。

所以内置场景的正确位置是:

hardware/config calibration
version regression comparison
toolchain smoke test
rough bottleneck reproduction

不是:

we ran tpcb-like
therefore pg36_shop supports N orders/s

自定义脚本先定义 transaction boundary

一个 pgbench script 被调度一次,pgbench 就把它计作一个“transaction”,但 脚本未必真的包含一个 SQL transaction:

-- one pgbench transaction, one autocommit statement
SELECT ...;
-- one pgbench transaction, one explicit DB transaction
BEGIN;
UPDATE ...;
INSERT ...;
COMMIT;
-- dangerous semantic mismatch:
-- one pgbench transaction contains two committed DB transactions
BEGIN;
INSERT ...;
COMMIT;
BEGIN;
UPDATE ...;
COMMIT;

最后一种会让 retry 与业务原子性变得难以解释。PostgreSQL 文档也提醒,若脚本 包含多个 transaction,serialization/deadlock retry 会重放整个脚本,已经成功 提交的 transaction 可能再次执行。

本章 place-order.sql 保持“一次 script = 一个显式 DB transaction”:

\set customer_id random(1, :customer_count)
\set product_id random_zipfian(1, :product_count, 1.10)
\set quantity random(1, 3)

BEGIN;

SELECT price_cents
FROM shopbench.product
WHERE product_id = :product_id
\gset

UPDATE shopbench.inventory
SET quantity = quantity - :quantity,
    updated_at = clock_timestamp()
WHERE product_id = :product_id
  AND quantity >= :quantity;

-- append one order with a unique synthetic request_ref
INSERT ...;

COMMIT;

它保留:

  • product read;
  • hot-ish inventory row lock;
  • durable heap/index insert;
  • foreign key 与 unique index;
  • commit、WAL、replication 与 archive 成本。

它省略:

  • application authorization;
  • network calls;
  • payment provider;
  • outbox consumer;
  • 多 line item;
  • idempotency reconciliation;
  • HAProxy/PgBouncer;
  • real production data distribution。

这些 omission 必须进入报告,不能藏在脚本外面。

用权重组合 operation

pgbench 支持:

--file=read-product.sql@50
--file=read-order.sql@30
--file=place-order.sql@20

每次选择 script 的概率按相对整数权重决定。权重不必和为 100,但和为 100 更 容易审查。

正式报告仍应读取 transaction log:

script_no -> operation
0         -> read-product
1         -> read-order
2         -> place-order

检查实际 count。随机选择不会保证每个短 run 精确等于 50/30/20。

prepared、extended 与 simple 不是可随意切换的“优化”

pgbench -M

mode 行为 适合
simple simple query protocol 模拟文本批次或 baseline
extended parse/bind/execute 参数协议
prepared 第二次起复用 parse prepared workload

参考 run 固定:

protocol = prepared

它与应用是否使用 prepared statement 必须一致。改变 mode 会改变:

  • parse/plan CPU;
  • network round trip;
  • parameter typing;
  • generic/custom plan 行为;
  • PgBouncer transaction pooling 兼容边界;
  • pg_stat_statements shape。

参考脚本曾在真实预热中暴露:

operator is not unique: unknown * unknown

原因是 prepared placeholder 的两个 operand 都是 unknown。修复不是切回 simple, 而是明确:

(:price_cents)::integer * (:quantity)::integer

这类问题说明 smoke/warm-up 必须使用正式 protocol。

内置与自定义可以组成两层基线

推荐:

Layer A engine calibration
  same PostgreSQL version/config/hardware
  built-in select/update

Layer B service workload
  custom schema, constraints, mix and distribution

Layer A 漂移、Layer B 也漂移:

可能是 engine/hardware/config change

Layer A 稳定、Layer B 漂移:

优先检查 schema/data/mix/plan/application path

这比只有一条“总 TPS”更容易定位回归。

初始化也属于合同

内置 pgbench -i 会创建并可能销毁标准表。官方文档明确警告,初始化会删除 同名表,应使用独立数据库。业务脚本也必须同样谨慎。

本章:

database pg36_capacity
role     dbuser_pg36bench
marker   exact shared-object comments
schema   shopbench
data     synthetic

existing database/role 一律拒绝覆盖;完整 evidence 后只删除 marker 精确匹配且 无其他 session 的 fixture。

26.2.2 参数分布、事务混合和数据倾斜

uniform 往往是最不真实的默认

均匀分布:

P(X=k)=1N P(X=k)=\frac{1}{N}

意味着每个 customer/product 被访问的概率相同。真实业务常见:

few popular products
large tenants
new orders read more often
recent time windows
one campaign SKU
one settlement account

它们改变 cache locality 和 contention。

pgbench 提供:

random(lb, ub)
random_exponential(lb, ub, parameter)
random_gaussian(lb, ub, parameter)
random_zipfian(lb, ub, parameter)

参考合同:

operation key distribution
read-product product Zipf 1.15
read-order customer Zipf 1.08
place-order customer uniform
place-order product Zipf 1.10

这让少数 inventory row 更热,但不是从 production trace 拟合的分布。参数是教学 假设。

skew 同时可能更快和更慢

更多 hot key:

cache hit rises
  -> reads may become faster

same rows updated
  -> lock queue and cache-line contention rise

所以“Zipf 比 uniform 更真实”仍然不完整。要同时验证:

  • read locality;
  • update collision;
  • tenant fairness;
  • hot partition/page;
  • index leaf split;
  • per-key rate cap。

保留变量之间的相关性

独立随机:

customer=random(...)
product=random(...)
region=random(...)

会生成现实中不存在的组合。真实 workload 可能:

tenant -> region
region -> product catalog
customer -> order history
campaign -> product set
time -> status distribution

相关性会影响:

  • multi-column statistics;
  • join cardinality;
  • partition pruning;
  • index selectivity;
  • row-level security;
  • cache sharing。

高质量 fixture 应从脱敏 trace/分布参数生成,而不是把每列独立 random()

数据形状不仅是 row count

相同 1 亿行:

形状 影响
narrow fixed-width cache density 高
wide JSON/TOAST decompression、I/O、CPU
many NULL index/tuple size 与 selectivity
monotonically increasing key rightmost index page 热点
random UUID locality 与 page split
high update churn dead tuple、vacuum、bloat
many partitions planning/catalog

记录:

row width distribution
TOAST ratio
index count and width
key correlation
live/dead tuple
bloat state
statistics target and analyze time

只写 scale factor 不够。

transaction mix 应来自同一测量边界

错误组合:

reads from HTTP logs
writes from pg_stat_database
batch from scheduler estimates

分母不同,无法相加。

更可靠:

application operation counter
  -> eligible attempt
  -> mapped DB transaction class
  -> trace/queryid corroboration

第 25 章 signal contract 尚未提供真实 pg36_shop application SLI,所以第 26 章的 50/30/20 是显式 synthetic assumption,而不是从在线业务观测得出的事实。

写操作要保留写放大

一个 place-order 不只增加一行:

heap tuple
primary-key index
customer recent-order index
unique request_ref index
foreign-key lookup
inventory heap update
inventory index/heap visibility effects
WAL and possible full-page image
replica replay
archive and backup repository
future vacuum

若 benchmark 去掉约束与索引,TPS 更高,但它测的是另一个数据模型。

failed branch 也要模拟

业务中存在:

insufficient inventory
duplicate idempotency key
invalid transition
serialization failure
lock timeout
statement timeout

参考脚本为保证 30-run pipeline 稳定,把 inventory 初始化为很大的值,未覆盖 sold-out branch。报告把它列为 unknown。生产 workload 应显式给每个 outcome 权重,并决定:

  • 是否计入 eligible 分母;
  • 是否 rollback;
  • 是否 retry;
  • 响应 latency;
  • 是否产生 WAL/log。

seed 只固定随机序列

参考公式:

$$ \text{seed}

2026072900 +100 \times scale +10 \times clients +repetition $$

固定 seed 能帮助重放 script selection 和 key sequence,但不能固定:

  • process scheduling;
  • checkpoint/autovacuum;
  • page cache;
  • network timing;
  • replica/archive activity;
  • virtual-machine neighbor;
  • query plan 受 statistics 漂移;
  • concurrent transaction interleaving。

“同 seed”不是 bit-for-bit performance reproducibility。

26.2.3 think time、连接方式和客户端瓶颈

closed-loop 与 open-loop 回答不同问题

默认 pgbench:

client starts transaction
  -> waits for completion
      -> immediately starts next

这是 closed-loop。对 $C$ 个 client:

XCR+Z X \approx \frac{C}{R+Z}

其中 $R$ 是 response time,$Z$ 是 think time。参考 run:

Z = 0

当 server 变慢,client 发得也慢;offered load 自动下降。这会隐藏真实系统中仍在 到达并排队、超时或放弃的 request。

open-loop:

pgbench --rate=2000 --latency-limit=250 ...

PostgreSQL 18 的 --rate 按 Poisson timeline 调度 transaction;报告的 latency 从 scheduled start 计算,包括 schedule lag。若已经来不及满足 latency limit, transaction 会被记为 skipped。详见 pgbench --rate

生产 SLO envelope 更需要:

offered rate
achieved rate
schedule lag
queue time
execution time
late
skipped
failed

参考实验只做 closed-loop,所以 production_sustainable_tps=null

think time 是 workload,不是装饰

真实用户:

read page
think
click
wait

worker queue:

fetch job
process externally
write result
sleep/poll

如果要模拟 closed user population,加入 \sleep 或外部 pacing;如果要模拟 arrival rate,优先使用 rate schedule。不要一边设 think time,一边把结果称为 “数据库最大 TPS”。

连接模式要单独测试

持久连接:

pgbench -c 8 ...

每个 client 保留 connection。

每事务重连:

pgbench -C ...

测量:

  • TCP/TLS;
  • authentication;
  • backend fork/init;
  • session GUC;
  • extension hooks;
  • connection storm。

它不是正常 pool 模式的替代。

连接路径至少分:

engine baseline
  direct primary

pool baseline
  PgBouncer service

service baseline
  HAProxy + PgBouncer

user baseline
  application edge + service path

将它们混在一条曲线里,回归后无法知道变化发生在哪里。

PgBouncer 不会增加 PostgreSQL CPU

pool 的价值主要是:

limit active server sessions
absorb idle client connections
queue admission
reuse authentication/session setup
reduce reconnect storm

它不能凭空创造 CPU、I/O 或 WAL capacity。若 server 已在资源 knee,扩大 pool 只会让更多 request 同时争抢。

参考 run 刻意绕过 PgBouncer/HAProxy;第 22 章已经定义了连接和路由合同。本章 后续生产 baseline 必须增加 service-path cell,而不是把 direct 数字当服务数字。

load generator 必须有自己的 telemetry

PostgreSQL 官方建议在高并发时把 pgbench 放到另一台机器,必要时使用多个 client host,因为 pgbench 自己可能成为瓶颈。

观察:

client CPU
client run queue
network throughput/retransmit
pgbench jobs
file/logging I/O
schedule lag
multiple generator agreement

参考 run:

server  pg-test-1  1 vCPU
client  pg-meta-1  2 vCPU

c8 cell 的 client work median:

scale client work server work
S 29.5% 84.2%
M 29.1% 84.0%
L 27.8% 84.2%

因此没有证据表明 load generator CPU 是本次 ceiling。仍不能排除:

  • network round trip;
  • pgbench single process coordination;
  • two jobs 的调度;
  • meta host 同时承载监控;
  • virtual hypervisor sharing。

ClientRead 不等于“客户端瓶颈”

PostgreSQL backend 在 ClientRead 时等待 client 发下一条 protocol message。 prepared multi-statement transaction 中,短暂 ClientRead 很常见。它可能表示:

  • client think/pacing;
  • network;
  • client CPU;
  • 正常 statement boundary;
  • application 在 transaction 内做外部工作。

必须结合:

backend state
wait duration
client CPU
network
transaction age
protocol

不能看到 Client wait 占多数就宣布“数据库没问题”。

workload contract 最小模板

id: shop-mix-v1
arrival:
  model: closed-loop
  clients: [1, 8]
  think_time_ms: 0
connection:
  path: direct-primary
  lifetime: persistent
  protocol: prepared
transactions:
  read-product:
    weight: 50
    product_distribution: zipf-1.15
  read-order:
    weight: 30
    customer_distribution: zipf-1.08
  place-order:
    weight: 20
    product_distribution: zipf-1.10
    customer_distribution: uniform
retry:
  max_tries: 1
latency:
  origin: pgbench-client
  limit_ms: 250
omissions:
  - application
  - HAProxy
  - PgBouncer
  - WAN
  - payment

没有这份合同,数字不能离开终端。


上一节:从需求建立容量模型 · 返回本章目录 · 下一节:建立可信实验 · 查看全书目录 · 查看索引中心

26.3 建立可信实验

可信实验不等于“环境完全没有噪声”。更现实的标准是:

问题明确
因素声明
控制可验证
响应可重算
噪声被观察
顺序偏差被限制
失败没有消失
结论不越过证据

性能实验有两类误差:

random error
  run-to-run fluctuation
  -> repetition / interval may reveal

systematic error
  wrong workload, wrong path, client bottleneck, warm-only cache
  -> more repetitions do not repair

跑 1,000 次错误 workload,只会非常精确地回答错误问题。

26.3.1 固定硬件、版本、配置、数据与随机种子

把实验写成一个不可缺字段的 tuple

result =
  f(
    hardware,
    virtualization,
    kernel,
    filesystem/storage,
    PostgreSQL build/version,
    extensions,
    configuration,
    schema,
    data,
    statistics,
    connection path,
    client,
    workload,
    time/background state
  )

少一个字段,结果就多一种解释。

硬件不只记录“8C32G”

至少记录:

CPU model / architecture / socket / core / SMT
frequency policy / steal time / power state
NUMA topology
memory total / bandwidth / swap
storage device / controller / filesystem / mount
IOPS / latency / throughput / queue assumptions
network path / RTT / bandwidth
virtualization / cloud instance / noisy-neighbor boundary

参考 run:

architecture  aarch64
client        pg-meta-1, 2 vCPU, 4,089,262,080 bytes RAM
server        pg-test-1, 1 vCPU, 2,048,679,936 bytes RAM
storage       Vagrant virtual disk on shared laptop hypervisor

所以它不能支持 production IOPS、endurance 或 failure-domain claim。

PostgreSQL provenance

版本至少包括完整 build:

SELECT version();
SHOW server_version_num;

还要:

extension versions
compiler/architecture
block size / WAL segment size
checksums
locale / encoding
huge pages / io_method

参考:

PostgreSQL 18.6 Ubuntu build
server_version_num 180006
block size         8192
data checksums     on

升级 minor version 也应重新跑 regression baseline。minor release 可能修复 planner、 executor、WAL、I/O 或 correctness 问题;“major 没变”不是等价环境。

配置要保存 value、source 与 pending restart

只保存 postgresql.conf 不够:

SELECT
    name,
    setting,
    unit,
    source,
    sourcefile,
    sourceline,
    pending_restart
FROM pg_settings
ORDER BY name;

容量关键项:

shared_buffers
work_mem / hash_mem_multiplier
maintenance_work_mem / autovacuum_work_mem
max_connections
max_worker_processes / parallel workers
effective_cache_size
random_page_cost / effective_io_concurrency
io_method / io_workers
checkpoint_timeout / max_wal_size
wal_compression / full_page_writes
synchronous_commit / fsync
track_io_timing / track_wal_io_timing

effective_cache_size 是 planner estimate,不是分配的 cache。work_mem 是每个 operation 可能使用的基础限制,不是整台 server 的总内存。PostgreSQL resource consumption 提醒,一个复杂 query 可能有多个 sort/hash,多 session 和 parallel worker 会把 总内存放大许多倍。

数据版本必须可证明

记录:

schema migration/version
DDL hash
generator/seed
row counts
relation and index sizes
distribution parameters
statistics/analyze timestamp
live/dead tuple and bloat state
partition set
sequence position

参考 fixture 每个 scale 都重建 immutable table:

customer
product
order_history

每次 measured run 前只重置:

inventory
order_live

这样五次 run 有相同 initial mutable state,同时保留自然 cache 与 background 演化。它不代表 production bloat/churn。

随机 seed 与 run order 都要固定

seed 固定:

  • script choice;
  • random key;
  • Zipf sample;
  • rate schedule(若使用)。

run order 也会影响结果。若总是:

c1 -> c8

c8 总是得到更暖 cache,也总是承受 c1 留下的 dirty page。

参考采用 counterbalanced order:

r1 c1 -> c8
r2 c8 -> c1
r3 c1 -> c8
r4 c8 -> c1
r5 c1 -> c8

scale 仍按 S -> M -> L,因为重建大数据代价高。这是保留的 order confound, 报告必须写明。

source hash 绑定执行实现

正式 evidence 保存 21 个 source file SHA-256:

contracts
SQL
pgbench scripts
samplers
runner
validator
reviewer
task

若脚本改了一个 cast、weight 或采样窗口,旧 evidence 不再声称由当前 source 产生。参考 run 在 CPU sampler 口径修正后完整重跑,而不是把旧 transaction 结果与新 CPU 算法拼在一起。

manifest 最小字段

{
  "run_id": "...",
  "captured_at": "...",
  "target": "...",
  "versions": {},
  "settings": {},
  "source_hashes": {},
  "dataset": {},
  "workload_id": "...",
  "run_order": [],
  "raw_files": {},
  "cleanup": {},
  "claims_not_made": []
}

raw file 要有 size/hash;只保存总结无法重算 quantile。

26.3.2 预热、重复、置信区间与异常值

预热要说明预热什么

可能需要预热:

client process and connections
authentication/TLS
prepared statements
PostgreSQL shared buffers
OS page cache
JIT compilation
relation extension
filesystem allocation
storage cache
statistics/exporter discovery

“跑 5 秒预热”只是动作,不是证明。

参考 run 对每个 scale/concurrency 做 5 秒自然预热,随后 reset mutable table。 它能:

  • 验证正式 protocol 与 script;
  • 建立连接;
  • 触碰部分 immutable working set;
  • 暴露 parameter typing。

它不能保证 899 MiB L dataset 全部 warm,也不能制造 cold-cache baseline。

正式时长取决于最慢周期

观察窗口要覆盖:

checkpoint cycle
autovacuum cycle
backup overlap
storage cache behavior
CPU thermal/power changes
application burst length
replica/archive catch-up

PostgreSQL 官方建议至少几分钟,可能需要数小时。

本章每 run 8 秒,是为了在 disposable laptop sandbox 中:

  • 演示完整 30-run evidence pipeline;
  • 观察短时 curve 与测量偏差;
  • 保持负载有界;
  • 不批准生产数字。

它不是 production baseline 的推荐时长。

重复的实验单位必须独立定义

参考每次 run:

reset order_live and inventory
snapshot native counters
start wait and OS samplers
run 8 s pgbench
stop at exact measured window
wait for stats flush
snapshot counters

immutable data 与 cache 不重建,所以 run 并非完全独立;它是“同一 scale 阶段内的 重复”。报告不能把五次当五台独立机器。

生产比较建议:

  • 多个 run;
  • 多个时间段;
  • 至少一次 fresh environment;
  • 若硬件采购,多个同型 node;
  • candidate/control 交错,而不是先跑完 A 再跑 B。

不要用一个平均数吞掉分布

每个 run 保存 transaction latency sample:

{x1,x2,,xn} \{x_1,x_2,\dots,x_n\}

cell pooled p95:

Q0.95(r=15Xr) Q_{0.95}\left( \bigcup_{r=1}^{5} X_r \right)

它回答“该 cell 所有保存 transaction sample 的 p95”。

run-level p95:

qr=Q0.95(Xr) q_r=Q_{0.95}(X_r)

再报告 $q_r$ 的 median/interval,回答“一个典型 run 的 p95 如何波动”。

这两者不能混名。

参考 public summary 保存:

pooled p50/p95/p99/max
run TPS median
run TPS deterministic bootstrap 95 interval

bootstrap interval 的边界

对五个 TPS:

resample five runs with replacement
compute median
repeat 10,000 times with fixed bootstrap seed
take 2.5% and 97.5%

这给出教学用 interval,但:

  • n=5 很小;
  • run 不完全独立;
  • systematic bias 不会进入 interval;
  • interval 不是“真实 TPS 有 95% 概率在里面”;
  • performance distribution 可能非平稳。

它主要让读者看见“不确定性不是零”。

异常值不能看结果再删除

L-c8 五次 TPS:

2216.087
2739.827
2774.267
2824.359
2798.272

第一轮显著低,p95 也更高。可能原因:

  • L 初始化尾部 dirty write;
  • working set 尚未稳定;
  • background checkpoint/archive/replication;
  • virtual neighbor;
  • random interleaving。

本章保留它,因此:

median TPS         2774.267
bootstrap interval [2216.087, 2824.359]

正确流程:

  1. 在实验前声明 outlier rule;
  2. 保存原始样本;
  3. 检查 concurrent evidence;
  4. 报告含/不含敏感性分析;
  5. 只有明确测量故障才排除;
  6. 排除要留 audit record。

“看起来不正常”不是删除依据。

candidate 与 baseline 的比较

对每个 matched run:

$$ \Delta_r

\frac{candidate_r-baseline_r}{baseline_r} $$

优先比较 paired distribution,而不是两个独立平均。验收可以是:

throughput regression < 3%
p95 regression < 5%
p99 no material tail expansion
error/late/skipped unchanged
resource demand does not worsen beyond budget

阈值来自业务风险与实验噪声,不是固定模板。

26.3.3 冷热缓存、后台任务与邻居噪声

PostgreSQL 有至少两层 cache

PostgreSQL shared buffers
  -> OS page cache
      -> device/controller cache
          -> physical/virtual storage

blks_hit

found in PostgreSQL shared buffers

blks_read

PostgreSQL requested a block read

不代表 physical disk read。OS 可能立即从 page cache 返回。

参考:

S/M cells  blks_read = 0 during measured runs
L-c1       2,500
L-c8       14,491

可以说 L 触发了 PostgreSQL-level reads,不能说发生了同样数量的 physical I/O。 因此 runner 同时采 /proc disk counters,并用 Pigsty node metrics 做较粗的 corroboration。

真 cold cache 是破坏性实验

常见做法:

restart PostgreSQL
drop OS page cache
reboot host
recreate VM

它们会影响同机其他 workload,不能在共享或生产节点随便执行。若要测 cold start:

  • dedicated environment;
  • 明确授权;
  • 固定动作与复位;
  • 同时记录 storage cache;
  • 区分 crash recovery、clean restart 和 first read;
  • 多次重建,不把一次 boot 当分布。

本章禁止 drop cache、restart 和 checkpoint on demand,只声明 warm-natural。

autovacuum 是 workload 的一部分

PostgreSQL 官方 pgbench 指南提醒,dead row/space 与 autovacuum 会改变结果。

两种实验都合理:

controlled engine microbenchmark
  deliberately isolate maintenance

production representative benchmark
  keep realistic autovacuum and churn

错误做法是关闭 autovacuum 获得数字,却不在 claim 里写。

若保留 autovacuum,记录:

SELECT
    relname,
    n_live_tup,
    n_dead_tup,
    autovacuum_count,
    autoanalyze_count,
    last_autovacuum,
    last_autoanalyze
FROM pg_stat_user_tables;

并观察 progress、I/O、WAL 和 latency。

checkpoint 与 full-page image

checkpoint 后第一次修改某 page,full_page_writes=on 时可能产生 full-page image。于是相同 transaction mix 的 WAL/tx 会随 checkpoint phase 改变。

观察:

pg_stat_checkpointer
pg_stat_wal.wal_fpi
pg_stat_wal.wal_bytes
Pigsty PGSQL Persist

不要为了“稳定”关闭 full-page writes;那会改变 durability。

PostgreSQL WAL 配置 说明这些参数的恢复语义。本章保持 fsync=onfull_page_writes=onsynchronous_commit=on

backup、archive 与 replica 会消耗真实资源

写 workload 同时驱动:

primary WAL generation
WAL sender
replica receive/write/replay
archive command
backup repository

参考全实验 Pigsty window:

WAL rate max            46.2 MB/s
replica replay gap max   3.47 MB
replay gap median        0

该 window 包含 bulk initialization、warm-up 和 measured run。max 不能归因给 某个 cell;median 0 也不证明 read-your-writes。它只是“复制路径在实验期间有过 推进和短时距离”的 corroboration。

virtual neighbor 让“同一台机器”也不相同

shared hypervisor 可能同时运行:

  • load generator;
  • database VMs;
  • monitoring;
  • local build;
  • host desktop workload。

guest 中看到的 CPU busy 不包含所有 host contention 细节。需要:

steal time
host load
storage latency
run-to-run spread
dedicated-host rerun

本章把 shared-hypervisor 写入 why_null,而不是用五次重复假装消除。

background inventory

每次 run 保存:

pg_stat_database delta
pg_stat_wal delta
pg_stat_io delta
pg_stat_checkpointer delta
pg_stat_bgwriter delta
pg_stat_statements queryid-only delta
pg_stat_activity wait sample
client/server /proc sample
Pigsty full-window range

统计不 reset。PostgreSQL cumulative statistics 默认有更新延迟,并可能在 transaction 内缓存 snapshot;官方 viewing statistics 解释了 PGSTAT_MIN_INTERVALstats_fetch_consistencypg_stat_clear_snapshot()。runner 在 run 后等待 flush,并比较 reset timestamp。

26.3.4 绝对性能结论只适用于记录过的环境

一条绝对数字的完整名称

不是:

PostgreSQL = 2920 TPS

而是:

pg36_shop shop-mix-v1
S dataset, 8 closed-loop clients, zero think time
prepared persistent sessions
direct pg-meta-1 -> pg-test-1 primary
PostgreSQL 18.6, Pigsty v4.5.0 sandbox
five 8-second teaching runs
median 2920.5 TPS
pooled p95 9.448 ms
zero observed failures/late
server work median 84.2%

标题很长,因为 claim 的边界本来就长。

absolute capacity 与 regression baseline 分开

短、噪声较大的实验仍可做 CI regression signal:

same controlled environment
same workload
same run order
candidate vs baseline interleaved

它不能自动成为生产采购依据。

用途 证据要求
script smoke 能运行、事务语义正确
CI regression 相对、matched、噪声已知
sandbox demand estimate OS/PG evidence + 明确边界
production capacity representative environment + SLO load
procurement production candidate hardware + failure/maintenance

环境变化触发 rebaseline

至少在以下变化后重建:

  • PostgreSQL/Pigsty/kernel/extension 升级;
  • CPU、RAM、storage、filesystem、VM type;
  • shared_buffers、WAL、checkpoint、I/O、pool 配置;
  • schema/index/partition/constraint;
  • query/plan/protocol;
  • operation mix、key skew、data size/bloat;
  • backup/replica/topology;
  • application retry/timeout/admission;
  • SLO 或 failure model。

不能把三年前、旧 instance type 的 TPS 线性乘 CPU 核数。

外推必须带假设

参考 CPU 投影:

$$ \lambda_{65}

\frac{0.65}{D_{cpu}} $$

这是 local linear model。它没有证明:

$$ D_{cpu}(\lambda)

\text{constant} $$

靠近 contention/knee 后,service demand 可能随负载增加。外推距离越大,越需要 新测量点。

生产 gate 的证据矩阵

维度 sandbox reference production gate
hardware shared laptop VMs target compute/storage/network
run length 8 s × 5 minutes/hours covering cycles
arrival closed-loop c1/c8 open-loop rate sweep
path direct primary app + HAProxy + PgBouncer
data synthetic S/M/L representative shape/skew/bloat
failure none declared node/path loss
maintenance observed incidental scheduled overlap
backup incidental backup/restore window
cache warm-natural declared warm/cold scenarios
approval sandbox passed independent review

任何一行空缺,production claim 就保持 pending。

可信实验检查单

  • target 与 production boundary 已验证;
  • load generator 和 server 身份独立;
  • source、版本、配置、schema、data 有 hash/manifest;
  • seed 与 run order 固定;
  • warm-up 的对象与限制明确;
  • 正式时长覆盖所需周期;
  • 每个 cell 有足够重复;
  • raw transaction sample 可重算 quantile;
  • failed/retry/late/skipped 没有消失;
  • client/server/PG/Pigsty 证据时间对齐;
  • statistics reset timestamp 未变化;
  • cache/background/neighbor 被观察;
  • outlier rule 预先声明;
  • absolute、relative、production claim 分级;
  • unknown 与 cleanup evidence 同时发布。

上一节:设计代表性工作负载 · 返回本章目录 · 下一节:找到饱和点与瓶颈 · 查看全书目录 · 查看索引中心

26.4 找到饱和点与瓶颈

资源到 100% 不是 saturation 的唯一定义。

系统可能先在:

latency SLO
error/timeout
lock queue
pool wait
WAL/replica/archive lag
memory pressure
storage latency

上失效,而 dashboard 中 CPU 还没满。

容量曲线必须同时画:

offered load
achieved throughput
latency distribution
failure/late/skipped
queue/wait
resource utilization

瓶颈不是“最高的那条曲线”,而是当前最先限制目标的 service center。

26.4.1 吞吐—延迟曲线与排队拐点

典型曲线有三个区域

throughput
  ^
  |                  __________ maximum / collapse region
  |              ___/
  |          ___/
  |      ___/
  |  ___/
  +--------------------------------> offered load / concurrency
       linear      knee       saturated

对应 latency:

latency
  ^
  |                         /
  |                      __/
  |                   __/
  |__________________/
  +--------------------------------> offered load / concurrency

区域:

区域 throughput latency queue
linear 近似随 load 增长 稳定 很小
knee 增益变小 tail 开始快速上升 累积
saturated 持平或下降 非线性恶化 持续/失败

capacity 通常选在 knee 左侧,并留 failure/maintenance headroom,而不是取最高 TPS 点。

两个并发点只能 bracket

参考只有 c1/c8:

scale c1 TPS c8 TPS gain c1 p95 c8 p95 p95 multiplier
S 1,508.5 2,920.5 1.936x 2.152 9.448 4.390x
M 1,555.8 2,911.5 1.871x 2.123 9.398 4.427x
L 1,423.6 2,774.3 1.949x 2.259 10.087 4.465x

c8 throughput 仍比 c1 高约 1.9 倍,不能说 knee 已经位于 c1–c8;p95 却放大 约 4.4 倍,说明排队/并行争用已明显增加。

公共结果因此写:

{
  "interpretation": "knee-not-bracketed-by-one-and-eight-clients",
  "exact_knee_known": false
}

下一轮应补:

c2 c4 c8 c12 c16 c24 c32

或以 open-loop rate 做更密的 SLO sweep。只有两点画出的“曲线”是连线,不是 找到拐点。

closed-loop 曲线的横轴不是 offered arrival

零 think time、$C$ 个 client:

XCR X \approx \frac{C}{R}

当 latency 上升,throughput 自动降低。这会形成自我节流:

server slows
  -> each client waits longer
      -> fewer new transactions arrive

真实应用 request 可能继续到达并在 pool/queue 中堆积。因此 closed-loop c8 回答:

eight persistent clients can complete how much work

不回答:

system can admit N external requests/s while meeting p95 and error SLO

open-loop 要同时看 offered 与 achieved

--rate

pgbench \
  --rate=2200 \
  --latency-limit=250 \
  --time=300 \
  ...

记录:

$$ \text{completion ratio}

\frac{\text{successful}}{\text{scheduled}} $$

以及:

schedule lag
late
skipped
failed
queue depth

若 achieved TPS 看起来稳定,但 skipped 快速增加,系统不是健康保持吞吐,而是 丢弃工作。

用 Little’s Law 检查 queue

对 database active/queued population:

L=λW L=\lambda W

c8、2,920 TPS、mean latency 粗略约:

W829202.74ms W \approx \frac{8}{2920} \approx 2.74 ms

这个量接近 mixed mean,不是 p95。若观测到平均 active+wait session 与它严重 不一致,检查:

  • transaction log boundary;
  • pool queue 未计;
  • client think/network;
  • sampler bias;
  • multiple transactions per script;
  • offered/achieved 混用。

knee 是多目标决策

可能的 acceptance:

p95 <= 250 ms
p99 <= 500 ms
failed <= 0.1%
skipped = 0
CPU target <= 65%
disk latency within budget
replica/archive catch up within 5 min
pool wait p95 <= 20 ms

最先违反的条件定义当前可用边界。它可能远早于最大 TPS。

throughput collapse

超过 knee 后可能:

more clients
  -> more context switches
  -> more lock queue
  -> more cache churn
  -> more memory/IO pressure
  -> longer transactions
  -> locks held longer
  -> even more queue

形成正反馈,throughput 反而下降。压测 runner 应有 stop condition:

  • failure/late 超阈值;
  • latency 超 hard limit;
  • replica/archive gap 无界增长;
  • disk/memory safety floor;
  • server health/HA 状态变化;
  • client lost;
  • cleanup 不再可保证。

26.4.2 CPU、内存、I/O、WAL 与锁的证据

每个候选瓶颈需要至少两层证据

假设 PostgreSQL OS/Pigsty 反证
CPU active no wait、query exec time CPU busy/run queue client/lock wait
data I/O pg_stat_io, blks_read/time disk bytes/latency/queue OS cache
WAL pg_stat_wal, WAL IO disk write, archive/replay data write
lock wait event, pg_locks blocker low CPU possible client wait
memory temp bytes, backend count available/PSI/OOM cache reclaim
connection activity/pool states client queue active execution

一个指标不是 root cause。

CPU:utilization、run queue 与 service demand

参考 measured-window median:

cell server work client work
S-c1 48.9% 17.7%
S-c8 84.2% 29.5%
M-c1 50.1% 17.8%
M-c8 84.0% 29.1%
L-c1 46.3% 16.2%
L-c8 84.2% 27.8%

c8 的 server work 显著更高,client 仍有余量。可以说:

server-side work is a stronger limiting candidate than load-generator CPU

不能说:

CPU is the sole root cause

因为约 16% 时间没有被 work 计入,可能是 idle、virtual scheduling、commit path、network 或其他等待;c8 也存在 lock/LWLock/IO samples。

单位 CPU demand:

$$ D_{cpu}

\frac{U_{cpu}\times T}{N} $$

参考 c8 median run:

scale CPU s / mixed tx
S 0.0002906
M 0.0002924
L 0.0003070

它包含 server 上所有 CPU work,不只 SQL executor;这是容量视角需要的总需求, 但 shared VM background 会引入噪声。

memory:不要用 available 一张图下结论

PostgreSQL memory:

shared memory
+ backend/session memory
+ per-node work_mem/hash memory
+ parallel workers
+ maintenance/autovacuum
+ extension/JIT
+ OS page cache

风险模型:

Mpeak≉shared_buffers+max_connections×work_mem M_{\text{peak}} \not\approx shared\_buffers + max\_connections \times work\_mem

work_mem 是每个 sort/hash operation 的基础限制,一条 query 可能多个,一台 server 可能多个 query/worker。

观察:

  • MemAvailable minimum;
  • swap/OOM/PSI;
  • backend count 与 active count;
  • temp file/bytes;
  • hash/sort spill;
  • parallel workers;
  • cgroup/VM limit。

参考 cells:

temp_bytes 0
swap       0

只说明这个短 workload 未观测到 database temp spill;它不是 memory capacity 证明。

I/O:先分 data、WAL 与 background

pg_stat_io 按:

backend_type
object
context
reads/writes/extends/fsyncs
bytes/time

聚合。track_io_timing=on 才有部分 timing;参考 track_wal_io_timing=off,所以不能声称有 WAL write/fsync timing。

cell database block evidence:

cell block reads block hits
S-c1 0 675,976
S-c8 0 1,323,054
M-c1 0 733,198
M-c8 0 1,326,143
L-c1 2,500 629,335
L-c8 14,491 1,344,632

L 出现 PostgreSQL-level reads,与 dataset 大于 shared buffers 一致;仍不能把 每个 read 当 physical disk。

Pigsty 全实验 window:

node disk read max   13.99 MB/s
node disk write max  81.22 MB/s

包含 3.2M row 初始化,不用于 cell arithmetic。per-run /proc 与 native delta 才用于解释 measured cell。

WAL:按 transaction 和 operation 分解

mixed WAL/tx:

cell WAL bytes / mixed tx
S-c1 160.3
S-c8 159.5
M-c1 176.0
M-c8 161.9
L-c1 157.6
L-c8 196.2

不要用六个值平均成“PostgreSQL 每事务 168 bytes”。它只适用于当前 80% read、 20% write mix,还受 full-page image、background 和短窗口影响。

更好的生产模型:

WAL/read-product
WAL/read-order
WAL/place-order
WAL/background

通过 operation/queryid-scoped delta 或隔离 workload 校准,再按预测 mix 加权。

WAL rate 还决定:

  • replica network/write/replay;
  • archive bandwidth;
  • retained WAL;
  • PITR repository;
  • recovery/catch-up time。

lock:等待样本不是完整 lock history

c8 cell 观察到少量:

Lock
LWLock
IO
Client
CPU

所有 cell deadlock=0。解释边界:

sampled wait
  catches only state at 250 ms sample points

deadlock counter
  counts detected deadlocks

neither proves no short lock waits

要解释 lock:

  • blocker/waiter graph;
  • lock mode/object;
  • transaction age;
  • wait duration;
  • hot key;
  • statement/queryid;
  • timeout/deadlock log。

Client wait 多也不能自动反证 lock;backend 可能在 statement boundary 等 client,另外 client 可能在等另一个 backend。

checkpoint、archive 与 replication

Pigsty PGSQL Persist dashboard 把 WAL、XID、checkpoint、archive 和 I/O 放在同一观察面。

实验期间:

replica replay gap median 0
replica replay gap max    3,471,848 bytes

这表示 exporter 的 replay-distance metric 曾观测到短时差距。不能推出:

  • commit token 已在特定 replica 可见;
  • RPO=0;
  • failure 时一定无数据丢失;
  • catch-up time 在生产 storage 上相同。

第 20 章的 HA contract 与第 25 章的 freshness signal 仍然生效。

用 hypothesis table 组织诊断

hypothesis supporting contradicting next test
client CPU ceiling none strong client <30%, server ~84% distributed client
PostgreSQL CPU knee server rises to ~84% throughput still gains ~1.9x c12/c16/c24
data I/O for L block reads appear c8 iowait median low larger-than-RAM/open-loop
hot-row lock Lock/LWLock samples no deadlock, gain still large change Zipf/uniform
WAL limit L-c8 WAL/tx higher no lag growth proven write-heavy mix

结论不是“选一行”,而是设计下一个最能区分假设的实验。

26.4.3 连接数增加为何可能降低吞吐

max_connections 是上限,不是目标

max_connections=500 只说明 PostgreSQL 接受的 backend 数上限。它不证明 500 active query 是健康并发。

每个 backend 带来:

  • process 与 private memory;
  • transaction/snapshot;
  • lock table entries;
  • plan/executor state;
  • network socket;
  • statistics/logging;
  • scheduling/cache footprint。

active concurrency 超过 bottleneck 并行度后,额外 backend 主要排队。

CPU 调度与 cache locality

1 vCPU 上 32 个 CPU-bound backend:

no more execution capacity
more runnable processes
more context switching
instruction/data cache disruption
longer transaction duration

长 transaction 又让 lock 持有更久,形成二阶影响。

观察:

CPU utilization
run queue/load
context switches
active sessions
per-query service demand

lock queue

若多个 place-order 更新同一 hot inventory row:

one holder
many waiters

增加 client 不增加该 key 的 service rate,只增加 queue:

Wq,Xconstant W_q \uparrow,\quad X \approx constant

若 transaction 在拿锁前还做很多工作,rollback/retry 成本更高。

措施不是盲目加连接:

  • partition/shard hot key;
  • shorten transaction;
  • fixed lock order;
  • admission per key/tenant;
  • move external calls outside transaction;
  • optimistic/versioned update;
  • reduce retry storm。

memory amplification

更多 active query:

more sort/hash
more parallel workers
more temp spill
less OS cache
more I/O

然后 latency 上升,连接占用更久,再增加在途数。

storage queue

存储有可用并行深度,但不是无限:

low concurrency
  underutilized device

optimal concurrency
  latency acceptable, throughput high

excess queue
  throughput flat, latency rises

同一 device 上 data、WAL、checkpoint、archive staging 和 backup 可能相互影响。

parallel query 与 client concurrency 相乘

若一个 query 最多 4 workers:

8 active queries
may request up to 8 leaders + 32 workers

PostgreSQL resource 文档提醒 parallel worker 是独立 process,CPU/memory impact 类似额外 session,work_mem 也按 worker/operation 作用。

所以容量合同要同时固定:

client concurrency
max_parallel_workers
max_parallel_workers_per_gather
query plan

pool 的正确目标是限制 server concurrency

例如:

10,000 idle application connections
  -> PgBouncer
      -> 32 server connections
          -> 8 active budget

pool queue 是可控 admission,前提是:

  • queue latency 可观测;
  • client timeout 大于/匹配 policy;
  • cancellation 正确;
  • transaction pooling 兼容 session feature;
  • retry 不放大;
  • queue 有上限。

没有上限的 pool 只是把 outage memory 从 PostgreSQL 移到中间层。

找到健康并发的实验

对每个 data/mix:

c1 c2 c4 c8 c12 c16 c24 c32

每点:

  • 固定 offered model;
  • 重复;
  • 画 throughput、p95/p99、failed、late、queue;
  • 同时画 server/client CPU、I/O、WAL、lock;
  • 记录 pool/direct path。

选择:

first point before material tail acceleration
and within headroom/failure policy

不是:

highest observed TPS

本节结论

参考证据支持:

  • c8 比 c1 吞吐高约 1.9x;
  • c8 pooled p95 高约 4.4x;
  • client CPU 不是明显 ceiling;
  • server work、lock/LWLock/IO 与 L block-read 都值得下一轮区分;
  • 两点没有定位 exact knee。

它不支持“最佳连接数=8”。下一轮实验而不是措辞强度,才能减少未知项。


上一节:建立可信实验 · 返回本章目录 · 下一节:从测量推导容量与成本 · 查看全书目录 · 查看索引中心

26.5 从测量推导容量与成本

一次压测给出的 TPS 不是容量答案。它只是某个 workload、数据规模、配置和时间窗口下 的一组观测。容量规划要把观测转换为三个可以行动的模型:

resource model
  one business operation consumes how much CPU / IO / WAL / storage

growth model
  data, WAL, backup and maintenance workspace grow how fast

decision model
  at what threshold, considering lead time and failure, should we change capacity

转换过程中最危险的捷径是:

benchmark maximum TPS × server count = future capacity

它忽略了 workload mix、尾延迟、后台工作、故障冗余、增长、复制、备份以及扩容 提前期。可信的容量模型必须保留条件、区间和未知项。

26.5.1 单位业务量的资源消耗

先定义业务单位

“每事务成本”只有在 transaction 的业务语义稳定时才有意义。第 26 章实验的 transaction mix 是:

read-product  50%
read-order    30%
place-order   20%

其中一次 place-order 会写入订单、订单行并扣减库存;一次 read operation 则主要 读取。若把三者统称为“请求”,mixed average 会随权重变化:

$$ D_{\text{mix}}

\sum_{i=1}^{n} w_iD_i $$

其中:

  • $w_i$ 是第 $i$ 类 operation 的比例;
  • $D_i$ 是该 operation 的资源需求;
  • $\sum w_i=1$。

今天写比例 20%,明天促销时写比例 45%,即使每一种 operation 的实现都没变, mixed resource/transaction 也会变化。因此模型应优先保存:

CPU seconds / read-product
CPU seconds / read-order
CPU seconds / place-order
WAL bytes / place-order
durable data bytes / place-order

而不是只保存一个 blended TPS。

CPU service demand

假设测量窗口内:

N_cpu       logical CPU count
U_cpu       non-idle CPU fraction
X           completed transactions / second

粗略的 mixed CPU demand:

Dcpu,mixNcpuUcpuXCPU-seconds / transaction D_{\text{cpu,mix}} \approx \frac{N_{\text{cpu}}U_{\text{cpu}}}{X} \quad \text{CPU-seconds / transaction}

例如单核 server 上,L-c8 的 server work ratio 约 0.842、吞吐中位数约 2,774 TPS:

Dcpu,mix1×0.84227740.000304 CPU-s/tx D_{\text{cpu,mix}} \approx \frac{1\times0.842}{2774} \approx 0.000304 \text{ CPU-s/tx}

即约 0.304 CPU-ms/tx。这个值只能用于同一 workload mix 的粗略推演。它包含 shared overhead,未把 checkpointer、WAL writer、autovacuum 等后台成本可靠地 分摊给 operation,也没有证明 CPU 是唯一限制。

要得到 per-operation demand,应设计独立或正交实验。设三种 operation 的未知 CPU demand 为 $D_1,D_2,D_3$,运行至少三组不同且可识别的 mix,得到:

$$ \begin{bmatrix} w_{11} & w_{12} & w_{13} \ w_{21} & w_{22} & w_{23} \ w_{31} & w_{32} & w_{33} \end{bmatrix} \begin{bmatrix} D_1\D_2\D_3 \end{bmatrix}

\begin{bmatrix} D_{\text{mix},1}\D_{\text{mix},2}\D_{\text{mix},3} \end{bmatrix} $$

更简单的做法是分别运行 read-only、write-only 与代表性 mixed workload,然后用 mixed run 验证模型,而不是直接拿三次结果线性拼接。

从 service demand 估算目标 CPU

若 forecast 中每类业务到达率为 $\lambda_i$:

$$ C_{\text{cpu required}}

\frac{\sum_i \lambda_iD_{\text{cpu},i}} {U_{\text{target}}} $$

目标利用率 $U_{\text{target}}$ 不是物理极限。它还要容纳:

forecast error
traffic burst
autovacuum / checkpoint / backup
failover and degraded topology
software regression
host and storage variance

参考 run 为便于教学,计算了一个 sandbox-only、线性、65% CPU 投影:

scale 65% CPU 条件投影 TPS
S 2,236.79
M 2,222.96
L 2,116.98

这只是:

X65%Xc8×0.65Uc8 X_{65\%} \approx X_{\text{c8}} \times \frac{0.65}{U_{\text{c8}}}

它不代表 production sustainable TPS,因为:

  • c8 已高于 65%,这是回算而非直接在 65% 稳态测量;
  • 两个并发点没有找到 knee;
  • workload 是 closed-loop、warm-cache、短窗口;
  • 后台维护、故障、恢复、备份与真实网络未被充分施压;
  • 只有一个虚拟化小节点。

公共结果把 production sustainable TPS 保持为 null,这比填一个看似精确的数字 更诚实。

WAL 与 durable growth 必须按写操作归一

参考实验给出了:

cell mixed WAL bytes/tx durable bytes/place-order
S-c1 160.33 415.21
S-c8 159.53 409.12
M-c1 175.95 412.81
M-c8 161.88 405.97
L-c1 157.56 411.50
L-c8 196.18 407.88

WAL bytes/tx 的分母包含 80% read transaction,因此不能直接说“一笔订单产生 约 160–196 字节 WAL”。若读操作完全不写,按 20% place-order 做第一阶换算, 写操作的 WAL 可约为 mixed value 的五倍;但真实系统还有:

  • hint bit 与 full-page image;
  • checkpoint 后首次页修改;
  • index 数量与 page split;
  • autovacuum、freeze 与 catalog activity;
  • logical decoding;
  • sequence 与 extension 行为。

正确做法是用 pg_stat_wal.wal_bytes 的窗口 delta,除以该窗口实际成功的 write operation 数:

$$ \text{WAL bytes/write op}

\frac{\Delta\text{wal_bytes}} {\text{successful write operations}} $$

同理:

$$ \text{durable growth/write op}

\frac{\Delta\text{relation bytes after stabilization}} {\text{successful write operations}} $$

这里的 “after stabilization” 很重要。heap 与 index 的 allocated size 不是每 写一行都线性增加;页内 free space 会先被消耗,扩页是离散事件。短窗口的 relation size delta / order 容易受页边界影响。应在足够长窗口、多个数据规模 和 vacuum 周期上重复测量。

PostgreSQL 原生统计可以分别回答:

SELECT wal_bytes, wal_records, wal_fpi
FROM pg_stat_wal;

SELECT
    datname,
    xact_commit,
    xact_rollback,
    blks_read,
    blks_hit,
    temp_bytes,
    deadlocks
FROM pg_stat_database
WHERE datname = current_database();

统计视图是累计计数器。必须保存 start/end snapshot、server restart/reset 时间, 用 delta 计算;不能把 dashboard 当前值当作本次 run 的成本。

I/O 成本不能用 cache hit ratio 代替

一个 operation 的 I/O 模型至少分:

logical buffer access
physical data read
data write / writeback
WAL write / sync
temp read/write
backup/archive/network bytes

参考 run 的 S、M cell 没有记录到 PostgreSQL block read,L-c1/L-c8 分别出现 2,500/14,491 次 block read。这只说明 L 数据规模在该 warm-cache 窗口触碰了更多 未驻留 block;它不证明小规模生产 workload “不需要磁盘”。

PostgreSQL 18 的 pg_stat_io 可按 backend type、object 与 context 提供 read/write/extend/fsync 等累计信息。 它仍需和 OS/Pigsty 的 device latency、queue、throughput 对齐,因为:

  • PostgreSQL read 可能由 OS page cache 满足;
  • write() 完成不等于设备持久化完成;
  • shared storage 与 hypervisor 可隐藏或放大延迟;
  • data I/O、WAL I/O 和 backup I/O 可能落在不同设备。

latency 是容量成本的一部分

若只追求最低 unit CPU cost,常会把 server 推到更高 batch/concurrency;throughput 可能提高,但 queueing 令 p95/p99 恶化。容量成本函数应含 SLO penalty:

$$ \text{effective cost}

\text{infrastructure cost} + \text{failure risk} + \text{latency penalty} + \text{operational complexity} $$

因此一份 unit economics 表至少包含:

指标 分母 条件
CPU-s operation type mix、并发、数据规模
logical/physical I/O operation type cache state、plan
WAL bytes successful write op checkpoint/FPI 状态
durable bytes business entity schema/index/vacuum 周期
network bytes API or transaction TLS、pool、result set
p50/p95/p99 completed op offered/achieved load
failure/late scheduled op timeout/retry policy

只给 $ / TPS 而没有这些条件,不是成本模型,只是报价除法。

26.5.2 增长、保留、备份与维护空间

数据库大小不是一条线

磁盘容量要同时容纳:

live heap
indexes
TOAST
free space and bloat
temporary files
WAL working set
archive staging / backlog
base backups and incrementals
restore and verification workspace
maintenance rewrite workspace
logs, packages and operating system
failure/rebuild headroom

先用原生 size function 建立对象层 inventory:

SELECT
    pg_size_pretty(pg_database_size(current_database())) AS database_size;

SELECT
    n.nspname,
    c.relname,
    pg_relation_size(c.oid) AS main_fork_bytes,
    pg_indexes_size(c.oid) AS index_bytes,
    pg_total_relation_size(c.oid) AS total_bytes
FROM pg_class AS c
JOIN pg_namespace AS n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r', 'm')
  AND n.nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY pg_total_relation_size(c.oid) DESC;

这些函数的语义与可选 fork 见官方 Database Object Size Functions

注意:

pg_relation_size(table)
  main fork only by default

pg_table_size(table)
  table forks + TOAST, excluding indexes

pg_total_relation_size(table)
  table + TOAST + indexes

pg_database_size(database)
  database total

不要把其中两项相加造成重复计数。

从业务增长率推导 live data

对业务实体 $i$:

$$ G_{\text{live/day}}

\sum_i N_{i,\text{day}} \times B_{i,\text{durable}}

G_{\text{purged/day}} $$

若保留 $R$ 天,且每日量以 $g$ 增长:

$$ N_R

N_0 \sum_{d=0}^{R-1}(1+g)^d $$

不要先把“当前日增量 × 保留天数”写死,再在运营增长时惊讶。还要单独模拟:

  • 热数据、温数据、冷数据保留;
  • update/delete churn;
  • 索引增长;
  • partition attach/detach;
  • 法规保留与 legal hold;
  • tenant/region/skew;
  • schema 变更增加的列和索引。

容量表应按周或月保存 actual vs forecast:

date
live rows
heap bytes
index bytes
TOAST bytes
dead tuples
WAL bytes/day
backup bytes
archive backlog
forecast P50/P90

forecast 不是只更新未来;它也要用实际误差校准。

bloat 与 free space 不是同义词

PostgreSQL MVCC update/delete 会产生 dead tuple。普通 VACUUM 通常把空间留在 relation 内供复用,并不把大部分文件空间还给文件系统。于是:

relation allocated bytes
  != live tuple bytes
  != irrecoverable waste

容量规划需要同时问:

  • 这部分 free space 能否被相同 relation 的后续写入复用?
  • workload 的 update pattern 会不会跨页,使复用率低?
  • index 是否发生结构性膨胀?
  • autovacuum 能否在增长速度下追上?
  • freeze horizon 和 long transaction 是否阻止清理?
  • 真正回收文件系统空间需要哪种 rewrite,以及多大 workspace?

不要看到 30% “bloat estimate” 就自动采购 30% 磁盘,也不要因为 vacuum 后文件没缩小 就认定 vacuum 无效。

WAL 是速率、窗口与失效状态的乘积

WAL 容量不是固定 “max_wal_size”。要考虑:

steady generation rate
checkpoint behavior
archive throughput and outage
replication slots
slow/offline replicas
backup and restore windows
wal_keep_size / sender retention
logical decoding
promotion and timeline history

若 WAL 生成率为 $W$ bytes/s,允许 archive 中断 $T_a$ 秒,安全因子为 $h$:

Sarchive backlogW×Ta×h S_{\text{archive backlog}} \ge W \times T_a \times h

若 replication slot 的 consumer 可离线 $T_s$:

Sslot retentionW×Ts×h S_{\text{slot retention}} \ge W \times T_s \times h

但 slot retention 可能无界,除非使用并验证 max_slot_wal_keep_size 等保护。 PostgreSQL 的 WAL configurationlog-shipping standby 说明了 checkpoint、归档、streaming 与 retention 之间的关系。

容量规划至少演练:

archive destination unavailable
replica offline
logical subscriber stalled
backup exceeds expected duration
write burst crosses checkpoint

正常状态的 WAL 目录大小不能代表这些失效状态。

备份容量用 retention graph,而不是压缩率愿望

备份 inventory 应包括:

full/base backup
incremental/differential data
WAL needed for PITR
retention generations
off-site copies
temporary upload/download
restore verification copy
metadata/catalog
encryption/compression overhead

备份压缩率随 data type、page utilization、encryption 与 compression method 变化。 应直接测量:

$$ \text{compression ratio}

\frac{\text{stored backup bytes}} {\text{logical source bytes}} $$

并分别保存 full 与 WAL 的 ratio。生产采购不能假设日志、JSON、already-compressed payload 与 encrypted data 都能获得同一压缩率。

retention 示例:

7 daily + 4 weekly + 12 monthly

不能简单乘以 23,因为增量链可能共享 block,full schedule 也不同;应从备份系统 catalog 汇总真实 object graph,并验证删除一代不会破坏仍保留的恢复链。

维护和迁移需要临时的第二份数据

以下动作可能需要额外 workspace:

动作 可能的额外空间
CREATE INDEX / REINDEX 新 index + sort/temp + WAL
table rewrite 新 heap + indexes + WAL
VACUUM FULL / CLUSTER relation rewrite + indexes + WAL
major upgrade in-place/link/copy 策略不同
restore rehearsal 一份可启动的数据 + WAL + logs
replica rebuild base backup staging + receive WAL
logical migration source 与 target 重叠期

ALTER TABLE 并不总是 rewrite,pg_upgrade --link 也不等于没有风险;模型要按实际 变更计划估算,不应用一个永久的 “2x” 代替 runbook。

一个实用的 local filesystem 约束是:

Sfreemax(Sroutine headroom,Slargest planned maintenance,Sfailure backlog) S_{\text{free}} \ge \max( S_{\text{routine headroom}}, S_{\text{largest planned maintenance}}, S_{\text{failure backlog}} )

若多个动作可重叠,应改为相应的和,而不是最大值。

用 time-to-limit 驱动行动

设当前使用量 $S_0$、目标上限 $S_{\text{limit}}$、净增长速率 $G$:

$$ T_{\text{limit}}

\frac{S_{\text{limit}}-S_0}{G} $$

但 $G$ 不应只有一条平均线。至少计算:

P50 trend
P90/P95 growth or burst scenario
retention policy change
failure backlog scenario
largest scheduled maintenance

如果增长非线性,就用时间序列/业务 forecast,不能继续用直线外推。

26.5.3 扩容触发线、提前期与失效假设

trigger 必须早于 capacity limit

一次扩容从发现到获得容量通常经历:

signal sustained
  -> diagnosis and forecast review
  -> decision / budget / approval
  -> procurement or quota
  -> provision
  -> data copy / rebalance / catch-up
  -> validation
  -> traffic shift
  -> rollback observation window

总提前期:

$$ T_{\text{lead}}

\sum_j T_j $$

若保守预测到达上限的时间为 $T_{\text{limit,P90}}$,行动条件应是:

Tlimit,P90Tlead+Tvalidation+Tsafety T_{\text{limit,P90}} \le T_{\text{lead}} + T_{\text{validation}} + T_{\text{safety}}

“磁盘 90% 再扩”不是通用策略。一个需要四周采购、两周复制的集群,90% 可能已经 太晚;一个可在数分钟无状态扩出的 read pool,则可使用不同 trigger。

同时定义 utilization、SLO 与 growth trigger

容量触发器不应只看 CPU:

类别 示例信号 触发语义
demand request/write rate forecast 业务增长越过已验证 envelope
SLO p95/p99、timeout、pool wait 在当前 load 下目标开始失守
CPU busy/run queue/service demand 持续值与 burst/failure headroom
memory working set、temp、swap/PSI reclaim 或 spill 开始恶化
storage latency/queue/throughput 设备服务时间接近边界
space time-to-limit 小于 lead time + safety
WAL generation/archive/slot backlog consumer 无法在允许窗口追上
maintenance autovacuum/checkpoint/backup duration 后台工作超过可用窗口
HA N+1/degraded capacity 故障后 SLO 无法维持

trigger 应具有:

threshold
duration
forecast horizon
scope
owner
runbook
rollback / stop condition

瞬时 CPU 65% 不应自动购买主机;持续两周的需求趋势、在 N+1 状态下预测将超过已验证 SLO envelope,才是可行动的 signal。

以失效状态写容量预算

先定义状态,而不是笼统说 “预留 30%”:

N0  normal: all primary/replica/pool nodes healthy
N1  one data node unavailable or rebuilding
M1  backup + autovacuum + checkpoint overlap
F1  archive destination unavailable for allowed window
F2  one replica/slot consumer stalled for allowed window

每个状态分别计算:

available CPU / storage / IOPS / connection slots
traffic redistribution
WAL accumulation
replication/rebuild load
latency and error SLO
maximum tolerable duration

例如三台 read replica 各承载 30% 峰值,不能据此宣称 N+1;失去一台后其余两台会 各承载 45%,还要叠加 cache coldness、reconnect 与 recovery traffic。N+1 必须在 failure drill 中测。

扩容不等于只加 CPU

观测到瓶颈后,选择与根因匹配的动作:

限制 候选动作 必须验证
CPU execution vertical scale、query/index、JIT/parallel policy plan、tail、failure
read throughput replica/read cache、query optimization freshness、一致性、routing
write/WAL batch/schema/index、faster WAL device durability、replica/archive
lock/contention shorten tx、partition key space、admission correctness、公平性
connection PgBouncer、pool sizing、admission session semantics、queue SLO
storage latency storage class/layout、reduce random I/O fsync tail、failure behavior
data volume retention/partition/archive/shard restore、query、operability

max_connections 变大通常不是 capacity expansion。它可能把受控 queue 从 pool 移入 database,并增加 backend memory、context switch 和 lock competition。

Pigsty 把 topology、HAProxy/PgBouncer、PostgreSQL、监控和运维入口组织在一起, 方便执行变更;它不会替代业务 workload 与 SLO 的容量判断。扩容后仍要在实际 connection path 上重跑 baseline,并用 Pigsty PGSQL dashboards 与 PostgreSQL 原生统计双重验证。

把容量结论写成有期限的决策记录

一条可审计的容量结论应包含:

decision: "当前 topology 是否足以覆盖下一预测窗口"
environment: "硬件、版本、配置、connection path"
workload: "operation catalog、mix、arrival model、data scale"
slo: "latency/error/freshness/durability"
evidence: "run ids、raw artifacts、dashboards、queries"
validated_envelope: "哪些点通过,哪些点失败"
failure_state: "N0/N1/M1/F1/F2 中验证了哪些"
forecast: "P50/P90 demand and growth"
trigger: "threshold + duration + horizon"
lead_time: "provision + copy + validation + shift"
unknowns: "尚未测量的条件"
expires_at: "何时必须复审"
owner: "谁监控、谁决策、谁执行"

任何一个重大条件变化都使 baseline 进入复审:

PostgreSQL/Pigsty or kernel/storage upgrade
schema/index/query change
workload mix or SLO change
data crosses tested scale
topology/routing/pool change
hardware or cloud instance change
backup/replication/durability policy change
material performance incident

容量模型的价值不是预测一个永远正确的 TPS,而是让团队在还有选择的时候,看见:

what is known
under which conditions
how fast the boundary is approaching
how long the response takes
which failure would invalidate the plan

上一节:找到饱和点与瓶颈 · 返回本章目录 · 下一节:实战:pg36_shop 容量基线 · 查看全书目录 · 查看索引中心

26.6 实战:`pg36_shop` 容量基线

本节把前五节收敛成一份可执行实验。目标不是发布一个炫目的 TPS,而是完整回答:

在记录过的 Pigsty 教学沙箱上,pg36_shop 的固定读写混合,面对三档合成 数据规模和一、八两个并发档位时,吞吐、延迟、CPU、I/O、WAL 与空间怎样变化?

实验边界刻意很窄:

environment   Pigsty disposable teaching sandbox
server        pg-test-1, primary, 1 vCPU, about 2 GiB
client        pg-meta-1, separate host, 2 vCPU, about 4 GiB
PostgreSQL    18.6
path          direct primary :5432
workload      synthetic shop-mix-v1
arrival       closed-loop, zero think time
protocol      prepared, persistent connections
scale         S / M / L
clients       1 / 8
repetitions   5 per cell

它不测 HAProxy、PgBouncer、应用、WAN,也不审批生产容量。完整合同在 lab-contract.md,公共参考结果在 capacity-run.json

26.6.1 运行三个规模和两个并发档位

先辨认风险等级

本章 runner 有六个动作:

动作 等级 行为
lint L0 本地校验合同和反例
capture L0 只读采集目标身份与环境
exercise L2 bounded 创建、装载、压测、清理专用 fixture
verify L0 验证已有证据
review L0 独立复算与审阅已有证据
all L2 bounded 顺序执行完整流程

exercise/all 会真实消耗 CPU、I/O、WAL、复制与归档资源。只允许在 disposable teaching sandbox 运行,绝不能把“只创建测试库”误判为 L0。

runner 只接受:

cluster       pg-test
primary       pg-test-1 / 10.10.10.11
database      pg36_capacity
role          dbuser_pg36bench
environment   pg36-l2-vagrant disposable teaching sandbox

若目标身份、primary、marker、预存对象或 session 状态不符合合同,它会 fail closed,而不是“尽量继续”。

fixture 与权限边界

实验创建两个一次性对象:

database  pg36_capacity
login     dbuser_pg36bench, non-superuser

库和角色都带本次 run 的 shared-object comment marker。清理前必须重新读回 marker, 并确认没有非本章会话;清理不用:

DROP DATABASE ... WITH (FORCE)

也不终止其他 session。

Pigsty 当前内网 HBA 通过 +dbrole_readonly 角色组识别业务连接。runner 不修改 HBA; 它把临时 login 加入 dbrole_readwrite,后者已间接属于 readonly 组,同时把临时 membership 设置为:

INHERIT FALSE
SET FALSE

这个 membership 只参与 HBA member 判断,不允许 benchmark login 继承或切换成平台 角色。实验结束后角色与 membership 一并删除。

这条细节揭示了压测中常见的错误:为了“让测试先跑起来”修改 HBA、使用超级用户, 最终测到的 connection/auth path 与生产不同,还留下永久权限。

数据规模

setup.sql 用固定 seed 生成三档 synthetic data:

scale factor customers products historical orders
S 1 10,000 2,000 100,000
M 8 80,000 16,000 800,000
L 32 320,000 64,000 3,200,000

参考 run 实际初始化结果:

scale schema bytes database bytes initialization
S 29,704,192 37,869,247 2.17 s
M 235,175,936 243,594,943 17.09 s
L 942,268,416 950,982,335 84.82 s

三档不是简单复制行数。每次切换 scale 都重新生成、ANALYZE 并捕获 row count、 relation size 与统计 provenance。压测报告必须保存 observed size,而不是只写 scale=32

事务合同

operation weight key distribution transaction
read-product 50% product Zipf 1.15 商品与库存点查
read-order 30% customer Zipf 1.08 最近五个历史订单
place-order 20% customer uniform、product Zipf 1.10 行锁、扣库存、追加订单

三个 pgbench script 分开保存:

每个 script 自己定义 transaction 边界。place-order 的锁和库存检查是 workload 语义的一部分,不应为了提高 TPS 删除。

实验矩阵

每个 scale 分别运行 c1、c8,每个 cell 五次:

3 scales × 2 client counts × 5 repetitions = 30 measured runs

每次:

5 s natural warm-up
8 s measured window
prepared protocol
persistent connection
zero think time
250 ms latency limit
max tries = 1

并发顺序做 counterbalance,seed 由公开公式从 scale、clients、repetition 推导。 这不能消除所有 background noise,但能避免总是先跑 c1、后跑 c8 的固定时间偏差。

runner 明确禁止:

statistics reset
OS cache drop
forced checkpoint
PostgreSQL restart
failover
autovacuum pause
archive/replica/backup pause
fsync or synchronous_commit disable
unlogged production-equivalent tables

如果 baseline 只有在关掉 durability 后才“通过”,它没有测量目标系统。

运行方法

先做不连接目标的本地检查:

static/labs/ch26/task.sh lint

完整实验要使用一个不存在或为空的 私密绝对路径

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

也可分步:

export PG36_EVIDENCE_DIR=/absolute/private/new-empty/ch26-run
static/labs/ch26/task.sh capture
static/labs/ch26/task.sh exercise
static/labs/ch26/task.sh verify
static/labs/ch26/task.sh review

capture/all 拒绝覆盖非空 evidence directory。verifyreview 可以反复读取 已有 evidence,不重跑 workload。

不要把私密目录放进 web root 或 Git。它包含 raw transaction log、system sample、 SQL snapshot 和监控响应;公共仓库只提交经过 allowlist 的摘要。

参考 run 的结果

正式参考 run:

run id          6c44ebdb-2206-48c3-8089-d90fdff45204
transactions    511,709
measured runs   30
failures        0
late            0
deadlocks       0
temp bytes      0

每个 cell 的 TPS 是五次 repetition 的中位数;括号是 repetition bootstrap 95% 区间。latency quantile 从该 cell 的 raw transaction sample 合并后重算,不是 五个 p95 再取平均。

cell TPS median (bootstrap 95%) p50 p95 p99 max
S-c1 1,508.5 (1,446.5–1,546.3) 0.334 2.152 2.595 20.434
S-c8 2,920.5 (2,869.3–3,005.1) 1.334 9.448 12.927 56.481
M-c1 1,555.8 (1,462.0–1,570.1) 0.330 2.123 2.598 10.774
M-c8 2,911.5 (2,755.4–2,930.3) 1.370 9.398 12.571 41.174
L-c1 1,423.6 (1,226.1–1,431.4) 0.360 2.259 2.847 30.307
L-c8 2,774.3 (2,216.1–2,824.4) 1.447 10.087 14.494 94.330

latency 单位均为 ms。L-c8 的第一次 repetition 约 2,216 TPS,后续约 2,740–2,824 TPS;实验没有把它删成“异常值”。它可能包含初始化/cache/background 效应,宽区间本身就是证据,下一轮应延长 duration 并增加 repetition。

PostgreSQL 官方 pgbench good practices 明确提醒,数秒测试不能得到可信的平均值,且 client machine 也可能成为瓶颈。 所以这组 8 s × 5 结果用于教学如何建立证据管道和发现下一问题,不是 production characterization。

26.6.2 用 Pigsty 与原生视图解释饱和点

四层证据各回答一个问题

每个 measured run 同时采集:

pgbench transaction log
  -> client observed completion and latency distribution

PostgreSQL start/end snapshots
  -> committed work, blocks, temp, deadlock, WAL delta

PostgreSQL wait samples
  -> active session currently executing or waiting for what

client/server OS samples
  -> CPU work/iowait and load-generator saturation

Pigsty time series
  -> full exercise context and independent corroboration

不能用其中一层替代所有层。例如 p95 增加并不能单独证明 CPU 饱和;CPU 84% 也不能 单独证明 p95 来自 CPU queue。

sampler 必须严格裁剪 measured window

原始 sampler 从 warm-up 前启动、pgbench 后结束是正常的,但 cell arithmetic 必须 只使用:

measured pgbench start <= sample timestamp <= measured pgbench end

参考实现保存 start/end monotonic timestamp,并对 client/server sample 严格裁剪。 如果把 pgbench 结束后两秒 idle tail 算入 CPU median,单位事务 CPU 会被系统性低估。 增加 repetition 无法修复这种 systematic error。

Pigsty query 的 full exercise window 则故意保留:

initialization
warm-up
measured runs
between-run gaps
cleanup

因此:

native per-run evidence
  authoritative for cell arithmetic

Pigsty full-window evidence
  authoritative for context and corroboration

两个窗口不能直接做逐项相等断言。

server 已进入高 CPU 区域,client 没有饱和

参考结果:

cell server work median server iowait median client work median
S-c1 48.94% 7.66% 17.74%
S-c8 84.19% 0.21% 29.50%
M-c1 50.10% 7.52% 17.79%
M-c8 83.96% 0.20% 29.07%
L-c1 46.31% 8.20% 16.17%
L-c8 84.22% 0.31% 27.81%

client 是独立 2-vCPU 主机,c8 work ratio 仍低于 30%;当前证据反对“load generator 已先饱和”这一解释。server 只有 1 vCPU,c8 约 84% work,CPU pressure 是合理的 候选瓶颈。

但不能立即下结论:

root cause = CPU

还要检查 active/wait、lock、WAL sync、I/O、context switch 与 plan。单核上的 84% aggregate work 也不等于每一毫秒都有可运行 SQL。

throughput 增长约 1.9 倍,p95 放大约 4.4 倍

scale c8/c1 TPS c8/c1 p95
S 1.936x 4.390x
M 1.871x 4.427x
L 1.949x 4.465x

解释:

c1 -> c8
  throughput still rises materially
  tail latency rises much faster
  server CPU moves toward high utilization

这说明 queue/competition 已增加,却仍不能定位精确 knee。只有两个并发点:

exact_knee_known = false
interpretation = knee-not-bracketed-by-one-and-eight-clients

下一轮至少补:

c2 c4 c8 c12 c16 c24 c32

并用更长 duration。若目标是 arrival SLO,再做 open-loop rate sweep,记录 offered、 achieved、late、skipped、failure 和 schedule lag。

数据规模开始改变 I/O 行为

cell block reads block hits
S-c1 0 675,976
S-c8 0 1,323,054
M-c1 0 733,198
M-c8 0 1,326,143
L-c1 2,500 629,335
L-c8 14,491 1,344,632

S/M 的 measured window 没记录到 PostgreSQL block read,L 则开始读 block。这里的 边界是:

  • 自然 warm cache,不代表 cold start;
  • PostgreSQL block read 可能由 OS page cache 服务;
  • 短窗口未覆盖完整 working set;
  • aggregate hit ratio 会掩盖特定 table/index;
  • 虚拟磁盘不能代表 production storage。

下一轮可以选择:

保持自然状态,延长到跨越多个 working-set cycle
增加 XL 数据量,使 working set 显著超过 RAM
分别测试 steady warm 和 restart-recovery 场景
从 pg_stat_io 分解 relation/temp/WAL context

不要为了“可重复 cold cache”在共享主机随意 drop_caches。那是高影响系统动作, 而且同时改变整台主机。

WAL 与持久化证据

cell WAL bytes/mixed tx durable bytes/place-order
S-c1 160.33 415.21
S-c8 159.53 409.12
M-c1 175.95 412.81
M-c8 161.88 405.97
L-c1 157.56 411.50
L-c8 196.18 407.88

这些值是窗口 delta 除以相应分母,不是 PostgreSQL 配置常量。L-c8 的 WAL/tx 较高 可能与 page state、full-page images 或背景活动有关;仅凭 aggregate delta 不能 定位。下一轮应同时比较:

wal_records
wal_fpi
wal_bytes
checkpoint timing
per-operation successful count

实验没有关闭 fsyncfull_page_writessynchronous_commit,也没有强制 checkpoint。若要研究 checkpoint phase,应把它作为显式 factor,而不是偷偷在每次 run 前 checkpoint。

lock、failure 与 temp 的反证

30 个 measured run 中:

failed transactions  0
late transactions    0
deadlocks            0
temp bytes           0

这反对“当前点因 deadlock/temp spill 失效”,但不能证明:

there is no lock wait
there can never be a deadlock
all queries are memory-safe
250 ms SLO will hold at higher offered load

deadlock counter 只计被 detector 处理的 deadlock;普通 row-lock queue 不是 deadlock。temp_bytes=0 也只针对本窗口与当前 plan/data。

在 Pigsty 中做独立旁证

Pigsty 的 PGSQL dashboards 把 Overview、Activity、Persist、Database、Table、Query 等视角连接起来。实验时 至少对齐:

PGSQL Activity
  active/wait/backend state

PGSQL Persist
  WAL/checkpoint/archive/replication

PGSQL Database
  commit/rollback/cache/temp/deadlock

PGSQL Table / Query
  relation workload and statement behavior

NODE / disk dashboards
  client and server CPU, disk, network

参考 run 的 Pigsty full-window 摘要:

signal median maximum
server CPU busy 64.37% 100.00%
server CPU iowait 2.77% 5.10%
client CPU busy 23.00% 28.97%
server disk read 0.41 MiB/s 13.34 MiB/s
server disk write 4.69 MiB/s 77.45 MiB/s
WAL generation 44.04 MiB/s
database commit 2,459.53/s
replica replay byte gap 0 median 3,471,848 max

full-window 含初始化和间隔,所以 commit max 不应等于 cell TPS,disk write max 也不应 除以 measured transaction。它们用于确认“什么时候发生了什么”和检查观测层是否 互相矛盾。

replica replay byte gap 最大约 3.47 MiB,不能直接转译为:

replica stale for N milliseconds

byte gap、replay time lag、应用 freshness 与 synchronous durability 是不同语义。 容量实验只记录这一旁证,没有声称 replica correctness。

建立 hypothesis table

这次实验后的调查表:

假设 支持证据 反证/缺口 下一实验
c8 有 CPU pressure server work ~84%、client <30% 无 run queue/更密曲线 c2–c32 sweep
client 先饱和 client work <30% 增大 clients 并监控 client queue
L 进入数据读路径 block reads >0 可能命中 OS cache XL、pg_stat_io、更长运行
lock 是主瓶颈 写事务会行锁 无 deadlock,wait 需更细分 提高热点/写比例
WAL 限制 throughput L-c8 WAL/tx 增加 iowait 低,缺 WAL fsync tail write-only + checkpoint factor
exact knee 已知 仅 c1/c8 密集 closed/open-loop sweep

这张表比“CPU 是瓶颈”更有用,因为它告诉下一笔实验预算该花在哪里。

26.6.3 输出可复现实验报告、容量模型和未知项

私密 evidence bundle

完整运行产生:

preflight-evidence.json
remote-benchmark.log
remote/
  capacity-evidence.json
  initialize-{S,M,L}.txt
  runs/<cell-run>/
    pgbench.stdout
    pgbench.stderr
    transactions.*
    stats-before.json
    stats-after.json
    client-system.jsonl
    server-system.jsonl
    database-waits.jsonl
remote-cleanup.json
validation-report.json
negative-report.json
public-summary.json
review.txt

证据包目录权限为 0700,文件为 0600。review 会检查 mode,避免 raw evidence 因默认 umask 变成多用户可读。

raw transaction log 对 latency 重算必不可少,却可能含时间、client/session 与 workload 行为;SQL snapshot、queryid 明细和监控 payload 也不应直接公开。发布流程 是:

private raw evidence
  -> schema and invariant validation
  -> independent recomputation
  -> explicit allowlist
  -> public capacity-run.json

不是“从 raw JSON 删除几个看起来敏感的键”。

报告中的 provenance

一份可复现实验报告至少固定:

run id and timestamps
target identity and topology
client/server hardware
PostgreSQL/Pigsty/OS version
settings with source
schema and source hashes
data generator and seeds
connection path and protocol
workload scripts and weights
arrival model and retry policy
warm-up/duration/repetition/order
measured-window boundaries
raw artifact inventory
cleanup evidence
known unknowns and claim gate

参考 run 验证了 255 个 raw artifact,并对约 21.8 MB evidence 做 secret scan。这个 数字不是质量本身;它的意义在于 manifest 能证明报告引用的 sample 没有悄悄缺失。

对抗性验证

negative-cases.json 定义了 26 个变体,validator 必须逐一拒绝。类别包括:

wrong target / primary / environment
pre-existing or marker-mismatched fixture
client and server accidentally same host
missing runs or unbalanced matrix
wrong script weight / seed / protocol
statistics reset
sampler outside measured window
client saturation hidden
quantiles averaged instead of recomputed
failed/late/skipped hidden
cleanup unproven
raw/query/secret fields leaked into public summary
exact knee or production TPS fabricated

“positive fixture 能通过”只证明 happy path;错误 evidence 也能通过的 validator 不能保护结论。

参考 run 的验收:

positive validation       passed
counterexamples rejected  26 / 26
database cleanup          verified
role cleanup              verified
remote temp cleanup       verified
nonchapter sessions killed 0
production gate           pending

容量模型只发布条件结论

公共报告给出的 65% CPU 投影:

scale CPU-s/mixed tx conditional TPS at 65% CPU
S 0.000291 2,236.79
M 0.000292 2,222.96
L 0.000307 2,116.98

它依赖四个显式假设:

50/30/20 mix unchanged
CPU demand near observation scales linearly
cache/IO/lock/client/background work does not become tighter
same recorded sandbox

所以报告写:

{
  "production_sustainable_tps": null,
  "exact_knee_known": false
}

null 不是实验失败,而是 claim 与 evidence 对齐。

未知项就是下一轮 backlog

本轮没有回答:

  1. c8 左右的精确 knee 在哪里;
  2. open-loop offered rate 下的 completion、late、skipped 与 queue;
  3. 真实应用经 HAProxy/PgBouncer/TLS 的端到端成本;
  4. realistic think time、connection churn、retry storm;
  5. 真实 query 和 tenant/key skew;
  6. cold/restart、working set 超 RAM 与 production storage;
  7. checkpoint、autovacuum、backup overlap;
  8. replica loss、failover、rebuild、archive outage;
  9. 长稳态下 bloat、WAL、slot 与空间增长;
  10. production hardware 的 N+1 SLO envelope。

将它们排成下一轮最小实验:

Experiment A
  same sandbox, M scale
  c2/c4/c8/c12/c16/c24/c32
  5 min × repetitions
  purpose: bracket knee

Experiment B
  rates around accepted SLO point
  open-loop, realistic timeout/retry
  purpose: offered-load envelope

Experiment C
  direct / PgBouncer / HAProxy / application path
  same workload and target
  purpose: attribute service-path cost

Experiment D
  accepted load + maintenance/failure scenarios
  purpose: validate headroom

Experiment E
  production-like hardware and data
  N0 and N1
  purpose: production capacity decision

一次只改变少量 factor,保留 control;否则结果发生变化时无法归因。

独立练习

练习一:改变 mix。

把写比例从 20% 提到 40%,保持总权重 100%。运行 lint,解释哪些合同 hash、WAL 模型 和 capacity claim 必须失效。不要直接修改并覆盖正式参考结果。

练习二:增加并发点。

复制 experiment contract 到个人分支,加入 c2/c4/c12/c16,设计 counterbalanced order。 先写出“怎样才算 bracket knee”的 machine-checkable rule,再运行。

练习三:检查窗口偏差。

从一个私密 evidence bundle 取某次 run,分别计算:

all sampler records CPU median
strict measured-window CPU median

解释 idle tail 对 CPU-s/transaction 和 65% 投影的方向性影响。

练习四:设计 production gate。

为自己的应用补齐:

SLO
operation catalog
arrival forecast
N+1 topology
maintenance overlap
lead time
pass/fail rule
stop condition

如果仍写不出 production sustainable TPS,保留 null,并列出最小缺失证据。

完成检查表

  • 能区分 business request、database transaction 与 SQL statement。
  • workload mix、arrival model、key distribution 和 connection path 已固定。
  • client 与 server 分离,且两侧都监控。
  • warm-up、duration、repetition、seed 与 run order 可复现。
  • per-run sample 严格裁剪 measured window。
  • quantile 从 raw sample 重算,没有平均 p99。
  • PostgreSQL counter 以 snapshot delta 计算,没有 reset shared stats。
  • Pigsty full-window 指标没有冒充 cell arithmetic。
  • failure、late、skipped、deadlock、temp 与 cleanup 均显式报告。
  • unit resource cost 的分母是明确 operation。
  • exact knee 不由两个并发点伪造。
  • production TPS 在生产证据不足时保持 null
  • 未知项已变成带目的和 pass/fail rule 的下一实验。

做到这些,容量压测才从“跑过一张 TPS 截图”变成可重复、可反驳、可用于决策的工程 过程。


上一节:从测量推导容量与成本 · 返回本章目录 · 下一章:精益求精:参数调优与资源治理 · 查看全书目录 · 查看索引中心