pg_rewind实现原理简单分析

pg_rewind的功能是在主备切换后回退旧主库上多余的事务变更,以便可以作为新主的备机和新主建立复制关系。通过pg_rewind可以在故障切换后快速恢复旧主,避免整库重建。对于大库,整库重建会很耗时间。

如何识别旧主上多余的变更?

这就用到了PostgreSQL独有的时间线技术,数据库实例的初始时间线是1。以后每次主备切换时,需要提升备库为新主。提升操作会将新主的时间线加1,并且会记录提升时间线的WAL位置(LSN)。这个LSN位点我们称其为新主旧主在时间线上的分叉点。

我们只要扫描旧主上在分叉点之后的WAL记录,就能找到旧主上所有多余的变更。

如何回退旧主上多余的变更?

可能有人会想到可以通过解析分叉点以后的WAL,生成undo SQL,再逆序执行undo SQL实现事务回退。这也是mysql上常用的实现方式。 PG同样也有walminer插件可以支持WAL到undo SQL的解析。但是,这种方式存在很多限制,数据一致性也难以保证。

pg_rewind使用的是不同的方式,过程概述如下

  1. 解析旧主上分叉点后的WAL,记录这些事务修改了哪些数据块
  2. 对数据文件以外的文件,直接从新主上拉取后覆盖旧主上的文件
  3. 对于数据文件,只从新主拉取被旧主修改了的数据块,并覆盖旧主数据文件中对应的数据块
  4. 从新主上拉取最新的WAL,覆盖旧主的WAL
  5. 把旧主改成恢复模式,恢复的起点则设置为分叉点前的最近一次checkpoint
  6. 启动旧主,旧主进入宕机恢复过程,旧主应用完从新主拷贝来的所有WAL后,数据就和新主一致了。

如何保证主备一致?

分叉点之后,新主和旧主上可能有各种各样的变更。除了常规的数据的增删改,还有truncate,表结构变更,表的DROP和CREATE,数据库参数配置变更等等。如何保证pg_rewind之后这些都能一致呢?

下面我们重点看一下pg_rewind对数据文件的拷贝处理。

  1. 通过比较target和source节点的数据目录,构建filemap

    filemap中对每个文件记录了不同的处理方式(即action),对于数据文件,如下

    • 仅存在于新主:FILE_ACTION_COPY
    • 仅存在于旧主:FILE_ACTION_REMOVE
    • 新主文件size>旧主:FILE_ACTION_COPY_TAIL
    • 新主文件size<旧主:FILE_ACTION_TRUNCATE
    • 新主文件size=旧主:FILE_ACTION_NONE
  2. 读取旧主本地WAL,获取并记录影响的数据块到filemap中对应的file_entry中的pagemap

    pagemap属于块级别的拷贝。为了避免文件级别的拷贝做重复的事情,提取影响的块号是做了一些过滤,具体如下:

    • FILE_ACTION_NONE:只记录小于等于新主size的块
    • FILE_ACTION_TRUNCATE:只记录小于等于新主size的块
    • FILE_ACTION_COPY_TAIL:只记录小于等于旧主size的块
    • 其他:不记录
  3. 遍历filemap,对其中每个file_entry,从新主拷贝必要的数据

    • 从新主拷贝pagemap中记录的块覆盖旧主
    • 根据action,执行不同的文件拷贝操作
      • FILE_ACTION_NONE:无需处理
      • FILE_ACTION_COPY:从新主拷贝数据,只拷贝到生成action时看到的新主上的size
      • FILE_ACTION_TRUNCATE:旧主truncate到新主的size
      • FILE_ACTION_COPY_TAIL:从新主拷贝尾部数据,即新主size超出旧主的部分
      • FILE_ACTION_CREATE:创建目录
      • FILE_ACTION_REMOVE:删除文件(或目录)

