26.4 找到饱和点与瓶颈
资源到 100% 不是 saturation 的唯一定义。
系统可能先在:
latency SLO
error/timeout
lock queue
pool wait
WAL/replica/archive lag
memory pressure
storage latency上失效,而 dashboard 中 CPU 还没满。
容量曲线必须同时画:
offered load
achieved throughput
latency distribution
failure/late/skipped
queue/wait
resource utilization瓶颈不是“最高的那条曲线”,而是当前最先限制目标的 service center。
26.4.1 吞吐—延迟曲线与排队拐点
典型曲线有三个区域
throughput
^
| __________ maximum / collapse region
| ___/
| ___/
| ___/
| ___/
+--------------------------------> offered load / concurrency
linear knee saturated对应 latency:
latency
^
| /
| __/
| __/
|__________________/
+--------------------------------> offered load / concurrency区域:
| 区域 | throughput | latency | queue |
|---|---|---|---|
| linear | 近似随 load 增长 | 稳定 | 很小 |
| knee | 增益变小 | tail 开始快速上升 | 累积 |
| saturated | 持平或下降 | 非线性恶化 | 持续/失败 |
capacity 通常选在 knee 左侧,并留 failure/maintenance headroom,而不是取最高 TPS 点。
两个并发点只能 bracket
参考只有 c1/c8:
| scale | c1 TPS | c8 TPS | gain | c1 p95 | c8 p95 | p95 multiplier |
|---|---|---|---|---|---|---|
| S | 1,508.5 | 2,920.5 | 1.936x | 2.152 | 9.448 | 4.390x |
| M | 1,555.8 | 2,911.5 | 1.871x | 2.123 | 9.398 | 4.427x |
| L | 1,423.6 | 2,774.3 | 1.949x | 2.259 | 10.087 | 4.465x |
c8 throughput 仍比 c1 高约 1.9 倍,不能说 knee 已经位于 c1–c8;p95 却放大 约 4.4 倍,说明排队/并行争用已明显增加。
公共结果因此写:
{
"interpretation": "knee-not-bracketed-by-one-and-eight-clients",
"exact_knee_known": false
}下一轮应补:
c2 c4 c8 c12 c16 c24 c32或以 open-loop rate 做更密的 SLO sweep。只有两点画出的“曲线”是连线,不是 找到拐点。
closed-loop 曲线的横轴不是 offered arrival
零 think time、$C$ 个 client:
$$ X \approx \frac{C}{R} $$
当 latency 上升,throughput 自动降低。这会形成自我节流:
server slows
-> each client waits longer
-> fewer new transactions arrive真实应用 request 可能继续到达并在 pool/queue 中堆积。因此 closed-loop c8 回答:
eight persistent clients can complete how much work不回答:
system can admit N external requests/s while meeting p95 and error SLOopen-loop 要同时看 offered 与 achieved
用 --rate:
pgbench \
--rate=2200 \
--latency-limit=250 \
--time=300 \
...记录:
$$ \text{completion ratio}
\frac{\text{successful}}{\text{scheduled}} $$
以及:
schedule lag
late
skipped
failed
queue depth若 achieved TPS 看起来稳定,但 skipped 快速增加,系统不是健康保持吞吐,而是 丢弃工作。
用 Little’s Law 检查 queue
对 database active/queued population:
$$ L=\lambda W $$
c8、2,920 TPS、mean latency 粗略约:
$$ W \approx \frac{8}{2920} \approx 2.74 ms $$
这个量接近 mixed mean,不是 p95。若观测到平均 active+wait session 与它严重 不一致,检查:
- transaction log boundary;
- pool queue 未计;
- client think/network;
- sampler bias;
- multiple transactions per script;
- offered/achieved 混用。
knee 是多目标决策
可能的 acceptance:
p95 <= 250 ms
p99 <= 500 ms
failed <= 0.1%
skipped = 0
CPU target <= 65%
disk latency within budget
replica/archive catch up within 5 min
pool wait p95 <= 20 ms最先违反的条件定义当前可用边界。它可能远早于最大 TPS。
throughput collapse
超过 knee 后可能:
more clients
-> more context switches
-> more lock queue
-> more cache churn
-> more memory/IO pressure
-> longer transactions
-> locks held longer
-> even more queue形成正反馈,throughput 反而下降。压测 runner 应有 stop condition:
- failure/late 超阈值;
- latency 超 hard limit;
- replica/archive gap 无界增长;
- disk/memory safety floor;
- server health/HA 状态变化;
- client lost;
- cleanup 不再可保证。
26.4.2 CPU、内存、I/O、WAL 与锁的证据
每个候选瓶颈需要至少两层证据
| 假设 | PostgreSQL | OS/Pigsty | 反证 |
|---|---|---|---|
| CPU | active no wait、query exec time | CPU busy/run queue | client/lock wait |
| data I/O | pg_stat_io, blks_read/time | disk bytes/latency/queue | OS cache |
| WAL | pg_stat_wal, WAL IO | disk write, archive/replay | data write |
| lock | wait event, pg_locks blocker | low CPU possible | client wait |
| memory | temp bytes, backend count | available/PSI/OOM | cache reclaim |
| connection | activity/pool states | client queue | active execution |
一个指标不是 root cause。
CPU:utilization、run queue 与 service demand
参考 measured-window median:
| cell | server work | client work |
|---|---|---|
| S-c1 | 48.9% | 17.7% |
| S-c8 | 84.2% | 29.5% |
| M-c1 | 50.1% | 17.8% |
| M-c8 | 84.0% | 29.1% |
| L-c1 | 46.3% | 16.2% |
| L-c8 | 84.2% | 27.8% |
c8 的 server work 显著更高,client 仍有余量。可以说:
server-side work is a stronger limiting candidate than load-generator CPU不能说:
CPU is the sole root cause因为约 16% 时间没有被 work 计入,可能是 idle、virtual scheduling、commit
path、network 或其他等待;c8 也存在 lock/LWLock/IO samples。
单位 CPU demand:
$$ D_{cpu}
\frac{U_{cpu}\times T}{N} $$
参考 c8 median run:
| scale | CPU s / mixed tx |
|---|---|
| S | 0.0002906 |
| M | 0.0002924 |
| L | 0.0003070 |
它包含 server 上所有 CPU work,不只 SQL executor;这是容量视角需要的总需求, 但 shared VM background 会引入噪声。
memory:不要用 available 一张图下结论
PostgreSQL memory:
shared memory
+ backend/session memory
+ per-node work_mem/hash memory
+ parallel workers
+ maintenance/autovacuum
+ extension/JIT
+ OS page cache风险模型:
$$ M_{\text{peak}} \not\approx shared_buffers + max_connections \times work_mem $$
work_mem 是每个 sort/hash operation 的基础限制,一条 query 可能多个,一台
server 可能多个 query/worker。
观察:
- MemAvailable minimum;
- swap/OOM/PSI;
- backend count 与 active count;
- temp file/bytes;
- hash/sort spill;
- parallel workers;
- cgroup/VM limit。
参考 cells:
temp_bytes 0
swap 0只说明这个短 workload 未观测到 database temp spill;它不是 memory capacity 证明。
I/O:先分 data、WAL 与 background
pg_stat_io 按:
backend_type
object
context
reads/writes/extends/fsyncs
bytes/time聚合。track_io_timing=on 才有部分 timing;参考
track_wal_io_timing=off,所以不能声称有 WAL write/fsync timing。
cell database block evidence:
| cell | block reads | block hits |
|---|---|---|
| S-c1 | 0 | 675,976 |
| S-c8 | 0 | 1,323,054 |
| M-c1 | 0 | 733,198 |
| M-c8 | 0 | 1,326,143 |
| L-c1 | 2,500 | 629,335 |
| L-c8 | 14,491 | 1,344,632 |
L 出现 PostgreSQL-level reads,与 dataset 大于 shared buffers 一致;仍不能把 每个 read 当 physical disk。
Pigsty 全实验 window:
node disk read max 13.99 MB/s
node disk write max 81.22 MB/s包含 3.2M row 初始化,不用于 cell arithmetic。per-run /proc 与 native delta
才用于解释 measured cell。
WAL:按 transaction 和 operation 分解
mixed WAL/tx:
| cell | WAL bytes / mixed tx |
|---|---|
| S-c1 | 160.3 |
| S-c8 | 159.5 |
| M-c1 | 176.0 |
| M-c8 | 161.9 |
| L-c1 | 157.6 |
| L-c8 | 196.2 |
不要用六个值平均成“PostgreSQL 每事务 168 bytes”。它只适用于当前 80% read、 20% write mix,还受 full-page image、background 和短窗口影响。
更好的生产模型:
WAL/read-product
WAL/read-order
WAL/place-order
WAL/background通过 operation/queryid-scoped delta 或隔离 workload 校准,再按预测 mix 加权。
WAL rate 还决定:
- replica network/write/replay;
- archive bandwidth;
- retained WAL;
- PITR repository;
- recovery/catch-up time。
lock:等待样本不是完整 lock history
c8 cell 观察到少量:
Lock
LWLock
IO
Client
CPU所有 cell deadlock=0。解释边界:
sampled wait
catches only state at 250 ms sample points
deadlock counter
counts detected deadlocks
neither proves no short lock waits要解释 lock:
- blocker/waiter graph;
- lock mode/object;
- transaction age;
- wait duration;
- hot key;
- statement/queryid;
- timeout/deadlock log。
Client wait 多也不能自动反证 lock;backend 可能在 statement boundary 等
client,另外 client 可能在等另一个 backend。
checkpoint、archive 与 replication
Pigsty PGSQL Persist dashboard 把 WAL、XID、checkpoint、archive 和 I/O 放在同一观察面。
实验期间:
replica replay gap median 0
replica replay gap max 3,471,848 bytes这表示 exporter 的 replay-distance metric 曾观测到短时差距。不能推出:
- commit token 已在特定 replica 可见;
- RPO=0;
- failure 时一定无数据丢失;
- catch-up time 在生产 storage 上相同。
第 20 章的 HA contract 与第 25 章的 freshness signal 仍然生效。
用 hypothesis table 组织诊断
| hypothesis | supporting | contradicting | next test |
|---|---|---|---|
| client CPU ceiling | none strong | client <30%, server ~84% | distributed client |
| PostgreSQL CPU knee | server rises to ~84% | throughput still gains ~1.9x | c12/c16/c24 |
| data I/O for L | block reads appear | c8 iowait median low | larger-than-RAM/open-loop |
| hot-row lock | Lock/LWLock samples | no deadlock, gain still large | change Zipf/uniform |
| WAL limit | L-c8 WAL/tx higher | no lag growth proven | write-heavy mix |
结论不是“选一行”,而是设计下一个最能区分假设的实验。
26.4.3 连接数增加为何可能降低吞吐
max_connections 是上限,不是目标
max_connections=500 只说明 PostgreSQL 接受的 backend 数上限。它不证明 500
active query 是健康并发。
每个 backend 带来:
- process 与 private memory;
- transaction/snapshot;
- lock table entries;
- plan/executor state;
- network socket;
- statistics/logging;
- scheduling/cache footprint。
active concurrency 超过 bottleneck 并行度后,额外 backend 主要排队。
CPU 调度与 cache locality
1 vCPU 上 32 个 CPU-bound backend:
no more execution capacity
more runnable processes
more context switching
instruction/data cache disruption
longer transaction duration长 transaction 又让 lock 持有更久,形成二阶影响。
观察:
CPU utilization
run queue/load
context switches
active sessions
per-query service demandlock queue
若多个 place-order 更新同一 hot inventory row:
one holder
many waiters增加 client 不增加该 key 的 service rate,只增加 queue:
$$ W_q \uparrow,\quad X \approx constant $$
若 transaction 在拿锁前还做很多工作,rollback/retry 成本更高。
措施不是盲目加连接:
- partition/shard hot key;
- shorten transaction;
- fixed lock order;
- admission per key/tenant;
- move external calls outside transaction;
- optimistic/versioned update;
- reduce retry storm。
memory amplification
更多 active query:
more sort/hash
more parallel workers
more temp spill
less OS cache
more I/O然后 latency 上升,连接占用更久,再增加在途数。
storage queue
存储有可用并行深度,但不是无限:
low concurrency
underutilized device
optimal concurrency
latency acceptable, throughput high
excess queue
throughput flat, latency rises同一 device 上 data、WAL、checkpoint、archive staging 和 backup 可能相互影响。
parallel query 与 client concurrency 相乘
若一个 query 最多 4 workers:
8 active queries
may request up to 8 leaders + 32 workersPostgreSQL resource 文档提醒 parallel worker 是独立 process,CPU/memory impact
类似额外 session,work_mem 也按 worker/operation 作用。
所以容量合同要同时固定:
client concurrency
max_parallel_workers
max_parallel_workers_per_gather
query planpool 的正确目标是限制 server concurrency
例如:
10,000 idle application connections
-> PgBouncer
-> 32 server connections
-> 8 active budgetpool queue 是可控 admission,前提是:
- queue latency 可观测;
- client timeout 大于/匹配 policy;
- cancellation 正确;
- transaction pooling 兼容 session feature;
- retry 不放大;
- queue 有上限。
没有上限的 pool 只是把 outage memory 从 PostgreSQL 移到中间层。
找到健康并发的实验
对每个 data/mix:
c1 c2 c4 c8 c12 c16 c24 c32每点:
- 固定 offered model;
- 重复;
- 画 throughput、p95/p99、failed、late、queue;
- 同时画 server/client CPU、I/O、WAL、lock;
- 记录 pool/direct path。
选择:
first point before material tail acceleration
and within headroom/failure policy不是:
highest observed TPS本节结论
参考证据支持:
- c8 比 c1 吞吐高约 1.9x;
- c8 pooled p95 高约 4.4x;
- client CPU 不是明显 ceiling;
- server work、lock/LWLock/IO 与 L block-read 都值得下一轮区分;
- 两点没有定位 exact knee。
它不支持“最佳连接数=8”。下一轮实验而不是措辞强度,才能减少未知项。
上一节:建立可信实验 · 返回本章目录 · 下一节:从测量推导容量与成本 · 查看全书目录 · 查看索引中心