跳至内容
26.2 设计代表性工作负载

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 baselineTPC-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行为适合
simplesimple query protocol模拟文本批次或 baseline
extendedparse/bind/execute参数协议
prepared第二次起复用 parseprepared 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)=\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)

参考合同:

operationkeydistribution
read-productproductZipf 1.15
read-ordercustomerZipf 1.08
place-ordercustomeruniform
place-orderproductZipf 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-widthcache density 高
wide JSON/TOASTdecompression、I/O、CPU
many NULLindex/tuple size 与 selectivity
monotonically increasing keyrightmost index page 热点
random UUIDlocality 与 page split
high update churndead tuple、vacuum、bloat
many partitionsplanning/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
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:

scaleclient workserver work
S29.5%84.2%
M29.1%84.0%
L27.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

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


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

最后更新于