上面的过程汇总后如下:

  • 仅存在于新主(FILE_ACTION_COPY)
    • 从新主拷贝数据,只拷贝到生成action时看到的新主上的size
  • 仅存在于旧主(FILE_ACTION_REMOVE)
    • 删除文件
  • 新主文件 size > 旧主(FILE_ACTION_COPY_TAIL)
    • 对偏移位置小于等于旧主文件 size 的块,从新主拷贝受旧主分叉后更新影响的块
    • 偏移位置为旧主文件 size ~ 新主文件 size 之间的块,从新主拷贝
  • 新主文件 size < 旧主(FILE_ACTION_TRUNCATE)
    • 对偏移位置小于等于新主文件 size 的块,从新主拷贝受旧主分叉后更新影响的块
    • 对偏移位置大于新主文件 size 的块,truncate 掉
  • 新主文件 size = 旧主(FILE_ACTION_NONE)
    • 对偏移位置小于等于新主文件 size 的块,从新主拷贝受旧主分叉后更新影响的块

针对上面的流程,现在回答几个关键的问题

pg_rewind拷贝数据时,新主还处于活动中,拷贝的这些数据块不在同一个事务一致点上,如何将不一致的数据状态变成一致的?

这里用到的技术,就是数据块最擅长的宕机恢复的技术。通过启动旧主后,回放WAL使数据库达到一致的状态。

我们以一个微观的数据块为例进行说明。

具体到某一个数据块,只有三种情况,我们分别讨论

  1. 需要从新主拷贝

    • 如果拷贝时发现新主上这个块所在的文件被删掉了,那么也会删掉旧主上的文件。

    • 如果拷贝时新主上这个块被 truncate 掉了,会忽略这个块的拷贝。

    • 如果拷贝时这个块正在被修改,可能导致pg_rewind读到了一个不一致的块。一半是修改前的,另一半是修改后的。

      这并没有关系,因为如果这个块被变更了,变更这个块的事务已经记录到WAL了,回放这个WAL时可以修复数据到一致状态。pg_rewind运行的前提条件时数据库必须开启 full_page_write,开启full_page_write后WAL中会记录每个checkpoint后第一次修改的page的完整镜像,宕机恢复时,使用这个镜像,就可以修复数据文件中损坏的数据块。

  2. 保留旧主

    • 如果后来新主上这个块变更了,回放WAL时自然可以追到一致的状态
  3. 需要从旧主删除

    • 如果后来新主上有新增了这个数据块,同样,回放WAL时自然可以追到一致的状态

如果一个表先被删了,之后又创建一个表结构不一样的同名的表,pg_rewind处理这个表时会不会有问题?

PostgreSQL中数据文件名是filenode,初始时它等于表的oid,当表被重写(比如执行truncate或vacuum full)后,会赋值为下一个oid。因此先后创建的同名表,它们对应的文件名是不一样的(MySQL采用表名作为数据文件名。虽然比较直观,但会有很多潜在的问题,比如特殊字符,PostgreSQL的filenode的方式要严谨得多)。

PostgreSQL回放WAL时如何保证可正常执行?

具体要回答几个问题

  1. 宕机恢复阶段,回放建表的WAL时,如果对应的文件已存在,结果如何?
  2. 宕机恢复阶段,回放删表的WAL时,如果对应的文件不存在,结果如何?
  3. 宕机恢复阶段,回放extend数据文件的WAL时,如果对应的块已存在,结果如何?
  4. 宕机恢复阶段,回放write数据文件的WAL时,如果对应的块或者文件不存在,结果如何?
  5. 宕机恢复阶段,回放truncate数据文件的WAL时,如果对应的块或者文件不存在,结果如何?

