本文介紹了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