阿里雲PolarDB及其共享儲存PolarFS技術實現分析(下)

網易雲社群發表於2018-10-12


上篇
介紹了PolarDB資料庫及其後端共享儲存PolarFS系統的基本架構和組成模組,是最基礎的部分。本篇重點分析PolarFS的資料IO流程,後設資料更新流程,以及PolarDB資料庫節點如何適配PolarFS這樣的共享儲存系統。

PolarFS的資料IO操作

寫操作

阿里雲PolarDB及其共享儲存PolarFS技術實現分析(下)

一般情況下,寫操作不會涉及到捲上檔案系統的後設資料更新,因為在寫之前就已經通過libpfs的pfs_posix_fallocate()這個API將Block預分配給檔案,這就避免在讀寫IO路徑上出現代價較高的檔案系統後設資料同步過程。上圖是PolarFS的寫操作流程圖,每步操作解釋如下:

  1. POLARDB通過libpfs傳送一個寫請求Request1,經由ring buffer傳送到PolarSwitch;

  2. PolarSwitch根據本地快取的後設資料,將Request1傳送至對應Chunk的Leader節點(ChunkServer1);

  3. Request1到達ChunkServer1後,節點上的RDMA NIC將Request1放到一個預分配好的記憶體buffer中,基於Request1構造一個請求物件,並將該物件加到請求佇列中。一個IO輪詢執行緒不斷輪詢這個請求佇列,一旦發現有新請求則立即開始處理;

  4. IO處理執行緒通過非同步呼叫將Request1通過SPDK寫到Chunk對應的WAL日誌塊上,同時將請求通過RDMA非同步發向給Chunk的Follower節點(ChunkServer2、ChunkServer3)。由於都是非同步呼叫,所以資料傳輸是併發進行的;

  5. 當Request1請求到達ChunkServer2、ChunkServer3後,同樣通過RDMA NIC將其放到預分配好的記憶體buffer並加入到複製佇列中;

  6. Follower節點上的IO輪詢執行緒被觸發,Request1通過SPDK非同步地寫入該節點的Chunk副本對應的WAL日誌塊上;

  7. 當Follower節點的寫請求成功後,會在回撥函式中通過RDMA向Leader節點傳送一個應答響應;

  8. Leader節點收到ChunkServer2、ChunkServer3任一節點成功的應答後,即形成Raft組的majority。主節點通過SPDK將Request1寫到請求中指定的資料塊上;

  9. 隨後,Leader節點通過RDMA NIC向PolarSwitch返回請求處理結果;

  10. PolarSwitch標記請求成功並通知上層的POLARDB。

讀請求無需這麼複雜的步驟,lipfs發起的讀請求直接通過PolarSwitch路由到資料對應Chunk的Leader節點(ChunkServer1),從其中讀取對應的資料返回即可。需要說明的是,在ChunkServer上有個子模組叫IoScheduler,用於保證發生併發讀寫訪問時,讀操作能夠讀到最新的已提交資料。

基於使用者態的網路和IO路徑

在本地IO處理上,PolarFS基於預分配的記憶體buffer來處理請求,將buffer中的內容直接使用SPDK寫入WAL日誌和資料塊中。PolarFS讀寫資料基於SPDK套件直接通過DMA操作硬體裝置(SSD卡)而不是作業系統核心IO協議棧,解決了核心IO協議棧慢的問題;通過輪詢的方式監聽硬體裝置IO完成事件,消除了上下文切換和中斷的開銷。還可以將IO處理執行緒和CPU進行一一對映,每個IO處理執行緒獨佔CPU,相互之間處理不同的IO請求,繫結不同的IO裝置硬體佇列,一個IO請求生命週期從頭到尾都在一個執行緒一顆CPU上處理,不需要鎖進行互斥。這種技術實現最大化的和高速裝置進行效能互動,實現一顆CPU達每秒約20萬次IO處理的能力,並且保持線性的擴充套件能力,也就意味著4顆CPU可以達到每秒80萬次IO處理的能力,在效能和經濟型上遠高於核心。

