pg_rewind实现原理简单分析

本文涉及的产品
云原生数据库 PolarDB MySQL 版,通用型 2核4GB 50GB
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
简介: 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
  1. 读取旧主本地WAL,获取并记录影响的数据块到filemap中对应的file_entry中的pagemap

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

  • FILE_ACTION_NONE:只记录小于等于新主size的块
  • FILE_ACTION_TRUNCATE:只记录小于等于新主size的块
  • FILE_ACTION_COPY_TAIL:只记录小于等于旧主size的块
  • 其他:不记录
  1. 遍历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);
相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
相关文章
|
5月前
|
存储 关系型数据库 MySQL
MySQL Change Buffer 深入解析:概念、原理及使用
MySQL Change Buffer 深入解析:概念、原理及使用
MySQL Change Buffer 深入解析:概念、原理及使用
|
5月前
|
SQL JSON 数据库
实时计算 Flink版操作报错合集之写入Hudi时,遇到从 COW(Copy-On-Write)表类型转换为 MOR(Merge-On-Read)表类型时报字段错误,该怎么办
在使用实时计算Flink版过程中,可能会遇到各种错误,了解这些错误的原因及解决方法对于高效排错至关重要。针对具体问题,查看Flink的日志是关键,它们通常会提供更详细的错误信息和堆栈跟踪,有助于定位问题。此外,Flink社区文档和官方论坛也是寻求帮助的好去处。以下是一些常见的操作报错及其可能的原因与解决策略。
|
5月前
|
关系型数据库 MySQL 分布式数据库
PolarDB操作报错合集之遇到报错“Reading record is now unsafe on slave” ,该如何解决
在使用阿里云的PolarDB(包括PolarDB-X)时,用户可能会遇到各种操作报错。下面汇总了一些常见的报错情况及其可能的原因和解决办法:1.安装PolarDB-X报错、2.PolarDB安装后无法连接、3.PolarDB-X 使用rpm安装启动卡顿、4.PolarDB执行UPDATE/INSERT报错、5.DDL操作提示“Lock conflict”、6.数据集成时联通PolarDB报错、7.编译DN报错(RockyLinux)、8.CheckStorage报错(源数据库实例被删除)、9.嵌套事务错误(TDDL-4604)。
129 0
|
5月前
|
存储 关系型数据库 MySQL
MySQL Doublewrite Buffer(双写缓冲区)深入解析:原理及作用
MySQL Doublewrite Buffer(双写缓冲区)深入解析:原理及作用
|
关系型数据库 数据库 PostgreSQL
PostgreSQL pg_rewind报错分析
PostgreSQL pg_rewind报错分析
190 0
|
算法 关系型数据库 MySQL
【MySQL】join_buffer_size=2M,是干什么的?底层原理是什么?
【MySQL】join_buffer_size=2M,是干什么的?底层原理是什么?
208 0
|
SQL 存储 缓存
分析MySQL执行的流程(连接、缓存、分析、优化、执行、Undo Log、Binlog、Redo Log)
熟悉MySQL的都知道MySQL服务端实现主要分为Server层和存储引擎层。Server层负责接收和管理客户端连接、管理缓存、解析SQL、优化SQL、调用存储引擎执行SQL;存储引擎层主要负责存储、查询数据。
分析MySQL执行的流程(连接、缓存、分析、优化、执行、Undo Log、Binlog、Redo Log)
|
SQL 存储 JSON
pg流复制详解
pg流复制详解
436 0