《Taurus Database: How to be Fast, Available, and Frugal in the Cloud》閱讀筆記


注:本筆記不是對論文的直接翻譯,內容上有些論文比較完整地就省略了,會有一些自己思考的補充信息。

1. Introduction

論文認為,一個好的DBaas架構服務必須提供 durability,scalability, performance,availability, cost-effectiveness 五個方面的保證。

  • 不同節點上的三副本通常認為已經足以保證durability;Taurus全文主要討論的就是如何保證數據庫層的durability (log as database)
  • 計算存儲分離及其各自的分布式實現可以提供scalability
  • performance是個相對概念,在Evaluation chapter中的對比會提到Taurus replication strategy(相比quorum-based)和僅有兩層網絡划分(相比Socrates的四層)導致了Taurus在和Aurora和Socrates相比較時帶來的性能收益。
  • availability是本文的一個核心,主要是Taurus replication機制和PageStore模塊。
  • cost-effectiveness沒有細講,也是個相對概念,文中主要是說明了在沒有額外增加硬件成本開銷的情況下達到了更好的可靠性和性能。

Taurus提出將durability和availability分離,實際也就是將log和data分離。durability由WAL log保證,文中相關的為LogStore;availability依靠PageStore的PLog三副本保證。

2. 基本架構形態

Taurus是個基於共享存儲的計算存儲分離和一寫多讀的雲數據庫,DB前端目前是一個改版的MySQL 8.0,未來會支持PGSQL等。DB引擎通過Storage Abstraction Layer(SAL) 和共享存儲交互,SAL可以對標PolarDB的libpfs.a,也是以lib形式存在,但不同的是SAL並未局限為一個文件系統,擁有更多對整體DB和存儲融合的適配空間。Master(RW節點)在寫入時會先寫log到LogStore,再寫數據到PageStore。Read replica(RO節點)會從Master同步寫位點,並不斷從LogStore讀相差的日志,apply到自己本地的bufferpool中。

每個PageStore會被划分成若干個10GB大小的Slice,三副本的對象是Slice,即不同機器的三個Slice組成三副本(類似PolarFS的chunk)。PageStore自身的三副本保證寫的可靠性(不依靠Log),三副本會通過gossip分布式協議去互相保證數據完整性和可靠性,換句話說,如果寫入的三副本其中一個副本失敗被拉起,他會依靠另外兩個副本做恢復。

3. 模塊細節

LogStore

一個集群中通常有幾百個LogStore服務。LogStore的核心是PLog,PLog是一個大小有限(64MB limit)、跨LogStore、append only的多副本存儲表示對象(Taurus就是三副本,即三個LogStore);另一種理解是PLog就是一個對上層的可靠性封裝,日志寫進PLog成功即可視為成功,而實際上PLog寫成功響應的前提是需要PLog內三副本LogStore都寫成功。如果PLog寫超時或失敗,cluster manager會重新選擇另外三個LogStore創建一個新的PLog。這個策略保證了只要時間允許和有至少三個健康的不同節點的LogStore存活,日志寫一定會最終能成功;並且Taurus保證日志寫重試不會重試回舊的LogStore以提高成功概率。

日志讀只要PLog有一個replica存活就能讀成功。日志讀通常發生在兩種場景:1. Read replica 讀最新日志追趕Master;2. 數據庫恢復。第一種情況是高頻情況,因此LogStore做了一層FIFO cache緩存最新的日志數據。

PLog會分為metadata PLog和data PLog。對於同一組PLog,由於前面提及的超時等原因存在,可能會先后創建多個PLog對象,但是他們負責的是同一批日志。這樣一個有序的data PLog列表的增刪操作日志和一些映射信息會被記錄到metadata中,當metadata滿的時候,系統會創建一個新的metadata PLog,將前面的操作日志purge(也許有合並)后,僅將latest的必要的metadata寫到新的metadata PLog,舊的會被直接刪掉。

PageStore

PageStore主要是對上服務於DB節點的read page請求,對下管理Slices。它不是一個通用的存儲,而是具有持久化頁的視角和讀寫管理功能的模塊。Master發給PageEngine的寫請求實際也是log record,PageEngine會將其apply到存儲的pages上。

PageStore通過四個主要的API向SAL提供能力:

  • WriteLogs 接收若干log records的寫請求
  • ReadPage 按照特定頁版本去讀某個頁。其中 PageVersion=(PageID, LSN)
  • SetRecycleLSN 在這個LSN之前的版本已固化且不會再被上層數據庫事務訪問。通常這個LSN適用於purge,比如purge多版本頁的舊版本。該接口會被DB引擎周期性調用以保證purge的效率。
  • GetPersistentLSN 相當於PageStore的readpoint,小於等於這個LSN的數據是對上可見的。

