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.4
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 reviewcapture/all 拒绝覆盖非空 evidence directory。verify 与 review 可以反复读取
已有 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实验没有关闭 fsync、full_page_writes 或 synchronous_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 loaddeadlock 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 millisecondsbyte 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
本轮没有回答:
- 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。
将它们排成下一轮最小实验:
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 截图”变成可重复、可反驳、可用于决策的工程 过程。
上一节:从测量推导容量与成本 · 返回本章目录 · 下一章:精益求精:参数调优与资源治理 · 查看全书目录 · 查看索引中心