数据库基础体系 · 第 96/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。
PostgreSQL WAL 与 Checkpoint:提交、崩溃恢复、归档和写入性能
在 PostgreSQL 中,事务提交、数据页落盘、WAL 刷盘、Checkpoint、归档和崩溃恢复是相互关联、但并不等价的几个过程。
一个常见误解是:
事务提交时,所有数据页都已经写入数据文件;Checkpoint 就是一次全库保存;WAL 归档就是复制数据库文件。
这三句话都不准确。理解 PostgreSQL 的持久化机制,首先要区分以下对象:
- 数据页:表和索引在内存中的页面,最终写入数据文件。
- WAL 记录:描述页面变化、事务状态变化或其他恢复所需操作的日志记录。
- WAL LSN:WAL 中的位置,用于表示日志进度。
- 事务提交记录:表示某个事务已经提交的 WAL 记录。
- Checkpoint:记录一个可用于崩溃恢复的起点,并推动脏数据页写出。
- 归档 WAL:把已经完成的 WAL 段保存到独立存储,用于 PITR、备份恢复或构建备用库。
本文以 PostgreSQL 官方稳定版本公开语义为准。涉及复制时,默认讨论物理流复制和归档式恢复;涉及磁盘持久化时,默认 fsync=on,并假设操作系统、文件系统和存储设备对 flush/fdatasync 等操作提供正常语义。
一、先建立三个不同的“写入完成”概念
讨论 WAL 之前,需要先定义三个经常被混为一谈的阶段。
1. WAL 写入共享内存
事务执行修改时,PostgreSQL 会生成 WAL 记录,并把它们放入 WAL 缓冲区。此时 WAL 可能还没有进入操作系统文件缓存,更没有保证存储设备已经持久化。
可以把这个阶段称为:
WAL inserted
它只说明 WAL 已经被插入 PostgreSQL 的 WAL 缓冲结构。
2. WAL 写入操作系统文件
后台 WAL writer 或前台后端进程可以把 WAL 缓冲区内容写入 pg_wal 中的 WAL 文件。此时数据可能已经进入操作系统页缓存,但仍不一定已经稳定存储在磁盘介质上。
这个阶段可以称为:
WAL written
“写入文件”不等于“断电后仍然存在”。
3. WAL 刷入持久存储
PostgreSQL 调用相应的同步机制,例如 fsync、fdatasync 或等价机制,使 WAL 数据达到数据库所依赖的持久化边界。
这个阶段可以称为:
WAL flushed / durable
在正常配置和正常存储语义下,只有 WAL 刷盘之后,PostgreSQL 才能安全地向客户端确认一个需要持久化保证的提交。
因此,事务提交的核心顺序是:
修改数据页
↓
生成 WAL 记录
↓
WAL 至少包含本事务的提交记录
↓
按提交策略写入并刷盘 WAL
↓
向客户端返回 COMMIT 成功
而不是:
修改数据页
↓
把所有数据页写入表文件
↓
向客户端返回 COMMIT 成功
这就是 PostgreSQL 的 WAL 预写式日志原则:
在某个数据页的修改能够安全落盘之前,描述该修改的 WAL 必须已经先达到足够可靠的持久化状态。
形式化地说,假设某个数据页版本为 ,它由 WAL 记录 产生,那么必须满足:
这里的“不晚于”不是时间戳意义上的严格同步,而是持久化顺序约束:如果崩溃后数据页已经包含 的修改,那么恢复过程必须能够读取到 。
二、WAL 记录了什么
WAL 是按顺序追加的日志流。每条记录通常包含:
- 记录类型;
- 目标资源标识,例如某个关系文件的某个页面;
- 页面修改所需的信息;
- 事务或恢复相关信息;
- 记录长度、校验信息和前后关联信息。
WAL 并不是 SQL 日志。它一般不会保存:
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
而是保存恢复执行所需的更底层操作,例如某个数据页上的元组插入、页面初始化、索引页面修改或事务提交状态。
WAL 的主要用途包括:
- 崩溃恢复:从最近的恢复起点重放数据页修改。
- 保证提交持久性:先刷 WAL,再确认提交。
- 物理流复制:主库向备用库发送 WAL。
- PITR:基础备份加连续 WAL,实现时间点恢复。
- 在线备份一致性:在备份期间使用 WAL 补齐数据文件之间的时间差。
LSN:WAL 中的位置
LSN 是 Log Sequence Number,用于表示 WAL 流中的位置。它不是事务 ID,也不是时间戳。
可以使用以下函数观察当前 WAL 位置:
SELECT pg_current_wal_lsn();
计算两个 LSN 之间产生的 WAL 字节数:
SELECT pg_wal_lsn_diff(
'0/2000000',
'0/1000000'
);
结果是两个位置之间的字节差,而不是事务数或数据页数。
需要注意:
- LSN 只在特定 WAL 流中有意义;
- 主库切换时间线后,LSN 的解释还需要结合 timeline;
- WAL 字节数不等于业务数据量;
- 一次业务更新可能产生多个 WAL 记录;
- 索引维护、全页镜像、事务提交等都可能增加 WAL。
三、事务提交:为什么提交通常只等待 WAL,而不等待数据页
假设一个事务修改了表 orders 的某个页面。
执行过程可以简化为:
第一步:在共享缓冲区中修改页面
数据页进入 shared_buffers,页面被标记为 dirty。此时数据文件可能完全没有变化。
第二步:生成数据修改 WAL
PostgreSQL 生成描述这次页面变化的 WAL 记录,并为修改后的页面维护对应的页面 LSN。
页面上的 page LSN 表示:
这个页面已经包含了不超过该 LSN 的 WAL 变化。
第三步:生成提交记录
事务执行 COMMIT 时,会生成提交相关 WAL 记录。提交记录位于本事务 WAL 的后部。
因此,如果事务的最后一个 WAL 位置是 ,那么只要 WAL 已经持久化到至少 ,恢复时就能同时看到:
- 事务修改的 WAL;
- 事务已经提交的事实。
第四步:刷 WAL,而不是刷所有数据页
在默认的 synchronous_commit=on 下,PostgreSQL 通常会等待本地 WAL 刷盘,然后返回提交成功。
事务修改的数据页可以稍后由以下组件写出:
- 后端进程;
- background writer;
- checkpointer;
- 其他相关写入路径。
因此,提交成功后立刻检查数据文件,不能据此判断事务是否安全。真正的持久性依据是对应提交 WAL 是否达到持久化边界。
四、synchronous_commit 改变的是什么
synchronous_commit 控制事务提交时对 WAL 持久化以及同步复制确认的等待方式。它不改变 WAL 的生成,也不意味着关闭后就不需要 WAL。
常见值包括:
onofflocalremote_writeremote_apply
其中 remote_write 和 remote_apply 只有在配置同步复制、且存在相应同步备用库时才具有远端等待意义。
synchronous_commit=on
本地提交通常等待本地 WAL 刷盘。
如果没有配置同步复制,通常不等待备用库确认。
synchronous_commit=off
事务可以在 WAL 尚未刷盘时向客户端返回成功。后台进程会尽快刷 WAL。
它的效果不是“事务没有 WAL”,而是:
客户端看到 COMMIT 成功
↓
PostgreSQL 仍可能尚未完成 WAL 持久化
如果此时数据库主机崩溃,最近一小段已经向客户端确认的事务可能丢失。正常情况下不会因此产生数据库逻辑损坏,因为恢复会从持久化 WAL 重新建立一致状态;但应用层不能再把“收到提交成功”理解为“该事务一定不会丢失”。
这是一种持久性和提交延迟之间的明确取舍,不能只当作普通性能开关。
同步复制下的远端等待
配置同步复制时,提交确认可能还要等待备用库:
- 收到 WAL;
- 写入备用库;
- 刷到备用库持久存储;
- 在某些设置下重放并应用。
因此,事务的确认点可以从:
本地主库 WAL 已刷盘
扩展为:
本地主库 WAL 已刷盘
且满足同步备用库的确认条件
同步复制并不能替代本地 WAL 持久化,也不能自动解决所有故障。同步备用库本身的磁盘、网络和故障切换策略仍然影响最终保证。
五、Group Commit:为什么多个提交可以共享一次刷盘
多个后端进程可能同时提交事务。它们不一定各自独立执行一次磁盘同步,而是可以形成 group commit。
假设三个事务的提交 WAL 位置依次为:
T1 commit LSN = 1000
T2 commit LSN = 1100
T3 commit LSN = 1200
如果刷盘操作最终把 WAL 推进到 1200,那么:
T1、T2、T3 的提交 WAL 都已经包含在持久化范围内
于是一次 WAL flush 可以满足三个事务的本地持久性等待。
这带来两个结果:
- 高并发提交时,单事务平均刷盘成本可能下降;
- 存储设备的 flush 延迟仍然会直接影响提交延迟。
因此,测量写入性能时不能只看 WAL 生成速度,还要区分:
- WAL 生成速度;
- WAL 写入速度;
- WAL flush 次数;
- WAL flush 延迟;
- 提交等待时间;
- 同步复制等待时间。
在较新的 PostgreSQL 版本中,可以从 pg_stat_wal 等统计视图观察 WAL 产生和写入情况。统计视图的字段会随版本变化,生产诊断应以当前版本文档和实际字段为准。
六、数据页为什么可以晚于提交落盘
WAL 的设计使数据页可以延迟写入。
例如:
事务 T 修改页面 P
↓
页面 P 只在 shared_buffers 中变脏
↓
T 的修改 WAL 和提交记录刷盘
↓
向客户端返回 COMMIT 成功
↓
数秒后 checkpoint 才写出页面 P
如果此时主机崩溃:
- 恢复程序读取页面 P 的旧版本;
- 从合适的 WAL 位置开始重放;
- 重放 T 对 P 的修改;
- 看到 T 的提交记录;
- 恢复后的页面表现为已提交状态。
这就是“先写日志、后写数据页”的意义。
反过来,如果数据页已经先写入磁盘,但对应 WAL 尚未持久化,崩溃后恢复可能无法重建该数据页。因此 PostgreSQL 在写数据页前必须保证相关 WAL 已经满足预写条件。
七、Checkpoint 是什么
Checkpoint 是一个恢复边界。它主要完成两件事:
- 在 WAL 中记录一个可用于恢复的检查点位置;
- 推动共享缓冲区中的脏页写入数据文件,使系统不必从很久以前的 WAL 开始恢复。
Checkpoint 不是:
- 事务提交;
- 全库备份;
- 把所有 WAL 永久保存;
- 复制到备用库;
- 立即删除所有旧 WAL。
Checkpoint 的基本过程
抽象过程如下:
1. 发起 checkpoint
2. 确定 checkpoint 相关 WAL 位置
3. 确保该位置以前的必要 WAL 已经写出
4. 扫描并写出脏数据页
5. 记录和更新 checkpoint 状态
6. 后续崩溃恢复可以从该 checkpoint 附近开始
实际实现包含并发和后台写入细节,但核心约束是:
checkpoint 写出的数据页,其所依赖的 WAL 必须已经先达到满足要求的持久化状态。
Checkpoint 与恢复起点
假设某次 checkpoint 对应恢复起点为 ,崩溃时最后可用 WAL 位置为 ,那么恢复通常需要处理:
恢复时间大致取决于:
- 需要重放的 WAL 数量;
- WAL 记录类型;
- 存储读取速度;
- CPU;
- 检查点和恢复实现细节;
- 是否还需要处理事务状态、文件扩展等操作。
Checkpoint 越频繁,恢复区间通常越短,但运行期间会增加写入和刷盘压力。Checkpoint 越稀疏,运行期间可能更平稳,却可能产生更长的恢复时间和更多 WAL 保留需求。
八、Checkpoint 由哪些因素触发
常见触发因素包括:
checkpoint_timeout到期;- WAL 量达到
max_wal_size附近; - 管理员执行
CHECKPOINT; - 某些管理操作或启动恢复路径;
- 其他内部条件。
可以查看相关配置:
SELECT name, setting, unit, source
FROM pg_settings
WHERE name IN (
'checkpoint_timeout',
'checkpoint_completion_target',
'max_wal_size',
'min_wal_size',
'full_page_writes',
'wal_compression'
);
max_wal_size 不是硬上限
max_wal_size 是触发检查点的重要目标值,不是任何情况下都不会超过的硬上限。
WAL 可能因为以下原因继续超过该值:
- 高并发写入;
- 检查点尚未完成;
- 大事务;
- WAL 归档延迟;
- 复制槽阻止回收;
- 需要保留旧 WAL 的其他原因;
- 估算和实际写入节奏之间存在差异。
因此,不能把:
max_wal_size = 10GB
理解为:
pg_wal 永远不会超过 10GB
checkpoint_completion_target
该参数控制 checkpoint 写脏页时的目标节奏。目标是尽量把写入分散到两个 checkpoint 之间的时间窗口,而不是在 checkpoint 开始时一次性集中写完。
如果 checkpoint 写入过于集中,可能出现:
checkpoint 开始
↓
大量脏页集中写出
↓
写延迟和 I/O 队列突然升高
↓
业务请求抖动
适当分散写入可以降低写入尖峰,但也会让脏页写出持续更久。它不是“越大越快”的单调开关,最终仍受存储吞吐、WAL 生成速度和脏页数量限制。
九、为什么 Checkpoint 会增加 WAL:全页写入
PostgreSQL 为了防止“页面撕裂”问题,在 full_page_writes=on 时,某些页面在 checkpoint 后第一次被修改时,会把整个页面作为 full-page image 写入 WAL。
页面撕裂是什么
假设一个页面大小为 8 KiB,而底层设备一次写入不是原子完成的。系统崩溃时,可能出现:
页面前半部分来自新版本
页面后半部分来自旧版本
这会产生一个既不是旧页面、也不是新页面的损坏页面。
如果 checkpoint 后第一次修改该页面时,把完整页面镜像放入 WAL,那么恢复时可以先用完整镜像替换损坏页面,再重放后续修改。
为什么是“checkpoint 后第一次修改”
在一次 checkpoint 之后,磁盘上的页面被视为已经具有某个一致恢复基础。某页面第一次再次被修改时,PostgreSQL 需要额外记录完整页面,建立新的恢复保护边界。
简化示例:
Checkpoint C0 完成
↓
页面 P 第一次修改:WAL 包含 P 的完整镜像 + 修改
↓
页面 P 第二次修改:通常只需要记录增量修改
↓
Checkpoint C1 完成
↓
页面 P 下一次修改:可能再次出现 full-page image
实际是否产生 full-page image 还取决于页面状态和具体 WAL 记录路径,不应把它理解为每次更新都复制整个页面。
影响
当 checkpoint 较频繁、更新页面分布较广时,full-page image 可能显著增加 WAL 量。可以用 pg_stat_wal 中与 full-page image 相关的统计观察这一现象。
wal_compression 可以对某些 full-page image 进行压缩,以减少 WAL 写入量,但会增加 CPU 消耗。它是否有利取决于:
- 页面可压缩程度;
- CPU 余量;
- 存储和网络瓶颈;
- 归档与复制带宽。
full_page_writes=off 可能减少 WAL,但在正常持久化生产环境中关闭它会牺牲页面撕裂保护,不能作为普通性能优化手段。
十、崩溃恢复:从 Checkpoint 到一致状态
1. 崩溃时可能出现什么状态
假设系统在下面时刻断电:
事务 T 修改了页面 P
T 的修改 WAL 已刷盘
T 的提交记录已刷盘
P 尚未写入数据文件
恢复时,P 仍是旧页面,但 WAL 中有完整修改和提交记录。恢复程序重放 WAL,P 被恢复为新状态。
另一个例子:
事务 T 修改了页面 P
修改 WAL 已刷盘
T 的提交记录尚未刷盘
P 已经被写入数据文件
恢复时,P 可能已经含有 T 的修改,但系统找不到 T 的提交记录。该修改不会对普通事务可见,后续会由事务状态和 MVCC 机制处理。
这说明:
数据页中出现某个修改,不等于该事务已经提交。
2. PostgreSQL 主要采用重做,而不是传统撤销
PostgreSQL 的崩溃恢复核心是 WAL redo。它会从恢复起点开始读取 WAL,并将必要的页面变化重放到数据文件中。
对于崩溃前未提交的事务,恢复通常不会把所有物理页面修改逐条“反向执行”。原因是 PostgreSQL 使用 MVCC 和事务状态信息判断可见性:
- 已提交事务的修改最终可见;
- 未提交或已中止事务的修改对普通查询不可见;
- 后续 vacuum 等过程清理无效版本。
所以,不能用传统数据库中“恢复时对每个未提交事务执行 undo”来准确描述 PostgreSQL 的主要恢复方式。
3. 恢复过程的简化步骤
1. 读取控制信息,找到最近可用 checkpoint
2. 从 checkpoint 指定的 WAL 位置开始扫描
3. 按 WAL 顺序重放页面和系统状态变化
4. 恢复事务状态信息
5. 处理文件扩展、关系页面和其他恢复动作
6. 到达可用 WAL 末端后,数据库进入一致状态
如果使用归档 WAL,恢复过程可能继续从归档目录读取 WAL;如果存在更高时间线,还需要按照 timeline history 选择正确的 WAL 分支。
4. 崩溃恢复不是 PITR
普通崩溃恢复的目标是:
恢复到崩溃前已经存在的最新一致状态
PITR 的目标是:
恢复到某个指定时间、LSN 或恢复目标
两者都使用 WAL,但恢复目标不同。
十一、WAL 归档:为什么必须先有基础备份
WAL 归档是把已经完成的 WAL 段复制到独立位置。归档本身不能单独构成完整备份,因为 WAL 描述的是变化,不包含数据库在任意时刻的全部基础数据。
PITR 至少需要:
一个一致的基础备份
+
从该基础备份覆盖到目标时间的连续 WAL
逻辑关系是:
基础备份时刻 B
↓
WAL W1、W2、W3、...
↓
目标恢复时刻 T
如果基础备份之后缺少中间任意一段 WAL,通常就不能连续恢复到后续目标。
启用归档
典型配置包括:
archive_mode = on
archive_command = '...'
archive_mode 通常需要重启才能改变。archive_command 在服务器配置中执行,成功退出表示该 WAL 段已经被归档。
一个仅用于演示的本地归档命令可以是:
archive_command = 'test ! -f /backup/wal/%f && cp %p /backup/wal/%f'
其中:
%p是待归档 WAL 文件的路径;%f是 WAL 文件名;test ! -f防止覆盖已经存在的文件;cp成功退出后,PostgreSQL 才会把该段视为归档成功。
生产环境不能只复制命令而忽略以下问题:
- 目标目录是否位于独立故障域;
- 复制是否完整;
- 目标文件是否需要临时文件加原子重命名;
- 网络存储中断后是否可靠重试;
- 归档命令是否可能永久阻塞;
- 归档目标是否有足够容量;
- 是否会错误覆盖同名但内容不同的文件。
归档命令失败时,PostgreSQL 会保留该 WAL 段并重试。归档堆积可能使 pg_wal 持续增长,最终耗尽磁盘。
可以查看归档状态:
SELECT archived_count,
failed_count,
last_archived_wal,
last_archived_time,
last_failed_wal,
last_failed_time
FROM pg_stat_archiver;
字段名和统计视图在长期版本演进中应以当前版本为准,但诊断重点不变:查看最近成功归档、最近失败归档及失败持续时间。
archive_timeout 的边界
默认情况下,一个 WAL 段通常在写满或发生切换后才归档。低写入量系统可能很久都不产生一个完整 WAL 段。
archive_timeout 可以强制服务器在一段时间后切换 WAL,使较新的 WAL 尽快进入归档流程:
archive_timeout = 60s
它的语义不是“每 60 秒生成一个只包含实际内容的归档文件”。强制切换会产生新的 WAL 段,低写入量下可能增加归档文件和存储开销。
此外,archive_timeout 不是严格的 RPO 保证:
- 归档命令本身可能失败;
- 网络可能不可用;
- WAL 归档需要完成复制;
- 目标存储可能延迟确认;
- 服务器故障可能发生在预期切换前。
十二、归档 WAL、复制槽和 WAL 回收
WAL 文件何时能够被删除或复用,不仅取决于 checkpoint。
PostgreSQL 可能因为以下原因继续保留 WAL:
- 归档尚未成功;
- 物理复制备用库尚未追上;
- 复制槽要求保留;
- 服务器仍需要它进行恢复;
- 其他内部保留条件。
复制槽的关键风险
物理复制槽会记录备用库需要的 WAL 起点。备用库长时间离线时,主库不能删除槽认为仍需要的 WAL。
因此,复制槽可以防止备用库因为缺少 WAL 而无法继续复制,但也可能造成:
备用库停止
↓
复制槽保留 WAL
↓
pg_wal 持续增长
↓
主库磁盘耗尽
现代 PostgreSQL 提供了限制复制槽 WAL 保留量的相关配置能力,但具体参数和版本语义应按部署版本核对。无论是否设置限制,都应监控:
- 槽的
restart_lsn; - 主库当前 WAL 位置;
- 槽造成的 WAL 保留量;
- 备用库接收和重放延迟;
pg_wal文件系统使用率。
删除或重置复制槽不是普通清理操作,可能直接导致备用库缺少所需 WAL,必须准备重新搭建备用库或从新的基础备份恢复。
十三、PITR 的数据流
一次典型的 PITR 数据流如下:
主库
├─ 生成 WAL
├─ 完成 WAL 段
├─ archive_command 复制到归档存储
└─ pg_basebackup 生成基础备份
恢复服务器
├─ 还原基础备份
├─ 准备 restore_command
├─ 读取归档 WAL
├─ 按顺序重放
└─ 到达恢复目标后停止或切换
基础备份可以使用:
pg_basebackup \
-D /backup/base \
-Fp \
-X stream \
-P
示例含义:
-D /backup/base:基础备份目标目录;-Fp:普通文件格式;-X stream:备份期间通过流方式取得所需 WAL;-P:显示进度。
实际执行前需要:
- 目标目录为空或满足工具要求;
- 备份用户具备复制权限;
pg_hba.conf允许复制连接;- 如果还需要归档,主库已正确配置并验证归档链路;
- 目标存储有足够容量。
PITR 恢复目录中的常见配置示意:
restore_command = 'cp /backup/wal/%f %p'
并在数据目录放置 recovery.signal,再配置恢复目标,例如:
recovery_target_time = '2025-01-10 12:00:00+00'
restore_command 成功时应把 %f 指定的 WAL 文件放到 %p 指定路径,并以成功退出表示获取成功。找不到文件时不能简单地把所有错误都伪装成成功,否则恢复过程可能错误地认为 WAL 已经提供。
恢复目标可以是时间、LSN、事务 ID 或命名恢复点等。时间恢复依赖服务器时间、事务提交记录和目标定义,不能把它理解为任意 SQL 语句执行时间的精确快照。
十四、Checkpoint 不是备份
执行:
CHECKPOINT;
只会请求一次检查点。它不能替代:
pg_basebackup;- 文件系统快照的一致性流程;
- WAL 归档;
- 备份验证;
- 恢复演练。
原因很直接:
Checkpoint 解决“从哪里开始崩溃恢复”
基础备份解决“恢复时数据文件从哪里来”
归档 WAL 解决“基础备份之后如何继续前进”
即使刚刚执行了 Checkpoint,如果数据库目录随后被彻底删除,也没有基础数据文件可供恢复。反过来,只有基础备份而没有覆盖所需的 WAL,也不能恢复到目标时间。
十五、一个可观察的 WAL 示例
以下示例在测试库中执行。
先记录 WAL 位置:
SELECT pg_current_wal_lsn() AS before_lsn;
创建表并插入数据:
CREATE TABLE wal_demo (
id bigint PRIMARY KEY,
payload text NOT NULL
);
INSERT INTO wal_demo
SELECT g, repeat('x', 1000)
FROM generate_series(1, 10000) AS g;
再查看 WAL 位置:
SELECT pg_current_wal_lsn() AS after_lsn;
计算差值:
WITH p AS (
SELECT
pg_current_wal_lsn() AS current_lsn,
'0/0'::pg_lsn AS zero_lsn
)
SELECT pg_wal_lsn_diff(current_lsn, zero_lsn)
FROM p;
这个最后的查询只适合展示 LSN 是一种单调位置,不适合精确计算本次插入量,因为它把数据库启动以来的全部 WAL 都算在内。更可靠的方式是先在客户端保存 before_lsn,操作后再计算:
SELECT pg_wal_lsn_diff(
'操作前保存的 LSN'::pg_lsn,
'操作后保存的 LSN'::pg_lsn
);
实际脚本应把两个 LSN 作为变量保存,而不是手工复制结果。
执行检查点:
CHECKPOINT;
检查点完成后,脏页更可能已经写入数据文件,但这仍不意味着所有相关 WAL 都会立即被删除或所有历史 WAL 都不再需要。
在测试环境中还可以手工切换 WAL:
SELECT pg_switch_wal();
它会请求切换到新的 WAL 段,使前一个 WAL 段更容易进入归档流程。它不是“立即把整个数据库备份下来”,也不保证归档命令已经成功完成。
十六、写入性能由哪些路径共同决定
写入性能至少包含以下几个阶段:
生成 WAL
↓
WAL 插入竞争
↓
WAL 写入
↓
WAL flush
↓
数据页写出
↓
Checkpoint 和后台写入
↓
归档或复制传输
每个阶段可能成为瓶颈。
1. WAL 生成量
WAL 生成量受以下因素影响:
- 表数据修改量;
- 索引数量和索引类型;
- 大量随机更新;
- 页面初始化;
- full-page image;
- 大事务;
- 批量操作方式;
- 大对象和系统目录变化;
- 复制相关需求。
例如,一次更新既要产生堆表 WAL,也可能产生多个索引页面修改 WAL。业务上只更新一行,不代表只产生一条小日志。
2. WAL 刷盘延迟
如果提交等待本地 WAL flush,那么存储设备的 flush 延迟会直接进入事务延迟。
可用近似关系理解提交延迟:
其中:
- :锁和并发等待;
- :WAL 插入和缓冲区竞争;
- :本地 WAL 刷盘时间;
- :同步复制额外等待时间,没有同步复制时可近似为零。
这不是 PostgreSQL 的精确性能模型,但可以帮助定位方向:如果 WAL flush 延迟很高,增加 CPU 通常不能解决提交延迟。
3. 数据页写出
即使 WAL flush 很快,脏数据页仍需要写出。如果数据页生成速度长期超过存储设备写入速度,脏页会积累,最终导致:
- checkpoint 时间变长;
max_wal_size更频繁触发;- 写入延迟抖动;
- 恢复时间增长;
- 文件系统空间压力增加。
4. Checkpoint 写入尖峰
Checkpoint 集中写出大量脏页时,会和业务写入争用 I/O。适当的 checkpoint 间隔和完成目标可以改善平滑性,但无法突破存储设备的长期写吞吐上限。
5. 归档和复制带宽
如果 WAL 产生速度为 ,而归档链路的长期处理速度为 ,当:
并且差距持续存在时,归档积压必然增长。
复制同理。备用库接收速度、重放速度或网络速度不足时,主库可能因为保留 WAL 而增加磁盘压力。
十七、为什么“关闭 WAL”不是普通优化
某些场景可以使用 UNLOGGED 表。UNLOGGED 表的数据修改不按普通永久表方式写入 WAL,因此可能减少 WAL 和复制压力。
但它有明确边界:
- 不提供普通永久表级别的崩溃持久性;
- 崩溃后可能被清空;
- 不会按普通方式复制到物理备用库;
- 不适合存放必须恢复的数据。
临时表又是另一种语义,生命周期和可见范围不同。
因此,下面两种说法不能混用:
减少不必要的 WAL
和:
关闭 WAL 来提升性能
前者可能通过减少无效更新、合理索引、批量提交、调整 checkpoint 或启用适当压缩实现;后者可能直接改变数据可靠性和复制能力。
十八、常见误解与实际失败表现
误解一:COMMIT 成功就表示数据页已经写入表文件
不对。默认提交主要等待 WAL 刷盘,数据页可能仍在共享缓冲区或操作系统缓存中。
正确判断是:
提交成功
≈ 相关提交 WAL 已满足当前提交策略
而不是:
提交成功
= 所有数据页都已写出
误解二:Checkpoint 越频繁越安全
Checkpoint 频繁通常缩短崩溃恢复需要重放的 WAL 区间,但会增加:
- 脏页写出频率;
- full-page image 机会;
- I/O 压力;
- WAL 生成量;
- 写入抖动风险。
安全性还取决于 WAL 持久化、备份和归档是否正确。Checkpoint 不是可靠性的唯一开关。
误解三:max_wal_size 是 pg_wal 的容量限制
不是。归档失败、复制槽、备用库延迟和大规模写入都可能使 WAL 超过该值。
误解四:启用了 archive_mode 就代表可以做 PITR
不完整。还需要:
- 实际成功的 WAL 归档;
- 可恢复的基础备份;
- 覆盖恢复范围的连续 WAL;
- 可用的恢复配置;
- 对备份和 WAL 的验证。
误解五:归档命令退出成功就代表远端已经安全
只代表 PostgreSQL 认为命令成功。若命令只是把文件写入本机缓存、挂载目录或不可靠的网络路径,真正的故障域和持久性仍需单独验证。
误解六:同步复制等于多副本都已应用数据
不一定。同步复制的具体确认阶段由同步提交模式和复制配置共同决定:
- 只收到网络数据;
- 已写入备用库;
- 已刷入备用库;
- 已在备用库应用。
这些阶段不是同一个保证。
十九、如何诊断 WAL、Checkpoint 和归档问题
1. 先区分 WAL 产生慢还是 WAL 刷盘慢
观察:
- 业务提交延迟;
- WAL 产生速率;
- WAL write/sync 次数;
- WAL write/sync 时间;
- 磁盘延迟和 I/O 队列;
- 是否存在同步备用库等待。
如果提交延迟和 WAL flush 延迟同步升高,优先检查 WAL 所在存储,而不是先调大内存。
2. 检查归档是否失败
SELECT *
FROM pg_stat_archiver;
重点关注:
- 最近成功归档时间;
- 最近失败归档时间;
failed_count是否持续增长;- 失败的 WAL 文件名;
- 归档目标是否可写、空间是否足够。
同时检查服务器日志,因为归档命令的退出状态和错误输出通常会被记录。
3. 检查 WAL 是否被复制槽保留
SELECT slot_name,
slot_type,
active,
restart_lsn,
confirmed_flush_lsn
FROM pg_replication_slots;
物理槽通常重点看 restart_lsn。如果它长时间远落后于当前 WAL 位置,就需要确认对应备用库是否仍然存在、是否能恢复,不能直接删除槽。
4. 检查主库和备用库的进度
主库:
SELECT pid,
application_name,
client_addr,
state,
sync_state,
sent_lsn,
write_lsn,
flush_lsn,
replay_lsn
FROM pg_stat_replication;
这些 LSN 用于区分:
主库已经发送到哪里
备用库已经写到哪里
备用库已经刷到哪里
备用库已经重放到哪里
备用库还可以查看:
SELECT pg_last_wal_receive_lsn(),
pg_last_wal_replay_lsn(),
pg_is_in_recovery();
不同 PostgreSQL 版本的统计字段可能增加或调整,应以实际版本视图为准。
5. 检查 Checkpoint 是否成为写入瓶颈
关注:
- checkpoint 是否频繁由 WAL 量触发;
- checkpoint 是否持续时间过长;
- checkpoint 期间 I/O 延迟是否升高;
- WAL 量是否因 full-page image 明显增加;
pg_wal是否因归档或槽长期保留;- 存储长期写吞吐是否低于业务产生速度。
不要只看到 checkpoint 日志就判断“Checkpoint 导致了问题”。需要结合时间线确认:
Checkpoint 开始
↓
I/O 延迟升高
↓
提交延迟升高
还是:
存储本来就拥塞
↓
Checkpoint 被拖慢
因果方向可能相反。
二十、配置取舍的正确边界
fsync
fsync=on 是正常生产持久化的基础。关闭它可能显著改变崩溃后的损坏风险,不应作为一般性能调优手段。
full_page_writes
正常生产环境通常应保持开启,以防页面撕裂。即使文件系统或存储设备声称支持原子写入,也不能仅凭宣传语句修改数据库恢复假设。
synchronous_commit
适合在明确接受极短时间窗口内已确认事务丢失的场景使用 off。必须把该语义传递给应用和业务负责人,而不是仅以“延迟更低”描述。
checkpoint_timeout 与 max_wal_size
两者共同影响 checkpoint 频率。增加它们可能降低 checkpoint 频率和 full-page image 比例,但也可能:
- 增加故障恢复时间;
- 增加 WAL 保留量;
- 使单次 checkpoint 处理更多脏页;
- 加大归档和复制带宽需求。
wal_compression
适合在 WAL 或网络带宽紧张、CPU 仍有余量时评估。它不是无成本减少 WAL,应该通过实际的 WAL 字节量、CPU 使用率和写延迟验证收益。
wal_buffers
WAL 缓冲区不足时可能增加等待,但扩大它不能替代低延迟持久化存储,也不能解决归档积压和复制槽保留问题。现代 PostgreSQL 会根据配置和系统情况自动设置合适的默认值,只有在测量证明 WAL 缓冲竞争存在时才应重点调整。
二十一、从一次提交到一次恢复的完整链路
把前面的机制串起来,可以得到如下完整因果链:
客户端执行 UPDATE
↓
后端修改 shared_buffers 中的数据页
↓
数据页变为 dirty
↓
生成描述页面变化的 WAL
↓
生成事务提交 WAL
↓
synchronous_commit=on 时等待本地 WAL flush
↓
客户端收到 COMMIT 成功
↓
后台进程或 checkpoint 后续写出数据页
↓
WAL 段完成后执行归档
↓
主库可将 WAL 发送给备用库
↓
某一时刻主机崩溃
↓
从最近 checkpoint 开始读取 WAL
↓
重放数据页变化和事务状态
↓
已提交事务的修改可见,未提交事务不可见
↓
数据库恢复到一致状态
如果还需要 PITR,则在最后增加:
基础备份
↓
连续读取归档 WAL
↓
在指定时间、LSN 或恢复目标处停止
如果使用同步复制,则提交链路中还会插入:
本地 WAL flush
↓
等待满足同步备用库确认级别
↓
返回 COMMIT 成功
结语
WAL、Checkpoint 和归档分别解决不同层次的问题:
- WAL:记录可恢复的变化,并为提交提供持久化依据;
- Checkpoint:建立较新的崩溃恢复起点,并推动脏页写出;
- 归档:保存连续 WAL,使基础备份能够恢复到更晚的时间点;
- 崩溃恢复:从 Checkpoint 开始重放 WAL,重建一致状态;
- 写入性能:同时受 WAL 生成、WAL flush、数据页写出、Checkpoint、归档和复制链路影响。
最重要的判断原则是:
提交成功主要取决于 WAL 是否达到提交策略要求的持久化边界;数据页可以晚于提交写出;Checkpoint 缩短恢复路径但不是备份;归档保存的是恢复所需的 WAL,而不是完整数据库;性能调优必须在明确可靠性边界后进行。
系列导航与关联阅读
- 系列入口:数据库完整学习路线:从关系模型、事务索引到分布式与向量检索
- 上一篇:PostgreSQL 存储内部:Heap Page、Tuple、TOAST、FSM 和可见性图
- 下一篇:PostgreSQL 锁与 Serializable SSI:谓词冲突、死锁和咨询锁
- 延伸:PostgreSQL 架构全景:进程模型、共享内存、WAL 与存储布局
- 延伸:PostgreSQL 复制与高可用:流复制、复制槽、PITR 和故障切换
官方资料
本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。

评论
0 条讨论