# 从测量推导容量与成本

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

---

一次压测给出的 TPS 不是容量答案。它只是某个 workload、数据规模、配置和时间窗口下
的一组观测。容量规划要把观测转换为三个可以行动的模型：

```text
resource model
  one business operation consumes how much CPU / IO / WAL / storage

growth model
  data, WAL, backup and maintenance workspace grow how fast

decision model
  at what threshold, considering lead time and failure, should we change capacity
```

转换过程中最危险的捷径是：

```text
benchmark maximum TPS × server count = future capacity
```

它忽略了 workload mix、尾延迟、后台工作、故障冗余、增长、复制、备份以及扩容
提前期。可信的容量模型必须保留条件、区间和未知项。

## 26.5.1 单位业务量的资源消耗 {#item-26-5-1}

### 先定义业务单位

“每事务成本”只有在 transaction 的业务语义稳定时才有意义。第 26 章实验的
transaction mix 是：

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

其中一次 `place-order` 会写入订单、订单行并扣减库存；一次 read operation 则主要
读取。若把三者统称为“请求”，mixed average 会随权重变化：

$$
D_{\text{mix}}
=
\sum_{i=1}^{n} w_iD_i
$$

其中：

- $w_i$ 是第 $i$ 类 operation 的比例；
- $D_i$ 是该 operation 的资源需求；
- $\sum w_i=1$。

今天写比例 20%，明天促销时写比例 45%，即使每一种 operation 的实现都没变，
mixed resource/transaction 也会变化。因此模型应优先保存：

```text
CPU seconds / read-product
CPU seconds / read-order
CPU seconds / place-order
WAL bytes / place-order
durable data bytes / place-order
```

而不是只保存一个 blended TPS。

### CPU service demand

假设测量窗口内：

```text
N_cpu       logical CPU count
U_cpu       non-idle CPU fraction
X           completed transactions / second
```

粗略的 mixed CPU demand：

$$
D_{\text{cpu,mix}}
\approx
\frac{N_{\text{cpu}}U_{\text{cpu}}}{X}
\quad
\text{CPU-seconds / transaction}
$$

例如单核 server 上，L-c8 的 server work ratio 约 0.842、吞吐中位数约
2,774 TPS：

$$
D_{\text{cpu,mix}}
\approx
\frac{1\times0.842}{2774}
\approx
0.000304
\text{ CPU-s/tx}
$$

即约 0.304 CPU-ms/tx。这个值只能用于同一 workload mix 的粗略推演。它包含
shared overhead，未把 checkpointer、WAL writer、autovacuum 等后台成本可靠地
分摊给 operation，也没有证明 CPU 是唯一限制。

要得到 per-operation demand，应设计独立或正交实验。设三种 operation 的未知 CPU
demand 为 $D_1,D_2,D_3$，运行至少三组不同且可识别的 mix，得到：

$$
\begin{bmatrix}
w_{11} & w_{12} & w_{13} \\
w_{21} & w_{22} & w_{23} \\
w_{31} & w_{32} & w_{33}
\end{bmatrix}
\begin{bmatrix}
D_1\\D_2\\D_3
\end{bmatrix}
=
\begin{bmatrix}
D_{\text{mix},1}\\D_{\text{mix},2}\\D_{\text{mix},3}
\end{bmatrix}
$$

更简单的做法是分别运行 read-only、write-only 与代表性 mixed workload，然后用
mixed run 验证模型，而不是直接拿三次结果线性拼接。

### 从 service demand 估算目标 CPU

若 forecast 中每类业务到达率为 $\lambda_i$：

$$
C_{\text{cpu required}}
=
\frac{\sum_i \lambda_iD_{\text{cpu},i}}
{U_{\text{target}}}
$$

目标利用率 $U_{\text{target}}$ 不是物理极限。它还要容纳：

```text
forecast error
traffic burst
autovacuum / checkpoint / backup
failover and degraded topology
software regression
host and storage variance
```

参考 run 为便于教学，计算了一个 **sandbox-only**、线性、65% CPU 投影：

| scale | 65% CPU 条件投影 TPS |
|---|---:|
| S | 2,236.79 |
| M | 2,222.96 |
| L | 2,116.98 |

这只是：

$$
X_{65\%}
\approx
X_{\text{c8}}
\times
\frac{0.65}{U_{\text{c8}}}
$$

