背景介紹

PolarDB採用了共享儲存一寫多讀架構,讀寫節點RW和多個只讀節點RO共享同一份儲存,讀寫節點可以讀寫共享儲存中的資料;只讀節點僅能各自通過回放日誌,從共享儲存中讀取資料,而不能寫入,只讀節點RO通過記憶體同步來維護資料的一致性。此外,只讀節點可同時對外提供服務用於實現讀寫分離與負載均衡,在讀寫節點異常crash時,可將只讀節點提升為讀寫節點,保證叢集的高可用。基本架構圖如下所示:

傳統share nothing的架構下,只讀節點RO有自己的記憶體及儲存,只需要接收RW節點的WAL日誌進行回放即可。如下圖所示,如果需要回放的資料頁不在Buffer Pool中,需將其從儲存檔案中讀至Buffer Pool中進行回放,從而帶來CacheMiss的成本,且持續性的回放會帶來較頻繁的Buffer Pool淘汰問題。

此外,RW節點多個事務之間可並行執行,RO節點則需依照WAL日誌的順序依次進行序列回放,導致RO回放速度較慢,與RW節點的延遲逐步增大。

與傳統share nothing架構不同,共享儲存一寫多讀架構下RO節點可直接從共享儲存上獲取需要回放的WAL日誌。若共享儲存上的資料頁是最新的,那麼RO可直接讀取資料頁而不需要再進行回放操作。基於此,PolarDB設計了LogIndex來加速RO節點的日誌回放。

RO記憶體同步架構

LogIndex中儲存了資料頁與修改該資料頁的所有LSN的對映關係,基於LogIndex可快速獲取到修改某個資料頁的所有LSN,從而可將該資料頁對應日誌的回放操作延遲到真正訪問該資料頁的時刻進行。LogIndex機制下RO記憶體同步的架構如下圖所示。

RW/RO的相關流程相較傳統share nothing架構下有如下區別:

  • 讀寫節點RW與只讀節點RO之間不再傳輸完整的WAL日誌,僅傳輸WAL Meta,減少網路資料傳輸量,降低了RO與RW節點的延遲;
  • 讀寫節點RW依據WAL meta生成LogIndex寫入LogIndex Memory Table中,LogIndex Memory Table寫滿之後落盤,儲存至共享儲存的LogIndex Table中,已落盤的LogIndex Memory Table可以被複用;
  • 讀寫節點RW通過LogIndex Meta檔案保證LogIndex Memory Table IO操作的原子性,LogIndex Memory Table落盤後會更新LogIndex Meta檔案,落盤的同時還會生成Bloom Data,通過Bloom Data可快速檢索特定Page是否存在於某LogIndex Table中,從而忽略不必掃描的LogIndex Table提升效率;
  • 只讀節點RO接收RW所傳送的WAL Meta,並基於WAL Meta在記憶體中生成相應的LogIndex,同樣寫入其記憶體的LogIndex Memory Table中,同時將WAL Meta對應已存在於Buffer Pool中的頁面標記為Outdate,該階段RO節點並不進行真正的日誌回放,無資料IO操作,可去除cache miss的成本;
  • 只讀節點RO基於WAL Meta生成LogIndex後即可推進回放位點,日誌回放操作被交由背景程序及真正訪問該頁面的backend程序執行,由此RO節點也可實現日誌的並行回放;
  • 只讀節點RO生成的LogIndex Memory Table不會落盤,其基於LogIndex Meta檔案判斷已滿的LogIndex Memory Table是否在RW節點已落盤,已落盤的LogIndex Memory Table可被複用,當RW節點判斷儲存上的LogIndex Table不再使用時可將相應的LogIndex Table Truncate。

PolarDB通過僅傳輸WAL Meta降低RW與RO之間的延遲,通過LogIndex實現WAL日誌的延遲迴放+並行回放以加速RO的回放速度,以下則對這兩點進行詳細介紹。

WAL Meta

WAL日誌又稱為XLOG Record,如下圖,每個XLOG Record由兩部分組成:

  • 通用的首部部分general header portion:該部分即為XLogRecord結構體,固定長度。主要用於存放該條XLOG Record的通用資訊,如XLOG Record的長度、生成該條XLOG Record的事務ID、該條XLOG Record對應的資源管理器型別等;
  • 資料部分data portion:該部分又可以劃分為首部和資料兩個部分,其中首部部分header part包含0~N個XLogRecordBlockHeader結構體及0~1個XLogRecordDataHeader[Short|Long]結構體。資料部分data part則包含block data及main data。每一個XLogRecordBlockHeader對應資料部分的一個Block data,XLogRecordDataHeader[Short|Long]則與資料部分的main data對應。

