2. PostgreSQL物理存储与WAL机制

理解 PostgreSQL 的备份恢复能力,必须先看清它在磁盘上到底“长什么样”。PostgreSQL 的持久化模型可以概括为一句话:数据页是主体,WAL 是变更的账本,检查点是账本与主体对齐的锚点,LSN 是贯穿两者的时间坐标。本节围绕这条主线,把数据目录、表空间、数据文件、WAL、检查点、LSN 与恢复基础串成一个可观察、可验证的整体。

一、数据目录:集群的物理边界

通过 initdb 初始化的一个数据目录(PGDATA)就是一个数据库集群。它包含全局对象、每个数据库的子目录、WAL、事务状态、配置与运行标识文件。常用观察命令如下:

# 查看数据目录位置与关键子目录
psql -c "SHOW data_directory;"
ls -1 $PGDATA
# base/  global/  pg_wal/  pg_xact/  pg_tblspc/  postgresql.conf  PG_VERSION  postmaster.pid

其中 base/ 按数据库 OID 存放各库的物理文件,global/ 存放跨库共享的系统表,pg_wal/ 是 WAL 段文件目录,pg_xact/ 保存事务提交状态(CLOG),pg_tblspc/ 是表空间的挂载入口。数据库 OID 与目录名的对应关系可直接查询:

SELECT oid, datname FROM pg_database ORDER BY oid;

二、表空间与数据文件:逻辑对象如何落到磁盘

表空间是“逻辑对象到物理路径”的映射层。默认表空间 pg_default 指向 base/,pg_global 指向 global/;自定义表空间在 pg_tblspc/ 下以表空间 OID 建立符号链接,指向真实目录。创建与查看:

CREATE TABLESPACE ts_app LOCATION '/data/pg/ts_app';
SELECT spcname, oid, pg_tablespace_location(oid) FROM pg_tablespace;

表的数据文件命名规则是 relfilenode,而不是表名。表名到物理文件的链路为:pg_class.relname → relfilenode → 表空间路径/数据库OID/relfilenode。定位某个表的物理文件:

SELECT pg_relation_filepath('public.orders');
-- 输出示例:pg_tblspc/16385/PG_16_202307071/16386/24576

数据文件默认按 1GB 切分为多个段(24576、24576.1、24576.2……),内部以 8KB 页(page)为单位组织。页头包含 pd_lsn,记录“该页最后一次被修改时所对应的 WAL 位置”,这正是恢复时判断页面是否需要重放的关键依据。

三、WAL 日志:先写日志,再写数据

PostgreSQL 遵循 WAL(Write-Ahead Logging)原则:任何数据页修改在落盘前,描述该修改的 WAL 记录必须先持久化。WAL 段文件默认 16MB,命名如 000000010000000000000001,由时间线 ID、逻辑日志号、段号三部分组成。查看当前写入位置:

SELECT pg_current_wal_lsn(), pg_walfile_name(pg_current_wal_lsn());
-- 示例:0/1A2B3C48 | 000000010000000000000001

WAL 记录包含:事务 ID、操作类型(INSERT/UPDATE/DELETE/COMMIT)、目标数据块的块号与偏移、修改前后的元组信息,以及本条记录自身的 LSN。恢复时按 LSN 顺序重放,即可把数据文件从旧状态推进到一致状态。

四、检查点、LSN 与恢复基础

检查点(CHECKPOINT)做两件事:把共享缓冲区中的脏页刷到数据文件,并在 WAL 中写入一条检查点记录,标记“此 LSN 之前的所有修改已落盘”。因此恢复只需从最近一次检查点的 redo 位置开始重放 WAL,而不必从集群创建之初重放。相关观察命令:

CHECKPOINT;
SELECT pg_control_checkpoint();  -- 查看 checkpoint_lsn、redo_lsn、timeline_id
SELECT pg_current_wal_insert_lsn(), pg_current_wal_flush_lsn();

