Skip to content

WAL 与检查点优化 ​

WAL(Write Ahead Log,预写日志)是 PostgreSQL 保证持久性的核心机制,也是写入型负载上最大的 I/O 来源。而检查点(checkpoint)则是「把内存脏页刷到数据文件、并让旧 WAL 段可以被回收」的周期性动作。两者配合得好,写入平稳、恢复时间可控;配合得不好,就会表现为「周期性卡顿」「fsync 抖动」「checkpoints are occurring too frequently 日志刷屏」。

本节不再重复 WAL 的内部结构与检查点进程的工作细节(那部分在架构章节),而是从参数调优与监控的角度出发:每个参数该调成什么、调完怎么验证、出问题怎么定位。WAL 的物理格式与写入流程见 WAL 概述,检查点的进程内部机制见 Checkpointer(检查点进程)。

前置知识

阅读本节前,建议先了解:连接池配置(PgBouncer)

基础概念 ​

WAL 解决的是什么问题 ​

数据库的写入顺序是「先写 WAL,后写数据文件」(Write Ahead):

text
一次 UPDATE 的写入路径

  ┌────────────┐   ① 生成 WAL 记录   ┌────────────┐
  │ 后端进程    │ ─────────────────▶ │ WAL 缓冲区  │
  └────────────┘                     └────────────┘
        │                                   │ ② 提交时 fsync
        │ ③ 修改数据页                       ▼
        ▼                            ┌────────────┐
  ┌────────────┐                     │  pg_wal/   │  ← 顺序写,速度快
  │shared_buffers│   (脏页,稍后刷盘)  └────────────┘
  └────────────┘
        │ ④ 检查点/后台写入
        ▼
  ┌────────────┐
  │  数据文件    │  ← 随机写,速度慢
  └────────────┘

这样设计的好处:提交时只需保证 WAL 落盘(顺序写),不必等待随机写的数据页落盘。崩溃后靠重放 WAL 恢复到一致状态。

代价是:所有写操作都要额外写一份 WAL。WAL 量可能与数据量相当甚至更大(尤其开启了全页写入时)。因此 WAL 相关参数直接决定了写入性能的上限。

检查点为什么会导致性能抖动 ​

检查点要做两件事:

  1. 把 shared_buffers 中的脏页刷到数据文件(随机写,量大);
  2. 记录一个 redo point,使得此前的 WAL 段可以被回收/归档。

问题出在第 1 步:如果脏页集中在一个很短的时间窗口内刷出去,就会形成 I/O 尖峰,其他正常查询被迫排队 → 表现为「每隔几分钟卡一下」。

text
不理想(集中刷脏)              理想(checkpoint_completion_target 摊平)

I/O  │   █                       I/O  │   ▄▄▄▄▄▄▄▄▄▄▄▄
     │   █                            │   ▄▄▄▄▄▄▄▄▄▄▄▄
     │   █                            │   ▄▄▄▄▄▄▄▄▄▄▄▄
     └───────────────▶ 时间           └───────────────▶ 时间
      尖峰 → 查询延迟抖动              平缓 → 延迟稳定

同时,全页写入(full-page writes)会放大 WAL 量:每次检查点之后,一个页的第一次修改会把整页(8kB)写进 WAL。检查点越频繁,全页写入的重复次数越多,WAL 量随之暴涨。这就是「检查点频率」与「WAL 体积」之间的耦合关系。

语法与用法 ​

WAL 级别与持久性参数 ​

参数默认值可选值是否需重启说明
wal_levelreplicaminimal / replica / logical是决定写入 WAL 的信息量
fsynconon / off是关闭后断电可能数据损坏,生产必须开
synchronous_commitonremote_apply / on / remote_write / local / off否提交返回前需要完成多少 WAL 处理
full_page_writesonon / off是检查点后首次修改页时写整页,关闭有损坏风险
wal_compressionoffoff / pglz / lz4 / zstd否压缩全页镜像,省 WAL 但费 CPU

wal_level 三个级别的取舍:

级别支持能力WAL 体积适用
minimal仅崩溃恢复;不支持归档与流复制最小一次性批量导入、临时实例
replica(默认)崩溃恢复 + 归档 + 流复制 + 只读从库中生产默认
logical以上全部 + 逻辑解码(逻辑复制、CDC)最大需要逻辑复制或变更捕获