網路也是類似的情況。過去傳統的乙太網,網路卡發一個報文到另一臺機器,中間通過一跳交換機,大概需要一百到兩百微秒。POLARDB支援ROCE乙太網,通過RDMA網路,直接將本機的記憶體寫入另一臺機器的記憶體地址,或者從另一臺機器的記憶體讀一塊資料到本機,中間的通訊協議編解碼、重傳機制都由RDMA網路卡來完成,不需要CPU參與,使效能獲得極大提升,傳輸一個4K大小報文只需要6、7微秒的時間。

阿里雲PolarDB及其共享儲存PolarFS技術實現分析(下)

如同核心的IO協議棧跟不上高速儲存裝置能力,核心的TCP/IP協議棧跟不上高速網路裝置能力,也被POLARDB的使用者態網路協議棧代替。這樣就解決了HDFS和Ceph等目前的分散式檔案系統存在的效能差、延遲大的問題。

基於ParallelRaft的資料可靠性保證

在PolarFS中,位於不同ChunkServer上的3個Chunk資料副本使用改進型Raft協議ParallelRaft來保障可靠性,通過快速主從切換和majority機制確保能夠容忍少部分Chunk副本離線時仍能夠持續提供線上讀寫服務,即資料的高可用。

在標準的Raft協議中,raft日誌是按序被Follower節點確認,按序被Leader節點提交的。這是因為Raft協議不允許出現空洞,一條raft日誌被提交,意味著它之前的所有raft日誌都已經被提交。在資料庫系統中,對不同資料的併發更新是常態,也正因為這點,才有了事務的組提交技術,但如果引入Raft協議,意味著組提交技術在PolarFS資料多副本可靠性保障這一層退化為序列提交,對於效能會產生很大影響。通過將多個事務batch成一個raft日誌,通過在一個Raft Group的Leader和Follower間建立多個連線來同時處理多個raft日誌這兩種方式(batching&pipelining)能夠緩解效能退化。但batch會導致額外延遲,batch也不能過大。pipelining由於Raft協議的束縛,仍然需要保證按序確認和提交,如果出現由於網路等原因導致前後pipeline上的raft日誌傳送往follow或回覆leader時亂序,那麼就不可避免得出現等待。

為了進一步優化效能,PolarFS對Raft協議進行了改進。核心思想就是解除按序確認,按序提交的束縛。將其變為亂序確認,亂序提交和亂序應用。首先看看這樣做的可行性,假設每個raft日誌代表一個事務,多個事務能夠並行提交說明其不存在衝突,對應到儲存層往往意味著沒有修改相同的資料,比如事務T1修改File1的Block1,事務T2修改File1的Block2。顯然,先修改Block1還是Block2對於儲存層還是資料庫層都沒有影響。這真是能夠亂序的基礎。下圖為優化前後的效能表現:

阿里雲PolarDB及其共享儲存PolarFS技術實現分析(下)

但T1和T2都修改了同一個表的資料,導致表的統計資訊發生了變化,比如T1執行後表中有10條記錄,T2執行後變為15條(舉例而已,不一定正確)。所以,他們都需要更新儲存層的相同BlockX,該更新操作就不能亂序了。

為了解決上述所說的問題,ParallelRaft協議引入look behind buffer(LBB)。每個raft日誌都有個LBB,快取了它之前的N個raft日誌所修改的LBA資訊。LBA即Logical Block Address,表示該Block在Chunk中的偏移位置,從0到10GB。通過判斷不同的raft日誌所包含的LBA是否有重合來決定能否進行亂序/並行應用,比如上面的例子,先後修改了BlockX的raft日誌就可以通過LBB發現,如果T2對BlockX的更新先完成了確認和提交,在應用前通過LBB發現所依賴的T1對BlockX的修改還沒有應用。那麼就會進入pending佇列,直到T1對BlockX完成應用。

另外,亂序意味著日誌會有空洞。因此,Leader選舉階段額外引入了一個Merge階段,填補Leader中raft日誌的空洞,能夠有效保障協議的Leader日誌的完整性。

阿里雲PolarDB及其共享儲存PolarFS技術實現分析(下)

PolarFS後設資料管理與更新

PolarFS各節點後設資料維護

