跳转到主要内容

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:

XCR 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 SLO

open-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=λW L=\lambda W

c8、2,920 TPS、mean latency 粗略约:

W829202.74ms 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

风险模型:

Mpeak≉shared_buffers+max_connections×work_mem 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 demand

lock queue

若多个 place-order 更新同一 hot inventory row:

one holder
many waiters

增加 client 不增加该 key 的 service rate,只增加 queue:

Wq,Xconstant 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 workers

PostgreSQL resource 文档提醒 parallel worker 是独立 process,CPU/memory impact 类似额外 session,work_mem 也按 worker/operation 作用。

所以容量合同要同时固定:

client concurrency
max_parallel_workers
max_parallel_workers_per_gather
query plan

pool 的正确目标是限制 server concurrency

例如:

10,000 idle application connections
  -> PgBouncer
      -> 32 server connections
          -> 8 active budget

pool 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”。下一轮实验而不是措辞强度,才能减少未知项。


上一节:建立可信实验 · 返回本章目录 · 下一节:从测量推导容量与成本 · 查看全书目录 · 查看索引中心