# WAL、检查点与写入平滑

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

---

写事务的磁盘路径不是“把 table row 写到文件后提交”。

近似链路：

```text
change shared buffer
  -> generate WAL record
      -> insert into WAL buffer
          -> write/flush WAL according to commit policy
              -> acknowledge commit
                  -> data page later written by backend/bgwriter/checkpoint
```

WAL 让 data page 可以延后写，却把 commit latency、checkpoint、archive、replication
和 crash recovery 连接成一个系统。调其中一项，必须看整条链。

## 27.3.1 WAL 生成、刷盘与提交延迟 {#item-27-3-1}

### write、flush 与 acknowledge

需要区分：

```text
WAL generated
WAL copied/written to kernel
WAL flushed to durable storage
WAL sent to standby
WAL written/flushed/replayed on standby
client receives success
```

不同 `synchronous_commit` level 改变 acknowledge 等待的阶段。它不是“开/关性能”：

- `on`：本地 durable，并遵守 synchronous standby 配置；
- `remote_apply`：还等待 synchronous standby replay；
- `remote_write`：等待 standby 写入 OS；
- `local`：等待本地 flush，不等待 synchronous standby；
- `off`：允许本地 WAL 尚未 flush 就向 client 确认。

具体 durability 还受 `synchronous_standby_names`、`synchronous_standby_slots`、
standby availability 和 application route 影响。不要看到 `on` 就自动推导
multi-node zero-loss，也不要看到 `off` 就说“数据会损坏”；它改变的是 crash 时最近
成功确认 transaction 的 loss window。

### `fsync` 与 `full_page_writes` 不是普通 candidate

```text
fsync=off
full_page_writes=off
```