libpfs僅維護檔案塊(塊在檔案中的偏移位置)到卷塊(塊在卷中的偏移位置)的對映關係,並未涉及到卷中Chunk跟ChunkServer間的關係(Chunk的物理位置資訊),這樣libpfs就跟儲存層解耦,為Chunk分配實際物理空間時無需更新libpfs層的後設資料。而Chunk到ChunkServer的對映關係,也就是物理儲存空間到卷的分配行為由PolarCtrl元件負責,PolarCtrl完成分配後會更新PolarSwitch上的快取,確保libpfs到ChunkServer的IO路徑是正確的。

Chunk中Block的LBA到Block真實實體地址的對映表,以及每塊SSD盤的空閒塊點陣圖均全部快取在ChunkServer的記憶體中,使得使用者資料IO訪問能夠全速推進。

PolarFS後設資料更新流程

前面我們介紹過,PolarDB為每個資料庫例項建立了一個volume/卷,它是一個檔案系統,建立時生成了對應的後設資料資訊。由於PolarFS是個可多點掛載的共享訪問分散式檔案系統,需要確保一個掛載點更新的後設資料能夠及時同步到其他掛載點上。比如一個節點增加/刪除了檔案,或者檔案的大小發生了變化,這些都需要持久化到PolarFS的後設資料上並讓其他節點感知到。下面我們來討論PolarFS如何更新後設資料並進行同步。

PolarFS的每個卷/檔案系統例項都有相應的Journal檔案和與之對應的Paxos檔案。Journal檔案記錄了檔案系統後設資料的修改歷史,是該卷各個掛載點之間後設資料同步的中心。Journal檔案邏輯上是一個固定大小的迴圈buffer,PolarFS會根據水位來回收Journal。如果一個節點希望在Journal檔案中追加項,其必需使用DiskPaxos演算法來獲取Journal檔案控制權。

正常情況下,為了確保檔案系統後設資料和資料的一致性,PolarFS上的一個卷僅設定一個計算節點進行讀寫模式掛載,其他計算節點以只讀形式掛載檔案系統,讀寫節點鎖會在後設資料記錄持久化後馬上釋放鎖。但是如果該讀寫節點crash了,該鎖就不會被釋放,為此加在Journal檔案上的鎖會有過期時間,在過期後,其他節點可以通過執行DiskPaxos來重新競爭對Journal檔案的控制權。當PolarFS的一個掛載節點開始同步其他節點修改的後設資料時,它從上次掃描的位置掃描到Journal末尾,將新entry更新到節點的本地快取中。PolarFS同時使用push和pull方式來進行節點間的後設資料同步。

下圖展示了檔案系統後設資料更新和同步的過程:

阿里雲PolarDB及其共享儲存PolarFS技術實現分析(下)

  1. Node 1是讀寫掛載點,其在pfs_fallocate()呼叫中將卷的第201個block分配給FileID為316的檔案後,通過Paxos檔案請求互斥鎖,並順利獲得鎖。

  2. Node 1開始記錄事務至journal中。最後寫入項標記為pending tail。當所有的項記錄之後,pending tail變成journal的有效tail。

  3. Node1更新superblock,記錄修改的後設資料。與此同時,node2嘗試獲取訪問互斥鎖,由於此時node1擁有的互斥鎖,Node2會失敗重試。

  4. Node2在Node1釋放lock後(可能是鎖的租約到期所致)拿到鎖,但journal中node1追加的新項決定了node2的本地後設資料是過時的。

  5. Node2掃描新項後釋放lock。然後node2回滾未記錄的事務並更新本地metadata。最後Node2進行事務重試。

  6. Node3開始自動同步後設資料,它只需要load增量項並在它本地重放即可。

PolarFS的元速度更新機制非常適合PolarDB一寫多讀的典型應用擴充套件模式。正常情況下一寫多讀模式沒有鎖爭用開銷,只讀例項可以通過原子IO無鎖獲取Journal資訊,從而使得PolarDB可以提供近線性的QPS效能擴充套件。

資料庫如何適配PolarFS

大家可能認為,如果讀寫例項和只讀例項共享了底層的資料和日誌,只要把只讀資料庫配置檔案中的資料目錄換成讀寫例項的目錄,貌似就可以直接工作了。但是這樣會遇到很多問題,MySQL適配PolarFS有很多細節問題需要處理,有些問題只有在真正做適配的時候還能想到,下面介紹已知存在的問題並分析資料庫層是如何解決的。

資料快取和資料一致性

