WAL日志并行回放

本文介绍了PolarDB PostgreSQL版的WAL日志并行回放功能。

前提条件

支持的PolarDB PostgreSQL版的版本如下:

  • PostgreSQL 14(内核小版本14.5.1.0及以上)

  • PostgreSQL 11(内核小版本1.1.17及以上)

说明

您可通过如下语句查看PolarDB PostgreSQL版的内核小版本的版本号:

  • PostgreSQL 14

    select version();
  • PostgreSQL 11

    show polar_version;

背景信息

PolarDB PostgreSQL版的一写多读架构下,只读节点(Replica 节点)运行过程中,LogIndex后台回放进程(LogIndex Background Worker)和会话进程(Backend)分别使用LogIndex数据在不同的Buffer上回放WAL日志,本质上达到了一种并行回放WAL日志的效果。

鉴于WAL日志回放在PolarDB集群的高可用中起到至关重要的作用,将并行回放WAL日志的方法用到常规的日志回放路径上,是一种很好的优化思路。

并行回放WAL日志至少可以在以下三个场景下发挥优势:

  • 主库节点、只读节点以及备库节点崩溃恢复(Crash Recovery)的过程。

  • 只读节点LogIndex BGW进程持续回放WAL日志的过程。

  • 备库节点Startup进程持续回放WAL日志的过程。

术语

  • Block:数据块。

  • WAL:Write-Ahead Logging,预写日志。

  • Task Node:并行执行框架中的子任务执行节点,可以接收并执行一个子任务。

  • Task Tag:子任务的分类标识,同一类的子任务执行顺序有先后关系。

  • Hold List:并行执行框架中,每个子进程调度执行回放子任务所使用的链表。

