数据库基础体系 · 第 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 调用相应的同步机制,例如 fsyncfdatasync 或等价机制,使 WAL 数据达到数据库所依赖的持久化边界。

这个阶段可以称为:

WAL flushed / durable

在正常配置和正常存储语义下,只有 WAL 刷盘之后,PostgreSQL 才能安全地向客户端确认一个需要持久化保证的提交。

因此,事务提交的核心顺序是:

修改数据页
    ↓
生成 WAL 记录
    ↓
WAL 至少包含本事务的提交记录
    ↓
按提交策略写入并刷盘 WAL
    ↓
向客户端返回 COMMIT 成功

而不是:

修改数据页
    ↓
把所有数据页写入表文件
    ↓
向客户端返回 COMMIT 成功

这就是 PostgreSQL 的 WAL 预写式日志原则

在某个数据页的修改能够安全落盘之前,描述该修改的 WAL 必须已经先达到足够可靠的持久化状态。

形式化地说,假设某个数据页版本为 P1P_1,它由 WAL 记录 W1W_1 产生,那么必须满足:

durable(W1)durable(P1)\text{durable}(W_1) \leq \text{durable}(P_1)

这里的“不晚于”不是时间戳意义上的严格同步,而是持久化顺序约束:如果崩溃后数据页已经包含 W1W_1 的修改,那么恢复过程必须能够读取到 W1W_1


二、WAL 记录了什么

WAL 是按顺序追加的日志流。每条记录通常包含:

  • 记录类型;
  • 目标资源标识,例如某个关系文件的某个页面;
  • 页面修改所需的信息;
  • 事务或恢复相关信息;
  • 记录长度、校验信息和前后关联信息。

WAL 并不是 SQL 日志。它一般不会保存:

UPDATE accounts SET balance = balance - 100 WHERE id = 1;

而是保存恢复执行所需的更底层操作,例如某个数据页上的元组插入、页面初始化、索引页面修改或事务提交状态。

WAL 的主要用途包括:

  1. 崩溃恢复:从最近的恢复起点重放数据页修改。
  2. 保证提交持久性:先刷 WAL,再确认提交。
  3. 物理流复制:主库向备用库发送 WAL。
  4. PITR:基础备份加连续 WAL,实现时间点恢复。
  5. 在线备份一致性:在备份期间使用 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 位置是 LcL_c,那么只要 WAL 已经持久化到至少 LcL_c,恢复时就能同时看到:

  • 事务修改的 WAL;
  • 事务已经提交的事实。

第四步:刷 WAL,而不是刷所有数据页

在默认的 synchronous_commit=on 下,PostgreSQL 通常会等待本地 WAL 刷盘,然后返回提交成功。

事务修改的数据页可以稍后由以下组件写出:

  • 后端进程;
  • background writer;
  • checkpointer;
  • 其他相关写入路径。

因此,提交成功后立刻检查数据文件,不能据此判断事务是否安全。真正的持久性依据是对应提交 WAL 是否达到持久化边界。


四、synchronous_commit 改变的是什么

synchronous_commit 控制事务提交时对 WAL 持久化以及同步复制确认的等待方式。它不改变 WAL 的生成,也不意味着关闭后就不需要 WAL。

常见值包括:

  • on
  • off
  • local
  • remote_write
  • remote_apply

其中 remote_writeremote_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 可以满足三个事务的本地持久性等待。

这带来两个结果:

  1. 高并发提交时,单事务平均刷盘成本可能下降;
  2. 存储设备的 flush 延迟仍然会直接影响提交延迟。

因此,测量写入性能时不能只看 WAL 生成速度,还要区分:

  • WAL 生成速度;
  • WAL 写入速度;
  • WAL flush 次数;
  • WAL flush 延迟;
  • 提交等待时间;
  • 同步复制等待时间。

在较新的 PostgreSQL 版本中,可以从 pg_stat_wal 等统计视图观察 WAL 产生和写入情况。统计视图的字段会随版本变化,生产诊断应以当前版本文档和实际字段为准。


六、数据页为什么可以晚于提交落盘

WAL 的设计使数据页可以延迟写入。

例如:

