八、PostgreSQL WAL日志

1. WAL概述

  • WAL 全称是 write ahead log,是 PG 中的 online redo log;
  • WAL 是一种保证数据完整性的标准方法;
  • 数据文件的改变必须先写入日志,即日志记录刷新到永久储存之后,才能被写到内存;
  • 遵循这个过程,不需要在每个事务提交时都刷新数据页到磁盘;
  • 在宕机可以用日志来恢复数据库;
  • 时任何没有应用到数据页上的改动都可以根据日志记录重做(即回滚恢复 REDO);

每条 WAL record 包含:

字段 说明
RelFileNode 表的物理文件标识(tablespace OID + database OID + relfilenode)
ForkNumber 哪个 fork(main、fsm、vm)
BlockNumber 具体是哪个 page(页面编号)
Offset + Data 页面内的偏移位置和修改内容

所以恢复流程大致是:

读取 WAL record
    ↓
从 record 中获取 RelFileNode + BlockNumber
    ↓
把对应的 page 从磁盘读入 shared buffer(如果还没在内存中)
    ↓
检查 page 的 pd_lsn 是否 < WAL record 的 LSN
    ↓
  是 → 将修改应用到 page(redo)
  否 → 跳过(说明这条修改已经落盘了)

pd_lsn 的作用就在这里 — 之前 Heap Page 文档里提到的 pd_lsn 字段,就是用来判断"这个 page 是否已经包含了这条 WAL 的修改"。如果 pd_lsn >= WAL record LSN,说明修改已经在 page 上了,不需要重做。

 

2. WAL的优势

  1. 当宕机发生时,Data Buffer的内容还没有全部写入到永久存储中,数据丢失,但是WAL Buffer的内容已写入磁盘,根据WAL日志的内容,可以恢复库丢失的内容

  2. 在提交交时,仅把WAL刷新到了磁盘,而不是Data刷新。

    • ✓ 从IO次数来说,WAL刷新是少量IO,Data刷新是大量IO,WAL刷新次数少得多
    • ✓ 从IO花销来说,WAL刷新是连续IO,Data刷新是随机IO,WAL刷新花销小得多
    • ✓ WAL机制在保证事务持久性和数据完整性的同时,成功地提升了系统性能

3. WAL的应用场景

 

image

 

 

 

4. WAL的机制

image

  1. 写数据的过程中加入了对应的写 wal log 的过程,步骤是先到 Buffer,再刷新到 Disk
  2. Change 发生时:
    1. 先将变更后内容记入 WAL Buffer
    2. 再将更新后的数据写入 Data Buffer
  3. Commit 发生时:
    1. WAL Buffer 刷新到 Disk
    2. Data Buffer 写磁盘推迟
  4. Checkpoint 发生时:
  5. 将所有 Data Buffer 刷新到磁盘

Full Page Write

操作系统写一个 8KB 的 page 不是原子操作。Full Page Write(FPW)是 PostgreSQL 防止 torn page 导致数据不可恢复的关键机制。
1. 问题:Torn Page(部分写入 / 撕裂页)
PostgreSQL 的 page 默认是 8KB,但操作系统和磁盘硬件通常以 512 字节(或 4KB)为单位写入。如果在写一个 8KB page 到磁盘的过程中发生宕机:
8KB page 写入磁盘:
  [前 4KB] ← 已写入(新数据)
  [后 4KB] ← 未写入(还是旧数据)
            ↑
         这时宕机了
结果磁盘上这个 page 一半是新的、一半是旧的,这就是 torn page。
2. 为什么普通 WAL 记录救不了?
普通 WAL record 只记录增量修改(比如"在 page 100 的 offset 200 处写入 50 字节")。增量修改的前提是:page 的基础状态是正确的。如果 page 本身已经撕裂损坏了,在损坏的基础上重放增量修改,结果还是错的:
场景
公式
结果
正常 page
page + WAL 增量
✅ 正确
撕裂 page
page + WAL 增量
❌ 错误

3. Full Page Write 如何解决?

Checkpoint 之后,第一次修改某个 page 时,WAL 会把整个 page 的完整副本(8KB)写进去:
    A[Checkpoint 完成] --> B[第一次修改 page 100]
    B --> C["WAL record = 完整 page 副本 + 增量修改"]
    C --> D[后续修改 page 100]
    D --> E["WAL record = 只记录增量\n(已有完整备份)"]
4. 恢复流程
宕机后启动恢复时:
    A["发现 page 100 可能损坏\n(pd_lsn 不对)"] --> B["从 WAL 中找到 checkpoint 后\n第一条 full page image"]
    B --> C[用完整副本直接替换\n磁盘上的 torn page]
    C --> D[在正确的 page 基础上\n继续重放后续增量 WAL]
    D --> E["page 完全恢复 ✅"]
pd_lsn 是 Page Header 中的字段,记录页面最后一次 WAL 修改对应的 LSN。恢复时通过比较 pd_lsn 和 WAL record 的 LSN 来判断该页面是否需要 redo。
5. 代价与权衡
Full Page Write 的缺点是 WAL 体积增大,尤其在 checkpoint 刚完成后会有一波写入高峰——大量 page 被首次修改时都要写完整副本。这也是为什么调整 checkpoint_timeoutmax_wal_size 需要权衡:
情况
影响
checkpoint 太频繁
FPW 越多 → WAL 越大 → I/O 开销增加
checkpoint 太稀疏
恢复时需要重放更多 WAL → 恢复时间变长
 