從資料庫到硬體,存在很多層快取,對基於共享儲存的資料庫方案有影響的快取層包括資料庫快取,檔案系統快取。

資料庫快取主要是InnoDB的Buffer Pool(BP),存在2個問題:

  1. 讀寫節點的資料更改會快取在bp上,只有完成刷髒頁操作後polarfs才能感知,所以如果在刷髒之前只讀節點發起讀資料操作,讀到的資料是舊的;

  2. 就算PolarFS感知到了,只讀節點的已經在BP中的資料還是舊的。所以需要解決不同節點間的快取一致性問題。

PolarDB採用的方法是基於redolog複製的節點間資料同步。可能我們會想到Primary節點通過網路將redo日誌傳送給ReadOnly/Replica節點,但其實並不是,現在採用的方案是redo採用非ring buffer模式,每個檔案固定大小,大小達到後Rotate到新的檔案,在寫模式上走Direct IO模式,確保磁碟上的redo資料是最新的,在此基礎上,Primary節點通過網路通知其他節點可以讀取的redo檔案及偏移位置,讓這些節點自主到共享儲存上讀取所需的redo資訊,並進行回放。流程如下圖所示:

阿里雲PolarDB及其共享儲存PolarFS技術實現分析(下)

由於StandBy節點與讀寫節點不共享底層儲存,所以需要走網路傳送redo的內容。節點在回放redo時需區分是ReadOnly節點還是StandBy節點,對於ReadOnly節點,其僅回放對應的Page頁已在BP中的redo,未在BP中的page不會主動從共享儲存上讀取,且BP中Apply過的Page也不會回刷到共享儲存。但對於StandBy節點,需要全量回放並回刷到底層儲存上。

檔案系統快取主要是後設資料快取問題。檔案系統快取包括Page Cache,Inode/Dentry Cache等,對於Page Cache,可以通過Direct IO繞過。但對於VFS(Virtual File System)層的Inode Cache,無法通過Direct IO模式而需採用o_sync的訪問模式,但這樣導致效能嚴重下降,沒有實際意義。vfs層cache無法通過direct io模式繞過是個很嚴重的問題,這就意味著讀寫節點建立的檔案,只讀節點無法感知,那麼針對這個新檔案的後續IO操作,只讀節點就會報錯,如果採用核心檔案系統,不好進行改造。

PolarDB通過後設資料同步來解決該問題,它是個使用者態檔案系統,資料的IO流程不走核心態的Page Cache,也不走VFS的Inode/Dentry Cache,完全自己掌控。共享儲存上的檔案系統後設資料通過前述的更新流程實現即可。通過這種方式,解決了最基本的節點間資料同步問題。

事務的資料可見性問題

一、MySQL/InnoDB通過Undo日誌來實現事務的MVCC,由於只讀節點跟讀寫節點屬於不同的mysqld程式,讀寫節點在進行Undo日誌Purge的時候並不會考慮此時在只讀節點上是否還有事務要訪問即將被刪除的Undo Page,這就會導致記錄舊版本被刪除後,只讀節點上事務讀取到的資料是錯誤的。

針對該問題,PolarDB提供兩種解決方式:

  • 所有ReadOnly定期向Primary彙報自己的最大能刪除的Undo資料頁,Primary節點統籌安排;

  • 當Primary節點刪除Undo資料頁時候,ReadOnly接收到日誌後,判斷即將被刪除的Page是否還在被使用,如果在使用則等待,超過一個時間後還未有結束則直接給客戶端報錯。

二、還有個問題,由於InnoDB BP刷髒頁有多種方式,其並不是嚴格按照oldest modification來的,這就會導致有些事務未提交的頁已經寫入共享儲存,只讀節點讀到該頁後需要通過Undo Page來重建可見的版本,但可能此時Undo Page還未刷盤,這就會出現只讀上事務讀取資料的另一種錯誤。

