# 从需求建立容量模型

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

---

容量规划的第一份输入不是 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 开始：

```text
operation
  arrival process
  transaction boundary
  SQL and data shape
  consistency and durability
  retry and timeout
  latency/error/correctness objective
  growth and retention
  overlap and failure state
```

第 24 章把服务承诺写成 SLO；第 25 章把承诺落实为观察合同；本节把同一套语义
变成容量输入。三章必须使用同一个 operation name 和 eligible event 定义。

## 26.1.1 事务类型、读写比、并发和数据增长 {#item-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` 可能：

```text
HTTP request
  -> auth/cache reads
  -> BEGIN
      SELECT product
      UPDATE inventory
      INSERT order
      INSERT order_item
     COMMIT
  -> publish outbox later
```

于是：

```text
1 request
!= 1 SQL
may equal 1 or more database transactions
may cause background transactions after response
```

如果把 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 |

同为“读”：

```text
indexed point lookup
index-only recent-N
wide range scan
hash aggregate spilling to temp
JSON path evaluation
vector nearest-neighbor
```

CPU、buffer、I/O、`work_mem`、parallel worker 和 lock footprint 完全不同。

同为“写”：

```text
HOT-eligible update
indexed-column update
append-only insert
upsert on a hot unique key
multi-table transaction with foreign keys
bulk COPY
```

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% 写”没有说明计数单位：

```text
by request?
by transaction?
by statement?
by tuple?
by byte?
by CPU second?
by WAL byte?
```

参考实验按 pgbench transaction 选择脚本：

```text
read-product 50
read-order   30
place-order  20
```

这是 **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 做第一轮自洽检查

稳定系统中，平均在途量满足：

$$
L = \lambda W
$$

若：

```text
arrival rate   2,000 request/s
mean latency   80 ms = 0.08 s
```

则平均在途 request：

$$
L = 2000 \times 0.08 = 160
$$

这不是说“必须给 PostgreSQL 160 个连接”。应用中的在途量可能分布在：

```text
edge queue
application worker
pool wait
database execution
external call
response serialization
```

如果 transaction 只占 15 ms：

$$
L_{db} = 2000 \times 0.015 = 30
$$

一个 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。

至少分开：

```text
logical rows/day
logical bytes/day
table bytes/day
index bytes/day
TOAST bytes/day
WAL bytes/day
archive bytes/day
backup repository growth/day
```