可能显著提高 benchmark TPS，但会改变 crash safety。PostgreSQL 官方
[`WAL settings`](https://www.postgresql.org/docs/18/runtime-config-wal.html)
明确说明，在可能发生 OS/硬件 crash 的系统中关闭相关保护可能导致不可恢复或静默
损坏。

教学 benchmark 若关闭它们，测的是另一个产品合同。

### group commit

多个 backend 可以共享一次 flush：

```text
backend A commits
backend B commits nearby
  -> one WAL flush may make both durable
```

因此：

- 单 client commit latency 不代表多 client；
- TPS 与 fsync count 不一定 1:1；
- storage fsync tail 比平均 throughput 更重要；
- batch/transaction boundary 改变 group opportunity。

`commit_delay`/`commit_siblings` 可以人为等待以扩大 group，但等待本身增加 latency，
只有在高并发、flush 昂贵且实验支持时才考虑。默认不是越大越好。

### 测 WAL composition

累计层：

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

PostgreSQL 18 把 WAL I/O bytes/timing 纳入 `pg_stat_io`；不要照抄旧版本
`pg_stat_wal.wal_write_time/wal_sync_time` 查询：

```sql
SELECT
    backend_type,
    object,
    context,
    reads,
    read_bytes,
    writes,
    write_bytes,
    fsyncs,
    write_time,
    fsync_time
FROM pg_stat_io
WHERE object = 'wal'
ORDER BY backend_type, context;
```

列与可用 timing 取决于 PostgreSQL major version 与
`track_wal_io_timing`。升级时先查 system view schema。

statement 层：

```sql
SELECT
    queryid,
    calls,
    wal_records,
    wal_fpi,
    wal_bytes
FROM pg_stat_statements
ORDER BY wal_bytes DESC
LIMIT 20;
```

不要公开 query text；用 queryid 对回内部 catalog。

### 分母要有业务语义

```text
WAL bytes / mixed transaction
WAL bytes / write transaction
WAL bytes / order
WAL bytes / changed row
```

不是同一个指标。

第 26 章 mixed workload 只有 20% place-order，约 158–196 WAL bytes/mixed tx。
不能把它写成“每个订单 196 bytes”。需要按成功 write operation 归一，并把 background
activity 与 full-page image phase 作为误差。

### WAL buffer full

`wal_buffers_full` 增加表示 WAL buffer 空间不足时 backend 被迫写 WAL，但不能只看到
counter 就把 `wal_buffers` 手工调大：

- 默认 `-1` 会基于 shared buffers 自动选择；
- upper bound 通常一个 WAL segment；
- 写入/flush device 可能才是限制；
- checkpoint/FPI 或大 transaction 可能改变 burst；
- counter 是累计 delta。

先对齐发生时间与 WAL generation/write/fsync。

### commit latency 的证据链

```text
client transaction latency
  -> server wait event
      -> WAL I/O timing/count
          -> device fsync latency/queue
              -> synchronous standby wait
                  -> network/replay
```

如果 backend 等 `SyncRep`，加快本地 WAL device 未必改善；如果 server CPU 已饱和，
启用更重 WAL compression 可能反而恶化。

## 27.3.2 检查点频率、写突发与恢复时间 {#item-27-3-2}

### checkpoint 做什么

checkpoint 建立 recovery 起点，并把需要的 dirty buffer 写出，使 crash recovery
不必从无限久之前 replay WAL。它不是“定期 fsync 一次”这么简单。

触发：

```text
time: checkpoint_timeout
WAL volume: max_wal_size soft threshold
manual/requested: CHECKPOINT or operational action
shutdown/recovery transitions
```

PostgreSQL 18 的证据：

```sql
SELECT *
FROM pg_stat_checkpointer;

SELECT *
FROM pg_stat_bgwriter;
```

checkpointer 与 bgwriter 已分开统计。关注：

```text
num_timed / num_requested / num_done
write_time / sync_time
buffers_written
backend writes/fsync
checkpoint warnings/log duration
```

不要用旧版本列名硬编码跨版本 dashboard。

### `max_wal_size` 是 soft limit

它不是：

```text
pg_wal hard cap
archive backlog cap
slot retention cap
disk safety guarantee
```

在 archive failure、replication slot、`wal_keep_size`、重负载等条件下，WAL 可超过
`max_wal_size`。把 disk provision 写成 `max_wal_size + 10%` 是错误模型。

### 太小的症状

```text
requested/WAL-driven checkpoints frequent
checkpoint_warning
WAL FPI rate high
write/sync burst
backend writes increase
throughput/latency periodic sawtooth
```

因为每次 checkpoint 后，某页第一次修改需要 full-page image（在保护开启时），过于
频繁会增加 WAL。

### 太大的代价

增加 `max_wal_size`/`checkpoint_timeout` 可能：

- 减少 checkpoint 频率；
- 降低 checkpoint-induced FPI；
- 让 dirty work 有更长时间平滑；
- 增加 crash recovery 要 replay 的 WAL；
- 增加 `pg_wal` working space；
- 延后 dirty page write，扩大 failure/burst 状态；
- 改变 archive/replica catch-up。

优化点不是“checkpoint 越少越好”，而是满足：

```text
foreground SLO
write smoothness
crash RTO
WAL disk headroom
archive/replica behavior
```

### `checkpoint_completion_target`

它控制 checkpoint 在 checkpoint interval 中用于完成的目标比例。默认较高是为了把
I/O 分散到大部分 interval。降低会让 checkpoint 更快完成：

```text
higher instantaneous write rate
  -> then longer quiet period
```

通常不是想要的平滑。参考 Pigsty 沙箱为 0.95；改变它前要看 checkpoint progress、
device latency 与 SLO，而不是复制旧时代 0.7。

### 写入平滑不是平均值

画 time series：

```text
WAL bytes/s
checkpoint begin/end
data write bytes/s
fsync latency
dirty buffers
backend writes
archive rate/gap
replica receive/replay gap
OLTP p95/p99
```

平均 20MB/s 可能是：

```text
持续 20MB/s
```

或：

```text
每 10 秒 200MB/s，其余为 0
```

设备和 tail latency 面对的是后者。

### 恢复时间要实测

粗略：

$$
T_{\text{recovery}}
\approx
\frac{\text{WAL to replay}}
{\text{effective replay throughput}}
+
\text{startup/finalization}
$$

replay throughput 受：

```text
WAL record mix
CPU
data/WAL storage
full-page images
compression/decompression
extension
prefetch
recovery conflicts
```

影响。不能用 WAL device sequential bandwidth 代替。

checkpoint 调整 acceptance 应含 crash/restart 或 replica rebuild drill，而不是只跑
steady primary。

### 不要为 benchmark 强制 checkpoint

每次 run 前：

```sql
CHECKPOINT;
```

会人为同步 phase：

- 一开始 full-page image 更多；
- dirty state 被清空；
- I/O burst 与 production natural phase 不同。

如果研究 checkpoint phase，应把 “immediately after checkpoint / mid-cycle” 作为显式
factor；否则让背景系统自然运行并记录 checkpoint。

## 27.3.3 压缩、全页写与归档代价 {#item-27-3-3}

### full-page image 的正确性作用

当 `full_page_writes=on`，checkpoint 后某页第一次被修改时，WAL 通常记录整页 image，
以防 torn page 让 recovery 无法还原。于是：

```text
frequent checkpoint
  -> more first modifications
      -> more FPI
          -> more WAL
```

但关系受 working set 与 write locality 影响。修改同一小批 hot page 与随机修改巨大
dataset 的 FPI 比例不同。

### `wal_compression`

PostgreSQL 18 支持的 method 取决于 build：

```text
off
pglz
lz4
zstd
```

它主要压缩 full-page image，不是压缩所有 WAL record。tradeoff：

```text
less WAL bytes
  vs
more primary compression CPU
more replay decompression CPU
different archive/network/storage load
```

参考沙箱：

```text
wal_compression = lz4
source          = configuration file
context         = superuser
```

第 26 章 c8 已有 CPU pressure，却没有证明 WAL I/O 是 throughput limit。因此“开启更强
compression 减少 WAL”不符合当前 service center evidence。

### compression 实验矩阵

至少比较：

| 维度 | 指标 |
|---|---|
| primary | TPS、p95、CPU、wal_bytes、wal_fpi |
| WAL device | write/fsync bytes/latency |
| network/archive | bytes/s、backlog、catch-up |
| replica | replay CPU、lag、recovery |
| backup/PITR | archive compatibility、restore time |
| failure | crash replay、promotion |

只看 `wal_bytes` 降低不能决定接受。

### archive 与 slot 可能让 WAL 无界

```text
WAL generated
  -> pg_wal recycle/remove eligibility
      depends on checkpoint
      + archive completion
      + replication consumers/slots
      + wal_keep_size
      + backup/recovery needs
```

若 archive throughput $A$ 小于 WAL generation $W$：

$$
\text{backlog growth}
=
W-A
$$

增加 `max_wal_size` 只推迟 disk full，不修复 consumer。

若 slot inactive：

```sql
SELECT
    slot_name,
    slot_type,
    active,
    restart_lsn,
    confirmed_flush_lsn,
    wal_status,
    safe_wal_size
FROM pg_replication_slots;
```

列随版本变化，先查当前 view。还要配置/验证 `max_slot_wal_keep_size` 等 guard，但 guard
触发可能使 consumer 无法继续，属于 recovery/runbook 决策。

### `min_wal_size` 与 recycle

`min_wal_size` 让一定量旧 WAL segment 被 recycle 供未来使用，减少 burst 时反复创建/
删除。它不是最小 WAL generation，也不是 retention policy。

filesystem 类型可能影响：

```text
wal_init_zero
wal_recycle
```

尤其 CoW filesystem。但这些是 storage-specific candidate，应以文件系统、allocation
行为和 crash test 证明，不能把云盘经验搬到本地 SSD。

### 归档压缩与 WAL compression 是两层

```text
wal_compression
  compresses full-page images inside WAL records

archive/backup compression
  compresses WAL files/backup objects during transport/storage
```

二者可叠加，CPU 消耗发生在不同进程/节点/阶段。容量模型要测最终 archive object
bytes 和两侧 CPU，不要把 primary `wal_bytes` ratio 当作备份压缩率。

### 参数 ADR

WAL/checkpoint 变更应写：

```yaml
symptom:
  requested_checkpoints_per_hour: ...
  wal_fpi_ratio: ...
hypothesis:
  parameter: max_wal_size
  mechanism: reduce WAL-driven checkpoint frequency
baseline:
  checkpoint_interval: ...
  wal_bytes_s: ...
  crash_recovery_p95: ...
candidate:
  value: ...
benefit_gate:
  foreground_p95: ...
non_regression:
  pg_wal_peak: ...
  archive_gap: ...
  replica_replay: ...
  crash_recovery: ...
rollback:
  value/source: ...
  restart_or_reload: ...
```

没有 recovery/retention 证据的 checkpoint 优化，只完成了一半。

---

[上一节：内存预算](../02/) · [返回本章目录](../) · [下一节：规划器、并行与连接参数](../04/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
