#頭條創作挑戰賽#
一、前言
之前的文章已經寫過 ,Broker高可用架構的原理:RocketMQ源碼分析之主從同步服務元件HAService、RocketMQ源碼分析之核心磁盤資料結構CommitLog、選舉流程,讓我們知道這個CommitLog是幹什麼用的,如何選舉。
接下來我們看一下,為什麼要引入DLedger,DLedger的設計理念是怎樣的,使用場景以及如何優化;
二、DLedger引入目的
在 RocketMQ 4.5 版本之前,RocketMQ 隻有 Master/Slave 一種部署方式,一組 broker 中有一個 Master ,有零到多個Slave,Slave 通過同步複制或異步複制的方式去同步 Master 資料。Master/Slave 部署模式,提供了一定的高可用性。
但這樣的部署模式,有一定缺陷。比如故障轉移方面,如果主節點挂了,還需要人為手動進行重新開機或者切換,無法自動将一個從節點轉換為主節點。是以,我們希望能有一個新的多副本架構,去解決這個問題。
新的多副本架構首先需要解決自動故障轉移的問題,本質上來說是自動選主的問題。這個問題的解決方案基本可以分為兩種:
- 利用第三方協調服務叢集完成選主,比如 zookeeper 或者 etcd。這種方案會引入了重量級外部元件,加重部署,運維和故障診斷成本,比如在維護 RocketMQ 叢集還需要維護 zookeeper 叢集,并且 zookeeper 叢集故障會影響到 RocketMQ 叢集。
- 利用 raft 協定來完成一個自動選主,raft 協定相比前者的優點是不需要引入外部元件,自動選主邏輯內建到各個節點的程序中,節點之間通過通信就可以完成選主。
是以最後選擇用 raft 協定來解決這個問題,而 DLedger 就是一個基于 raft 協定的 commitlog 存儲庫,也是 RocketMQ 實作新的高可用多副本架構的關鍵。
三、DLedger 設計理念
1. DLedger 定位
Raft 協定是複制狀态機的實作,這種模型應用到消息系統中就會存在問題。對于消息系統來說,它本身是一個中間代理,commitlog 狀态是系統最終狀态,并不需要狀态機再去完成一次狀态建構。是以 DLedger 去掉了 raft 協定中狀态機的部分,但基于raft協定保證commitlog 是一緻的,并且是高可用的。
另一方面 DLedger 又是一個輕量級的 java library。它對外提供的 API 非常簡單,append 和 get。Append 向 DLedger 添加資料,并且添加的資料會對應一個遞增的索引,而 get 可以根據索引去獲得相應的資料。是以 DLedger 是一個 append only 的日志系統。
2. DLedger 應用場景
(1)作為 RocketMQ 的消息存儲
架構如下圖所示:其中:
DLedgerCommitlog 用來代替現有的 Commitlog 存儲實際消息内容,它通過包裝一個 DLedgerServer 來實作複制;
依靠 DLedger 的直接存取日志的特點,消費消息時,直接從 DLedger 讀取日志内容作為消息傳回給用戶端;
依靠 DLedger 的 Raft 選舉功能,通過 RoleChangeHandler 把角色變更透傳給 RocketMQ 的Broker,進而達到主備自動切換的目标;
(2)實作一個高可用的嵌入式 KV 存儲
DLedger 其中一個應用就是在分布式消息系統中,RocketMQ 4.5 版本釋出後,可以采用 RocketMQ on DLedger 方式進行部署。DLedger commitlog 代替了原來的 commitlog,使得 commitlog 擁有了選舉複制能力,然後通過角色透傳的方式,raft 角色透傳給外部 broker 角色,leader 對應原來的 master,follower 和 candidate 對應原來的 slave。
是以 RocketMQ 的 broker 擁有了自動故障轉移的能力。在一組 broker 中, Master 挂了以後,依靠 DLedger 自動選主能力,會重新選出 leader,然後通過角色透傳變成新的 Master。
DLedger 還可以建構高可用的嵌入式 KV 存儲。我們把對一些資料的操作記錄到 DLedger 中,然後根據資料量或者實際需求,恢複到hashmap 或者 rocksdb 中,進而建構一緻的、高可用的 KV 存儲系統,應用到元資訊管理等場景。
源碼分析參考:RocketMQ源碼之broker高可用CommitLog管理元件DLedgerCommitLog
四、DLedger 的優化
1. 性能優化
Raft 協定複制過程可以分為四步,先是發送消息給 leader,leader 除了本地存儲之外,會把消息複制給 follower,然後等待follower 确認,如果得到多數節點确認,該消息就可以被送出,并向用戶端傳回發送成功的确認。DLedger 中如何去優化這一複制過程?
(1)異步線程模型
DLedger 采用一個異步線程模型,異步線程模型可以減少等待。在一個系統中,如果阻塞點越少,每個線程處理請求時能減少等待,就能更好的利用 CPU,提高吞吐量和性能。
以 DLedger 處理 Append 請求的整個過程來講述 DLedger 異步線程模型。圖中粗箭頭表示 RPC 請求,實作箭頭表示資料流,虛線表示控制流。
首先用戶端發送 Append 請求,由 DLedger 的通信子產品處理,目前 DLedger 預設的通信子產品是利用 Netty 實作的,是以 Netty IO 線程會把請求交給業務線程池中的線程進行處理,然後 IO 線程直接傳回,處理下一個請求。業務處理線程處理 Append 請求有三個步驟,首先是把 Append 資料寫入自己日志中,也就是 pagecache 中。然後生成 Append CompletableFuture ,放入一個 Pending Map 中,由于該日志還沒有得到多數的确認,是以它是一個判定狀态。第三步喚醒 EnrtyDispatcher 線程,通知該線程去向follower 複制日志。三步完成以後業務線程就可以去處理下一個 Append 請求,中間幾乎沒有任何等待。
另一方面,複制線程 EntryDispatcher 會向 follower 複制日志,每一個 follower 都對應一個 EntryDispatcher 線程,該線程去記錄自己對應 follower 的複制位點,每次位點移動後都會去通知 QurumAckChecker 線程,這個線程會根據複制位點的情況,判斷是否一條日志已經複制到多數節點上,如果已被複制到了多數節點,該日志就可以被送出,并去完成對應的 Append CompletableFuture ,通知通信子產品向用戶端傳回響應。
(2)獨立并發的複制過程
在 DLedger 中,leader 向所有 follower 發送日志也是完全互相獨立和并發的,leader 為每個 follower 配置設定一個線程去複制日志,并記錄相應的複制位點,然後再由一個單獨的異步線程根據位點情況檢測日志是否被複制到了多數節點上,傳回給用戶端響應。
(3)日志并行複制
傳統的線性複制是 leader 向 follower 複制日志,follower 确認後下一個日志條目再複制,也就是 leader 要等待 follower 對前一條日志确認後才能複制下一條日志。這樣的複制方式保證了順序性,且不會出錯,但吞吐量很低,時延也比較高,是以DLedger設計并實作日志并行複制的方案,不再需要等待前一個日志複制完成再複制下一個日志,隻需在 follower 中維護一個按照日志索引排序請求清單, follower 線程按照索引順序串行處理這些複制請求。而對于并行複制後可能出現資料缺失問題,可以通過少量資料重傳解決。
2. 可靠性優化
(1)DLedger對網絡分區的優化
如果出現上圖的網絡分區,n2與叢集中的其他節點發生了網絡隔離,按照 raft 論文實作,n2會一直請求投票,但得不到多數的投票,term 一直增大。一旦網絡恢複後,n2就會去打斷正在正常複制的n1和n3,進行重新選舉。為了解決這種情況,DLedger 的實作改進了 raft 協定,請求投票過程分成了多個階段,其中有兩個重要階段:WAIT_TO_REVOTE和WAIT_TO_VOTE_NEXT。WAIT_TO_REVOTE是初始狀态,這個狀态請求投票時不會增加 term,WAIT_TO_VOTE_NEXT則會在下一輪請求投票開始前增加 term。對于圖中n2情況,當有效的投票數量沒有達到多數量時。可以将節點狀态設定WAIT_TO_REVOTE,term 就不會增加。通過這個方法,提高了Dledger對網絡分區的容忍性。
(2)DLedger 可靠性測試
DLedger 還有非常高的容錯性。它可以容忍各種各樣原因導緻節點無法正常工作,比如:
- 程序異常崩潰
- 機器節點異常崩潰(機器斷電,作業系統崩潰)
- 慢節點(出現 Full GC,OOM 等)
- 網絡故障,各種各樣的網絡分區
為了驗證 DLedger 對這些故障的容忍性,除了本地對 DLedger 進行了各種各樣的測試,還利用分布式系統驗證與故障注入架構 Jepsen 來檢測 DLedger 存在的問題,并驗證系統的可靠性。
Jepsen 架構主要是在特定故障下驗證系統是否滿足一緻性。Jepsen 驗證系統由 6 個節點組成,一個控制節點(Control Node),五個 DB 節點(DB Node)。控制節點可以通過 SSH 登入到 DB 節點,通過控制節點的控制,可以在 DB 節點完成分布式系統的下載下傳,部署,組成一個待測試的叢集。測試開始後,控制節點會建立一組 Worker 程序,每一個 Worker 都有自己的分布式系統用戶端。Generator 産生每個用戶端執行的操作,用戶端程序将操作應用于待測試的分布式系統。每個操作的開始和結束以及操作結果記錄在曆史記錄中。同時,一個特殊的 Client 程序 Nemesis 将故障引入系統。測試結束後, Checker 分析曆史記錄是否正确,是否符合一緻性。
根據 DLedger 定位,它是一個基于 raft 協定的 commitlog 存儲庫,是一個 append only 的日志系統,采用 Jepsen 的 Set模型進行測試。Set 模型的測試流程分為兩個階段。第一階段由不同的用戶端并發地向待測試叢集添加不同的資料,中間會進行故障注入。第二階段,向待測試叢集進行一次最終讀取,獲得讀取的結果集。最後驗證每一個成功添加的元素都在最終結果集中,并且最終的結果集也僅包含企圖添加的元素。
上圖是 DLedger 其中一次測試結果,有30個用戶端程序并發地向待測試的 DLedger 叢集添加資料,中間會引入随機對稱網絡分區,故障引入的間隔時間預設是30s,也就是30s正常運作,30s故障引入,再30s正常運作、30s故障引入,一直循環。整個階段一共持續600s。可以看到最後一共發送了16萬個資料,中間沒有出現資料丢失,lost-count=0,也沒有出現不應該存在的資料,uexpected-count=0,一緻性測試通過。
上圖展示了該次測試中用戶端對DLedger叢集每一次操作情況,藍色小框表示添加成功,紅色小框表示添加失敗,黃色小框表示不确定是否添加成功(比如多數認證逾時),圖中灰色部分表示故障引入的時間段。可以看出一些故障引入時間段造成叢集短暫不可用,一些故障時間段則沒有,這是合理的。因為是随機網絡隔離,是以需要看隔離的節點會不會造成叢集重新選舉。但即使造成叢集重新選舉,一段時間後,DLedger叢集也會恢複可用性。
除了測試對稱網絡分區故障,還測試了其他故障下 Dledger 表現情況,包括随機殺死節點,随機暫停一些節點的程序模拟慢節點的狀況,以及 bridge、partition-majorities-ring 等複雜的非對稱網絡分區。在這些故障下,DLedger 都保證了一緻性,驗證了 DLedger 有很好可靠性。