最新公告
  • 欢迎您光临站壳网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入我们
  • 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);

    猜你喜欢

    1. 本站所有资源来源于用户上传和网络,因此不包含技术服务请大家谅解!如有侵权请邮件联系客服!80027422@qq.com
    2. 本站不保证所提供下载的资源的准确性、安全性和完整性,资源仅供下载学习之用!如有链接无法下载、失效或广告,请联系客服处理,有奖励!
    3. 您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容资源!如用于商业或者非法用途,与本站无关,一切后果请用户自负!
    4. 如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!

    站壳网 » pg_rewind实现原理简单分析» https://www.zhankr.net/327478.html

    发表评论

    • 12796会员总数(位)
    • 96577资源总数(个)
    • 432本周发布(个)
    • 23 今日发布(个)
    • 2130稳定运行(天)

    站壳网ZHANKR.NET国内最专业的站长资源平台

    开通VIP 主题推荐