5. WAL的核心作用

没有WAL的inster操作

image

有WAL的inster操作

image

 

6. WAL的类型

WAL的存放

存储在 $PGDATA/pg_wal 内,类似于 000000010000000200000D4 的文件存储

文件名结构:

部分 示例 含义
前 8 位 00000001 timeline
中 8 位 00000002 LogId
后 8 位 000000D4 LogSeg
  • Segment 由 2048 个 Block 组成,其大小为 16M

  • Block 为 WAL 日志的最小单位,其大小 8k

时间线历史文件 - 学习备份恢复的时候再补充

历史文件的命名规则:"8-digit new timelineId".history 比如:00000002.history

时间线历史记录文件至少包含一行,每行由以下三项组成:

  1. timelineId – 用于恢复的存档日志的 timelineId。
  2. LSN – WAL 段切换发生的 LSN 位置。
  3. 原因 – 时间线更改原因的解释。

TIMELINE是什么?

时间线的核心目的就是区分不同的恢复历史,让多次恢复互不干扰

image

  1. 首先,我们删除当前的数据库集群并恢复过去所做的基础备份,以便回到恢复的起点。
  2. 接下来,我们启动 PostgreSQL 服务器,它沿着初始时间线(timelineId 1)跟踪从 11.45分 创建的 REDO 点到恢复目标,重放归档日志中的 WAL 数据。
  3. 然后,新的时间线 ID 2 被分配给恢复的数据库集群,并且 PostgreSQL 在新的时间线上运行。

 

7. WAL切换

查看数据库当前使用的WAL日志

1. 查看当前的LSN

select pg_current_wal_lsn();
 pg_current_wal_lsn 
--------------------
 1/BFA16AA8
(1 row)

2. 根据LSN查看WAL文件

select pg_walfile_name('1/BFA16AA8');
     pg_walfile_name      
--------------------------
 0000000100000001000000BF
 
 --
 select pg_walfile_name(pg_current_wal_lsn());
     pg_walfile_name      
--------------------------
 0000000100000001000000BF

 

8.  WAL日志配置归档 更建议使用归档工具 - pgBackRest,待补充

  1. 创建归档目录
    mkdir -p /postgres/arch 
    chown -R postgres.postgres /postgres/
  2. 配置归档参数
    archive_mode = on
    archive_command = 'cp %p /postgres/arch/%f'
  3. 重启数据库

WAL什么时候会触发归档

  1. 手动强制切换 pg_switch_wal();

  2. wal 日志写满后会自动归档

    • wal 日志文件默认为 16MB
    • 这个值可以在编译 PostgreSQL 时通过参数 --with-wal-segsize 更改,编译后不能修改。
  3. 参数 archive_timeout

    • 在 postgresql.conf 文件中的参数 archive_timeout
    • 如果设置 archive_timeout=60s,意思是,wal 日志 60s 切换一次,同时会触发日志归档。

 

9. WAL日志膨胀的原因

1. 长事务

数据库中如果有长事务,PostgreSQL 数据库对于这个长事务开始后产生的所有 WAL 日志都不会清理。

下面监控超过 8 个小时的长事务的 SQL:

select pid, usename, xact_start from pg_stat_activity where now() - xact_start > interval '8 hours';

用户有 "Idle in transaction" 的连接,即一个连接开启了事务,然后什么事情也不干,一直空闲着,用下面的 SQL 查询 "Idle in transaction" 的连接:

select pid, client_addr, usename, datname, xact_start, state from pg_stat_activity where state not in ('active','idle') order by xact_start;

2. 主库的 WAL 日志的归档未成功

主库开启了归档,但是归档命令一直没有执行成功,或归档命令 hang 住,也可能是归档命令执行的太慢,来不及归档。

 

10. WAL核心参数

  1. wal_buffer # 基本不用修改
  2. wal_keep_size
  3. min_wal_size
  4. max_wal_size # 约等于wal_keep_size + min_wal_size

假设当前正在写的 WAL 文件为 000000A700000004000000060

  • wal_keep_size 控制 000000A70000000400000005E 到 000000A700000004000000060 的个数
  • min_wal_size 控制 000000A700000004000000060 到 000000A700000004000000064,即这一段至少要保留 min_wal_size 的 WAL 日志。

WAL 文件列表:

000000A70000000400000005E
000000A70000000400000005F
000000A700000004000000060 # 当前在写的WAL
000000A700000004000000061
000000A700000004000000062
000000A700000004000000063
000000A700000004000000064

简单来说:

  • wal_keep_size — 控制当前 WAL 文件之前保留多少(为 standby 流复制保留)
  • min_wal_size — 控制当前 WAL 文件之后至少预留多少(为将来写入预留空间,避免频繁创建/删除文件)

max_wal_size 与 检查点

实际上参数 max_wal_size 主要是为了控制 checkpoint 发生的频繁程度:

target = (double) ConvertToXSegs(max_wal_size_mb) / (2.0 + CheckPointCompletionTarget);

如果 checkpoint_completion_target 设置为 0.5 时,则每写了 max_wal_size / 2.5 的 WAL 日志时,就会发送一次 checkpoint。

checkpoint_completion_target 的范围为 0~1,那么结果就是写的 WAL 的日志量超过 max_wal_size 的 1/3 ~ 1/2 时,就会发生一次 checkpoint。

 

posted @ 2026-08-30 13:51  BinBin-HF  阅读(19)  评论(0)    收藏  举报