PageStore internal

當前的PageStore在寫密集場景下可以服務百萬級的log_record per sec,這里的apply logs涉及到一個概念 log固化:PageStore apply logs & produce and persist new pages 。由此,PageStore有三個實現上的特性:

  1. Independent log consolidation。即三副本的PageStore的log固化流程不依賴於某種raft或paxos的分布式協議,而是各自去執行對應流程。(但會借助gossip分布式協議去做部分replica寫失敗的校驗和修補)
  2. log固化的磁盤寫是一個append only的追加寫過程,保證寫盤性能。
  3. log固化過程中涉及到的page都會在PageStore的memory中,以保證在整個固化或涉及到page的更新都不會產生讀IO。

PageStore內的LogDirectory模塊復雜管理log records的location和當前PageStore上的Slices的頁和版本的映射關系(用一個無鎖hashtable)。為了避免LogDirectory膨脹導致PageStore性能跟不上,SAL側做了throttle機制,LogDirectory在purge或log truncation階段會被清理變小。

上圖是PageStore的主要工作流,其中:

  • step4 中可以發現PageStore自己也是有個bufferpool存在的,相當於個分散的二級緩存,采用LFU實現(論文通過測試認為LFU相比LRU更適合作為二級緩存的策略,hit rate可以提升25%)。
  • step5 刷臟。實際上只有page刷下去了,相應的persistent LSN才會被更新。
  • step6 LogDirectory元信息也會被周期持久化,減少recover時log回放重新生成LogDirectory的開銷。

圖上需注意的是,LogDirectory是Slice級別的(降低規模減少hash沖突),LogCache和BufferPool是PageStore進程全局的。

在收到一批log records時,決定先對哪個page做固化操作采用的是“log cacge-centric”,即先到達log cache的log record ordered group會被優先處理,處理完就會從log cache刪掉。實際上log cache是會被塞滿的,滿了之后到達的log records就會在磁盤上維護個隊列。這樣做法可以盡可能避免log cache的命中率下降,盡可能保證處理的都是內存里的log records。 這一段疑點很多,論文沒有展開講,背后問題很可能是PageStore apply log成為瓶頸,后面write path提到的只寫一個replica返回也是因為這個問題。論文一開始講了個不靠譜的借鑒自區塊鏈的最長鏈優先,根本不適用於log這種需要高效順序apply的場景,鏈短的page很可能就會餓死影響整個系統相關LSN推進。隨后寫了一整段的log cache-centric 如果沒有展現相關多線程實現的話,看上去就是個顯而易見的FIFO,log cache無非就是個內存隊列。

Storage Abstraction Layer

SAL可以理解為存儲層的前端和存儲數據分布式管理組件,它負責DB引擎與存儲的LogStore/PageStore交互,同時也負責創建、管理、刪除PageStore的Slices以及管理page在Slices上的映射關系(或者叫storage layout)。

Master寫的時候,SAL會有一個database log buffer來盡可能batch io避免小IO、SAL寫到active的LogStore replica成功之后,同一個buffer的log records會分發到per-slice buffers(即每個slice有自己的buffer),per-slice buffers會在滿或某一時間后觸發flush,發給對應PageStore。

SAL維護着cluster visible LSN (CVLSN),這個LSN代表着數據庫全局一致點。當日志對應的數據持久化到Slices時,當前持久化的latest LSN可以視作CVLSN。這里包含了兩個loose的隱性約定:

  1. Slice雖然有三副本,但是數據不需要三副本都寫完再返回持久化成功,這里replica可以是異步寫,Slice之間依靠gossip協議去保證可靠性。
  2. CVLSN是對於Master寫而言,更像是一個write point。因此Read replica同步數據的位點其實也應當是CVLSN。

這里的語義強依賴先寫LogStore成功再寫PageStore Slices這個前提,因此SAL還需維護一個臨時映射關系:當前log buffer和slices的多對多的映射關系,這個關系才能幫助SAL順利正確地推進CVLSN。

Read replicas (RO節點)

RO節點通常關注的問題:

  1. 與Master的同步:Master不會同步binlog,而是在寫完成后同步相關log records的元信息和寫完后更新的LSN。RO從LogStore去自行讀取log來apply到自己的bufferpool中並推進自己可讀位點(TVLSN,Transaction visible LSN)。
  2. 讀數據的原子性:Master同步log records給RO的時候一定是以一個完整的group同步,RO apply的時候保證不會有中間結果。比如,當某個線程正在分裂某個B+樹的page,這時候的改動會涉及到多個page,同時,有別的線程也在遍歷這一顆樹其他線程,那么為了確保線程看到一致的結果,分裂操作需要變成一個原子操作。加上group之后,RO要么看到分裂前,要么看到分裂后的,至少保證都是某一時期的完整數據。
  3. 跨節點之間讀一致性:論文並未顯示對這點有支持,目前機制無法支持節點間一致性讀,可能需要上層再加一個proxy帶着LSN讀來解決相關問題。
  4. 版本讀:引擎中的bufferpool可以存儲多版本頁。

