本文介绍了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日志的回放过程:
假设第
i
条WAL日志LSN为LSNi
,其修改了m
个数据块,则定义第i
条WAL日志修改的数据块列表Blocki=[Blocki,0,Blocki,1,...,Blocki,m]
。定义最小的回放子任务为
Taski,j=LSNi−>Blocki,j
,表示在数据块Blocki,j
上回放第i
条WAL日志。因此,一条修改了
m
个Block的WAL日志就可以表示成m
个回放子任务的集合:TASKi,∗=[Taski,0,Taski,1,...,Taski,m]
。进而,多条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:表示执行进程退出前需要执行的回收动作。
进程组中的进程从环形队列中获取一个Task Node,如果Task Node当前的状态是
Hold
,则将该Task Node插入到Hold List
的尾部。如果Task Node的状态为Running
,则调用TaskHandler
执行;如果TaskHandler
执行失败,则设置该Task Node重新执行需要等待调用的次数,默认为3,将该Task Node插入到Hold List
的头部。进程优先从
Hold List
头部搜索,获取可执行的Task。如果Task状态为Running
,且等待调用次数为0,则执行该Task;如果Task状态为Running
,但等待调用次数大于0,则将等待调用次数减去1。
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