# 设计代表性工作负载

LLMS 索引： [llms.txt](/llms.txt)

---

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

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

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

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

## 26.2.1 内置 pgbench 与业务自定义脚本 {#item-26-2-1}

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

PostgreSQL 18 提供：

```bash
pgbench --builtin=list
```

```text
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](https://www.postgresql.org/docs/18/pgbench.html)
明确要求默认场景的 scale 至少不小于最大 client，并提醒 dead tuple、vacuum
时机和 client bottleneck 会改变结果。

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

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

不是：

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

### 自定义脚本先定义 transaction boundary

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

```sql
-- one pgbench transaction, one autocommit statement
SELECT ...;
```

```sql
-- one pgbench transaction, one explicit DB transaction
BEGIN;
UPDATE ...;
INSERT ...;
COMMIT;
```

```sql
-- 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”：

```sql
\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 支持：

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

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

正式报告仍应读取 transaction log：

```text
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 固定：

```text
protocol = prepared
```

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

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

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

```text
operator is not unique: unknown * unknown
```

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

```sql
(:price_cents)::integer * (:quantity)::integer
```

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

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

推荐：

```text
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 也漂移：

```text
可能是 engine/hardware/config change
```

Layer A 稳定、Layer B 漂移：

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

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

### 初始化也属于合同

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

本章：

```text
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 参数分布、事务混合和数据倾斜 {#item-26-2-2}

### uniform 往往是最不真实的默认

均匀分布：

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

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

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

它们改变 cache locality 和 contention。

pgbench 提供：

```text
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：

```text
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。

### 保留变量之间的相关性

独立随机：

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

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

```text
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 |

记录：

```text
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 应来自同一测量边界

错误组合：

```text
reads from HTTP logs
writes from pg_stat_database
batch from scheduler estimates
```

分母不同，无法相加。

更可靠：

```text
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 不只增加一行：

```text
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 也要模拟

业务中存在：

```text
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、连接方式和客户端瓶颈 {#item-26-2-3}

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

默认 pgbench：

```text
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：

```text
Z = 0
```

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

open-loop：

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

PostgreSQL 18 的 `--rate` 按 Poisson timeline 调度 transaction；报告的 latency
从 scheduled start 计算，包括 schedule lag。若已经来不及满足 latency limit，
transaction 会被记为 skipped。详见
[`pgbench --rate`](https://www.postgresql.org/docs/18/pgbench.html)。

生产 SLO envelope 更需要：

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

参考实验只做 closed-loop，所以 `production_sustainable_tps=null`。

### think time 是 workload，不是装饰

真实用户：

```text
read page
think
click
wait
```

worker queue：

```text
fetch job
process externally
write result
sleep/poll
```

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

### 连接模式要单独测试

持久连接：

```bash
pgbench -c 8 ...
```

每个 client 保留 connection。

每事务重连：

```bash
pgbench -C ...
```

测量：

- TCP/TLS；
- authentication；
- backend fork/init；
- session GUC；
- extension hooks；
- connection storm。

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

连接路径至少分：

```text
engine baseline
  direct primary

pool baseline
  PgBouncer service

service baseline
  HAProxy + PgBouncer

user baseline
  application edge + service path
```

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

### PgBouncer 不会增加 PostgreSQL CPU

pool 的价值主要是：

```text
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 自己可能成为瓶颈。

观察：

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

参考 run：

```text
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 内做外部工作。

必须结合：

```text
backend state
wait duration
client CPU
network
transaction age
protocol
```

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

### workload contract 最小模板

```yaml
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
```

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

---

[上一节：从需求建立容量模型](../01/) · [返回本章目录](../) · [下一节：建立可信实验](../03/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