存储层的一些接口,已经考虑到REDO的使用场景,做了一些容错,支持幂等性。

  1. REDO中创建文件时,容忍文件已存在

    复制代码
    void
    mdcreate(SMgrRelation reln, ForkNumber forkNum, bool isRedo)
    {
    ...
        fd = PathNameOpenFile(path, O_RDWR | O_CREAT | O_EXCL | PG_BINARY);
    
        if (fd < 0)
        {
            int			save_errno = errno;
    
            if (isRedo)
                fd = PathNameOpenFile(path, O_RDWR | PG_BINARY);
    ...
  2. 删除文件时,容忍文件不存在

    复制代码
    static void
    mdunlinkfork(RelFileNodeBackend rnode, ForkNumber forkNum, bool isRedo)
    {
    ...
                ret = unlink(pcfile_path);
                if (ret < 0 && errno != ENOENT)
                    ereport(WARNING,
                            (errcode_for_file_access(),
                            errmsg("could not remove file \"%s\": %m", pcfile_path)));
  3. REDO中打开文件时,都会带上 O_CREAT flag,文件不存在时会创建一个空的

    复制代码
    static MdfdVec *
    _mdfd_getseg(SMgrRelation reln, ForkNumber forknum, BlockNumber blkno,
                 bool skipFsync, int behavior)
    {
            if ((behavior & EXTENSION_CREATE) ||
                (InRecovery && (behavior & EXTENSION_CREATE_RECOVERY)))
            {
            ...
                flags = O_CREAT;
  4. 读或写数据块时,可以 seek 到超出文件大小的位置

    复制代码
    void
    mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
            char *buffer, bool skipFsync)
    {
    ...
        v = _mdfd_getseg(reln, forknum, blocknum, skipFsync,
                         EXTENSION_FAIL | EXTENSION_CREATE_RECOVERY);
    ...
        nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, seekpos, WAIT_EVENT_DATA_FILE_WRITE);
  5. REDO中 truncate 时,如果文件大小已小于 truncate 目标,无视

    复制代码
    void
    mdtruncate(SMgrRelation reln, ForkNumber forknum, BlockNumber nblocks)
    {
    ...
    	curnblk = mdnblocks(reln, forknum);
    	if (nblocks > curnblk)
    	{
    		/* Bogus request ... but no complaint if InRecovery */
    		if (InRecovery)
    			return;
  6. 回放truncate记录时,先强制执行一次创建关系的操作

    复制代码
    void
    smgr_redo(XLogReaderState *record)
    {
    ...
    	else if (info == XLOG_SMGR_TRUNCATE)
    	{
    	
    ...
    		/*
    		 * Forcibly create relation if it doesn't exist (which suggests that
    		 * it was dropped somewhere later in the WAL sequence).  As in
    		 * XLogReadBufferForRedo, we prefer to recreate the rel and replay the
    		 * log as best we can until the drop is seen.
    		 */
    		smgrcreate(reln, MAIN_FORKNUM, true);
    ...
    			smgrtruncate(reln, forks, nforks, blocks);
相关推荐
编程爱好者熊浪1 小时前
两次连接池泄露的BUG
java·数据库
TDengine (老段)3 小时前
TDengine 字符串函数 CHAR 用户手册
java·大数据·数据库·物联网·时序数据库·tdengine·涛思数据
qq7422349843 小时前
Python操作数据库之pyodbc
开发语言·数据库·python
姚远Oracle ACE3 小时前
Oracle 如何计算 AWR 报告中的 Sessions 数量
数据库·oracle
Dxy12393102164 小时前
MySQL的SUBSTRING函数详解与应用
数据库·mysql
码力引擎4 小时前
【零基础学MySQL】第十二章:DCL详解
数据库·mysql·1024程序员节
杨云龙UP4 小时前
【MySQL迁移】MySQL数据库迁移实战(利用mysqldump从Windows 5.7迁至Linux 8.0)
linux·运维·数据库·mysql·mssql
l1t4 小时前
利用DeepSeek辅助修改luadbi-duckdb读取DuckDB decimal数据类型
c语言·数据库·单元测试·lua·duckdb
安当加密4 小时前
Nacos配置安全治理:把数据库密码从YAML里请出去
数据库·安全
ColderYY5 小时前
Python连接MySQL数据库
数据库·python·mysql