参考实验三档 `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` 文档](https://www.postgresql.org/docs/18/monitoring-stats.html#MONITORING-PG-STAT-IO-VIEW)
也明确指出，数据库 I/O 统计无法区分内核调用最终命中 page cache 还是访问
storage，必须结合 OS 证据。

### 本目产物：业务容量输入表

```yaml
forecast_window: 12 months
operations:
  place-order:
    eligible_peak_tps: 800
    peak_duration: 20m
    db_transactions_per_request: 1
    retry_amplification_p95: 1.04
    data_growth:
      orders_per_success: 1
      line_items_per_order_p95: 6
    objective:
      p95_ms: 250
      error_ratio: 0.001
      correctness: no unexplained duplicate
```

不要在这个阶段填“8 vCPU”。先把问题写完整。

## 26.1.2 平均值、峰值、突发与批处理叠加 {#item-26-1-2}

### 日平均掩盖容量风险

一天 86,400 秒。日订单 8,640,000：

$$
\text{daily average} = 100/s
$$

但若 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 需要：

```text
baseline
peak factor
burst factor and duration
seasonality
growth trend
event calendar
retry amplification
batch overlap
maintenance overlap
```

### 区分 arrival burst 与 backlog drain

两种看起来都像“TPS 突然上升”：

```text
new user demand
  arrival rises

backlog drain
  arrival may have fallen
  workers consume queued work faster
```

后者常在依赖恢复后发生：

```text
payment provider recovers
  -> retries released
  -> queue drain
  -> database write burst
  -> replicas/archive/backup lag
```

如果模型只用前台 request，遗漏 backlog，恢复本身就可能触发第二次事故。

### 重试是负载放大器

每个原始 operation 平均尝试次数：

$$
A
=
1 + r_1 + r_2 + \cdots
$$

更实用地：

$$
\lambda_{\text{database}}
=
\lambda_{\text{eligible}}
\times
\text{attempts per eligible operation}
$$

例如：

```text
eligible arrival          1,000/s
5% requests retry once
1% requests retry twice
```

平均尝试：

$$
1 + 0.05 + 2 \times 0.01 = 1.07
$$

数据库看到约 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 写成场景

```text
N0 normal peak
  interactive peak + routine background

N1 campaign peak
  interactive 2.5x + retries 1.1x

M1 maintenance overlap
  normal peak + vacuum/index build/backup

F1 one replica unavailable
  normal peak + catch-up/rebuild or read traffic reroute

F2 primary failover
  reconnect storm + retry burst + cold-ish cache + reduced topology
```

容量批准必须说明要满足哪个场景。只在 N0 通过，不等于生产通过。

### 平均、quantile 与最大值各有用途

```text
mean
  resource accounting, Little's Law, long-run throughput

p50
  typical transaction

p95/p99
  user tail and queue onset

max
  evidence lead, but sample-size sensitive
```

不能把每分钟 p99 再做平均并称为“全天 p99”。若需要跨 window 或 instance
聚合，保留原始 transaction sample 或可聚合 histogram。Prometheus 的
[histogram 指南](https://prometheus.io/docs/practices/histograms/)
解释了为什么直接平均预计算 quantile 在统计上没有意义。

## 26.1.3 延迟目标、错误预算与安全余量 {#item-26-1-3}

### 容量是带约束的可行域

把 capacity 定义为：

$$
\max \lambda
\quad \text{subject to}
\quad
\begin{cases}
P(W \le T) \ge S \\
\text{error ratio} \le E \\
\text{correctness invariant holds} \\
\text{resource and recovery limits hold}
\end{cases}
$$

例如：

```text
place-order p95 <= 250 ms
availability >= 99.9%
no unexplained duplicate order
replica/archive remain within declared recovery bounds
primary disk < 70%
one declared failure still has headroom
```

没有约束的“最大 TPS”只是最大努力点。

### 延迟目标要定义 measurement boundary

这些延迟不相等：

```text
client schedule -> response
application admission -> response
pool checkout -> release
transaction BEGIN -> COMMIT response
one SQL execute
server execution excluding network
```

参考实验的 latency：

```text
origin     pgbench client
boundary   one chosen script
protocol   prepared
connection persistent
path       pg-meta-1 -> pg-test-1:5432
```

它不包含 application edge、HAProxy、PgBouncer 和 WAN，所以不能与第 24 章
`place-order` 用户 SLO 直接画等号。

### 错误不能从分母消失

报告至少包含：

```text
attempted
processed successful
failed
retried transactions
total retries
late
skipped
client aborted
server disconnect
```

若只用：

$$
\text{TPS}
=
\frac{\text{successful}}{\text{elapsed}}
$$

系统可以通过拒绝慢请求让成功样本看起来更快。failed、skipped 和 admission
rejection 必须与 latency 并列。

参考实验固定 `--max-tries=1`，不让 pgbench 把 serialization/deadlock retry
隐藏到成功事务里；250 ms 只计 late。正式运行：

```text
failed       0
skipped      0
late         0
deadlock     0
```

这只适用于 511,709 个合成事务。零次观察不是“真实概率为零”的证明。

### 错误预算不是容量余量

SLO 允许 0.1% bad event，不代表正常运行可以把资源推到 99.9%：

```text
resource headroom
  absorbs burst, forecast error, maintenance and failure

error budget
  governs reliability trade-offs and release policy
```

若平时已经在 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 作**教学投影点**：

$$
\lambda_{65}
\approx
\frac{0.65}{D_{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 应由：

```text
failure model
burst duration
autoscaling/provisioning time
workload convexity
cost objective
operational experience
```

共同决定，而不是复制 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 只会更快地产生无意义数字。

---

[返回本章目录](../) · [下一节：设计代表性工作负载](../02/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