事务 T 修改页面 P
↓
页面 P 只在 shared_buffers 中变脏
↓
T 的修改 WAL 和提交记录刷盘
↓
向客户端返回 COMMIT 成功
↓
数秒后 checkpoint 才写出页面 P

如果此时主机崩溃:

  1. 恢复程序读取页面 P 的旧版本;
  2. 从合适的 WAL 位置开始重放;
  3. 重放 T 对 P 的修改;
  4. 看到 T 的提交记录;
  5. 恢复后的页面表现为已提交状态。

这就是“先写日志、后写数据页”的意义。

反过来,如果数据页已经先写入磁盘,但对应 WAL 尚未持久化,崩溃后恢复可能无法重建该数据页。因此 PostgreSQL 在写数据页前必须保证相关 WAL 已经满足预写条件。


七、Checkpoint 是什么

Checkpoint 是一个恢复边界。它主要完成两件事:

  1. 在 WAL 中记录一个可用于恢复的检查点位置;
  2. 推动共享缓冲区中的脏页写入数据文件,使系统不必从很久以前的 WAL 开始恢复。

Checkpoint 不是:

  • 事务提交;
  • 全库备份;
  • 把所有 WAL 永久保存;
  • 复制到备用库;
  • 立即删除所有旧 WAL。

Checkpoint 的基本过程

抽象过程如下:

1. 发起 checkpoint
2. 确定 checkpoint 相关 WAL 位置
3. 确保该位置以前的必要 WAL 已经写出
4. 扫描并写出脏数据页
5. 记录和更新 checkpoint 状态
6. 后续崩溃恢复可以从该 checkpoint 附近开始

实际实现包含并发和后台写入细节,但核心约束是:

checkpoint 写出的数据页,其所依赖的 WAL 必须已经先达到满足要求的持久化状态。

Checkpoint 与恢复起点

假设某次 checkpoint 对应恢复起点为 LcheckpointL_{checkpoint},崩溃时最后可用 WAL 位置为 LendL_{end},那么恢复通常需要处理:

[Lcheckpoint,Lend][L_{checkpoint}, L_{end}]

恢复时间大致取决于:

  • 需要重放的 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:

  1. 归档尚未成功;
  2. 物理复制备用库尚未追上;
  3. 复制槽要求保留;
  4. 服务器仍需要它进行恢复;
  5. 其他内部保留条件。

复制槽的关键风险

物理复制槽会记录备用库需要的 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 延迟会直接进入事务延迟。

可用近似关系理解提交延迟:

TcommitTlock+TWAL_insert+Tflush+TreplicationT_{commit} \approx T_{lock} + T_{WAL\_insert} + T_{flush} + T_{replication}

其中:

  • TlockT_{lock}:锁和并发等待;
  • TWAL_insertT_{WAL\_insert}:WAL 插入和缓冲区竞争;
  • TflushT_{flush}:本地 WAL 刷盘时间;
  • TreplicationT_{replication}:同步复制额外等待时间,没有同步复制时可近似为零。

这不是 PostgreSQL 的精确性能模型,但可以帮助定位方向:如果 WAL flush 延迟很高,增加 CPU 通常不能解决提交延迟。

3. 数据页写出

即使 WAL flush 很快,脏数据页仍需要写出。如果数据页生成速度长期超过存储设备写入速度,脏页会积累,最终导致:

  • checkpoint 时间变长;
  • max_wal_size 更频繁触发;
  • 写入延迟抖动;
  • 恢复时间增长;
  • 文件系统空间压力增加。

4. Checkpoint 写入尖峰

Checkpoint 集中写出大量脏页时,会和业务写入争用 I/O。适当的 checkpoint 间隔和完成目标可以改善平滑性,但无法突破存储设备的长期写吞吐上限。

5. 归档和复制带宽

如果 WAL 产生速度为 RwalR_{wal},而归档链路的长期处理速度为 RarchiveR_{archive},当:

Rwal>RarchiveR_{wal} > R_{archive}

并且差距持续存在时,归档积压必然增长。

复制同理。备用库接收速度、重放速度或网络速度不足时,主库可能因为保留 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_sizepg_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_timeoutmax_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,而不是完整数据库;性能调优必须在明确可靠性边界后进行。


系列导航与关联阅读

官方资料

本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。