WAL Structure(Physical-Structure)

在磁盘上,WAL 被存储在 PGDATA/pg_wal 目录中,以单独的文件(或称为段)的形式存在。它们的大小由只读参数 wal_segment_size 指示。

对于高负载系统,增加段大小可能是有意义的,因为这可以减少开销。但这个设置只能在集群初始化时修改(通过 initdb –wal-segsize)。

WAL 记录会写入当前文件,直到该文件空间耗尽;此时 PostgreSQL 会开始写入一个新文件。

我们可以确定某条记录位于哪个文件中,以及它在该文件起始位置的偏移量。

1
2
3
4
5
demo=# SELECT file_name, upper(to_hex(file_offset)) file_offset FROM pg_walfile_name_offset('1/EBDDC500');
file_name | file_offset
--------------------------+-------------
0000000100000001000000EB | DDC500
(1 row)

该文件的名称由两部分组成。最高的八位十六进制数字(4个字节)表示用于从备份中恢复的时间线(timeline),而其余部分(8个字节)表示 LSN(日志序列号)的高位比特(LSN 的低位比特则体现在 file_offset 字段中)。

要查看当前的 WAL 文件,可以调用以下函数:

1
2
3
4
5
6
7
demo=# SELECT *
FROM pg_ls_waldir()
WHERE name = '0000000100000001000000EB';
name | size | modification
--------------------------+----------+------------------------
0000000100000001000000EB | 16777216 | 2025-07-28 18:41:49+08
(1 row)

现在让我们使用 pg_waldump 工具查看新创建的 WAL 记录的头部信息。该工具既可以按 LSN 范围(就像这个例子中那样)过滤 WAL 记录,也可以按特定的事务 ID 过滤。

pg_waldump 工具应以 postgres 用户身份运行,因为它需要访问磁盘上的 WAL 文件。