它不代表 production sustainable TPS，因为：

- c8 已高于 65%，这是回算而非直接在 65% 稳态测量；
- 两个并发点没有找到 knee；
- workload 是 closed-loop、warm-cache、短窗口；
- 后台维护、故障、恢复、备份与真实网络未被充分施压；
- 只有一个虚拟化小节点。

公共结果把 production sustainable TPS 保持为 `null`，这比填一个看似精确的数字
更诚实。

### WAL 与 durable growth 必须按写操作归一

参考实验给出了：

| cell | mixed WAL bytes/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 |

`WAL bytes/tx` 的分母包含 80% read transaction，因此不能直接说“一笔订单产生
约 160–196 字节 WAL”。若读操作完全不写，按 20% place-order 做第一阶换算，
写操作的 WAL 可约为 mixed value 的五倍；但真实系统还有：

- hint bit 与 full-page image；
- checkpoint 后首次页修改；
- index 数量与 page split；
- autovacuum、freeze 与 catalog activity；
- logical decoding；
- sequence 与 extension 行为。

正确做法是用 `pg_stat_wal.wal_bytes` 的窗口 delta，除以该窗口实际成功的
write operation 数：

$$
\text{WAL bytes/write op}
=
\frac{\Delta\text{wal\_bytes}}
{\text{successful write operations}}
$$

同理：

$$
\text{durable growth/write op}
=
\frac{\Delta\text{relation bytes after stabilization}}
{\text{successful write operations}}
$$

这里的 “after stabilization” 很重要。heap 与 index 的 allocated size 不是每
写一行都线性增加；页内 free space 会先被消耗，扩页是离散事件。短窗口的
`relation size delta / order` 容易受页边界影响。应在足够长窗口、多个数据规模
和 vacuum 周期上重复测量。

PostgreSQL 原生统计可以分别回答：

```sql
SELECT wal_bytes, wal_records, wal_fpi
FROM pg_stat_wal;

SELECT
    datname,
    xact_commit,
    xact_rollback,
    blks_read,
    blks_hit,
    temp_bytes,
    deadlocks
FROM pg_stat_database
WHERE datname = current_database();
```

统计视图是累计计数器。必须保存 start/end snapshot、server restart/reset 时间，
用 delta 计算；不能把 dashboard 当前值当作本次 run 的成本。

### I/O 成本不能用 cache hit ratio 代替

一个 operation 的 I/O 模型至少分：

```text
logical buffer access
physical data read
data write / writeback
WAL write / sync
temp read/write
backup/archive/network bytes
```

参考 run 的 S、M cell 没有记录到 PostgreSQL block read，L-c1/L-c8 分别出现
2,500/14,491 次 block read。这只说明 L 数据规模在该 warm-cache 窗口触碰了更多
未驻留 block；它不证明小规模生产 workload “不需要磁盘”。