LSN(Log Sequence Number)是一个 64 位、单调递增的字节偏移量,格式为 X/Y,例如 0/1A2B3C48。它是 WAL 与数据页之间的公共坐标:数据页头记录 pd_lsn,WAL 记录自身携带 LSN,检查点记录 redo LSN。恢复的本质就是:从 redo LSN 开始,按 LSN 递增顺序重放 WAL,直到达到目标恢复点。

判断一个数据页是否需要重放,可比较页头 pd_lsn 与 WAL 记录的 LSN:若 pd_lsn < wal_lsn,说明该页落后于日志,需要应用这条 WAL 记录;否则可跳过。

五、把机制串起来:一次写入的完整路径

以 UPDATE orders SET status='paid' WHERE id=1001; 为例,物理层面依次发生:

  1. 在共享缓冲区找到目标页,若不在则从数据文件读入。
  2. 生成 WAL 记录,写入 WAL 缓冲区,记录本次修改的 LSN。
  3. 事务提交时,WAL 缓冲区刷盘(fsync),提交才算持久。
  4. 数据页在缓冲区中被标记为脏页,页头 pd_lsn 更新为本次 LSN。
  5. 检查点到来时,脏页被刷入数据文件,WAL 中写入检查点记录。

崩溃恢复时,系统读取控制文件中的检查点信息,从 redo_lsn 开始扫描 WAL,对每条记录检查目标页的 pd_lsn,决定是否重放,直到 WAL 末尾,再进入一致性状态。这就是物理备份、PITR(时间点恢复)和流复制共同的底层基础。

PostgreSQL 物理存储与 WAL 恢复链路 PGDATA 数据目录 base/ → 各库数据文件 按数据库 OID 分目录 global/ → 共享系统表 pg_database、pg_authid 等 pg_tblspc/ → 表空间链接 符号链接到自定义路径 pg_xact/ → 事务提交状态 CLOG,判断事务可见性 pg_wal/ → WAL 段文件 16MB/段,顺序写入 postgresql.conf / PG_VERSION 配置与版本标识 postmaster.pid 运行实例标识,崩溃后残留 数据文件与页结构 relfilenode 文件(如 24576) 按 1GB 切分为 24576.1 / .2 … 8KB 页:页头 + 行指针 + 元组 页头 pd_lsn = 最后修改的 WAL 位置 WAL 记录(pg_wal/) 段文件:000000010000000000000001 记录:事务ID + 块号 + 元组变更 每条记录携带自身 LSN 格式 X/Y,如 0/1A2B3C48 检查点与 LSN 坐标 CHECKPOINT 刷脏页 写入检查点记录 redo_lsn 恢复起点 pg_control_checkpoint() LSN 单调递增 WAL 与数据页的公共坐标 崩溃恢复流程 ① 读控制文件取 redo_lsn ② 从 redo_lsn 扫描 WAL ③ 比较 pd_lsn 与 wal_lsn 落后则重放,否则跳过 映射 写入 刷盘 重放 核心关系:数据页 pd_lsn ≤ WAL 记录 LSN ≤ 检查点 redo_lsn 之后 → 恢复按 LSN 顺序重放至一致

六、关键参数与观察点

与本节机制直接相关的参数包括:wal_level(决定 WAL 记录详细程度,归档与复制至少为 replica)、wal_segment_size(默认 16MB)、checkpoint_timeout 与 max_wal_size(控制检查点频率)、full_page_writes(崩溃后页撕裂保护)。日常巡检可关注:

SELECT pg_current_wal_lsn() AS cur_lsn,
       pg_walfile_name(pg_current_wal_lsn()) AS cur_seg,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), '0/0')) AS wal_bytes;

SELECT redo_lsn, checkpoint_lsn, timeline_id FROM pg_control_checkpoint();

当 pg_wal/ 持续增长、检查点频繁触发、或 pg_control_checkpoint() 中 redo_lsn 与当前 LSN 差距过大时,说明 WAL 生成速度与检查点刷盘速度失衡,需要调整检查点参数或排查长事务。这些观察点正是后续物理备份、PITR 与流复制章节的起点。