原理介绍

  • 概述

    一条WAL日志可能修改多个数据块Block,因此可以使用如下定义来表示WAL日志的回放过程:

    1. 假设第i条WAL日志LSN为LSNi,其修改了m个数据块,则定义第i条WAL日志修改的数据块列表Blocki​=[Blocki,0​,Blocki,1​,...,Blocki,m​]

    2. 定义最小的回放子任务为Taski,j​=LSNi​−>Blocki,j,表示在数据块Blocki,j上回放第i条WAL日志。

    3. 因此,一条修改了m个Block的WAL日志就可以表示成m个回放子任务的集合:TASKi,∗​=[Taski,0​,Taski,1​,...,Taski,m​]

    4. 进而,多条WAL日志就可以表示成一系列回放子任务的集合:TASK∗,∗​=[Task0,∗​,Task1,∗​,...,TaskN,∗​]

    在日志回放子任务集合Task∗,∗中,每个子任务的执行,有时并不依赖于前序子任务的执行结果。

    假设回放子任务集合如下:TASK∗,∗​=[Task0,∗​,Task1,∗​,Task2,∗​],其中:

    • Task0,∗​=[Task0,0​,Task0,1​,Task0,2​]

    • Task1,∗​=[Task1,0​,Task1,1​]

    • Task2,∗​=[Task2,0​]

    并且,Block0,0​=Block1,0​,Block0,1​=Block1,1​,Block0,2​=Block2,0​

    则可以并行回放的子任务集合有三个:[Task0,0​,Task1,0​]、[Task0,1​,Task1,1​]、[Task0,2​,Task2,0​]。

    综上所述,在整个WAL日志所表示的回放子任务集合中,存在很多子任务序列可以并行执行,而且不会影响最终回放结果的一致性。PolarDB借助这种思想,提出了一种并行任务执行框架,并成功运用到了WAL日志回放的过程中。

  • 并行任务执行框架

    将一段共享内存根据并发进程数目进行等分,每一段作为一个环形队列,分配给一个进程。通过配置参数设定每个环形队列的深度:共享内存分配

    • Dispatcher进程。

      • 通过将任务分发给指定的进程来控制并发调度。

      • 负责将进程执行完的任务从队列中删除。

    • 进程组。

      组内每一个进程从相应的环形队列中获取需要执行的任务,根据任务的状态决定是否执行。进程组

    • 任务

      环形队列的内容由Task Node组成,每个Task Node包含五个状态:Idle、Running、Hold、Finished、Removed。

      • Idle:表示该Task Node未分配任务。

      • Running:表示该Task Node已经分配任务,正在等待进程执行,或已经在执行。

      • Hold:表示该Task Node有前向依赖的任务,需要等待依赖的任务执行完再执行。

      • Finished:表示进程组中的进程已经执行完该任务。

      • Removed:当Dispatcher进程发现一个任务的状态已经为Finished,那么该任务所有的前置依赖任务也都应该为Finished状态,Removed状态表示Dispatcher进程已经将该任务以及该任务所有前置任务都从管理结构体中删除;可以通过该机制保证Dispatcher进程按顺序处理有依赖关系的任务执行结果。

      任务上述状态机的状态转移过程中,黑色线标识的状态转移过程在Dispatcher进程中完成,橙色线标识的状态转移过程在并行回放进程组中完成。

    • Dispatcher进程

      Dispatcher进程有三个关键数据结构:Task HashMap、Task Running Queue以及Task Idle Nodes。

      • Task HashMap负责记录Task Tag和相应的执行任务列表的hash映射关系。

        • 每个任务有一个指定的Task Tag,如果两个任务间存在依赖关系,则它们的Task Tag相同。

        • 在分发任务时,如果一个Task Node存在前置依赖任务,则状态标识为Hold,需等待前置任务先执行。

      • Task Running Queue负责记录当前正在执行的任务。

      • Task Idel Nodes负责记录进程组中不同进程,当前处于Idle状态的Task Node。

      Dispatcher调度策略如下:

      • 如果要执行的Task Node有相同Task Tag的任务在执行,则优先将该Task Node分配到该Task Tag链表最后一个Task Node所在的执行进程。目的是让有依赖关系的任务尽量被同一个进程执行,减少进程间同步的开销。

      • 如果期望优先分配的进程队列已满,或者没有相同的Task Tag在执行,则在进程组中按顺序选择一个进程,从中获取状态为Idle的Task Node来调度任务执行。目的是让任务尽量平均分配到不同的进程进行执行。

      调度策略

    • 进程组

      该并行执行针对的是相同类型的任务,它们具有相同的Task Node数据结构。在进程组初始化时配置SchedContext,指定负责执行具体任务的函数指针:

      • TaskStartup:表示进程执行任务前需要进行的初始化动作。

      • TaskHandler:根据传入的Task Node,负责执行具体的任务。

      • TaskCleanup:表示执行进程退出前需要执行的回收动作。

      进程组1

      进程组中的进程从环形队列中获取一个Task Node,如果Task Node当前的状态是Hold,则将该Task Node插入到Hold List的尾部。如果Task Node的状态为Running,则调用TaskHandler执行;如果TaskHandler执行失败,则设置该Task Node重新执行需要等待调用的次数,默认为3,将该Task Node插入到Hold List的头部。进程组2

      进程优先从Hold List头部搜索,获取可执行的Task。如果Task状态为Running,且等待调用次数为0,则执行该Task;如果Task状态为Running,但等待调用次数大于0,则将等待调用次数减去1。进程组3

  • WAL日志并行回放

    LogIndex数据中记录了WAL日志和其修改的数据块之间的对应关系,而且LogIndex数据支持使用LSN进行检索。因此,PolarDB数据库在Standby节点持续回放WAL日志过程中,引入了上述并行任务执行框架,并结合LogIndex数据将WAL日志的回放任务并行化,提高了Standby节点数据同步的速度。

    工作流程

    • Startup进程:解析WAL日志后,仅构建LogIndex数据而不真正回放WAL日志。

    • LogIndex BGW后台回放进程:成为上述并行任务执行框架的Dispatcher进程,利用LSN来检索LogIndex数据,构建日志回放的子任务,并分配给并行回放进程组。

    • 并行回放进程组内的进程:执行日志回放子任务,对数据块执行单个日志的回放操作。

    • Backend进程:主动读取数据块时,根据PageTag来检索LogIndex数据,获得修改该数据块的LSN日志链表,对数据块执行完整日志链的回放操作。工作流程

    • Dispatcher进程利用LSN来检索LogIndex数据,按照LogIndex插入顺序枚举PageTag和对应LSN,构建{LSN -> PageTag},组成相应的Task Node。

    • PageTag作为Task Node的Task Tag。

    • 将枚举组成的Task Node分发给并行执行框架中进程组的子进程进行回放。工作流程

使用指南

在Standby节点的postgresql.conf文件中添加以下参数,开启WAL日志并行回放功能。

polar_enable_parallel_replay_standby_mode = ON