針對該問題,PolarDB解決方法是:

  1. 限制讀寫節點刷髒頁機制,如果髒頁的redo還沒有被只讀節點回放,那麼該頁不能被刷回到儲存上。這就確保只讀節點讀取到的資料,它之前的資料鏈是完整的,或者說只讀節點已經知道其之前的所有redo日誌。這樣即使該資料的記錄版本當前的事務不可見,也可以通過undo構造出來。即使undo對應的page是舊的,可以通過redo構造出所需的undo page。

  2. replica需要快取所有未刷盤的資料變更(即RedoLog),只有primary節點把髒頁刷入盤後,replica快取的日誌才能被釋放。這是因為,如果資料未刷盤,那麼只讀讀到的資料就可能是舊的,需要通過redo來重建出來,參考第一點。另外,雖然buffer pool中可能已經快取了未刷盤的page的資料,但該page可能會被LRU替換出去,當其再次載入所以只讀節點必須快取這些redo。

DDL問題

如果讀寫節點把一個表刪了,反映到儲存上就是把檔案刪了。對於mysqld程式來說,它會確保刪除期間和刪除後不再有事務訪問該表。但是在只讀節點上,可能此時還有事務在訪問,PolarFS在完成檔案系統後設資料同步後,就會導致只讀節點的事務訪問儲存出錯。

PolarDB目前的解決辦法是:如果主庫對一個表進行了表結構變更操作(需要拷表),在操作返回成功前,必須通知到所有的ReadOnly節點(有一個最大的超時時間),告訴他們,這個表已經被刪除了,後續的請求都失敗。當然這種強同步操作會給效能帶來極大的影響,有進一步的優化的空間。

Change Buffer問題

Change Buffer本質上是為了減少二級索引帶來的IO開銷而產生的一種特殊快取機制。當對應的二級索引頁沒有被讀入記憶體時,暫時快取起來,當資料頁後續被讀進記憶體時,再進行應用,這個特性也帶來的一些問題,該問題僅存在於StandBy中。例如Primary節點可能因為資料頁還未讀入記憶體,相應的操作還快取在Change Buffer中,但是StandBy節點則因為不同的查詢請求導致這個資料頁已經讀入記憶體,可以直接將二級索引修改合併到資料頁上,無需經過Change Buffer了。但由於複製的是Primary節點的redo,且需要保證StandBy和Primary在儲存層的一致性,所以StandBy節點還是會有Change Buffer的資料頁和其對應的redo日誌,如果該髒頁回刷到儲存上,就會導致資料不一致。

為了解決這個問題,PolarDB引入shadow page的概念,把未修改的資料頁儲存到其中,將cChange Buffer記錄合併到原來的資料頁上,同時關閉該Mtr的redo,這樣修改後的Page就不會放到Flush List上。也就是StandBy例項的儲存層資料跟Primary節點保持一致。

效能測試

效能評估不是本文重點,官方的效能結果也不一定是靠譜的,只有真實測試過了才算數。在此僅簡單列舉阿里雲自己的效能測試結果,權當一個參考。

PolarFS效能

不同塊大小的IO延遲

阿里雲PolarDB及其共享儲存PolarFS技術實現分析(下)

4KB大小的不同請求型別

阿里雲PolarDB及其共享儲存PolarFS技術實現分析(下)

PolarDB整體效能

使用不同底層儲存時效能表現

阿里雲PolarDB及其共享儲存PolarFS技術實現分析(下)

對外展示的效能表現

阿里雲PolarDB及其共享儲存PolarFS技術實現分析(下)

與Aurora簡單對比

阿里雲的PolarDB和AWS Aurora雖然同為基於MySQL和共享儲存的Cloud-Native Database(雲原生資料庫)方案,很多原理是相同的,包括基於redo的物理複製和計算節點間狀態同步。但在實現上也存在很大的不同,Aurora在儲存層採用日誌即資料的機制,計算節點無需再將髒頁寫入到儲存節點,大大減少了網路IO量,但這樣的機制需要對InnoDB儲存引擎層做很大的修改,難度極大。而PolarDB基本上遵從了原有的MySQL IO路徑,通過優化網路和IO路徑來提高網路和IO能力,相對來說在資料庫層面並未有框架性的改動,相對容易些。個人認為Aurora在資料庫技術創新上更勝一籌,但PolarDB在資料庫系統級架構優化上做得更好,以儘可能小的代價獲得了足夠好的收益。

另附PolarFS的架構師曹偉在知乎上對PolarDB和Aurora所做的對比:

