PostgreSQL rewind 分析
最近在使用 PostgreSQL 的物理复制时,遇到了关于 PostgreSQL rewind 操作相关的问题,本文梳理了 pg_rewind 的工作流程,并针对 rewind 期间可能遇到的问题进行整理。
我将 pg_rewind 整个流程大致分为三个阶段:
- 初始阶段:根据
source和target的ControlFileData来判断是否需要进行rewind操作。 - 准备阶段:遍历
source和target数据目录并建立文件的映射表、确定各文件的操作。 - 执行阶段:根据准备阶段生成的文件映射表执行相应的操作,如拷贝数据块、删除文件等。
初始阶段
在初始阶段 pg_rewind 首先会根据 --source-server 和 --source-pgdata 来创建 rewind_source 对象,它主要用于封装远程在线实例和本地实例的访问。
--source-server- 通过 libpq 连接数据库,随后利用init_libpq_source()创建rewind_source对象。--source-pgdata- 通过init_local_source()创建rewind_source对象。
接下来,pg_rewind 解析 source 和 target 的 ControlFileData 并它们进行验证(CRC32 校验)。
pg_rewind 能执行的前提是它们来自同一个实例,sanityChecks() 函数用于完成这类检查,包括数据库系统标识符、版本号是否匹配,target 是否开启数据校验(data_checksums)或 WAL 日志提示(wal_log_hints)。
接着确定 source 和 target 的时间线,然后通过 getTimelineHistory() 获取 source 和 target 的时间线历史,并通过 findCommonAncestorTimeline() 查找其共同的祖先,从而找到分叉点(divergerec)。
最后通过读取 ControlFileData.checkPoint 并结合 ControlFileData.minRecoveryPoint 来获取 target 结束的 WAL 日志位置(target_wal_endrec)。如果 target_wal_endrec 大于 divergerec,那么需要进行 rewind 操作。
准备阶段
该阶段主要负责收集 source 和 target 之间的文件差异、建立文件映射表并决定每个文件应该执行的操作类型,为后续的执行阶段提供依据。
该阶段从 keepwal_init() 开始,它负责创建一个哈希表用于跟踪 WAL 日志文件,确保在 rewind 期间,这些 WAL 日志不会被删除掉。
随后通过 findLastCheckpoint() 函数查找 target 上分叉点(divergerec)之前的检查点信息。findLastCheckpoint() 从 divergerec 开始从后向前查找检查点,并将这个过程中访问的 WAL 日志保存在 keepwal_init() 创建的哈希表中。
接着通过 filehash_init() 函数初始化文件映射表,遍历 source 和 target 数据目录下的所有文件填充文件映射表。target 的遍历通过 traverse_datadir() 来完成。
source如果是--source-pgdata,则通过local_traverse_files()函数遍历数据目录。本质上是包裹traverse_datadir()函数。source如果是--source-server,则通过libpq_traverse_file()函数遍历数据目录。本质上是使用 SQL 语句获取文件列表,对应的 SQL 语句如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18WITH 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'
)
SELECT path || filename, size, isdir,
pg_tablespace_location(pg_tablespace.oid) AS link_target
FROM files
LEFT OUTER JOIN pg_tablespace ON files.path = 'pg_tblspc/'
AND oid::text = files.filename
;
在遍历数据目录的过程中,source 通过 process_source_file() 函数处理文件并将其加入到文件映射表中,而 target 则通过 process_target_file() 函数进行类似的操作。
当初始化 source 和 target 数据目录中的文件映射表之后,接着通过 extractPageMap() 函数从分叉点之前的最后一个检查点读取目标 WAL,以提取分叉后在 target 数据目录上修改的所有页面。其核心在 extractPageInfo() 函数中,它负责提取 WAL 修改的数据块信息。
最后,通过 decide_file_actions() 函数决定每个文件对应的操作类型,包含如下操作:
FILE_ACTION_UNDECIDED- 尚未确定FILE_ACTION_CREATE- 创建本地目录或符号链接FILE_ACTION_COPY- 拷贝整个文件,如果存在则覆盖原文件FILE_ACTION_COPY_TAIL- 负责文件末位部分FILE_ACTION_NONE- 不采取任何操作(可能仍会根据解析的 WAL 复制修改过的块)FILE_ACTION_TRUNCATE- 截断本地文件到指定大小FILE_ACTION_REMOVE- 删除本地文件/目录/链接
执行阶段
现在我们已经从 source 和 target 收集了我们需要的所有信息,并且我们准备开始修改 target 数据目录。
最后的执行阶段是通过 perform_rewind() 函数完成的。
- 遍历
filemap->nentries数组,并从source数据目录中获取数据。 - 如果该文件在
target数据目录中的 WAL 日志中有修改数据块,通过libpq_queue_fetch_range()或local_queue_fetch_range()获取相应的数据块。 - 根据
entry->action执行相应的动作,如截断文件truncate_target_file(),删除文件remove_target()等。 - 结束遍历,调用
local_finish_fetch()或libpq_finish_fetch()获取队列中的所有数据获取。 - 从
source中获取pg_control文件,并更新。
问题
从上面的实现可以得知,PostgreSQL 在 rewind 期间除了数据文件和忽略的文件外,都会完全的拷贝到从节点,因此,如果采用默认的 log_directory (即 $PGDATA/log),那么在 rewind 期间也会将这些日志同步到从节点。为了避免这个问题,我们可以将其从 $PGDATA 目录中移除。
此外,在 rewind 期间,PostgreSQL 同样会将 $PGDATA/pg_wal 下面的内容拷贝到从节点,但实际情况下,我们只需要拷贝发生分叉之后的日志即可。
参考
[1] https://thebuild.com/blog/2018/07/18/pg_rewind-and-checkpoints-caution/
[2] https://www.postgresql.org/message-id/9f568c97-87fe-a716-bd39-65299b8a60f4%40iki.fi
[3] https://www.postgresql.org/message-id/181b4c6fa9c.b8b725681941212.7547232617810891479%40viggy28.dev