胸有成竹:容量规划与压测基线
26 胸有成竹:容量规划与压测基线
一张写着“12 万 TPS”的截图,没有回答容量问题。
它可能来自:
也可能来自一个完全严谨的实验。数字本身无法告诉你是哪一种。
容量规划真正需要的是一条可审计推理链:
本章把 PostgreSQL 的原生证据与 Pigsty 的观察面放到同一条链上。目标不是教你 “跑一个 pgbench 命令”,而是让你能判断:
- workload 是否代表业务;
- 实验是否控制了足够多的变量;
- load generator、连接路径或缓存是否在替 server 背锅;
- throughput 上升是否以 tail latency、错误或排队为代价;
- 一次测量能支持什么结论,不能支持什么结论;
- 怎样把测量变成 CPU、WAL、存储和提前期模型;
- 为什么生产容量仍需要故障、维护、备份和 open-loop 场景。
本章实验先给出一个反直觉结论
第 26 章参考运行不是“性能榜单”,而是一组教学用、有界的证据链:
- Pigsty
v4.5.0教学沙箱; - PostgreSQL
18.6; - 独立
pg-meta-1load generator:2 vCPU、约 3.8 GiB RAM; pg-test-1primary: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:
这说明八个 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。
本章学习成果
完成本章后,你应该能独立完成:
- 把业务 forecast 转成 operation-class arrival model,而不是只写“读写比 8:2”;
- 区分 request、SQL statement、database transaction、connection 与并发;
- 用 Little’s Law 检查到达率、响应时间和在途量是否自洽;
- 选择内置 pgbench 作为 engine calibration,或写业务自定义脚本;
- 声明 key distribution、mix、think time、arrival model、protocol 和连接路径;
- 固定版本、配置、数据、随机 seed、预热、顺序和重复;
- 正确处理 pooled percentile、run-level estimate、置信区间与异常样本;
- 用
pg_stat_database、pg_stat_io、pg_stat_wal、wait event 和 OS 证据解释曲线; - 判断 load generator 是否成为瓶颈;
- 把 CPU seconds/transaction、WAL bytes/transaction 和 bytes/order 写进容量模型;
- 计算增长、保留、maintenance workspace、failure headroom 与 lead time;
- 明确保留 unknown,并拒绝把 sandbox 结果升级为生产承诺。
本章目录
26.1 从需求建立容量模型
26.2 设计代表性工作负载
26.3 建立可信实验
26.4 找到饱和点与瓶颈
26.5 从测量推导容量与成本
26.6 实战:pg36_shop 容量基线
阅读与实践路线
如果你负责应用:
重点是 operation contract、arrival model、idempotency/retry、connection path 和 load generator。
如果你负责平台:
重点是实验控制、native counters、Pigsty time series、headroom、failure model 和 provisioning lead time。
两条路线最终必须合流。平台无法从数据库指标猜出业务 mix;应用也不能从一张 TPS 表判断 WAL、backup、replica 和 maintenance 是否还有余量。
实验文件
它们分别固定:
| 文件 | 责任 |
|---|---|
| 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 |
参考资料
- PostgreSQL 18
pgbench - PostgreSQL 18 cumulative statistics
- PostgreSQL 18 resource consumption
- PostgreSQL 18 WAL configuration
- PostgreSQL 18 streaming replication
- Pigsty monitoring architecture
- Pigsty PostgreSQL dashboards
- Prometheus histogram and quantile practice
上一章:望闻问切:监控体系与可观测诊断 · 返回下卷导读 · 下一章:精益求精:参数调优与资源治理 · 查看全书目录 · 查看索引中心
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 开始:
第 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 可能:
于是:
如果把 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 |
同为“读”:
CPU、buffer、I/O、work_mem、parallel worker 和 lock footprint 完全不同。
同为“写”:
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% 写”没有说明计数单位:
参考实验按 pgbench transaction 选择脚本:
这是 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 做第一轮自洽检查
稳定系统中,平均在途量满足:
若:
则平均在途 request:
这不是说“必须给 PostgreSQL 160 个连接”。应用中的在途量可能分布在:
如果 transaction 只占 15 ms:
一个 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。
至少分开:
参考实验三档 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 证据。
本目产物:业务容量输入表
不要在这个阶段填“8 vCPU”。先把问题写完整。
26.1.2 平均值、峰值、突发与批处理叠加
日平均掩盖容量风险
一天 86,400 秒。日订单 8,640,000:
但若 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 需要:
区分 arrival burst 与 backlog drain
两种看起来都像“TPS 突然上升”:
后者常在依赖恢复后发生:
如果模型只用前台 request,遗漏 backlog,恢复本身就可能触发第二次事故。
重试是负载放大器
每个原始 operation 平均尝试次数:
$$ A
1 + r_1 + r_2 + \cdots $$
更实用地:
$$ \lambda_{\text{database}}
\lambda_{\text{eligible}} \times \text{attempts per eligible operation} $$
例如:
平均尝试:
数据库看到约 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 通过,不等于生产通过。
平均、quantile 与最大值各有用途
不能把每分钟 p99 再做平均并称为“全天 p99”。若需要跨 window 或 instance 聚合,保留原始 transaction sample 或可聚合 histogram。Prometheus 的 histogram 指南 解释了为什么直接平均预计算 quantile 在统计上没有意义。
26.1.3 延迟目标、错误预算与安全余量
容量是带约束的可行域
把 capacity 定义为:
例如:
没有约束的“最大 TPS”只是最大努力点。
延迟目标要定义 measurement boundary
这些延迟不相等:
参考实验的 latency:
它不包含 application edge、HAProxy、PgBouncer 和 WAN,所以不能与第 24 章
place-order 用户 SLO 直接画等号。
错误不能从分母消失
报告至少包含:
若只用:
$$ \text{TPS}
\frac{\text{successful}}{\text{elapsed}} $$
系统可以通过拒绝慢请求让成功样本看起来更快。failed、skipped 和 admission rejection 必须与 latency 并列。
参考实验固定 --max-tries=1,不让 pgbench 把 serialization/deadlock retry
隐藏到成功事务里;250 ms 只计 late。正式运行:
这只适用于 511,709 个合成事务。零次观察不是“真实概率为零”的证明。
错误预算不是容量余量
SLO 允许 0.1% bad event,不代表正常运行可以把资源推到 99.9%:
若平时已经在 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 作教学投影点:
得到 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 应由:
共同决定,而不是复制 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 首先是一个模型,然后才是一条命令。
模型遗漏 hot key,脚本跑得再稳定也只能稳定地回答错误问题;连接路径从 PgBouncer 换成 direct primary,数字仍然精确,但已经是另一个实验。
本节用 pgbench 作为 workload driver。它的优点是与 PostgreSQL 同源、部署
简单、支持自定义事务、权重、随机分布、rate、transaction log 和失败统计。
它不是应用模拟器,也不会自动知道你的业务语义。
26.2.1 内置 pgbench 与业务自定义脚本
内置场景适合 calibration,不是业务证明
PostgreSQL 18 提供:
用途:
| 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 会改变结果。
所以内置场景的正确位置是:
不是:
自定义脚本先定义 transaction boundary
一个 pgbench script 被调度一次,pgbench 就把它计作一个“transaction”,但 脚本未必真的包含一个 SQL transaction:
最后一种会让 retry 与业务原子性变得难以解释。PostgreSQL 文档也提醒,若脚本 包含多个 transaction,serialization/deadlock retry 会重放整个脚本,已经成功 提交的 transaction 可能再次执行。
本章 place-order.sql 保持“一次 script = 一个显式 DB transaction”:
它保留:
- 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 支持:
每次选择 script 的概率按相对整数权重决定。权重不必和为 100,但和为 100 更 容易审查。
正式报告仍应读取 transaction log:
检查实际 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 固定:
它与应用是否使用 prepared statement 必须一致。改变 mode 会改变:
- parse/plan CPU;
- network round trip;
- parameter typing;
- generic/custom plan 行为;
- PgBouncer transaction pooling 兼容边界;
pg_stat_statementsshape。
参考脚本曾在真实预热中暴露:
原因是 prepared placeholder 的两个 operand 都是 unknown。修复不是切回 simple, 而是明确:
这类问题说明 smoke/warm-up 必须使用正式 protocol。
内置与自定义可以组成两层基线
推荐:
Layer A 漂移、Layer B 也漂移:
Layer A 稳定、Layer B 漂移:
这比只有一条“总 TPS”更容易定位回归。
初始化也属于合同
内置 pgbench -i 会创建并可能销毁标准表。官方文档明确警告,初始化会删除
同名表,应使用独立数据库。业务脚本也必须同样谨慎。
本章:
existing database/role 一律拒绝覆盖;完整 evidence 后只删除 marker 精确匹配且 无其他 session 的 fixture。
26.2.2 参数分布、事务混合和数据倾斜
uniform 往往是最不真实的默认
均匀分布:
意味着每个 customer/product 被访问的概率相同。真实业务常见:
它们改变 cache locality 和 contention。
pgbench 提供:
参考合同:
| 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:
所以“Zipf 比 uniform 更真实”仍然不完整。要同时验证:
- read locality;
- update collision;
- tenant fairness;
- hot partition/page;
- index leaf split;
- per-key rate cap。
保留变量之间的相关性
独立随机:
会生成现实中不存在的组合。真实 workload 可能:
相关性会影响:
- 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 |
记录:
只写 scale factor 不够。
transaction mix 应来自同一测量边界
错误组合:
分母不同,无法相加。
更可靠:
第 25 章 signal contract 尚未提供真实 pg36_shop application SLI,所以第 26
章的 50/30/20 是显式 synthetic assumption,而不是从在线业务观测得出的事实。
写操作要保留写放大
一个 place-order 不只增加一行:
若 benchmark 去掉约束与索引,TPS 更高,但它测的是另一个数据模型。
failed branch 也要模拟
业务中存在:
参考脚本为保证 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:
这是 closed-loop。对 $C$ 个 client:
其中 $R$ 是 response time,$Z$ 是 think time。参考 run:
当 server 变慢,client 发得也慢;offered load 自动下降。这会隐藏真实系统中仍在 到达并排队、超时或放弃的 request。
open-loop:
PostgreSQL 18 的 --rate 按 Poisson timeline 调度 transaction;报告的 latency
从 scheduled start 计算,包括 schedule lag。若已经来不及满足 latency limit,
transaction 会被记为 skipped。详见
pgbench --rate。
生产 SLO envelope 更需要:
参考实验只做 closed-loop,所以 production_sustainable_tps=null。
think time 是 workload,不是装饰
真实用户:
worker queue:
如果要模拟 closed user population,加入 \sleep 或外部 pacing;如果要模拟
arrival rate,优先使用 rate schedule。不要一边设 think time,一边把结果称为
“数据库最大 TPS”。
连接模式要单独测试
持久连接:
每个 client 保留 connection。
每事务重连:
测量:
- TCP/TLS;
- authentication;
- backend fork/init;
- session GUC;
- extension hooks;
- connection storm。
它不是正常 pool 模式的替代。
连接路径至少分:
将它们混在一条曲线里,回归后无法知道变化发生在哪里。
PgBouncer 不会增加 PostgreSQL CPU
pool 的价值主要是:
它不能凭空创造 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 自己可能成为瓶颈。
观察:
参考 run:
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 内做外部工作。
必须结合:
不能看到 Client wait 占多数就宣布“数据库没问题”。
workload contract 最小模板
没有这份合同,数字不能离开终端。
上一节:从需求建立容量模型 · 返回本章目录 · 下一节:建立可信实验 · 查看全书目录 · 查看索引中心
26.3 建立可信实验
可信实验不等于“环境完全没有噪声”。更现实的标准是:
性能实验有两类误差:
跑 1,000 次错误 workload,只会非常精确地回答错误问题。
26.3.1 固定硬件、版本、配置、数据与随机种子
把实验写成一个不可缺字段的 tuple
少一个字段,结果就多一种解释。
硬件不只记录“8C32G”
至少记录:
参考 run:
所以它不能支持 production IOPS、endurance 或 failure-domain claim。
PostgreSQL provenance
版本至少包括完整 build:
还要:
参考:
升级 minor version 也应重新跑 regression baseline。minor release 可能修复 planner、 executor、WAL、I/O 或 correctness 问题;“major 没变”不是等价环境。
配置要保存 value、source 与 pending restart
只保存 postgresql.conf 不够:
容量关键项:
effective_cache_size 是 planner estimate,不是分配的 cache。work_mem 是每个
operation 可能使用的基础限制,不是整台 server 的总内存。PostgreSQL
resource consumption
提醒,一个复杂 query 可能有多个 sort/hash,多 session 和 parallel worker 会把
总内存放大许多倍。
数据版本必须可证明
记录:
参考 fixture 每个 scale 都重建 immutable table:
每次 measured run 前只重置:
这样五次 run 有相同 initial mutable state,同时保留自然 cache 与 background 演化。它不代表 production bloat/churn。
随机 seed 与 run order 都要固定
seed 固定:
- script choice;
- random key;
- Zipf sample;
- rate schedule(若使用)。
run order 也会影响结果。若总是:
c8 总是得到更暖 cache,也总是承受 c1 留下的 dirty page。
参考采用 counterbalanced order:
scale 仍按 S -> M -> L,因为重建大数据代价高。这是保留的 order confound, 报告必须写明。
source hash 绑定执行实现
正式 evidence 保存 21 个 source file SHA-256:
若脚本改了一个 cast、weight 或采样窗口,旧 evidence 不再声称由当前 source 产生。参考 run 在 CPU sampler 口径修正后完整重跑,而不是把旧 transaction 结果与新 CPU 算法拼在一起。
manifest 最小字段
raw file 要有 size/hash;只保存总结无法重算 quantile。
26.3.2 预热、重复、置信区间与异常值
预热要说明预热什么
可能需要预热:
“跑 5 秒预热”只是动作,不是证明。
参考 run 对每个 scale/concurrency 做 5 秒自然预热,随后 reset mutable table。 它能:
- 验证正式 protocol 与 script;
- 建立连接;
- 触碰部分 immutable working set;
- 暴露 parameter typing。
它不能保证 899 MiB L dataset 全部 warm,也不能制造 cold-cache baseline。
正式时长取决于最慢周期
观察窗口要覆盖:
PostgreSQL 官方建议至少几分钟,可能需要数小时。
本章每 run 8 秒,是为了在 disposable laptop sandbox 中:
- 演示完整 30-run evidence pipeline;
- 观察短时 curve 与测量偏差;
- 保持负载有界;
- 不批准生产数字。
它不是 production baseline 的推荐时长。
重复的实验单位必须独立定义
参考每次 run:
immutable data 与 cache 不重建,所以 run 并非完全独立;它是“同一 scale 阶段内的 重复”。报告不能把五次当五台独立机器。
生产比较建议:
- 多个 run;
- 多个时间段;
- 至少一次 fresh environment;
- 若硬件采购,多个同型 node;
- candidate/control 交错,而不是先跑完 A 再跑 B。
不要用一个平均数吞掉分布
每个 run 保存 transaction latency sample:
cell pooled p95:
它回答“该 cell 所有保存 transaction sample 的 p95”。
run-level p95:
再报告 $q_r$ 的 median/interval,回答“一个典型 run 的 p95 如何波动”。
这两者不能混名。
参考 public summary 保存:
bootstrap interval 的边界
对五个 TPS:
这给出教学用 interval,但:
- n=5 很小;
- run 不完全独立;
- systematic bias 不会进入 interval;
- interval 不是“真实 TPS 有 95% 概率在里面”;
- performance distribution 可能非平稳。
它主要让读者看见“不确定性不是零”。
异常值不能看结果再删除
L-c8 五次 TPS:
第一轮显著低,p95 也更高。可能原因:
- L 初始化尾部 dirty write;
- working set 尚未稳定;
- background checkpoint/archive/replication;
- virtual neighbor;
- random interleaving。
本章保留它,因此:
正确流程:
- 在实验前声明 outlier rule;
- 保存原始样本;
- 检查 concurrent evidence;
- 报告含/不含敏感性分析;
- 只有明确测量故障才排除;
- 排除要留 audit record。
“看起来不正常”不是删除依据。
candidate 与 baseline 的比较
对每个 matched run:
$$ \Delta_r
\frac{candidate_r-baseline_r}{baseline_r} $$
优先比较 paired distribution,而不是两个独立平均。验收可以是:
阈值来自业务风险与实验噪声,不是固定模板。
26.3.3 冷热缓存、后台任务与邻居噪声
PostgreSQL 有至少两层 cache
blks_hit:
blks_read:
不代表 physical disk read。OS 可能立即从 page cache 返回。
参考:
可以说 L 触发了 PostgreSQL-level reads,不能说发生了同样数量的 physical I/O。
因此 runner 同时采 /proc disk counters,并用 Pigsty node metrics 做较粗的
corroboration。
真 cold cache 是破坏性实验
常见做法:
它们会影响同机其他 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 会改变结果。
两种实验都合理:
错误做法是关闭 autovacuum 获得数字,却不在 claim 里写。
若保留 autovacuum,记录:
并观察 progress、I/O、WAL 和 latency。
checkpoint 与 full-page image
checkpoint 后第一次修改某 page,full_page_writes=on 时可能产生 full-page
image。于是相同 transaction mix 的 WAL/tx 会随 checkpoint phase 改变。
观察:
不要为了“稳定”关闭 full-page writes;那会改变 durability。
PostgreSQL WAL 配置
说明这些参数的恢复语义。本章保持 fsync=on、full_page_writes=on、
synchronous_commit=on。
backup、archive 与 replica 会消耗真实资源
写 workload 同时驱动:
参考全实验 Pigsty window:
该 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 细节。需要:
本章把 shared-hypervisor 写入 why_null,而不是用五次重复假装消除。
background inventory
每次 run 保存:
统计不 reset。PostgreSQL cumulative statistics 默认有更新延迟,并可能在
transaction 内缓存 snapshot;官方
viewing statistics
解释了 PGSTAT_MIN_INTERVAL、stats_fetch_consistency 与
pg_stat_clear_snapshot()。runner 在 run 后等待 flush,并比较 reset timestamp。
26.3.4 绝对性能结论只适用于记录过的环境
一条绝对数字的完整名称
不是:
而是:
标题很长,因为 claim 的边界本来就长。
absolute capacity 与 regression baseline 分开
短、噪声较大的实验仍可做 CI regression signal:
它不能自动成为生产采购依据。
| 用途 | 证据要求 |
|---|---|
| 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 的唯一定义。
系统可能先在:
上失效,而 dashboard 中 CPU 还没满。
容量曲线必须同时画:
瓶颈不是“最高的那条曲线”,而是当前最先限制目标的 service center。
26.4.1 吞吐—延迟曲线与排队拐点
典型曲线有三个区域
对应 latency:
区域:
| 区域 | 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 倍,说明排队/并行争用已明显增加。
公共结果因此写:
下一轮应补:
或以 open-loop rate 做更密的 SLO sweep。只有两点画出的“曲线”是连线,不是 找到拐点。
closed-loop 曲线的横轴不是 offered arrival
零 think time、$C$ 个 client:
当 latency 上升,throughput 自动降低。这会形成自我节流:
真实应用 request 可能继续到达并在 pool/queue 中堆积。因此 closed-loop c8 回答:
不回答:
open-loop 要同时看 offered 与 achieved
用 --rate:
记录:
$$ \text{completion ratio}
\frac{\text{successful}}{\text{scheduled}} $$
以及:
若 achieved TPS 看起来稳定,但 skipped 快速增加,系统不是健康保持吞吐,而是 丢弃工作。
用 Little’s Law 检查 queue
对 database active/queued population:
c8、2,920 TPS、mean latency 粗略约:
这个量接近 mixed mean,不是 p95。若观测到平均 active+wait session 与它严重 不一致,检查:
- transaction log boundary;
- pool queue 未计;
- client think/network;
- sampler bias;
- multiple transactions per script;
- offered/achieved 混用。
knee 是多目标决策
可能的 acceptance:
最先违反的条件定义当前可用边界。它可能远早于最大 TPS。
throughput collapse
超过 knee 后可能:
形成正反馈,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 仍有余量。可以说:
不能说:
因为约 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:
风险模型:
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:
只说明这个短 workload 未观测到 database temp spill;它不是 memory capacity 证明。
I/O:先分 data、WAL 与 background
pg_stat_io 按:
聚合。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:
包含 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 和短窗口影响。
更好的生产模型:
通过 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 观察到少量:
所有 cell deadlock=0。解释边界:
要解释 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 放在同一观察面。
实验期间:
这表示 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:
长 transaction 又让 lock 持有更久,形成二阶影响。
观察:
lock queue
若多个 place-order 更新同一 hot inventory row:
增加 client 不增加该 key 的 service rate,只增加 queue:
若 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:
然后 latency 上升,连接占用更久,再增加在途数。
storage queue
存储有可用并行深度,但不是无限:
同一 device 上 data、WAL、checkpoint、archive staging 和 backup 可能相互影响。
parallel query 与 client concurrency 相乘
若一个 query 最多 4 workers:
PostgreSQL resource 文档提醒 parallel worker 是独立 process,CPU/memory impact
类似额外 session,work_mem 也按 worker/operation 作用。
所以容量合同要同时固定:
pool 的正确目标是限制 server concurrency
例如:
pool queue 是可控 admission,前提是:
- queue latency 可观测;
- client timeout 大于/匹配 policy;
- cancellation 正确;
- transaction pooling 兼容 session feature;
- retry 不放大;
- queue 有上限。
没有上限的 pool 只是把 outage memory 从 PostgreSQL 移到中间层。
找到健康并发的实验
对每个 data/mix:
每点:
- 固定 offered model;
- 重复;
- 画 throughput、p95/p99、failed、late、queue;
- 同时画 server/client CPU、I/O、WAL、lock;
- 记录 pool/direct path。
选择:
不是:
本节结论
参考证据支持:
- 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、数据规模、配置和时间窗口下 的一组观测。容量规划要把观测转换为三个可以行动的模型:
转换过程中最危险的捷径是:
它忽略了 workload mix、尾延迟、后台工作、故障冗余、增长、复制、备份以及扩容 提前期。可信的容量模型必须保留条件、区间和未知项。
26.5.1 单位业务量的资源消耗
先定义业务单位
“每事务成本”只有在 transaction 的业务语义稳定时才有意义。第 26 章实验的 transaction mix 是:
其中一次 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 也会变化。因此模型应优先保存:
而不是只保存一个 blended TPS。
CPU service demand
假设测量窗口内:
粗略的 mixed CPU demand:
例如单核 server 上,L-c8 的 server work ratio 约 0.842、吞吐中位数约 2,774 TPS:
即约 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}}$ 不是物理极限。它还要容纳:
参考 run 为便于教学,计算了一个 sandbox-only、线性、65% CPU 投影:
| scale | 65% CPU 条件投影 TPS |
|---|---|
| S | 2,236.79 |
| M | 2,222.96 |
| L | 2,116.98 |
这只是:
它不代表 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 原生统计可以分别回答:
统计视图是累计计数器。必须保存 start/end snapshot、server restart/reset 时间, 用 delta 计算;不能把 dashboard 当前值当作本次 run 的成本。
I/O 成本不能用 cache hit ratio 代替
一个 operation 的 I/O 模型至少分:
参考 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 增长、保留、备份与维护空间
数据库大小不是一条线
磁盘容量要同时容纳:
先用原生 size function 建立对象层 inventory:
这些函数的语义与可选 fork 见官方
Database Object Size Functions。
注意:
不要把其中两项相加造成重复计数。
从业务增长率推导 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:
forecast 不是只更新未来;它也要用实际误差校准。
bloat 与 free space 不是同义词
PostgreSQL MVCC update/delete 会产生 dead tuple。普通 VACUUM 通常把空间留在
relation 内供复用,并不把大部分文件空间还给文件系统。于是:
容量规划需要同时问:
- 这部分 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”。要考虑:
若 WAL 生成率为 $W$ bytes/s,允许 archive 中断 $T_a$ 秒,安全因子为 $h$:
若 replication slot 的 consumer 可离线 $T_s$:
但 slot retention 可能无界,除非使用并验证 max_slot_wal_keep_size 等保护。
PostgreSQL 的
WAL configuration
和
log-shipping standby
说明了 checkpoint、归档、streaming 与 retention 之间的关系。
容量规划至少演练:
正常状态的 WAL 目录大小不能代表这些失效状态。
备份容量用 retention graph,而不是压缩率愿望
备份 inventory 应包括:
备份压缩率随 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 示例:
不能简单乘以 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 约束是:
若多个动作可重叠,应改为相应的和,而不是最大值。
用 time-to-limit 驱动行动
设当前使用量 $S_0$、目标上限 $S_{\text{limit}}$、净增长速率 $G$:
$$ T_{\text{limit}}
\frac{S_{\text{limit}}-S_0}{G} $$
但 $G$ 不应只有一条平均线。至少计算:
如果增长非线性,就用时间序列/业务 forecast,不能继续用直线外推。
26.5.3 扩容触发线、提前期与失效假设
trigger 必须早于 capacity limit
一次扩容从发现到获得容量通常经历:
总提前期:
$$ T_{\text{lead}}
\sum_j T_j $$
若保守预测到达上限的时间为 $T_{\text{limit,P90}}$,行动条件应是:
“磁盘 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 应具有:
瞬时 CPU 65% 不应自动购买主机;持续两周的需求趋势、在 N+1 状态下预测将超过已验证 SLO envelope,才是可行动的 signal。
以失效状态写容量预算
先定义状态,而不是笼统说 “预留 30%”:
每个状态分别计算:
例如三台 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 原生统计双重验证。
把容量结论写成有期限的决策记录
一条可审计的容量结论应包含:
任何一个重大条件变化都使 baseline 进入复审:
容量模型的价值不是预测一个永远正确的 TPS,而是让团队在还有选择的时候,看见:
上一节:找到饱和点与瓶颈 · 返回本章目录 · 下一节:实战:pg36_shop 容量基线 ·
查看全书目录 · 查看索引中心
26.6 实战:`pg36_shop` 容量基线
本节把前五节收敛成一份可执行实验。目标不是发布一个炫目的 TPS,而是完整回答:
在记录过的 Pigsty 教学沙箱上,
pg36_shop的固定读写混合,面对三档合成 数据规模和一、八两个并发档位时,吞吐、延迟、CPU、I/O、WAL 与空间怎样变化?
实验边界刻意很窄:
它不测 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 只接受:
若目标身份、primary、marker、预存对象或 session 状态不符合合同,它会 fail closed,而不是“尽量继续”。
fixture 与权限边界
实验创建两个一次性对象:
库和角色都带本次 run 的 shared-object comment marker。清理前必须重新读回 marker, 并确认没有非本章会话;清理不用:
也不终止其他 session。
Pigsty 当前内网 HBA 通过 +dbrole_readonly 角色组识别业务连接。runner 不修改 HBA;
它把临时 login 加入 dbrole_readwrite,后者已间接属于 readonly 组,同时把临时
membership 设置为:
这个 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 五次:
每次:
并发顺序做 counterbalance,seed 由公开公式从 scale、clients、repetition 推导。 这不能消除所有 background noise,但能避免总是先跑 c1、后跑 c8 的固定时间偏差。
runner 明确禁止:
如果 baseline 只有在关掉 durability 后才“通过”,它没有测量目标系统。
运行方法
先做不连接目标的本地检查:
完整实验要使用一个不存在或为空的 私密绝对路径:
也可分步:
capture/all 拒绝覆盖非空 evidence directory。verify 与 review 可以反复读取
已有 evidence,不重跑 workload。
不要把私密目录放进 web root 或 Git。它包含 raw transaction log、system sample、 SQL snapshot 和监控响应;公共仓库只提交经过 allowlist 的摘要。
参考 run 的结果
正式参考 run:
每个 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 同时采集:
不能用其中一层替代所有层。例如 p95 增加并不能单独证明 CPU 饱和;CPU 84% 也不能 单独证明 p95 来自 CPU queue。
sampler 必须严格裁剪 measured window
原始 sampler 从 warm-up 前启动、pgbench 后结束是正常的,但 cell arithmetic 必须 只使用:
参考实现保存 start/end monotonic timestamp,并对 client/server sample 严格裁剪。 如果把 pgbench 结束后两秒 idle tail 算入 CPU median,单位事务 CPU 会被系统性低估。 增加 repetition 无法修复这种 systematic error。
Pigsty query 的 full exercise window 则故意保留:
因此:
两个窗口不能直接做逐项相等断言。
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 是合理的 候选瓶颈。
但不能立即下结论:
还要检查 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 |
解释:
这说明 queue/competition 已增加,却仍不能定位精确 knee。只有两个并发点:
下一轮至少补:
并用更长 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。
下一轮可以选择:
不要为了“可重复 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 不能 定位。下一轮应同时比较:
实验没有关闭 fsync、full_page_writes 或 synchronous_commit,也没有强制
checkpoint。若要研究 checkpoint phase,应把它作为显式 factor,而不是偷偷在每次
run 前 checkpoint。
lock、failure 与 temp 的反证
30 个 measured run 中:
这反对“当前点因 deadlock/temp spill 失效”,但不能证明:
deadlock counter 只计被 detector 处理的 deadlock;普通 row-lock queue 不是
deadlock。temp_bytes=0 也只针对本窗口与当前 plan/data。
在 Pigsty 中做独立旁证
Pigsty 的 PGSQL dashboards 把 Overview、Activity、Persist、Database、Table、Query 等视角连接起来。实验时 至少对齐:
参考 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,不能直接转译为:
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
完整运行产生:
证据包目录权限为 0700,文件为 0600。review 会检查 mode,避免 raw evidence 因默认 umask 变成多用户可读。
raw transaction log 对 latency 重算必不可少,却可能含时间、client/session 与 workload 行为;SQL snapshot、queryid 明细和监控 payload 也不应直接公开。发布流程 是:
不是“从 raw JSON 删除几个看起来敏感的键”。
报告中的 provenance
一份可复现实验报告至少固定:
参考 run 验证了 255 个 raw artifact,并对约 21.8 MB evidence 做 secret scan。这个 数字不是质量本身;它的意义在于 manifest 能证明报告引用的 sample 没有悄悄缺失。
对抗性验证
negative-cases.json 定义了 26 个变体,validator 必须逐一拒绝。类别包括:
“positive fixture 能通过”只证明 happy path;错误 evidence 也能通过的 validator 不能保护结论。
参考 run 的验收:
容量模型只发布条件结论
公共报告给出的 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 |
它依赖四个显式假设:
所以报告写:
null 不是实验失败,而是 claim 与 evidence 对齐。
未知项就是下一轮 backlog
本轮没有回答:
- c8 左右的精确 knee 在哪里;
- open-loop offered rate 下的 completion、late、skipped 与 queue;
- 真实应用经 HAProxy/PgBouncer/TLS 的端到端成本;
- realistic think time、connection churn、retry storm;
- 真实 query 和 tenant/key skew;
- cold/restart、working set 超 RAM 与 production storage;
- checkpoint、autovacuum、backup overlap;
- replica loss、failover、rebuild、archive outage;
- 长稳态下 bloat、WAL、slot 与空间增长;
- production hardware 的 N+1 SLO envelope。
将它们排成下一轮最小实验:
一次只改变少量 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,分别计算:
解释 idle tail 对 CPU-s/transaction 和 65% 投影的方向性影响。
练习四:设计 production gate。
为自己的应用补齐:
如果仍写不出 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 截图”变成可重复、可反驳、可用于决策的工程 过程。
上一节:从测量推导容量与成本 · 返回本章目录 · 下一章:精益求精:参数调优与资源治理 · 查看全书目录 · 查看索引中心