wal_level = minimal 有硬性限制

  • 设置 max_wal_senders > 0 时,实例根本起不来。
  • 切换到 minimal 会让已有的基础备份无法用于 PITR。
  • 只适合「导入完就从备份重建」这类一次性任务。

synchronous_commit 各档位的能力矩阵:

取值本地提交持久从库崩溃后仍在从库 OS 崩溃后仍在从库查询可见
remote_apply✅✅✅✅
on✅✅✅❌
remote_write✅✅❌❌
local✅❌❌❌
off❌❌❌❌

关键认知:synchronous_commit = off 不会造成数据不一致,只是可能丢掉「最近已提交的少量事务」,其状态等同于这些事务被干净地回滚。最大丢失窗口是 wal_writer_delay 的 3 倍(默认 200ms × 3 = 600ms)。这与 fsync = off 的「可能物理损坏」有本质区别。

WAL 相关参数 ​

参数默认值作用调优方向
wal_buffers-1(自动:shared_buffers 的 1/32,下限 64kB,上限 16MB)WAL 未落盘数据的共享内存通常保持 -1;写热点极高可手动设 16MB(需重启)
min_wal_size80MBWAL 段回收下限,避免频繁重建写入大时随 max_wal_size 一起放大
max_wal_size1GB触发「请求式检查点」的 WAL 量,也是软上限写入大时必须调大,代价是恢复时间变长
wal_compressionoff压缩全页镜像CPU 有余量时可开 lz4(PG 15+)
wal_writer_delay200mswalwriter 每轮休眠时长提交延迟敏感时可减到 20ms
wal_writer_flush_after1MB累计多少字节后 walwriter 主动 fsync;0 为每轮都 flush高并发小事务可试 0
wal_init_zeroon新建 WAL 段是否填零COW 文件系统(如 ZFS)可设 off
wal_recycleon是否通过重命名复用 WAL 段COW 文件系统可设 off
wal_skip_threshold2MBwal_level = minimal 下,超过该大小的新数据改用 fsync 落盘一般不动
wal_sender_timeout / wal_receiver_timeout60s复制连接超时见复制章节
wal_keep_size0为从库保留的 WAL 量(PG 13+ 取代 wal_keep_segments)无复制槽时按需设置

检查点相关参数 ​

参数默认值作用调优方向
checkpoint_timeout5min两次检查点的最大时间间隔高写入可减到 1~15min,太长会拉长恢复时间
max_wal_size1GB达到该 WAL 量即触发请求式检查点与 shared_buffers 配套放大
checkpoint_completion_target0.9在「距下次检查点的时间」的多少比例内完成刷脏保持 0.9,不建议改 1.0
checkpoint_flush_after32(8kB 单位 = 256kB)每刷这么多就让内核回写SSD 可加大到 64~128(512kB~1MB)
checkpoint_warning30s检查点间隔短于该值时打警告日志出现警告说明 max_wal_size 偏小
log_checkpointsoff是否在日志中记录每次检查点生产建议开 on

详细说明 ​

检查点的两种触发方式 ​

text
检查点触发
├── ① 超时触发(timed)    checkpoint_timeout 到期(默认 5min)
│                          → num_timed 计数
│
└── ② 请求触发(requested) 累计 WAL 量达到 max_wal_size
                            → num_requested 计数

健康状态的判断标准只有一个:num_timed 应显著大于 num_requested。

text
num_requested << num_timed    → 检查点主要由时间驱动,写入压力不大 ✅
num_requested ≈  num_timed    → 检查点被 WAL 量频繁驱动,max_wal_size 偏小 ❌
num_requested  >  num_timed    → 严重偏小,会出现 checkpoint_warning 日志 ⚠️

另外还有一种特殊触发:执行 VACUUM FULL、CREATE DATABASE、pg_basebackup 等操作时,会通过 CHECKPOINT 命令主动请求检查点。

检查点频率与恢复时间的关系 ​

这是调优时的核心权衡:

text
max_wal_size 调大
    │
    ├▶ 检查点间隔变长 → 刷脏尖峰变少 → 写入更平稳 ✅
    ├▶ WAL 段保留更多 → 磁盘占用上升
    └▶ 崩溃恢复需重放的 WAL 变多 → RTO(恢复时间)变长 ⚠️

