# 找到饱和点与瓶颈

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

---

资源到 100% 不是 saturation 的唯一定义。

系统可能先在：

```text
latency SLO
error/timeout
lock queue
pool wait
WAL/replica/archive lag
memory pressure
storage latency
```

上失效，而 dashboard 中 CPU 还没满。

容量曲线必须同时画：

```text
offered load
achieved throughput
latency distribution
failure/late/skipped
queue/wait
resource utilization
```

瓶颈不是“最高的那条曲线”，而是当前最先限制目标的 service center。

## 26.4.1 吞吐—延迟曲线与排队拐点 {#item-26-4-1}

### 典型曲线有三个区域

```text
throughput
  ^
  |                  __________ maximum / collapse region
  |              ___/
  |          ___/
  |      ___/
  |  ___/
  +--------------------------------> offered load / concurrency
       linear      knee       saturated
```

对应 latency：

```text
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 倍，说明排队/并行争用已明显增加。

公共结果因此写：

```json
{
  "interpretation": "knee-not-bracketed-by-one-and-eight-clients",
  "exact_knee_known": false
}
```

下一轮应补：

```text
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 自动降低。这会形成自我节流：

```text
server slows
  -> each client waits longer
      -> fewer new transactions arrive
```

真实应用 request 可能继续到达并在 pool/queue 中堆积。因此 closed-loop c8
回答：

```text
eight persistent clients can complete how much work
```

不回答：

```text
system can admit N external requests/s while meeting p95 and error SLO
```

### open-loop 要同时看 offered 与 achieved

用 `--rate`：

```bash
pgbench \
  --rate=2200 \
  --latency-limit=250 \
  --time=300 \
  ...
```

记录：

$$
\text{completion ratio}
=
\frac{\text{successful}}{\text{scheduled}}
$$

以及：

```text
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：

```text
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 后可能：

```text
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 与锁的证据 {#item-26-4-2}

### 每个候选瓶颈需要至少两层证据

| 假设 | 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 仍有余量。可以说：

```text
server-side work is a stronger limiting candidate than load-generator CPU
```

不能说：

```text
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：

```text
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：

```text
temp_bytes 0
swap       0
```

只说明这个短 workload 未观测到 database temp spill；它不是 memory capacity
证明。

### I/O：先分 data、WAL 与 background

`pg_stat_io` 按：

```text
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：

```text
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 和短窗口影响。

更好的生产模型：

```text
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 观察到少量：

```text
Lock
LWLock
IO
Client
CPU
```

所有 cell deadlock=0。解释边界：

```text
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](https://pigsty.io/docs/pgsql/dashboard/)
把 WAL、XID、checkpoint、archive 和 I/O 放在同一观察面。

实验期间：

```text
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 连接数增加为何可能降低吞吐 {#item-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：

```text
no more execution capacity
more runnable processes
more context switching
instruction/data cache disruption
longer transaction duration
```

长 transaction 又让 lock 持有更久，形成二阶影响。

观察：

```text
CPU utilization
run queue/load
context switches
active sessions
per-query service demand
```

### lock queue

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

```text
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：

```text
more sort/hash
more parallel workers
more temp spill
less OS cache
more I/O
```

然后 latency 上升，连接占用更久，再增加在途数。

### storage queue

存储有可用并行深度，但不是无限：

```text
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：

```text
8 active queries
may request up to 8 leaders + 32 workers
```

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

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

```text
client concurrency
max_parallel_workers
max_parallel_workers_per_gather
query plan
```

### pool 的正确目标是限制 server concurrency

例如：

```text
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：

```text
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。

选择：

```text
first point before material tail acceleration
and within headroom/failure policy
```

不是：

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

---

[上一节：建立可信实验](../03/) · [返回本章目录](../) · [下一节：从测量推导容量与成本](../05/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
