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=listtpcb-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_statementsshape。
参考脚本曾在真实预热中暴露:
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 distributionLayer A 漂移、Layer B 也漂移:
可能是 engine/hardware/config changeLayer 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 syntheticexisting database/role 一律拒绝覆盖;完整 evidence 后只删除 marker 精确匹配且 无其他 session 的 fixture。
26.2.2 参数分布、事务混合和数据倾斜
uniform 往往是最不真实的默认
均匀分布:
$$ 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:
$$ 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
waitworker 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 vCPUc8 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没有这份合同,数字不能离开终端。
上一节:从需求建立容量模型 · 返回本章目录 · 下一节:建立可信实验 · 查看全书目录 · 查看索引中心