一个粗略的换算(重放速率依硬件而定):

text
RTO ≈ 待重放 WAL 量 / 重放速率

例:max_wal_size = 1GB,重放速率 200MB/s → RTO 约 5 秒
     max_wal_size = 8GB,重放速率 200MB/s → RTO 约 40 秒

所以「能接受多长的恢复时间」是决定 max_wal_size 的前提。对多数 OLTP 业务,30~60 秒的 RTO 是常见可接受区间。

checkpoint_completion_target 的推荐值 ​

该参数表示「本次检查点应在『到下次检查点的预计时间』的多少比例内完成」。默认 0.9 意味着留出 10% 的缓冲。

ini
# 不推荐:追求绝对平滑,但可能无法在本轮内完成,反而叠加下一轮
checkpoint_completion_target = 1.0

# 推荐:保持默认,留出缓冲
checkpoint_completion_target = 0.9

为什么不要设成 1.0

checkpoint_completion_target = 1.0 会让刷脏一直持续到下一次检查点开始。若某个时刻突发的业务写入挤占了 I/O,本轮刷脏无法完成,就会与下一轮叠加,形成更严重的积压。0.9 留出的缓冲正是为这种突发准备的。

全页写入(FPW)与 WAL 体积 ​

full_page_writes = on(默认)时,检查点后每个页的首次修改都会把整页写进 WAL:

text
检查点 A ────────────────────────────────── 检查点 B
     │                                          │
     │  页 P 第一次被改 → WAL 中记录 8kB 整页镜像
     │  页 P 之后被改   → WAL 中只记录行级变更(几十字节)
     │
     └─ 检查点 B 之后,页 P 再次被改 → 又记录一次 8kB 整页镜像

结论:检查点越频繁,同一个页被写整页镜像的次数越多,WAL 量越大。这是「调大 max_wal_size 反而可能减少总 I/O」的原因。

想减小这部分开销,除了拉长检查点间隔,还可以启用 wal_compression:

ini
# PG 15+ 支持 lz4 / zstd;更早版本只有 pglz
wal_compression = lz4

全页镜像通常压缩率很高(页内大量空闲空间),开启后 WAL 体积可明显下降,代价是写 WAL 时多一次压缩、重放时多一次解压。

统计视图的版本差异 ​

这是写监控脚本时最容易出错的地方。检查点统计在 PG 17 发生了「搬家」:

版本检查点统计位置后端刷脏统计位置
PG 16 及更早pg_stat_bgwriter(checkpoints_timed / checkpoints_req / checkpoint_write_time / checkpoint_sync_time / buffers_checkpoint)pg_stat_bgwriter.buffers_backend
PG 16+同上(早期版本)pg_stat_io 按 backend_type 分类
PG 17+pg_stat_checkpointerpg_stat_io

PG 17+ 上查 pg_stat_bgwriter.buffers_checkpoint 会直接报错

在 PG 17 及更高版本中,pg_stat_bgwriter 只剩 buffers_clean / maxwritten_clean / buffers_alloc / stats_reset。监控脚本必须按版本分支,否则会因「列不存在」而失败。

WAL 自身的统计则在 pg_stat_wal(PG 14+):

列含义
wal_records累计写入的 WAL 记录数
wal_fpi累计的全页镜像数(FPW 次数)
wal_bytes累计 WAL 字节数
wal_buffers_fullWAL 缓冲区满导致被迫落盘的次数
wal_write / wal_syncwalwriter 发起的 write / fsync 次数
wal_write_time / wal_sync_time对应耗时(毫秒)
stats_reset统计清零时间

其中两个比值最有价值:

text
wal_fpi / wal_records      → 全页镜像占比,偏高说明检查点太频繁
wal_sync_time / wal_write_time → fsync 相对开销,偏高说明存储 fsync 效率差

实战示例 ​

场景一:完整的 WAL 与检查点健康检查 ​

第一步:确认持久性相关参数

sql
SELECT name, setting, unit, source
FROM pg_settings
WHERE name IN ('wal_level', 'fsync', 'synchronous_commit', 'full_page_writes',
               'wal_compression', 'wal_buffers', 'min_wal_size', 'max_wal_size')