共享儲存模式下,讀寫節點RW與只讀節點RO之間無需傳輸完整的WAL日誌,僅傳輸WAL Meta資料,WAL Meta即為上圖中的general header portion + header part + main data,RO節點可基於WAL Meta從共享儲存上讀取完整的WAL日誌內容。該機制下,RW與RO之間傳輸WAL Meta的流程如下:

  1. 當RW節點中的事務對其資料進行修改時,會生成對應的WAL日誌並將其寫入WAL Buffer,同時拷貝對應的WAL meta資料至記憶體中的WAL Meta queue中;
  2. 同步流複製模式下,事務提交時會先將WAL Buffer中對應的WAL日誌flush到磁碟,此後會喚醒WalSender程序;
  3. WalSender程序發現有新的日誌可以傳送,則從WAL Meta queue中讀取對應的WAL Meta,通過已建立的流複製連線傳送到對端的RO;
  4. RO的WalReceiver程序接收到新的日誌資料之後,將其push到記憶體的WAL Meta queue中,同時通知Startup程序有新的日誌到達;
  5. Startup從WAL Meta queue中讀取對應的meta資料,解析生成對應的LogIndex memtable即可。

RW與RO節點的流複製不傳輸具體的payload資料,減少了網路資料傳輸量;此外,RW節點的WalSender程序從記憶體中的WAL Meta queue中獲取WAL Meta資訊,RO節點的WalReceiver程序接收到WAL Meta後也同樣將其儲存至記憶體的WAL Meta queue中,相較於傳統主備模式減少了日誌傳送及接收的磁碟IO過程,從而提升傳輸速度,降低RW與RO之間的延遲。

LogIndex

記憶體資料結構

LogIndex實質為一個HashTable結構,其key為PageTag,可標識一個具體資料頁,其value即為修改該page的所有LSN。LogIndex的記憶體資料結構如下圖所示,除了Memtable ID、Memtable儲存的最大LSN、最小LSN等資訊,LogIndex Memtable中還包含了三個陣列,分別為:

  • HashTable:HashTable陣列記錄了某個Page與修改該Page的LSN List的對映關係,HashTable陣列的每一個成員指向Segment陣列中一個具體的LogIndex Item;
  • Segment:Segment陣列中的每個成員為一個LogIndex Item,LogIndex Item有兩種結構,即下圖中的Item Head和Item Seg,Item Head為某個Page對應的LSN連結串列的頭部,Item Seg則為該LSN連結串列的後續節點。Item Head中的Page TAG用於記錄單個Page的元資訊,其Next Seg和Tail Seg則分別指向後續節點和尾節點,Item Seg儲存著指向上一節點Prev Seg和後續節點Next Seg的指標。Item Head和Item Seg中儲存的Suffix LSN與LogIndex Memtable中儲存的Prefix LSN可構成一個完整的LSN,避免了重複儲存Prefix LSN帶來的空間浪費。當不同Page TAG計算到HashTable的同一位置時,通過Item Head中的Next Item指向下一個具有相同hash值的Page,以此解決雜湊衝突;
  • Index Order:Index Order陣列記錄了LogIndex新增到LogIndex Memtable的順序,該陣列中的每個成員佔據2個位元組。每個成員的後12bit對應Segment陣列的一個下標,指向一個具體的LogIndex Item,前4bit則對應LogIndex Item中Suffix LSN陣列的一個下標,指向一個具體的Suffix LSN,通過Index Order可方便地獲取插入到該LogIndex Memtable的所有LSN及某個LSN與其對應修改的全部Page的對映關係。

記憶體中儲存的LogIndex Memtable又可分為Active LogIndex Memtable和Inactive LogIndex Memtable。如下圖所示,基於WAL Meta資料生成的LogIndex記錄會寫入Active LogIndex Memtable,Active LogIndex Memtable寫滿後會轉為Inactive LogIndex Memtable,並重新申請一個新的Active LogIndex Memtable,Inactive LogIndex Memtable可直接落盤,落盤後的Inactive LogIndex Memtable可再次轉為Active LogIndex Memtable。

磁碟資料結構

磁碟上儲存了若干個LogIndex Table,LogIndex Table與LogIndex Memtable結構類似,一個LogIndex Table可包含64個LogIndex Memtable,Inactive LogIndex Memtable落盤的同時會生成其對應的Bloom Filter。如下圖所示,單個Bloom Filter的大小為4096位元組,Bloom Filter記錄了該Inactive LogIndex Memtable的相關資訊,如儲存的最小LSN、最大LSN、該Memtable中所有Page在bloom filter bit array中的對映值等。通過Bloom Filter可快速判斷某個Page是否存在於對應的LogIndex Table中,從而可忽略無需掃描的LogIndex Table以加速檢索。

當Inactive LogIndex MemTable成功落盤後,LogIndex Meta檔案也被更新,該檔案可保證LogIndex Memtable檔案IO操作的原子性。如下,LogIndex Meta檔案儲存了當前磁碟上最小LogIndex Table及最大LogIndex Memtable的相關資訊,其Start LSN記錄了當前已落盤的所有LogIndex MemTable中最大的LSN。若Flush LogIndex MemTable時發生部分寫,系統會從LogIndex Meta記錄的Start LSN開始解析日誌,如此部分寫捨棄的LogIndex記錄也會重新生成,保證了其IO操作的原子性。