PostgreSQL 18 的 [`pg_stat_io`](https://www.postgresql.org/docs/18/monitoring-stats.html)
可按 backend type、object 与 context 提供 read/write/extend/fsync 等累计信息。
它仍需和 OS/Pigsty 的 device latency、queue、throughput 对齐，因为：

- PostgreSQL read 可能由 OS page cache 满足；
- write() 完成不等于设备持久化完成；
- shared storage 与 hypervisor 可隐藏或放大延迟；
- data I/O、WAL I/O 和 backup I/O 可能落在不同设备。

### latency 是容量成本的一部分

若只追求最低 unit CPU cost，常会把 server 推到更高 batch/concurrency；throughput
可能提高，但 queueing 令 p95/p99 恶化。容量成本函数应含 SLO penalty：

$$
\text{effective cost}
=
\text{infrastructure cost}
+
\text{failure risk}
+
\text{latency penalty}
+
\text{operational complexity}
$$

因此一份 unit economics 表至少包含：

| 指标 | 分母 | 条件 |
|---|---|---|
| CPU-s | operation type | mix、并发、数据规模 |
| logical/physical I/O | operation type | cache state、plan |
| WAL bytes | successful write op | checkpoint/FPI 状态 |
| durable bytes | business entity | schema/index/vacuum 周期 |
| network bytes | API or transaction | TLS、pool、result set |
| p50/p95/p99 | completed op | offered/achieved load |
| failure/late | scheduled op | timeout/retry policy |

只给 `$ / TPS` 而没有这些条件，不是成本模型，只是报价除法。

## 26.5.2 增长、保留、备份与维护空间 {#item-26-5-2}

### 数据库大小不是一条线

磁盘容量要同时容纳：

```text
live heap
indexes
TOAST
free space and bloat
temporary files
WAL working set
archive staging / backlog
base backups and incrementals
restore and verification workspace
maintenance rewrite workspace
logs, packages and operating system
failure/rebuild headroom
```

先用原生 size function 建立对象层 inventory：

```sql
SELECT
    pg_size_pretty(pg_database_size(current_database())) AS database_size;

SELECT
    n.nspname,
    c.relname,
    pg_relation_size(c.oid) AS main_fork_bytes,
    pg_indexes_size(c.oid) AS index_bytes,
    pg_total_relation_size(c.oid) AS total_bytes
FROM pg_class AS c
JOIN pg_namespace AS n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r', 'm')
  AND n.nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY pg_total_relation_size(c.oid) DESC;
```

这些函数的语义与可选 fork 见官方
[`Database Object Size Functions`](https://www.postgresql.org/docs/18/functions-admin.html#FUNCTIONS-ADMIN-DBSIZE)。

注意：

```text
pg_relation_size(table)
  main fork only by default

pg_table_size(table)
  table forks + TOAST, excluding indexes

pg_total_relation_size(table)
  table + TOAST + indexes

pg_database_size(database)
  database total
```

不要把其中两项相加造成重复计数。

### 从业务增长率推导 live data

对业务实体 $i$：

$$
G_{\text{live/day}}
=
\sum_i
N_{i,\text{day}}
\times
B_{i,\text{durable}}
-
G_{\text{purged/day}}
$$

若保留 $R$ 天，且每日量以 $g$ 增长：

$$
N_R
=
N_0
\sum_{d=0}^{R-1}(1+g)^d
$$

不要先把“当前日增量 × 保留天数”写死，再在运营增长时惊讶。还要单独模拟：

- 热数据、温数据、冷数据保留；
- update/delete churn；
- 索引增长；
- partition attach/detach；
- 法规保留与 legal hold；
- tenant/region/skew；
- schema 变更增加的列和索引。

容量表应按周或月保存 actual vs forecast：

```text
date
live rows
heap bytes
index bytes
TOAST bytes
dead tuples
WAL bytes/day
backup bytes
archive backlog
forecast P50/P90
```

forecast 不是只更新未来；它也要用实际误差校准。

### bloat 与 free space 不是同义词

PostgreSQL MVCC update/delete 会产生 dead tuple。普通 `VACUUM` 通常把空间留在
relation 内供复用，并不把大部分文件空间还给文件系统。于是：

```text
relation allocated bytes
  != live tuple bytes
  != irrecoverable waste
```

容量规划需要同时问：

- 这部分 free space 能否被相同 relation 的后续写入复用？
- workload 的 update pattern 会不会跨页，使复用率低？
- index 是否发生结构性膨胀？
- autovacuum 能否在增长速度下追上？
- freeze horizon 和 long transaction 是否阻止清理？
- 真正回收文件系统空间需要哪种 rewrite，以及多大 workspace？

不要看到 30% “bloat estimate” 就自动采购 30% 磁盘，也不要因为 vacuum 后文件没缩小
就认定 vacuum 无效。

### WAL 是速率、窗口与失效状态的乘积

WAL 容量不是固定 “`max_wal_size`”。要考虑：

```text
steady generation rate
checkpoint behavior
archive throughput and outage
replication slots
slow/offline replicas
backup and restore windows
wal_keep_size / sender retention
logical decoding
promotion and timeline history
```

若 WAL 生成率为 $W$ bytes/s，允许 archive 中断 $T_a$ 秒，安全因子为 $h$：

$$
S_{\text{archive backlog}}
\ge
W \times T_a \times h
$$

若 replication slot 的 consumer 可离线 $T_s$：

$$
S_{\text{slot retention}}
\ge
W \times T_s \times h
$$

但 slot retention 可能无界，除非使用并验证 `max_slot_wal_keep_size` 等保护。
PostgreSQL 的
[`WAL configuration`](https://www.postgresql.org/docs/18/runtime-config-wal.html)
和
[`log-shipping standby`](https://www.postgresql.org/docs/18/warm-standby.html)
说明了 checkpoint、归档、streaming 与 retention 之间的关系。

容量规划至少演练：

```text
archive destination unavailable
replica offline
logical subscriber stalled
backup exceeds expected duration
write burst crosses checkpoint
```

正常状态的 WAL 目录大小不能代表这些失效状态。

### 备份容量用 retention graph，而不是压缩率愿望

备份 inventory 应包括：

```text
full/base backup
incremental/differential data
WAL needed for PITR
retention generations
off-site copies
temporary upload/download
restore verification copy
metadata/catalog
encryption/compression overhead
```

备份压缩率随 data type、page utilization、encryption 与 compression method 变化。
应直接测量：

$$
\text{compression ratio}
=
\frac{\text{stored backup bytes}}
{\text{logical source bytes}}
$$

并分别保存 full 与 WAL 的 ratio。生产采购不能假设日志、JSON、already-compressed
payload 与 encrypted data 都能获得同一压缩率。

retention 示例：

```text
7 daily + 4 weekly + 12 monthly
```

不能简单乘以 23，因为增量链可能共享 block，full schedule 也不同；应从备份系统
catalog 汇总真实 object graph，并验证删除一代不会破坏仍保留的恢复链。

### 维护和迁移需要临时的第二份数据

以下动作可能需要额外 workspace：

| 动作 | 可能的额外空间 |
|---|---|
| `CREATE INDEX` / `REINDEX` | 新 index + sort/temp + WAL |
| table rewrite | 新 heap + indexes + WAL |
| `VACUUM FULL` / `CLUSTER` | relation rewrite + indexes + WAL |
| major upgrade | in-place/link/copy 策略不同 |
| restore rehearsal | 一份可启动的数据 + WAL + logs |
| replica rebuild | base backup staging + receive WAL |
| logical migration | source 与 target 重叠期 |

`ALTER TABLE` 并不总是 rewrite，`pg_upgrade --link` 也不等于没有风险；模型要按实际
变更计划估算，不应用一个永久的 “2x” 代替 runbook。

一个实用的 local filesystem 约束是：

$$
S_{\text{free}}
\ge
\max(
S_{\text{routine headroom}},
S_{\text{largest planned maintenance}},
S_{\text{failure backlog}}
)
$$

若多个动作可重叠，应改为相应的和，而不是最大值。

### 用 time-to-limit 驱动行动

设当前使用量 $S_0$、目标上限 $S_{\text{limit}}$、净增长速率 $G$：

$$
T_{\text{limit}}
=
\frac{S_{\text{limit}}-S_0}{G}
$$

但 $G$ 不应只有一条平均线。至少计算：

```text
P50 trend
P90/P95 growth or burst scenario
retention policy change
failure backlog scenario
largest scheduled maintenance
```

如果增长非线性，就用时间序列/业务 forecast，不能继续用直线外推。

## 26.5.3 扩容触发线、提前期与失效假设 {#item-26-5-3}

### trigger 必须早于 capacity limit

一次扩容从发现到获得容量通常经历：

```text
signal sustained
  -> diagnosis and forecast review
  -> decision / budget / approval
  -> procurement or quota
  -> provision
  -> data copy / rebalance / catch-up
  -> validation
  -> traffic shift
  -> rollback observation window
```

总提前期：

$$
T_{\text{lead}}
=
\sum_j T_j
$$

若保守预测到达上限的时间为 $T_{\text{limit,P90}}$，行动条件应是：

$$
T_{\text{limit,P90}}
\le
T_{\text{lead}}
+
T_{\text{validation}}
+
T_{\text{safety}}
$$

“磁盘 90% 再扩”不是通用策略。一个需要四周采购、两周复制的集群，90% 可能已经
太晚；一个可在数分钟无状态扩出的 read pool，则可使用不同 trigger。

### 同时定义 utilization、SLO 与 growth trigger

容量触发器不应只看 CPU：

| 类别 | 示例信号 | 触发语义 |
|---|---|---|
| demand | request/write rate forecast | 业务增长越过已验证 envelope |
| SLO | p95/p99、timeout、pool wait | 在当前 load 下目标开始失守 |
| CPU | busy/run queue/service demand | 持续值与 burst/failure headroom |
| memory | working set、temp、swap/PSI | reclaim 或 spill 开始恶化 |
| storage | latency/queue/throughput | 设备服务时间接近边界 |
| space | time-to-limit | 小于 lead time + safety |
| WAL | generation/archive/slot backlog | consumer 无法在允许窗口追上 |
| maintenance | autovacuum/checkpoint/backup duration | 后台工作超过可用窗口 |
| HA | N+1/degraded capacity | 故障后 SLO 无法维持 |

trigger 应具有：

```text
threshold
duration
forecast horizon
scope
owner
runbook
rollback / stop condition
```

瞬时 CPU 65% 不应自动购买主机；持续两周的需求趋势、在 N+1 状态下预测将超过已验证
SLO envelope，才是可行动的 signal。

### 以失效状态写容量预算

先定义状态，而不是笼统说 “预留 30%”：

```text
N0  normal: all primary/replica/pool nodes healthy
N1  one data node unavailable or rebuilding
M1  backup + autovacuum + checkpoint overlap
F1  archive destination unavailable for allowed window
F2  one replica/slot consumer stalled for allowed window
```

每个状态分别计算：

```text
available CPU / storage / IOPS / connection slots
traffic redistribution
WAL accumulation
replication/rebuild load
latency and error SLO
maximum tolerable duration
```

例如三台 read replica 各承载 30% 峰值，不能据此宣称 N+1；失去一台后其余两台会
各承载 45%，还要叠加 cache coldness、reconnect 与 recovery traffic。N+1 必须在
failure drill 中测。

### 扩容不等于只加 CPU

观测到瓶颈后，选择与根因匹配的动作：

| 限制 | 候选动作 | 必须验证 |
|---|---|---|
| CPU execution | vertical scale、query/index、JIT/parallel policy | plan、tail、failure |
| read throughput | replica/read cache、query optimization | freshness、一致性、routing |
| write/WAL | batch/schema/index、faster WAL device | durability、replica/archive |
| lock/contention | shorten tx、partition key space、admission | correctness、公平性 |
| connection | PgBouncer、pool sizing、admission | session semantics、queue SLO |
| storage latency | storage class/layout、reduce random I/O | fsync tail、failure behavior |
| data volume | retention/partition/archive/shard | restore、query、operability |

`max_connections` 变大通常不是 capacity expansion。它可能把受控 queue 从 pool 移入
database，并增加 backend memory、context switch 和 lock competition。

Pigsty 把 topology、HAProxy/PgBouncer、PostgreSQL、监控和运维入口组织在一起，
方便执行变更；它不会替代业务 workload 与 SLO 的容量判断。扩容后仍要在实际
connection path 上重跑 baseline，并用
[Pigsty PGSQL dashboards](https://pigsty.io/docs/pgsql/dashboard/) 与 PostgreSQL
原生统计双重验证。

### 把容量结论写成有期限的决策记录

一条可审计的容量结论应包含：

```yaml
decision: "当前 topology 是否足以覆盖下一预测窗口"
environment: "硬件、版本、配置、connection path"
workload: "operation catalog、mix、arrival model、data scale"
slo: "latency/error/freshness/durability"
evidence: "run ids、raw artifacts、dashboards、queries"
validated_envelope: "哪些点通过，哪些点失败"
failure_state: "N0/N1/M1/F1/F2 中验证了哪些"
forecast: "P50/P90 demand and growth"
trigger: "threshold + duration + horizon"
lead_time: "provision + copy + validation + shift"
unknowns: "尚未测量的条件"
expires_at: "何时必须复审"
owner: "谁监控、谁决策、谁执行"
```

任何一个重大条件变化都使 baseline 进入复审：

```text
PostgreSQL/Pigsty or kernel/storage upgrade
schema/index/query change
workload mix or SLO change
data crosses tested scale
topology/routing/pool change
hardware or cloud instance change
backup/replication/durability policy change
material performance incident
```

容量模型的价值不是预测一个永远正确的 TPS，而是让团队在还有选择的时候，看见：

```text
what is known
under which conditions
how fast the boundary is approaching
how long the response takes
which failure would invalidate the plan
```

---

[上一节：找到饱和点与瓶颈](../04/) · [返回本章目录](../) · [下一节：实战：`pg36_shop` 容量基线](../06/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