ORDER BY name;
text
         name         | setting | unit |  source
----------------------+---------+------+-----------
 fsync                | on      |      | default
 full_page_writes     | on      |      | default
 max_wal_size         | 1024    | MB   | default
 min_wal_size         | 80      | MB   | default
 synchronous_commit   | on      |      | default
 wal_buffers          | -1      |      | default
 wal_compression      | off     |      | default
 wal_level            | replica |      | default
(8 rows)

第二步:查看检查点统计(PG 17+)

sql
SELECT num_timed, num_requested,
       write_time, sync_time,
       buffers_written,
       round(sync_time::numeric / NULLIF(write_time, 0) * 100, 1) AS sync_pct,
       stats_reset
FROM pg_stat_checkpointer;
text
 num_timed | num_requested | write_time | sync_time | buffers_written | sync_pct |          stats_reset
-----------+---------------+------------+-----------+-----------------+----------+-------------------------------
        87 |            13 |     412038 |     88104 |         2411883 |     21.4 | 2026-09-20 08:00:01.123456+08

结果解读:

观察结论
num_timed = 87 ≫ num_requested = 13时间驱动为主,max_wal_size 充足 ✅
sync_pct = 21.4%fsync 占比不高,存储表现正常 ✅
buffers_written 与 num_timed + num_requested 相除可得平均单次刷脏量,用于估算尖峰强度

第三步:查看 WAL 统计(PG 14+)

sql
SELECT wal_records, wal_fpi, wal_bytes,
       wal_buffers_full, wal_write, wal_sync,
       wal_write_time, wal_sync_time,
       round(wal_fpi::numeric / NULLIF(wal_records, 0) * 100, 1) AS fpi_pct
FROM pg_stat_wal;
text
 wal_records | wal_fpi  |  wal_bytes  | wal_buffers_full | wal_write  | wal_sync | wal_write_time | wal_sync_time | fpi_pct
-------------+----------+-------------+------------------+------------+----------+----------------+---------------+---------
      9842103 |   412033 | 18439210483 |            12403 |    4410288 |  1084210 |        1820331 |        412088 |     4.2

结果解读:

  • fpi_pct = 4.2%:全页镜像只占 4.2%,检查点频率合理。若这个值升到 20% 以上,说明检查点过于频繁(每个页被反复写整页镜像)。
  • wal_buffers_full = 12403:WAL 缓冲区满的次数。若持续快速增长,可考虑手动调大 wal_buffers(需重启)。
  • wal_bytes = 18.4GB:据此可估算归档存储与 WAL 盘容量需求。

第四步:确认日志中没有频繁检查点警告

bash
grep "checkpoints are occurring too frequently" /var/log/postgresql/postgresql-18-main.log | tail -5

没有任何输出即为正常。

场景二:复现并消除「检查点太频繁」 ​

第一步:设置一个偏小的 max_wal_size

ini
# postgresql.conf —— 问题配置
max_wal_size = 256MB
min_wal_size = 80MB
checkpoint_timeout = 5min
checkpoint_completion_target = 0.9
bash
pg_ctl reload -D /var/lib/postgresql/18/main

第二步:施加写入负载

bash
pgbench -i -s 100 mydb
pgbench -c 16 -j 4 -T 120 mydb

第三步:观察日志与统计

bash
tail -50 /var/log/postgresql/postgresql-18-main.log
text
LOG:  checkpoints are occurring too frequently (14 seconds apart)
HINT:  Consider increasing the configuration parameter "max_wal_size".
LOG:  checkpoints are occurring too frequently (11 seconds apart)
HINT:  Consider increasing the configuration parameter "max_wal_size".
sql
SELECT num_timed, num_requested,
       round(num_requested::numeric / NULLIF(num_timed + num_requested, 0) * 100, 2) AS pct_requested
FROM pg_stat_checkpointer;
text
 num_timed | num_requested | pct_requested
-----------+---------------+---------------
        12 |            38 |         76.00

pct_requested = 76%:检查点主要被 WAL 量驱动,确认 max_wal_size 过小。

第四步:放大 max_wal_size 并同步提升配套参数

ini
# postgresql.conf —— 修正配置
max_wal_size = 8GB
min_wal_size = 1GB
checkpoint_timeout = 15min
checkpoint_completion_target = 0.9
checkpoint_flush_after = 1MB
checkpoint_warning = 30s
log_checkpoints = on
bash
pg_ctl reload -D /var/lib/postgresql/18/main

第五步:重新观察,确认 pct_requested 大幅下降

sql
SELECT num_timed, num_requested,
       round(num_requested::numeric / NULLIF(num_timed + num_requested, 0) * 100, 2) AS pct_requested
FROM pg_stat_checkpointer;
text
 num_timed | num_requested | pct_requested
-----------+---------------+---------------
        46 |             4 |          8.00

警告日志消失,pct_requested 从 76% 降到 8%,写入变得平稳。代价是崩溃恢复需要重放的 WAL 从约 0.25GB 涨到约 8GB,RTO 相应变长——这个取舍必须事先与业务确认。

场景三:用 log_checkpoints 量化单次检查点的代价 ​

开启日志后,每次检查点会输出一行摘要:

ini
log_checkpoints = on
text
LOG:  checkpoint starting: time
LOG:  checkpoint complete: wrote 38412 buffers (234.4 MB); wrote 1829 WAL file(s) total
      (28.6 MB); write=2.401 s, sync=0.884 s, total=3.285 s; sync files=521,
      longest=0.031 s, average=0.002 s; distance=188405 kB, estimate=190233 kB

关键字段解读:

字段含义关注点
wrote N buffers本次刷出的脏页数与体积数值越大,尖峰越明显
write=写脏页耗时与 sync 比较
sync=fsync 耗时占比高说明存储 fsync 慢
sync files=fsync 的文件数若 longest 明显偏高,说明有单次 I/O 卡顿
distance / estimate距上次检查点推进的 WAL 距离用来判断 max_wal_size 设置是否合理

longest=0.031 s 指的是单次 fsync 最长耗时 31ms。若这个值经常达到数百毫秒,说明存储层有抖动(可能被其他进程抢占 I/O),需要从系统层面排查。

场景四:批量导入时临时放宽持久性 ​

大批量导入(COPY、INSERT ... SELECT)时,可以让部分操作放弃同步落盘以换取速度。

方案 A:会话级放宽 synchronous_commit

sql
BEGIN;
SET LOCAL synchronous_commit = off;
COPY demo_orders FROM '/data/orders.csv' WITH (FORMAT csv);
COMMIT;

因为 SET LOCAL 只在当前事务内生效,事务提交后自动恢复,不影响其他会话。

方案 B:导入期间临时切换到 minimal 级别

sql
-- 需要超级用户;注意此操作会强制一次检查点
ALTER SYSTEM SET wal_level = 'minimal';
bash
# wal_level 需重启生效
pg_ctl restart -D /var/lib/postgresql/18/main
sql
-- 导入(含 CREATE TABLE / COPY / CREATE INDEX 等)
CREATE TABLE demo_orders_new (LIKE demo_orders INCLUDING ALL);
COPY demo_orders_new FROM '/data/orders.csv' WITH (FORMAT csv);
CREATE INDEX ON demo_orders_new (created_at);
ANALYZE demo_orders_new;
sql
-- 导入完成后必须改回 replica,否则无法做流复制与归档
ALTER SYSTEM SET wal_level = 'replica';
bash
pg_ctl restart -D /var/lib/postgresql/18/main

不要忘记改回 wal_level

停留在 minimal 会导致:无法开启归档、无法建流复制从库、已有基础备份失效。如果实例配置了 max_wal_senders > 0,甚至无法启动。建议把「改回 replica」写进操作清单。

方案 C:调整 max_wal_size 后再导

sql
ALTER SYSTEM SET max_wal_size = '8GB';
SELECT pg_reload_conf();
-- 导入完成后按需调回

场景五:定位 fsync 抖动 ​

当业务反馈「偶发提交慢」时,按这条链路排查:

text
提交慢
  │
  ├─ 1. 检查是不是 fsync 慢 → 看 pg_stat_wal 的 wal_sync_time / wal_sync
  │      偏高 → 存储 fsync 效率低,考虑带断电保护的写缓存
  │
  ├─ 2. 检查 WAL 缓冲区是否常满 → pg_stat_wal.wal_buffers_full
  │      持续增长 → 手动调大 wal_buffers(需重启)
  │
  ├─ 3. 检查是否有检查点尖峰 → pg_stat_checkpointer 与 log_checkpoints
  │      尖峰明显 → 调大 max_wal_size,或加大 checkpoint_flush_after
  │
  └─ 4. 检查系统层 I/O → iostat -x 1
          await 高 / %util 接近 100% → 存储已达瓶颈,需要换盘或分流

用 pg_test_fsync 可以直接测出单次 WAL flush 的真实耗时,为 commit_delay 之类的参数提供起点依据:

bash
pg_test_fsync -s 5
text
5 seconds per test
O_DIRECT supported on this platform for open_datasync and open_sync.

Compare file sync methods using one 8kB write:
(in wal_sync_method preference order, except fdatasync is Linux's default)
        open_datasync                     18203.442 ops/sec     55 usecs/op
        fdatasync                         17988.129 ops/sec     56 usecs/op
        fsync                             12041.882 ops/sec     83 usecs/op
        fsync_writethrough                            n/a
        open_sync                         16802.551 ops/sec     60 usecs/op

若 usecs/op 超过 200μs,说明单次 fsync 成本较高,此时提高组提交效率(如适度设置 commit_delay)的收益会更明显。

注意事项 ​

1. fsync = off 在生产环境是禁止项。 关闭后系统崩溃或断电可能导致不可恢复的数据损坏,而不仅仅是丢事务。仅适用于「可从外部数据完整重建」的场景(如初始化导入、一次性批处理)。想换取性能应使用 synchronous_commit = off,它没有损坏风险。

2. 扩大 shared_buffers 时务必同步放大 max_wal_size。 否则脏页总量变大而检查点仍被旧的 WAL 阈值频繁触发,写放大更严重。参考配置:

ini
shared_buffers = 8GB
max_wal_size = 8GB
min_wal_size = 1GB
checkpoint_completion_target = 0.9

3. wal_level 的修改必须重启。 它是 postmaster 级参数,pg_reload_conf() 不生效。同理 fsync、full_page_writes、wal_buffers、wal_init_zero、wal_recycle 也只能改配置文件。

4. wal_buffers 的显示单位是 8kB 块,不是字节。

sql
SELECT current_setting('wal_buffers') AS raw,
       pg_size_pretty(current_setting('wal_buffers')::bigint) AS human;
text
  raw  |  human
-------+---------
 16384 | 128 MB      ← raw=16384 指的是 16384 个 8kB 块 = 128MB

注意 -1(自动)在 SELECT 中显示的是解析后的实际值,需要与 shared_buffers 一起看才能判断是否触顶(上限 16MB)。

5. checkpoint_warning 只是「提示」,不会自动修复问题。 它只在日志里打一行 checkpoints are occurring too frequently。真正解决问题要调大 max_wal_size。

6. 不同版本的统计视图列名不同。 PG 17+ 用 pg_stat_checkpointer;PG 16 及更早用 pg_stat_bgwriter。跨版本巡检脚本必须分支处理,否则会报「列不存在」。

7. track_io_timing = on 是有代价的。 开启后 pg_stat_io 等视图的耗时字段才准确,但每次 I/O 都要取时钟,高频 I/O 场景下会带来可观开销。建议「诊断时开,平时关」。

8. 存档库(archive)与复制槽会持续占用 WAL。 pg_wal 目录被填满会导致实例停机。除了 max_wal_size,还要监控 pg_replication_slots 中 inactive 的槽:

sql
SELECT slot_name, active, restart_lsn, pg_size_pretty(
         pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots
ORDER BY pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) DESC;

9. min_wal_size 调得过小会导致 WAL 段频繁新建与删除。 段的重建本身有开销(尤其 wal_init_zero = on 时需要填零)。写入量大的实例把 min_wal_size 一起放大(如 1GB)能减少这层抖动。

10. synchronous_commit = off 的影响范围要评估清楚。 它影响的是整个会话或整个实例(若全局设置)。对「订单、支付」这类不可丢事务,应保持 on(或用 SET LOCAL off 只对可容忍丢失的批量任务生效)。

11. 检查点不能「完全消除」。 即使把 max_wal_size 设得极大,checkpoint_timeout 到期仍会触发检查点。能做的只是让它更平缓、频率更低,而不是没有。

12. 调参必须与恢复目标一起决策。 RTO 要求 5 分钟的系统与要求 30 秒的系统,max_wal_size 的合理区间完全不同。脱离 RTO 谈「最优 max_wal_size」是没有意义的。

最佳实践 ​

1. 一套可用的起点配置(写入量中等的 OLTP,16 核 / 64GB)

ini
# —— 持久性 ——
wal_level = replica
fsync = on
synchronous_commit = on
full_page_writes = on

# —— WAL ——
wal_buffers = -1               # 自动(shared_buffers 的 1/32)
min_wal_size = 1GB
max_wal_size = 8GB
wal_compression = lz4          # PG 15+;CPU 有余量时启用

# —— 检查点 ——
checkpoint_timeout = 15min
checkpoint_completion_target = 0.9
checkpoint_flush_after = 1MB
checkpoint_warning = 30s
log_checkpoints = on

2. 按「写入强度」分档调整 max_wal_size

场景max_wal_size 建议checkpoint_timeout说明
低写入(日增 < 1GB WAL)1GB(默认)5min保持默认即可
中等写入(日增 1~20GB)4~8GB15min与 shared_buffers 一起放大
高写入(日增 > 20GB)16~32GB15~30min必须结合 RTO 评估,注意 WAL 盘容量

3. 把三个核心指标纳入常规监控

sql
-- 检查点健康度:请求式检查点占比应 < 20%
SELECT num_timed, num_requested,
       round(num_requested::numeric / NULLIF(num_timed + num_requested, 0) * 100, 2) AS pct_requested
FROM pg_stat_checkpointer;
sql
-- 全页镜像占比:应 < 10%
SELECT round(wal_fpi::numeric / NULLIF(wal_records, 0) * 100, 1) AS fpi_pct,
       pg_size_pretty(wal_bytes) AS total_wal
FROM pg_stat_wal;
sql
-- WAL 缓冲区溢出:应基本不增长
SELECT wal_buffers_full, stats_reset FROM pg_stat_wal;

告警线参考:

指标警告严重
pct_requested> 50%> 70%
fpi_pct> 15%> 30%
wal_buffers_full 增速持续增长快速增长且伴随提交延迟抖动
pg_wal 目录占用> 2 × max_wal_size> 5 × max_wal_size

4. 生产环境打开 log_checkpoints。 单次检查点一行日志,开销可忽略,但它是事后回溯「卡顿是否由检查点引起」的唯一低成本证据。

5. 用 pg_stat_io(PG 16+)区分 I/O 归属。 定位是检查点刷脏、后端刷脏还是 WAL 写入造成的压力:

sql
SELECT backend_type, object, context,
       reads, writes, writebacks, extends,
       round(write_time::numeric / NULLIF(writes, 0), 3) AS ms_per_write
FROM pg_stat_io
WHERE writes > 0
ORDER BY writes DESC
LIMIT 15;

6. WAL 盘与数据盘分开。 pg_wal 是顺序写密集,数据文件是随机写密集。放在同一块盘上会互相干扰。若必须共用,优先保证 WAL 的写延迟。

7. 批量导入走「临时放宽」而非「永久放宽」。 用 SET LOCAL synchronous_commit = off 或导入期临时 wal_level = minimal,完成后恢复。切勿把放宽的设置留在生产配置里。

8. 建立「变更 → 观察 → 确认」的闭环。 每次调整 WAL/检查点参数后,都在 24 小时内对比这四项:pct_requested、fpi_pct、提交延迟 P99、pg_wal 目录占用。没有数据支撑的调参等于盲调。

9. 把 RTO 估值写进配置注释。 让后续维护者知道当前的 max_wal_size 对应多长的恢复时间:

ini
# max_wal_size = 8GB → 崩溃恢复约需重放 8GB WAL,按 200MB/s 估算 RTO ≈ 40s
max_wal_size = 8GB

10. 大版本升级前后重新核对视图与参数名。 pg_stat_bgwriter → pg_stat_checkpointer(PG 17)、wal_keep_segments → wal_keep_size(PG 13)这类变更,会让旧脚本静默失效或直接报错。

参考链接 ​

下一节

继续学习:磁盘 I/O 优化