pg_rewind实现原理简单分析

本文涉及的产品
PolarDB Agent Express,2核4GB
云数据库 PolarDB MySQL 版,列存表分析加速 4核8GB
简介: 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数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
相关文章
|
Oracle 关系型数据库 数据库
实战篇:Oracle 数据坏块的 N 种修复方式
实战篇:Oracle 数据坏块的 N 种修复方式
实战篇:Oracle 数据坏块的 N 种修复方式
|
10月前
|
运维 算法 机器人
阿里云AnalyticDB具身智能方案:破解机器人仿真数据、算力与运维之困
本文将介绍阿里云瑶池旗下的云原生数据仓库AnalyticDB MySQL推出的全托管云上仿真解决方案,方案采用云原生架构,为开发者提供从开发环境、仿真计算到数据管理的全链路支持。
|
11月前
|
存储 关系型数据库 分布式数据库
【赵渝强老师】基于PostgreSQL的分布式数据库:Citus
Citus 是基于 PostgreSQL 的开源分布式数据库,采用 shared nothing 架构,具备良好的扩展性。它以插件形式集成,部署简单,适用于处理大规模数据和高并发场景。本文介绍了 Citus 的基础概念、安装配置步骤及其在单机环境下的集群搭建方法。
944 2
|
机器学习/深度学习 人工智能 自然语言处理
AI如何预测体育比赛结果
AI预测体育比赛结果依赖于历史数据、球员表现、球队状态等多因素。通过数据收集与处理、机器学习模型(如回归分析、神经网络)、模拟与蒙特卡洛方法、实时数据分析及自然语言处理等技术,AI能识别影响比赛的关键模式,评估胜负概率,并结合统计学与优化算法不断调整预测,提升准确性。
|
运维 监控 关系型数据库
【一文搞懂PGSQL】7. PostgreSQL + repmgr + witness 高可用架构
该文档介绍了如何构建基于PostgreSQL的高可用架构,利用repmgr进行集群管理和故障转移,并引入witness节点增强网络故障检测能力。repmgr是一款轻量级的开源工具,支持一键部署、自动故障转移及分布式节点管理。文档详细描述了环境搭建步骤,包括配置postgresql参数、安装与配置repmgr、注册集群节点以及配置witness节点等。此外,还提供了故障手动与自动切换的方法及常用命令,确保集群稳定运行。
|
存储 SQL 缓存
PostgreSQL DirectIO开发实践
在数据库开源的背景下,基于PG的DirectIO的研发方案分享。
PostgreSQL DirectIO开发实践
|
关系型数据库 数据库 PostgreSQL
Postgresql为什么需要Freeze?
Freeze PG需要为每个事务分配事务ID,这是MVCC能工作的基础,类似于一个时间戳的概念。 时间戳的做基本动作就是比较大小,即需要得到哪条事务在先,哪条在后。   首先我们来看一下PG中事务ID的比较逻辑,这部分在之前的某一起DB月报中也介绍过。 /* * TransactionIdPrecedes --- is id1 logically < id2? */boo
2304 0
|
缓存 关系型数据库 Nacos
nacos常见问题之服务端不开启鉴权日志一直报403如何解决
Nacos是阿里云开源的服务发现和配置管理平台,用于构建动态微服务应用架构;本汇总针对Nacos在实际应用中用户常遇到的问题进行了归纳和解答,旨在帮助开发者和运维人员高效解决使用Nacos时的各类疑难杂症。
1297 2
|
存储 运维 Oracle
Oracle系列十八:Oracle RAC
Oracle系列十八:Oracle RAC
3024 0
|
SQL Oracle 关系型数据库
PostgreSQL pg_rewind,时间线修复,脑裂修复,flashback - 从库开启读写后,回退为只读从库。异步主从发生角色切换后,主库rewind为新主库的从库
标签 PostgreSQL , pg_rewind , 主从切换 , 时间线修复 , 脑裂修复 , 从库开启读写后,回退为只读从库 , 异步主从发生角色切换后,主库rewind为新主库的从库 背景 1、PG物理流复制的从库,当激活后,可以开启读写,使用pg_rewind可以将从库回退为只读从库的角色。而不需要重建整个从库。 2、当异步主从发生角色切换后,主库的wal目录中可能还有没完全
1518 0