26.5 从测量推导容量与成本
一次压测给出的 TPS 不是容量答案。它只是某个 workload、数据规模、配置和时间窗口下 的一组观测。容量规划要把观测转换为三个可以行动的模型:
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转换过程中最危险的捷径是:
benchmark maximum TPS × server count = future capacity它忽略了 workload mix、尾延迟、后台工作、故障冗余、增长、复制、备份以及扩容 提前期。可信的容量模型必须保留条件、区间和未知项。
26.5.1 单位业务量的资源消耗
先定义业务单位
“每事务成本”只有在 transaction 的业务语义稳定时才有意义。第 26 章实验的 transaction mix 是:
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 也会变化。因此模型应优先保存:
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
假设测量窗口内:
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}}$ 不是物理极限。它还要容纳:
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 原生统计可以分别回答:
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 模型至少分:
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
可按 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 增长、保留、备份与维护空间
数据库大小不是一条线
磁盘容量要同时容纳:
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:
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。
注意:
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:
date
live rows
heap bytes
index bytes
TOAST bytes
dead tuples
WAL bytes/day
backup bytes
archive backlog
forecast P50/P90forecast 不是只更新未来;它也要用实际误差校准。
bloat 与 free space 不是同义词
PostgreSQL MVCC update/delete 会产生 dead tuple。普通 VACUUM 通常把空间留在
relation 内供复用,并不把大部分文件空间还给文件系统。于是:
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”。要考虑:
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
和
log-shipping standby
说明了 checkpoint、归档、streaming 与 retention 之间的关系。
容量规划至少演练:
archive destination unavailable
replica offline
logical subscriber stalled
backup exceeds expected duration
write burst crosses checkpoint正常状态的 WAL 目录大小不能代表这些失效状态。
备份容量用 retention graph,而不是压缩率愿望
备份 inventory 应包括:
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 示例:
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$ 不应只有一条平均线。至少计算:
P50 trend
P90/P95 growth or burst scenario
retention policy change
failure backlog scenario
largest scheduled maintenance如果增长非线性,就用时间序列/业务 forecast,不能继续用直线外推。
26.5.3 扩容触发线、提前期与失效假设
trigger 必须早于 capacity limit
一次扩容从发现到获得容量通常经历:
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 应具有:
threshold
duration
forecast horizon
scope
owner
runbook
rollback / stop condition瞬时 CPU 65% 不应自动购买主机;持续两周的需求趋势、在 N+1 状态下预测将超过已验证 SLO envelope,才是可行动的 signal。
以失效状态写容量预算
先定义状态,而不是笼统说 “预留 30%”:
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每个状态分别计算:
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 与 PostgreSQL 原生统计双重验证。
把容量结论写成有期限的决策记录
一条可审计的容量结论应包含:
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 进入复审:
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,而是让团队在还有选择的时候,看见:
what is known
under which conditions
how fast the boundary is approaching
how long the response takes
which failure would invalidate the plan上一节:找到饱和点与瓶颈 · 返回本章目录 · 下一节:实战:pg36_shop 容量基线 ·
查看全书目录 · 查看索引中心