Buffer管理可知,一致性位點之前的所有WAL日誌修改的資料頁均已持久化到共享儲存中,RO節點無需回放該位點之前的WAL日誌,故LogIndex Table中小於一致性位點的LSN均可清除。RW據此Truncate掉儲存上不再使用的LogIndex Table,在加速RO回放效率的同時還可減少LogIndex Table佔用的空間。

日誌回放

延遲迴放

LogIndex機制下,RO節點的Startup程序基於接收到的WAL Meta生成LogIndex,同時將該WAL Meta對應的已存在於Buffer Pool中的頁面標記為Outdate後即可推進回放位點,Startup程序本身並不對日誌進行回放,日誌的回放操作交由背景回放程序及真正訪問該頁面的Backend程序進行,回放過程如下圖所示,其中:

  • 背景回放程序按照WAL順序依次進行日誌回放操作,根據要回放的LSN檢索LogIndex Memtable及LogIndex Table,獲取該LSN修改的Page List,若某個Page存在於Buffer Pool中則對其進行回放,否則直接跳過。背景回放程序按照LSN的順序逐步推進Buffer Pool中的頁面位點,避免單個Page需要回放的LSN數量堆積太多;
  • Backend程序則僅對其實際需要訪問的Page進行回放,當Backend程序需要訪問一個Page時,如果該Page在Buffer Pool中不存在,則將該Page讀到Buffer Pool後進行回放;如果該Page已經在Buffer Pool中且標記為outdate,則將該Page回放到最新。Backend程序依據Page TAG對LogIndex Memtable及LogIndex Table進行檢索,按序生成與該Page相關的LSN List,基於LSN List從共享儲存中讀取完整的WAL日誌來對該Page進行回放。

為降低迴放時讀取磁碟WAL日誌帶來的效能損耗,同時添加了XLOG Buffer用於快取讀取的WAL日誌。如下圖所示,原始方式下直接從磁碟上的WAL Segment File中讀取WAL日誌,新增XLog Page Buffer後,會先從XLog Buffer中讀取,若所需WAL日誌不在XLog Buffer中,則從磁碟上讀取對應的WAL Page到Buffer中,然後再將其拷貝至XLogReaderState的readBuf中;若已在Buffer中,則直接將其拷貝至XLogReaderState的readBuf中,以此減少回放WAL日誌時的IO次數,從而進一步加速日誌回放的速度。

Mini Transaction

與傳統share nothing架構下的日誌回放不同,LogIndex機制下,Startup程序解析WAL Meta生成LogIndex與Backend程序基於LogIndex對Page進行回放的操作是並行的,且各個Backend程序僅對其需要訪問的Page進行回放。由於一條XLog Record可能會對多個Page進行修改,以索引分裂為例,其涉及對Page_0, Page_1的修改,且其對Page_0及Page_1的修改為一個原子操作,即修改要麼全部可見,要麼全部不可見。針對此,設計了mini transaction鎖機制以保證Backend程序回放過程中記憶體資料結構的一致性。
如下圖所示,無mini transaction lock時,Startup程序對WAL Meta進行解析並按序將當前LSN插入到各個Page對應的LSN List中。若Startup程序完成對Page_0 LSN List的更新,但尚未完成對Page_1 LSN List的更新時,Backend_0和Backend_1分別對Page_0及Page_1進行訪問,Backend_0和Backend_1分別基於Page對應的LSN List進行回放操作,Page_0被回放至LSN_N+1處,Page_1被回放至LSN_N處,可見此時Buffer Pool中兩個Page對應的版本並不一致,從而導致相應記憶體資料結構的不一致。

mini transaction鎖機制下,對Page_0及Page_1 LSN List的更新被視為一個mini transaction。Startup程序更新Page對應的LSN List時,需先獲取該Page的mini transaction lock,如下先獲取Page_0對應的mtr lock,獲取Page mtr lock的順序與回放時的順序保持一致,更新完Page_0及Page_1 LSN List後再釋放Page_0對應的mtr lock。Backend程序基於LogIndex對特定Page進行回放時,若該Page對應在Startup程序仍處於一個mini transaction中,則同樣需先獲取該Page對應的mtr lock後再進行回放操作。故若Startup程序完成對Page_0 LSN List的更新,但尚未完成對Page_1 LSN List的更新時,Backend_0和Backend_1分別對Page_0及Page_1進行訪問,此時Backend_0需等待LSN List更新完畢並釋放Page_0 mtr lock之後才可進行回放操作,而釋放Page_0 mtr lock時Page_1的LSN List已完成更新,從而實現了記憶體資料結構的原子修改。

總結

PolarDB基於RW節點與RO節點共享儲存這一特性,設計了LogIndex機制來加速RO節點的記憶體同步,降低RO節點與RW節點之間的延遲,確保了RO節點的一致性與可用性。本文對LogIndex的設計背景、基於LogIndex的RO記憶體同步架構及具體細節進行了分析。除了實現RO節點的記憶體同步,基於LogIndex機制還可實現RO節點的Online Promote,可加速RW節點異常崩潰時,RO節點提升為RW節點的速度,從而構建計算節點的高可用,實現服務的快速恢復。