4. Replication

Write path

圖中是個寫路徑的描述,其中:

  • 2 需要log的三副本都寫成功才ack(強一致)
  • 3 日志寫完就可以ack給用戶了,因為Read replica也是依靠日志回放同步的;如果宕機,PageStore也是依靠LogStore做差異的日志恢復,所以不需要等PageStore寫完才返回。
  • 5 只要有一個Slice replica ack了,SAL就可以把相應部分的log buffer邏輯意義上釋放掉,這就是上文說的SAL需要維護一個臨時的多對多關系的原因。
  • 6 Slices三副本會通過gossip協議detect and recover missing buffers。
  • 7、8 和Log truncation有關,見下一節描述。

Log truncation

Log能被刪除的前提條件:

  1. 數據都已寫到所有的slice replicas
  2. 當前數據已對所有RO節點可見

前提1是由每個slice 的 persistent LSN判斷的,slice persistent LSN由對應的PageStore維護,可以由GetPersistentLSN()方法顯式返回或通過WriteLogs/ReadPage方法順帶隱式返回。當所有 slice persistent LSN 都大於等於log LSN時,則表示前提1被滿足,該動作發生在SAL。(即上圖write path中step 7)

SAL會總和所有三副本Slices,把其中還未完全達到同步完成的三副本中的最小的persistent LSN作為database persistent LSN(論文說法有點繞,按照實現方法描述了,實際換個角度可以理解為達到三副本同步完成最大的persistent LSN),該LSN會用來作為整個數據庫latest的恢復起始點。

SAL同時會追蹤每個PLog的LSN range,如果PLog max LSN < database persistent LSN,則該PLog可被刪除。(上圖step8)

Read path

bufferpool在這里有個修改,保證大於CVLSN的臟頁不會被換出bufferpool(RO節點也有可能,比如日志寫完了並被RO同步了,但PageStore持久化未完成),從而保證數據讀的正確性。若需要從存儲上讀頁數據,SAL會負責去對應PageStore上讀取。

為了避免一些部分操作帶來的中間數據(比如保證b+tree分裂的原子性),Master是以group為單位產生log,RO節點在同步log的時候也是以group為粒度apply的,從而避免割裂的單獨一條日志帶來的語義不完整性。

但此處存疑的一點是,論文沒有介紹對於不同RO節點數據的數據一致性怎么保證:即RO1同步的比較慢,RO2同步的比較快,某一時刻他倆看到的可能不是同樣的數據。group機制只能保證單位同步的原子性,不能解決多節點一致性問題。

5. Recovery

Taurus recovery分為short-term和long-term,15 min以內的宕機都認為是short-term。對於一個三副本,允許在short-term的時間內一副本掛掉僅有兩副本提供服務,但long-term必須完整恢復。

LogStore recovery

short-term failure不用做recovery,當前LogStore的PLog節點會停止寫入只能讀取。新的可寫入的PLog會在另外三個健康的LogStore上創建。如果達到了long-term,則該LogStore會被集群移除,尋找另一健康LogStore加入當前PLog做三副本服務於讀取。

PageStore recovery

Taurus依靠gossip協議去恢復PageStore三副本之間出問題的副本。

SAL recovery

SAL作為lib嵌進數據庫中,恢復時先恢復SAL,再恢復數據庫前端。SAL會讀取最后存儲的database persistent LSN,使其作為恢復的起始點。日志會回放給PageStore,中間可能會出現宕機前已經固化的數據被重新發給對應PageStore,但PageStore內部會有LSN記錄去忽略重復的數據。這一步相當於redo log recover。當SAL代表的底層存儲recover完成時,數據庫前端開始執行undo log去回滾那些未提交的事務。

6. Evaluation

最后Taurus和Aurora、Socrates分別做了比較,論文顯示結論是:

  1. 在Sysbench 1GB讀寫、1TB寫、 TPC-C(10G、100G)上均優於Aurora,認為主要原因是Taurus的replication IO策略比Aurora的quorum-based性能要好。
  2. 由於Socrates是SQLServer,架構完全不一樣不好比,Taurus借助了Socrates的結果和本地盤MySQL 8.0去比較來達到和Socrates間接比較的效果。最后Taurus認為自己性能較好的一個主要原因是Taurus IO路徑上只有兩層的計算存儲網絡分離,而Socrates涉及四個模塊的網絡IO。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM