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):
一次 UPDATE 的写入路径
┌────────────┐ ① 生成 WAL 记录 ┌────────────┐
│ 后端进程 │ ─────────────────▶ │ WAL 缓冲区 │
└────────────┘ └────────────┘
│ │ ② 提交时 fsync
│ ③ 修改数据页 ▼
▼ ┌────────────┐
┌────────────┐ │ pg_wal/ │ ← 顺序写,速度快
│shared_buffers│ (脏页,稍后刷盘) └────────────┘
└────────────┘
│ ④ 检查点/后台写入
▼
┌────────────┐
│ 数据文件 │ ← 随机写,速度慢
└────────────┘这样设计的好处:提交时只需保证 WAL 落盘(顺序写),不必等待随机写的数据页落盘。崩溃后靠重放 WAL 恢复到一致状态。
代价是:所有写操作都要额外写一份 WAL。WAL 量可能与数据量相当甚至更大(尤其开启了全页写入时)。因此 WAL 相关参数直接决定了写入性能的上限。
检查点为什么会导致性能抖动
检查点要做两件事:
- 把
shared_buffers中的脏页刷到数据文件(随机写,量大); - 记录一个 redo point,使得此前的 WAL 段可以被回收/归档。
问题出在第 1 步:如果脏页集中在一个很短的时间窗口内刷出去,就会形成 I/O 尖峰,其他正常查询被迫排队 → 表现为「每隔几分钟卡一下」。
不理想(集中刷脏) 理想(checkpoint_completion_target 摊平)
I/O │ █ I/O │ ▄▄▄▄▄▄▄▄▄▄▄▄
│ █ │ ▄▄▄▄▄▄▄▄▄▄▄▄
│ █ │ ▄▄▄▄▄▄▄▄▄▄▄▄
└───────────────▶ 时间 └───────────────▶ 时间
尖峰 → 查询延迟抖动 平缓 → 延迟稳定同时,全页写入(full-page writes)会放大 WAL 量:每次检查点之后,一个页的第一次修改会把整页(8kB)写进 WAL。检查点越频繁,全页写入的重复次数越多,WAL 量随之暴涨。这就是「检查点频率」与「WAL 体积」之间的耦合关系。
语法与用法
WAL 级别与持久性参数
| 参数 | 默认值 | 可选值 | 是否需重启 | 说明 |
|---|---|---|---|---|
wal_level | replica | minimal / replica / logical | 是 | 决定写入 WAL 的信息量 |
fsync | on | on / off | 是 | 关闭后断电可能数据损坏,生产必须开 |
synchronous_commit | on | remote_apply / on / remote_write / local / off | 否 | 提交返回前需要完成多少 WAL 处理 |
full_page_writes | on | on / off | 是 | 检查点后首次修改页时写整页,关闭有损坏风险 |
wal_compression | off | off / 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_size | 80MB | WAL 段回收下限,避免频繁重建 | 写入大时随 max_wal_size 一起放大 |
max_wal_size | 1GB | 触发「请求式检查点」的 WAL 量,也是软上限 | 写入大时必须调大,代价是恢复时间变长 |
wal_compression | off | 压缩全页镜像 | CPU 有余量时可开 lz4(PG 15+) |
wal_writer_delay | 200ms | walwriter 每轮休眠时长 | 提交延迟敏感时可减到 20ms |
wal_writer_flush_after | 1MB | 累计多少字节后 walwriter 主动 fsync;0 为每轮都 flush | 高并发小事务可试 0 |
wal_init_zero | on | 新建 WAL 段是否填零 | COW 文件系统(如 ZFS)可设 off |
wal_recycle | on | 是否通过重命名复用 WAL 段 | COW 文件系统可设 off |
wal_skip_threshold | 2MB | wal_level = minimal 下,超过该大小的新数据改用 fsync 落盘 | 一般不动 |
wal_sender_timeout / wal_receiver_timeout | 60s | 复制连接超时 | 见复制章节 |
wal_keep_size | 0 | 为从库保留的 WAL 量(PG 13+ 取代 wal_keep_segments) | 无复制槽时按需设置 |
检查点相关参数
| 参数 | 默认值 | 作用 | 调优方向 |
|---|---|---|---|
checkpoint_timeout | 5min | 两次检查点的最大时间间隔 | 高写入可减到 1~15min,太长会拉长恢复时间 |
max_wal_size | 1GB | 达到该 WAL 量即触发请求式检查点 | 与 shared_buffers 配套放大 |
checkpoint_completion_target | 0.9 | 在「距下次检查点的时间」的多少比例内完成刷脏 | 保持 0.9,不建议改 1.0 |
checkpoint_flush_after | 32(8kB 单位 = 256kB) | 每刷这么多就让内核回写 | SSD 可加大到 64~128(512kB~1MB) |
checkpoint_warning | 30s | 检查点间隔短于该值时打警告日志 | 出现警告说明 max_wal_size 偏小 |
log_checkpoints | off | 是否在日志中记录每次检查点 | 生产建议开 on |
详细说明
检查点的两种触发方式
检查点触发
├── ① 超时触发(timed) checkpoint_timeout 到期(默认 5min)
│ → num_timed 计数
│
└── ② 请求触发(requested) 累计 WAL 量达到 max_wal_size
→ num_requested 计数健康状态的判断标准只有一个:num_timed 应显著大于 num_requested。
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 命令主动请求检查点。
检查点频率与恢复时间的关系
这是调优时的核心权衡:
max_wal_size 调大
│
├▶ 检查点间隔变长 → 刷脏尖峰变少 → 写入更平稳 ✅
├▶ WAL 段保留更多 → 磁盘占用上升
└▶ 崩溃恢复需重放的 WAL 变多 → RTO(恢复时间)变长 ⚠️一个粗略的换算(重放速率依硬件而定):
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% 的缓冲。
# 不推荐:追求绝对平滑,但可能无法在本轮内完成,反而叠加下一轮
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:
检查点 A ────────────────────────────────── 检查点 B
│ │
│ 页 P 第一次被改 → WAL 中记录 8kB 整页镜像
│ 页 P 之后被改 → WAL 中只记录行级变更(几十字节)
│
└─ 检查点 B 之后,页 P 再次被改 → 又记录一次 8kB 整页镜像结论:检查点越频繁,同一个页被写整页镜像的次数越多,WAL 量越大。这是「调大 max_wal_size 反而可能减少总 I/O」的原因。
想减小这部分开销,除了拉长检查点间隔,还可以启用 wal_compression:
# 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_checkpointer | pg_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_full | WAL 缓冲区满导致被迫落盘的次数 |
wal_write / wal_sync | walwriter 发起的 write / fsync 次数 |
wal_write_time / wal_sync_time | 对应耗时(毫秒) |
stats_reset | 统计清零时间 |
其中两个比值最有价值:
wal_fpi / wal_records → 全页镜像占比,偏高说明检查点太频繁
wal_sync_time / wal_write_time → fsync 相对开销,偏高说明存储 fsync 效率差实战示例
场景一:完整的 WAL 与检查点健康检查
第一步:确认持久性相关参数
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; 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+)
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; 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+)
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; 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 盘容量需求。
第四步:确认日志中没有频繁检查点警告
grep "checkpoints are occurring too frequently" /var/log/postgresql/postgresql-18-main.log | tail -5没有任何输出即为正常。
场景二:复现并消除「检查点太频繁」
第一步:设置一个偏小的 max_wal_size
# postgresql.conf —— 问题配置
max_wal_size = 256MB
min_wal_size = 80MB
checkpoint_timeout = 5min
checkpoint_completion_target = 0.9pg_ctl reload -D /var/lib/postgresql/18/main第二步:施加写入负载
pgbench -i -s 100 mydb
pgbench -c 16 -j 4 -T 120 mydb第三步:观察日志与统计
tail -50 /var/log/postgresql/postgresql-18-main.logLOG: 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".SELECT num_timed, num_requested,
round(num_requested::numeric / NULLIF(num_timed + num_requested, 0) * 100, 2) AS pct_requested
FROM pg_stat_checkpointer; num_timed | num_requested | pct_requested
-----------+---------------+---------------
12 | 38 | 76.00pct_requested = 76%:检查点主要被 WAL 量驱动,确认 max_wal_size 过小。
第四步:放大 max_wal_size 并同步提升配套参数
# 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 = onpg_ctl reload -D /var/lib/postgresql/18/main第五步:重新观察,确认 pct_requested 大幅下降
SELECT num_timed, num_requested,
round(num_requested::numeric / NULLIF(num_timed + num_requested, 0) * 100, 2) AS pct_requested
FROM pg_stat_checkpointer; num_timed | num_requested | pct_requested
-----------+---------------+---------------
46 | 4 | 8.00警告日志消失,pct_requested 从 76% 降到 8%,写入变得平稳。代价是崩溃恢复需要重放的 WAL 从约 0.25GB 涨到约 8GB,RTO 相应变长——这个取舍必须事先与业务确认。
场景三:用 log_checkpoints 量化单次检查点的代价
开启日志后,每次检查点会输出一行摘要:
log_checkpoints = onLOG: 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
BEGIN;
SET LOCAL synchronous_commit = off;
COPY demo_orders FROM '/data/orders.csv' WITH (FORMAT csv);
COMMIT;因为 SET LOCAL 只在当前事务内生效,事务提交后自动恢复,不影响其他会话。
方案 B:导入期间临时切换到 minimal 级别
-- 需要超级用户;注意此操作会强制一次检查点
ALTER SYSTEM SET wal_level = 'minimal';# wal_level 需重启生效
pg_ctl restart -D /var/lib/postgresql/18/main-- 导入(含 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;-- 导入完成后必须改回 replica,否则无法做流复制与归档
ALTER SYSTEM SET wal_level = 'replica';pg_ctl restart -D /var/lib/postgresql/18/main不要忘记改回 wal_level
停留在 minimal 会导致:无法开启归档、无法建流复制从库、已有基础备份失效。如果实例配置了 max_wal_senders > 0,甚至无法启动。建议把「改回 replica」写进操作清单。
方案 C:调整 max_wal_size 后再导
ALTER SYSTEM SET max_wal_size = '8GB';
SELECT pg_reload_conf();
-- 导入完成后按需调回场景五:定位 fsync 抖动
当业务反馈「偶发提交慢」时,按这条链路排查:
提交慢
│
├─ 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 之类的参数提供起点依据:
pg_test_fsync -s 55 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 阈值频繁触发,写放大更严重。参考配置:
shared_buffers = 8GB
max_wal_size = 8GB
min_wal_size = 1GB
checkpoint_completion_target = 0.93. wal_level 的修改必须重启。 它是 postmaster 级参数,pg_reload_conf() 不生效。同理 fsync、full_page_writes、wal_buffers、wal_init_zero、wal_recycle 也只能改配置文件。
4. wal_buffers 的显示单位是 8kB 块,不是字节。
SELECT current_setting('wal_buffers') AS raw,
pg_size_pretty(current_setting('wal_buffers')::bigint) AS human; 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 的槽:
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)
# —— 持久性 ——
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 = on2. 按「写入强度」分档调整 max_wal_size
| 场景 | max_wal_size 建议 | checkpoint_timeout | 说明 |
|---|---|---|---|
| 低写入(日增 < 1GB WAL) | 1GB(默认) | 5min | 保持默认即可 |
| 中等写入(日增 1~20GB) | 4~8GB | 15min | 与 shared_buffers 一起放大 |
| 高写入(日增 > 20GB) | 16~32GB | 15~30min | 必须结合 RTO 评估,注意 WAL 盘容量 |
3. 把三个核心指标纳入常规监控
-- 检查点健康度:请求式检查点占比应 < 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;-- 全页镜像占比:应 < 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;-- 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 写入造成的压力:
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 对应多长的恢复时间:
# max_wal_size = 8GB → 崩溃恢复约需重放 8GB WAL,按 200MB/s 估算 RTO ≈ 40s
max_wal_size = 8GB10. 大版本升级前后重新核对视图与参数名。 pg_stat_bgwriter → pg_stat_checkpointer(PG 17)、wal_keep_segments → wal_keep_size(PG 13)这类变更,会让旧脚本静默失效或直接报错。
参考链接
- 预写式日志(Write-Ahead Logging, WAL)
- WAL 配置参数(Write Ahead Log)
- WAL 配置与调优(WAL Configuration)
- WAL 可靠性(Reliability and the Write-Ahead Log)
- 异步提交(Asynchronous Commit)
- 检查点统计视图(pg_stat_checkpointer,PG 17+)
- WAL 统计视图(pg_stat_wal,PG 14+)
- I/O 统计视图(pg_stat_io,PG 16+)
- 批量加载最佳实践(Populating a Database)
- pg_test_fsync 工具
下一节
继续学习:磁盘 I/O 优化