1
2
3
4
5
6
7
8
9
postgres@lavm-bar1guved6:/root$ pg_waldump -p /usr/local/pgsql/data/pg_wal -s  1/EBDDA4F8 -e 1/EBDDC500
rmgr: XLOG len (rec/tot): 49/ 109, tx: 0, lsn: 1/EBDDA4F8, prev 1/EBDDA4C0, desc: FPI_FOR_HINT , blkref #0: rel 1663/32814/376833 blk 0 FPW
rmgr: Heap len (rec/tot): 69/ 69, tx: 961, lsn: 1/EBDDA568, prev 1/EBDDA4F8, desc: HOT_UPDATE old_xmax: 961, old_off: 1, old_infobits: [], flags: 0x40, new_xmax: 0, new_off: 2, blkref #0: rel 1663/32814/376833 blk 0
rmgr: Standby len (rec/tot): 54/ 54, tx: 0, lsn: 1/EBDDA5B0, prev 1/EBDDA568, desc: RUNNING_XACTS nextXid 962 latestCompletedXid 960 oldestRunningXid 961; 1 xacts: 961
rmgr: XLOG len (rec/tot): 49/ 7777, tx: 961, lsn: 1/EBDDA5E8, prev 1/EBDDA5B0, desc: FPI_FOR_HINT , blkref #0: rel 1663/32814/2691 blk 19 FPW
rmgr: Standby len (rec/tot): 54/ 54, tx: 0, lsn: 1/EBDDC468, prev 1/EBDDA5E8, desc: RUNNING_XACTS nextXid 962 latestCompletedXid 960 oldestRunningXid 961; 1 xacts: 961
rmgr: Transaction len (rec/tot): 34/ 34, tx: 961, lsn: 1/EBDDC4A0, prev 1/EBDDC468, desc: COMMIT 2025-07-28 16:04:55.325979 CST
rmgr: Standby len (rec/tot): 50/ 50, tx: 0, lsn: 1/EBDDC4C8, prev 1/EBDDC4A0, desc: RUNNING_XACTS nextXid 962 latestCompletedXid 961 oldestRunningXid 962
postgres@lavm-bar1guved6:/root$
  1. FPI_FOR_HINT(全页镜像,为 Hint Bit)
    1
    rmgr: XLOG len (rec/tot): 49/109, tx: 0, lsn: 1/EBDDA4F8, prev 1/EBDDA4C0, desc: FPI_FOR_HINT , blkref #0: rel 1663/32814/376833 blk 0 FPW
  • rmgr: XLOG:表示这是 XLOG(日志)资源管理器记录。
  • FPI_FOR_HINT:全页镜像用于设置 Hint bit。为了避免 Hint bit 修改没有日志而导致数据页 checksum 校验失败,PostgreSQL 会把整个页面写入 WAL(FPW, Full Page Write)。
  • rel 1663/32814/376833 blk 0:指的是某个表的第 0 页(block 0),文件标识符是:数据库OID=32814,表OID=376833。
  • tx: 0:不是某个事务产生的,而是后台 hint bit 的写入。
  • FPW:全页写入。
  1. HOT_UPDATE(堆表中的更新)
    1
    rmgr: Heap len (rec/tot): 69/69, tx: 961, lsn: 1/EBDDA568, prev 1/EBDDA4F8, desc: HOT_UPDATE old_xmax: 961, old_off: 1, old_infobits: [], flags: 0x40, new_xmax: 0, new_off: 2, blkref #0: rel 1663/32814/376833 blk 0
  • rmgr: Heap:这是 Heap 表的更新记录。
  • HOT_UPDATE:表示使用了“Heap-Only Tuple”优化,即更新没有修改索引字段,所以新旧 tuple 都在一个页里。
  • tx: 961:由事务 961 发起。
  • old_off: 1 -> new_off: 2:第 1 个 tuple 更新为第 2 个位置的 tuple。
  • old_xmax: 961:原始 tuple 的删除者是当前事务。
  • new_xmax: 0:新 tuple 尚未被删除。
  • rel 1663/32814/376833 blk 0:仍然是这个表第 0 页
  1. RUNNING_XACTS(记录活跃事务信息)
    1
    rmgr: Standby len (rec/tot): 54/54, tx: 0, lsn: 1/EBDDA5B0, prev 1/EBDDA568, desc: RUNNING_XACTS nextXid 962 latestCompletedXid 960 oldestRunningXid 961; 1 xacts: 961
  • rmgr: Standby:这是为备机记录活跃事务信息。
  • nextXid: 962:下一个将被分配的事务 ID。
  • latestCompletedXid: 960:最后一个完成的事务。
  • oldestRunningXid: 961:最老的活跃事务。
  • 1 xacts: 961:当前只有一个活跃事务 961。
    这类记录有助于逻辑解码和备机恢复时判断哪些事务是已提交、未提交。
  1. FPI_FOR_HINT(另一个 hint bit 的全页写入)
    1
    rmgr: XLOG len (rec/tot): 49/7777, tx: 961, lsn: 1/EBDDA5E8, prev 1/EBDDA5B0, desc: FPI_FOR_HINT , blkref #0: rel 1663/32814/2691 blk 19 FPW
  • 又是一个 FPI_FOR_HINT,但这次是针对:
    rel 1663/32814/2691 blk 19:另外一个表的第 19 页。
  • 注意这次记录总长度达到了 7777 字节,很可能是完整的数据页写入(通常 8KB)。
  1. RUNNING_XACTS(再次记录活跃事务)

    1
    rmgr: Standby len (rec/tot): 54/54, tx: 0, lsn: 1/EBDDC468, prev 1/EBDDA5E8, desc: RUNNING_XACTS nextXid 962 latestCompletedXid 960 oldestRunningXid 961; 1 xacts: 961

    和之前类似,再次记录活跃事务 961。

  2. COMMIT(事务提交)

    1
    rmgr: Transaction len (rec/tot): 34/34, tx: 961, lsn: 1/EBDDC4A0, prev 1/EBDDC468, desc: COMMIT 2025-07-28 16:04:55.325979 CST
  • 事务 961 正式提交。
  • 提交时间 是 2025-07-28 16:04:55。
  1. RUNNING_XACTS(提交后活跃事务清空)
    1
    rmgr: Standby len (rec/tot): 50/50, tx: 0, lsn: 1/EBDDC4C8, prev 1/EBDDC4A0, desc: RUNNING_XACTS nextXid 962 latestCompletedXid 961 oldestRunningXid 962
  • 事务 961 已完成,现在没有活跃事务了。
  • nextXid 为 962,准备分配给下一个事务。

查看日志文件路径

1
2
3
4
5
demo=# SELECT pg_relation_filepath('wal');
pg_relation_filepath
----------------------
base/32814/376833
(1 row)

参考书目

  1. Egor Rogov, PostgreSQL 14 Internals, https://postgrespro.com/community/books/internals