PostgreSQL pg_rewind 执行过程分析

1. pg_rewind 功能描述

pg_rewind 是 PostgreSQL 提供的一个工具,在主库故障切换后,用于修复旧主的流复制。PG 主备切换后,流复制的时间线会发生变化,在异步复制场景下,旧主产生的 WAL 日志可能有部分未同步到备库,此时做故障切换,就会导致新的主库丢失一部分数据,而旧主恢复后,也无法与新主库建立复制关系,因为旧主有一些事务需要回滚,然后才能与新主建立复制关系。pg_rewind 工具能够自动将旧主多出的事务数据进行回滚,并与新主建立复制关系。

2. pg_rewind 实现细节

以下分析基于 PG 13 源码。

旧主库称之为 target,新主库称之为 source。

如果 target 的时间线与 source 的时间线相同,则无需做 pg_rewind,直接就可以建立流复制。时间线通过 global/pg_control 获取。其对应的源码结构如下:

struct ControlFileData
{
    ...
    CheckPoint	checkPointCopy; /* copy of last check point record */
    ...
}

if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
{
    pg_log_info("source and target cluster are on the same timeline");
    rewind_needed = false;
}

如果 target 的时间线与 source 的时间线不同,则需要根据分叉点(divergerec)与 target 最后一条 wal 记录的 lsn(target_wal_endrec) 进行对比,如果 target_wal_endrec 大于 divergerec,则需要进行 pg_rewind 回滚事务。

获取分叉点

通过函数 findCommonAncestorTimeline() 来获取分叉点和最新的共同时间线,主要逻辑是对比 pg_wal 目录下的 xxxxxxxx.history 文件,比如 00000003.history,找到最后一次共同的时间线以及分叉点。注意 .history 文件只有在发生主备切换后才会产生。

如果不需要回滚事务,直接写 recovery.conf 或者 postgresql.auto.conf 就可建立流复制,pg_rewind 结束。如果需要进行事务回滚,则进入下面的流程。

获取checkpoint

checkpoint 和 checkpoint record 是两个不同的概念,checkpoint 是 checkpoint 操作的开始点,当 checkpoint 操作结束时,会往 wal 里面写一条记录,称之为 checkpoint record。PG 做崩溃恢复是从 checkpoint 点开始,而不是 checkpoint record 点。

# checkpoint 与 checkpoint record 的关系,
# 其中 A,B,C,D,E 为普通的 wal 记录,checkpoint 指向一条普通的 wal 记录,这里就是 A
# checkpoint record 本身也是一条 wal 记录,它里面有个字段指向了 checkpoint 位置

--checkpoint--A--B--C-checkpoint record--D--E--

pg_rewind 调用函数 findLastCheckpoint(),获取分叉点之前最近一次的 checkpoint record 以及 checkpoint 位置。

对比 target 和 source 数据目录

pg_rewind 要将 target 多出的事务进行回滚,因此需要对比分叉点之后 target 与 source 之间的数据变化,使用 source 的数据对 target 进行覆盖或者删减,然后使用 source 的 wal 日志进行恢复。上述过程是 pg_rewind 中最为复杂的一步。

获取 source 数据目录下的所有文件信息,通过如下 SQL 获取:

WITH RECURSIVE files (path, filename, size, isdir) AS (
	SELECT '' AS path, filename, size, isdir FROM
	(SELECT pg_ls_dir('.', true, false) AS filename) AS fn,
	    pg_stat_file(fn.filename, true) AS this
	UNION ALL
	SELECT parent.path || parent.filename || '/' AS path,
	    fn, this.size, this.isdir
	  FROM files AS parent,
	       pg_ls_dir(parent.path || parent.filename, true, false) AS fn,
	       pg_stat_file(parent.path || parent.filename || '/' || fn, true) AS this
	       WHERE parent.isdir = 't'\n"
	)\n"
	SELECT path || filename, size, isdir,
	       pg_tablespace_location(pg_tablespace.oid) AS link_target
	FROM files\n"
	LEFT OUTER JOIN pg_tablespace ON files.path = 'pg_tblspc/'
	                             AND oid::text = files.filename;

对比 source 数据目录与本地 target 数据目录下的文件,如果目录不存在,则要创建,如果文件不存在,则要拷贝,如果文件大小不一致,则需要在文件末尾添加或者删减。如果本地 target 文件存在,而 source 侧不存在,则需要删除。将这些差异信息作为一个 file_entry_t 对象记录下来。

解析 target 从分叉点前的 checkpoint 开始,一直到最后的 wal lsn,这期间修改的所有 page 块信息,并进行记录,这些修改的块在下一步将从 source 进行拷贝并覆盖到 target。

调用 executeFileMap() 函数,从 source 拷贝修改的文件并应用到 target 数据目录中,从这一步开始,pg_rewind 不能再回头了。

更详细的过程,可参考文章:

https://developer.aliyun.com/article/772222

之后创建备份标志 backup_label,更新控制文件 global/pg_control,写入恢复配置文件,recovery.conf/postgresql.auto.conf。

3. 限制

pg_rewind 是基于 PG 物理流复制实现的,使用 source 的数据 page 页修复 target 时,可能导致 page 数据不一致,在做 recovery 时需要依赖 wal 的 full page write 机制,因此必须设置 full_page_write=on。

文章评论

0条评论