在設計方法上,阿里雲的PolarDB和Aurora走了不一樣的路,歸根結底是我們的出發點不同。AWS的RDS一開始就是架設在它的虛擬機器產品EC2之上的,使用的儲存是雲盤EBS。EC2和EBS之間通過網路通訊,因此AWS的團隊認為“網路成為資料庫的瓶頸”,在Aurora的論文中,他們開篇就提出“Instead, the bottleneck moves to the network between the database tier requesting I/Os and the storage tier that performs these I/Os.” Aurora設計於12到13年之際,當時網路主流是萬兆網路,確實容易成為瓶頸。而PolarDB是從15年開始研發的,我們見證了IDC從萬兆到25Gb RDMA網路的飛躍。因此我們非常大膽的判斷,未來幾年主機通過高速網路互聯,其傳輸速率會和本地PCIe匯流排儲存裝置頻寬打平,網路無論在延遲還是頻寬上都會接近匯流排,因此不再成為高效能伺服器的瓶頸。而恰恰是軟體,過去基於核心提供的syscall開發的軟體程式碼,才是拖慢系統的一環。Bottleneck resides in the software.

在架構上Aurora和PolarDB各有特色。我認為PolarDB的架構和技術更勝一籌。

1)現代雲端計算機型的演進和分化,計算機型向高主頻,多CPU,大記憶體的方向演進;儲存機型向高密度,低功耗方向發展。機型的分化可以大大提高機器資源的使用率,降低TCO。

因此PolarStore中大量採用OS-bypass和zero-copy的技術來節約CPU,降低處理單位I/O吞吐需要消耗的CPU資源,確儲存儲節點處理I/O請求的效率。而Aurora的儲存節點需要大量CPU做redolog到innodb page的轉換,儲存節點的效率遠不如PolarStore。

2)Aurora架構的最大亮點是,儲存節點具有將redolog轉換為innodb page的能力,這個改進看著很吸引眼球,事實上這個優化對關聯式資料庫的效能提升很有限,效能瓶頸真的不在這裡:),反而會拖慢關鍵路徑redolog落地的效能。btw,在PolarDB架構下,redolog離線轉換為innodb page的能力不難實現,但我們目前不認為這是高優先順序要做的。

3)Aurora的儲存多副本是通過quorum機制來實現的,Aurora是六副本,也就是說,需要計算節點向六個儲存節點分別寫六次,這裡其實計算節點的網路開銷又上去了,而且是發生在寫redolog這種關鍵路徑上。而PolarDB是採用基於RDMA實現的ParallelRaft技術來複制資料,計算節點只要寫一次I/O請求到PolarStore的Leader節點,由Leader節點保證quorum寫入其他節點,相當於多副本replication被offload到儲存節點上。

此外,在最終一致性上Aurora是用gossip協議來兜底的,在完備程度上沒有PolarDB使用的ParallelRaft演算法有保證。

4)Aurora的改動手術切口太大,使得它很難後面持續跟進社群的新版本。這也是AWS幾個資料庫產品線的通病,例如Redshift,如何吸收PostgrelSQL 10的變更是他們的開發團隊很頭疼的問題。對新版本做到與時俱進是雲資料庫的一個樸素需求。怎麼設計這個刀口,達到effect和cost之間的平衡,是對架構師的考驗。

總得來說,PolarDB將資料庫拆分為計算節點與儲存節點2個獨立的部分,計算節點在已有的MySQL資料庫基礎上進行修改,而儲存節點基於全新的PolarFS共享儲存。PolarDB通過計算和儲存分離的方式實現提供了即時生效的可擴充套件能力和運維能力,同時採用RDMA和SPDK等最新的硬體來優化傳統的過時的網路和IO協議棧,極大提升了資料庫效能,基本上解決了使用MySQL是會遇到的各種問題,除此之外本文並未展開介紹PolarDB的ParallelRaft,其依託上層資料庫邏輯實現IO亂序提交,大大提高多個Chunk資料副本達成一致性的效能。以上這些創新和優化,都成為了未來資料庫的發展方向。

引數資料:


本文來自網易雲社群 ,經作者溫正湖授權釋出。

網易雲免費體驗館,0成本體驗20+款雲產品!

更多網易研發、產品、運營經驗分享請訪問網易雲社群


相關文章:
【推薦】 流式處理框架storm淺析(上篇)
【推薦】 訊息中介軟體客戶端消費控制實踐
【推薦】 用scrapy資料抓取實踐


相關文章