作者:斜陽
在分布式系統(tǒng)中不可避免的會遇到網(wǎng)絡(luò)故障,機器宕機,磁盤損壞等問題,為了向用戶不中斷且正確的提供服務(wù),要求系統(tǒng)有一定的冗余與容錯能力。RocketMQ 在日志,統(tǒng)計分析,在線交易,金融交易等豐富的生產(chǎn)場景中發(fā)揮著至關(guān)重要的作用,而不同環(huán)境對基礎(chǔ)設(shè)施的成本與可靠性提出了不同的訴求。在 RocketMQ v4 版本中有兩種主流高可用設(shè)計,分別是主備模式的無切換架構(gòu)和基于 Raft 的多副本架構(gòu)(圖中左側(cè)和右側(cè)所示)。生產(chǎn)實踐中我們發(fā)現(xiàn),兩副本的冷備模式下備節(jié)點資源利用率低,主宕機時特殊類型消息存在可用性問題;而 Raft 高度串行化,基于多數(shù)派的確認機制在擴展只讀副本時不夠靈活,無法很好的支持兩機房對等部署,異地多中心等復(fù)雜場景。RocketMQ v5 版本融合了上述方案的優(yōu)勢,提出 DLedger Controller 作為管控節(jié)點(中間部分所示),將選舉邏輯插件化并優(yōu)化了數(shù)據(jù)復(fù)制的實現(xiàn)。
在 Primary-Backup 架構(gòu)的分布式系統(tǒng)中,一份數(shù)據(jù)將被復(fù)制成多個副本來避免數(shù)據(jù)丟失。處理相同數(shù)據(jù)的一組節(jié)點被稱為副本組(ReplicaSet),副本組的粒度可以是單個文件級別的(例如 HDFS),也可以是分區(qū)級 / 隊列級的(例如 Kafka),每個真實存儲節(jié)點上可以容納若干個不同副本組的副本,也可以像 RocketMQ 一樣粗粒度的獨占節(jié)點。獨占能夠顯著簡化數(shù)據(jù)寫入時確保持久化成功的復(fù)雜度,因為每個副本組上只有主副本會響應(yīng)讀寫請求,備機一般配置只讀來提供均衡讀負載,選舉這件事兒等價于讓副本組內(nèi)一個副本持有獨占的寫鎖。
(資料圖)
RocketMQ 為每個存儲數(shù)據(jù)的 Broker 節(jié)點配置 ClusterName,BrokerName 標識來更好的進行資源管理。多個 BrokerName 相同的節(jié)點構(gòu)成一個副本組。每個副本還擁有一個從 0 開始編號,不重復(fù)也不一定連續(xù)的 BrokerId 用來表示身份,編號為 0 的節(jié)點是這個副本組的 Leader / Primary / Master,故障時通過選舉來重新對 Broker 編號標識新的身份。例如 BrokerId = {0, 1, 3},則 0 為主,其他兩個為備。
一個副本組內(nèi),節(jié)點間共享數(shù)據(jù)的方式有多種,資源的共享程度由低到高來說一般有 Shared Nothing,Shared Disk,Shared Memory,Shared EveryThing。典型的 Shared Nothing 架構(gòu)是 TiDB 這類純分布式的數(shù)據(jù)庫,TiDB 在每個存儲節(jié)點上使用基于 RocksDB 封裝的 TiKV 進行數(shù)據(jù)存儲,上層通過協(xié)議交互實現(xiàn)事務(wù)或者 MVCC。相比于傳統(tǒng)的分庫分表策略來說,TiKV 易用性和靈活程度很高,更容易解決數(shù)據(jù)熱點與伸縮時數(shù)據(jù)打散的一系列問題,但實現(xiàn)跨多節(jié)點的事務(wù)就需要涉及到多次網(wǎng)絡(luò)的通信。另一端 Shared EveryThing 的案例是 AWS 的 Aurora,Aliyun 的 PolarStore,旁路 Kernal 的方式使應(yīng)用完全運行于用戶態(tài),以最大程度的存儲復(fù)用來減少資源消耗,一主多備完全共用一份底層可靠的存儲,實現(xiàn)一寫多讀,快速切換。
大多數(shù) KV 操作都是通過關(guān)鍵字的一致性哈希來計算所分配的節(jié)點,當(dāng)這個節(jié)點所在的主副本組產(chǎn)生存儲抖動,主備切換,網(wǎng)絡(luò)分區(qū)等情況下,這個分片所對應(yīng)的所有鍵都無法更新,局部會有一些操作失敗。消息系統(tǒng)的模型有所不同,流量大但跨副本組的數(shù)據(jù)交互極少,無序消息發(fā)送到預(yù)期分區(qū)失敗時還可以向其他副本組(分片)寫入,一個副本組的故障不影響全局,這在整體服務(wù)的層面上額外提供了跨副本組的可用性。此外,考慮到 MQ 作為 Paas 層產(chǎn)品,被廣泛部署于 Windows,Linux on Arm 等各種環(huán)境,只有減少和 Iaas 層產(chǎn)品的深度綁定,才能提供更好的靈活性。這種局部故障隔離和輕依賴的特性是 RocketMQ 選則 Shared Nothing 模型重要原因。
副本組中,各個節(jié)點處理的速度不同,也就有了日志水位的概念。Master 和與其差距不大的 Slave 共同組成了同步副本集(SyncStateSet)。如何定義差距不大呢?衡量的指標可以是日志水位(文件大小)差距較小,也可以是備落后的時間在一定范圍內(nèi)。在主宕機時,同步副本集中的其余節(jié)點有機會被提升為主,有時需要對系統(tǒng)進行容災(zāi)演練,或者對某些機器進行維護或灰度升級時希望定向的切換某一個副本成為新主,這又產(chǎn)生了優(yōu)先副本(PriorityReplica)的概念。選擇優(yōu)先副本的原則和策略很多,可以動態(tài)選擇水位最高,加入時間最久或 CommitLog 最長的副本,也可以支持機架,可用區(qū)優(yōu)先這類靜態(tài)策略。
從模型的角度來看,RocketMQ 單節(jié)點上 Topic 數(shù)量較多,如果像 kafka 以 topic / partition 粒度維護狀態(tài)機,節(jié)點宕機會導(dǎo)致上萬個狀態(tài)機切換,這種驚群效應(yīng)會帶來很多潛在風(fēng)險,因此 v4 版本時 RocketMQ 選擇以單個 Broker 作為切換的最小粒度來管理,相比于其他更細粒度的實現(xiàn),副本身份切換時只需要重分配 Broker 編號,對元數(shù)據(jù)節(jié)點壓力最小。由于通信的數(shù)據(jù)量少,可以加快主備切換的速度,單個副本下線的影響被限制在副本組內(nèi),減少管理和運維成本。這種實現(xiàn)也一些缺點,例如存儲節(jié)點的負載無法以最佳狀態(tài)在集群上進行負載均衡,Topic 與存儲節(jié)點本身的耦合度較高,水平擴展一般會改變分區(qū)總數(shù),這就需要在上層附加額外的處理邏輯。
為了更規(guī)范更準確的衡量副本組的可用性指標,學(xué)術(shù)上就引入了幾個名詞:
節(jié)點數(shù)量與可靠性關(guān)系密切,根據(jù)不同生產(chǎn)場景,RocketMQ 的一個副本組可能會有 1,2,3,5 個副本。
如何保證副本組中數(shù)據(jù)的最終一致性?那肯定是通過數(shù)據(jù)復(fù)制的方式實現(xiàn),我們該選擇邏輯復(fù)制還是物理復(fù)制呢?
邏輯復(fù)制:使用消息來進行同步的場景也很多,各種 connector 實現(xiàn)本質(zhì)上就是把消息從一個系統(tǒng)挪到另外一個系統(tǒng)上,例如將數(shù)據(jù)導(dǎo)入導(dǎo)出到 ES,F(xiàn)link 這樣的系統(tǒng)上進行分析,根據(jù)業(yè)務(wù)需要選擇特定 Topic / Tag 進行同步,靈活程度和可擴展性非常高。這種方案隨著 Topic 增多,系統(tǒng)還會有服務(wù)發(fā)現(xiàn),位點和心跳管理等上層實現(xiàn)造成的性能損失。因此對于消息同步的場景,RocketMQ 也支持以消息路由的形式進行數(shù)據(jù)轉(zhuǎn)移,將消息復(fù)制作為業(yè)務(wù)消費的特例來看待。
物理復(fù)制:大名鼎鼎的 MySQL 對于操作會記錄邏輯日志(bin log)和重做日志(redo log)兩種日志。其中 bin log 記錄了語句的原始邏輯,比如修改某一行某個字段,redo log 屬于物理日志,記錄了哪個表空間哪個數(shù)據(jù)頁改了什么。在 RocketMQ 的場景下,存儲層的 CommitLog 通過鏈表和內(nèi)核的 MappedFile 機制抽象出一條 append only 的數(shù)據(jù)流。主副本將未提交的消息按序傳輸給其他副本(相當(dāng)于 redo log),并根據(jù)一定規(guī)則計算確認位點(confirm offset)判斷日志流是否被提交。這種方案僅使用一份日志和位點就可以保證主備之間預(yù)寫日志的一致性,簡化復(fù)制實現(xiàn)的同時也提高了性能。
為了可用性而設(shè)計的多副本結(jié)構(gòu),很明顯是需要對所有需要持久化的數(shù)據(jù)進行復(fù)制的,選擇物理復(fù)制更加節(jié)省資源。RocketMQ 在物理復(fù)制時又是如何保證數(shù)據(jù)的最終一致性呢?這就涉及到數(shù)據(jù)的水位對齊。對于消息和流這樣近似 FIFO 的系統(tǒng)來說,越近期的消息價值越高,消息系統(tǒng)的副本組的單個節(jié)點不會像數(shù)據(jù)庫系統(tǒng)一樣,保留這個副本的全量數(shù)據(jù),Broker 一方面不斷的將冷數(shù)據(jù)規(guī)整并轉(zhuǎn)入低頻介質(zhì)來節(jié)約成本,同時對熱數(shù)據(jù)盤上的數(shù)據(jù)也會由遠及近滾動刪除。如果副本組中有副本宕機較久,或者在備份重建等場景下就會出現(xiàn)日志流的不對齊和分叉的復(fù)雜情況。在下圖中我們將主節(jié)點的 CommitLog 的首尾位點作為參考點,這樣就可以劃分出三個區(qū)間。在下圖中以藍色箭頭表示。排列組合一下就可以證明備機此時的 CommitLog 一定滿足下列 6 種情況之一。
下面對每種情況進行討論與分析:
前文提到 RocketMQ 每個副本組的主副本才接受外部寫請求,節(jié)點的身份又是如何決定的呢?
分布式系統(tǒng)一般分為中心化架構(gòu)和去中心化架構(gòu)。對于 MultiRaft,每個副本組包含三個或者五個副本,副本組內(nèi)可以通過 Paxos / Raft 這樣的共識協(xié)議來進行選主。典型的中心化架構(gòu),為了節(jié)省數(shù)據(jù)面資源成本會部署兩副本,此時依賴于外部 ZK,ETCD,或者 DLedger Controller 這樣的組件作為中心節(jié)點進行選舉。由外置組件裁決成員身份涉及到分布式中兩個重要的問題:1. 如何判斷節(jié)點的狀態(tài)是否正常。2. 如何避免雙主問題。
對于第一個問題,kubernetes 的解決方案相對優(yōu)雅,k8s 對與 Pod 的健康檢查包括存活檢測(Liveness probes)和就緒檢測(Readiness probes),Liveness probes 主要是探測應(yīng)用是否還活著,失敗時重啟 Pod。Readiness probes 來判斷探測應(yīng)用是否接受流量。簡單的心跳機制一般只能實現(xiàn)存活檢測,來看一個例子:假設(shè)有副本組中有 A、B、C 三個副本,另有一個節(jié)點 Q(哨兵) 負責(zé)觀測節(jié)點狀態(tài),同時承擔(dān)了全局選舉與狀態(tài)維護的職責(zé)。節(jié)點 A、B、C 周期性的向 Q 發(fā)送心跳,如果 Q 超過一段時間(一般是兩個心跳間隔 )收不到某個節(jié)點的心跳則認為這個節(jié)點異常。如果異常的是主副本,Q 將副本組的其他副本提升為主并廣播告知其他副本。
在工程實踐中,節(jié)點下線的可能性一般要小于網(wǎng)絡(luò)抖動的可能性。我們假設(shè)節(jié)點 A 是副本組的主,節(jié)點 Q 與節(jié)點 A 之間的網(wǎng)絡(luò)中斷。節(jié)點 Q 認為 A 異常。重新選擇節(jié)點 B 作為新的 Master,并通知節(jié)點 A、B、C 新的 Master 是節(jié)點 B。節(jié)點 A 本身工作正常,與節(jié)點 B、C 之間的網(wǎng)絡(luò)也正常。由于節(jié)點 Q 的通知事件到達節(jié)點 A、B、C 的順序是未知的,假如先達到 B,在這一時刻,系統(tǒng)中同時存在兩個工作的主,一個是 A,另一個是 B。假如此時 A、B 都接收外部請求并與 C 同步數(shù)據(jù),會產(chǎn)生嚴重的數(shù)據(jù)錯誤。上述 "雙主" 問題出現(xiàn)的原因在于雖然節(jié)點 Q 認為節(jié)點 A 異常,但節(jié)點 A 自己不認為自己異常,在舊主新主都接受寫入的時候就產(chǎn)生了日志流的分叉,其問題的本質(zhì)是由于網(wǎng)絡(luò)分區(qū)造成的系統(tǒng)對于節(jié)點狀態(tài)沒有達成一致。
租約是一種避免雙主的有效手段,租約的典型含義是現(xiàn)在中心節(jié)點承認哪個節(jié)點為主,并允許節(jié)點在租約有效期內(nèi)正常工作。如果節(jié)點 Q 希望切換新的主,只需等待前一個主的租約過期,則就可以安全的頒發(fā)新租約給新 Master 節(jié)點,而不會出現(xiàn)雙主問題。這種情況下系統(tǒng)對 Q 本身的可用性訴求非常高,可能會成為集群的性能瓶頸。生產(chǎn)中使用租約還有很多實現(xiàn)細節(jié),例如依賴時鐘同步需要頒發(fā)者的有效期設(shè)置的比接收者的略大,頒發(fā)者本身的切換也較為復(fù)雜。
在 RocketMQ 的設(shè)計中,希望以一種去中心化的設(shè)計降低中心節(jié)點宕機帶來的全局風(fēng)險,(這里認為中心化和是否存在中心節(jié)點是兩件事)所以沒有引入租約機制。在 Controller (對應(yīng)于 Q )崩潰恢復(fù)期間,由于 Broker 對自己身份會進行永久緩存,每個主副本會管理這個副本組的狀態(tài)機,RocketMQ Dledger Controller 這種模式能夠盡量保證在大部分副本組在哨兵組件不可用時仍然不影響收發(fā)消息的核心流程。而舊主由于永久緩存身份,無法降級導(dǎo)致了網(wǎng)絡(luò)分區(qū)時系統(tǒng)必須容忍雙主。產(chǎn)生了多種解決方案,用戶可以通過預(yù)配置選擇 AP 型可用性優(yōu)先,即允許系統(tǒng)通過短時分叉來保障服務(wù)連續(xù)性(下文還會繼續(xù)談?wù)劄槭裁聪⑾到y(tǒng)中分叉很難避免),還是 CP 型一致性優(yōu)先,通過配置最小副本 ack 數(shù)超過集群半數(shù)以上節(jié)點。此時發(fā)送到舊主的消息將因為無法通過 ha 鏈路將數(shù)據(jù)發(fā)送給備,向客戶端返回超時,由客戶端將發(fā)起重試到其他分片。客戶端經(jīng)歷一個服務(wù)發(fā)現(xiàn)的周期之后,客戶端就可以正確發(fā)現(xiàn)新主。
特別的,在網(wǎng)絡(luò)分區(qū)的情況下,例如舊主和備,Controller 之間產(chǎn)生網(wǎng)絡(luò)分區(qū),此時由于沒有引入租約機制,舊主不會自動降級,舊主可以配置為異步雙寫,每一條消息需要經(jīng)過主備的雙重確認才能向客戶端返回成功。而備在切換為主時,會設(shè)置自己只需要單個副本確認的同步寫盤模式。此時,客戶端短時間內(nèi)仍然可以向舊主發(fā)送消息,舊主需要兩副本確認才能返回成功,因此發(fā)送到舊主的消息會返回 SLAVE_NOT_AVAILABLE 的超時響應(yīng),通過客戶端重試將消息發(fā)往新的節(jié)點。幾秒后,客戶端從 NameServer / Controller 獲取新的路由時,舊主從客戶端緩存中移除,此時完成了備節(jié)點的提升。
外置的組件可以對節(jié)點身份進行分配,上圖展示了一個兩副本的副本組上線流程:
RocketMQ 弱依賴 Controller 的實現(xiàn)并不會打破 Raft 中每個 term 最多只有一個 leader 的假設(shè),工程中一般會使用 Leader Lease 解決臟讀的問題,配合 Leader Stickiness 解決頻繁切換的問題,保證主的唯一性。
注:Raft 認為具有最新已提交的日志的節(jié)點才有資格成為 Leader,而 Multi-Paxos 無此限制。
對于日志的連續(xù)性問題,Raft 在確認一條日志之前會通過位點檢查日志連續(xù)性,若檢查到日志不連續(xù)會拒絕此日志,保證日志連續(xù)性,Multi-Paxos 允許日志中有空洞。Raft 在 AppendEntries 中會攜帶 Leader 的 commit index,一旦日志形成多數(shù)派,Leader 更新本地的 commit index(對應(yīng)于 RocketMQ 的 confirm offset)即完成提交,下一條 AppendEntries 會攜帶新的 commit index 通知其它節(jié)點,Multi-Paxos 沒有日志連接性假設(shè),需要額外的 commit 消息通知其它節(jié)點。
除了網(wǎng)絡(luò)分區(qū),很多情況導(dǎo)致日志數(shù)據(jù)流分叉。有如下案例:三副本采用異步復(fù)制,異步持久化,A 為舊主 B C 為備,切換瞬間 B 日志水位大于 C,此時 C 成為新主,B C 副本上的數(shù)據(jù)會產(chǎn)生分叉,因為 B 還多出了一段未確認的數(shù)據(jù)。那么 B 是如何以一個簡單可靠的方法去判斷自己和 C 數(shù)據(jù)分叉的位點?
一個直觀的想法就是,直接將主備的 CommitLog 從前向后逐漸字節(jié)比較,一般生產(chǎn)環(huán)境下,主備都有數(shù)百 GB 的日志文件流,讀取和傳輸大量數(shù)據(jù)的方案費時費力。很快我們發(fā)現(xiàn),確定兩個大文件是否相同的一個好辦法就是比較數(shù)據(jù)的哈希值,需要對比的數(shù)據(jù)量一下子就從數(shù)百 GB 降低為了幾百個哈希值,對于第一個不相同的 CommitLog 文件,還可以采取局部哈希的方式對齊,這里仍然存在一些計算的代價。還有沒有優(yōu)化的空間呢,那就是利用任期 Epoch 和偏移量 StartOffset 實現(xiàn)一個新的截斷算法。這種 Epoch-StartOffset 滿足如下原則:
下面是一個選舉截斷的具體案例,選舉截斷算法思想和流程如下:
主 CommitLog Min = 300,Max = 2500,EpochMap = {<6, 200>, <7, 1200>, <8,2500>}備 CommitLog Min = 300,Max = 2500,EpochMap = {<6, 200>, <7, 1200>, <8,2250>}
實現(xiàn)的代碼如下:
public long findLastConsistentPoint(final EpochStore compareEpoch) { long consistentOffset = -1L; final Map descendingMap = new TreeMap<>(this.epochMap).descendingMap(); for (Map.Entry curLocalEntry : descendingMap.entrySet()) { final EpochEntry compareEntry = compareEpoch.findEpochEntryByEpoch(curLocalEntry.getKey()); if (compareEntry != null && compareEntry.getStartOffset() == curLocalEntry.getValue().getStartOffset()) { consistentOffset = Math.min(curLocalEntry.getValue().getEndOffset(), compareEntry.getEndOffset()); break; } } return consistentOffset;}
故障發(fā)生后,系統(tǒng)將會對分叉數(shù)據(jù)進行修復(fù),有很多小小細節(jié)值得深究與探討。
在實現(xiàn)數(shù)據(jù)截斷的過程中,有一個很特殊的動作,當(dāng)備切主的時候要把 ConsumeQueue 的 Confirm Offset 提升到 CommitLog 的 MaxPhyOffset,即使這一部分數(shù)據(jù)在主上是否被提交是未知的?;叵肫饚啄昵翱?Raft 的時候,當(dāng)一條日志被傳輸?shù)?Follower,F(xiàn)ollower 確認收到這條消息,主再把這條日志應(yīng)用到自己的狀態(tài)機時,通知客戶端和通知所有的 follower 去 commit 這條日志這兩件事是并行的,假如 leader 先回復(fù) client 處理成功,此時 leader 掛了,由于其他 follower 的確認位點 confirm offset 一般會略低于 leader,中間這段未決日志還沒應(yīng)用到 follower 的狀態(tài)機上,這時就出現(xiàn)了狀態(tài)機不一致的情況,即已經(jīng)寫入 leader 的數(shù)據(jù)丟失了。讓我們來舉一個具體的案例,假設(shè)兩副本一主一備:
所以當(dāng)備切換為主的時候,如果直接以 40 進行截斷,意味著客戶端已經(jīng)發(fā)送到服務(wù)端的消息丟失了,正確的水位應(yīng)該被提升至 100。但是備還沒有收到 2.3 的 confirm = 100 的信息,這個行為相當(dāng)于要提交了未決消息。事實上新 leader 會遵守 "Leader Completeness" 的約定,切換時任何副本都不會刪除也不會更改舊 leader 未決的 entry。新 leader 在新的 term 下,會直接應(yīng)用一個較大的版本將未決的 entry 一起提交,這里副本組主備節(jié)點的行為共同保證了復(fù)制狀態(tài)機的安全性。
那么備切換成功的標志是什么,什么時候才能接收 producer 新的流量呢?對于 Raft 來說一旦切換就可以,對于 RocketMQ 來說這個階段會被稍稍推遲,即索引已經(jīng)完全構(gòu)建結(jié)束的時候。RocketMQ 為了保證構(gòu)建 consume queue 的一致性,會在 CommitLog 中記錄 consume queue offset 的偏移量,此時 confirm offset 到 max offset 間的數(shù)據(jù)是副本作為備來接收的,這部分消息在 consume queue 中的偏移量已經(jīng)固定下來了,而 producer 新的流量時由于 RocketMQ 預(yù)計算位點的優(yōu)化,等到消息實際放入 CommitLog 的再真實的數(shù)據(jù)分發(fā)(dispatch)的時候就會發(fā)現(xiàn)對應(yīng)位置的 consume queue 已經(jīng)被占用了,此時就造成了主備索引數(shù)據(jù)不一致。本質(zhì)原因是 RocketMQ 存儲層預(yù)構(gòu)建索引的優(yōu)化對日志有一些侵入性,但切換時短暫等待的代價遠遠小于正常運行時提速的收益。
a. 元數(shù)據(jù)變更是否依賴于日志
目前 RocketMQ 對于元數(shù)據(jù)是在內(nèi)存中單獨管理的,備機間隔 5 秒向當(dāng)前的主節(jié)點同步數(shù)據(jù)。例如當(dāng)前主節(jié)點上創(chuàng)建了一個臨時 Topic 并接受了一條消息,在一個同步周期內(nèi)這個 Topic 又被刪除了,此時主備節(jié)點元數(shù)據(jù)可能不一致。又比如位點更新的時候,對于單個隊列而言,多副本架構(gòu)中存在多條消費位點更新鏈路,Consumer 拉取消息時更新,Consumer 主動向 broker 更新,管控重置位點,HA 鏈路更新,當(dāng)副本組發(fā)生主備切換時,consumer group 同時發(fā)生 consumer 上下線,由于路由發(fā)現(xiàn)的時間差,還可能造成同一個消費組兩個不同 consumer 分別消費同一副本組主備上同一個隊列的情況。
原因在于備機重做元數(shù)據(jù)更新和消息流這兩件事是異步的,這有一定概率會造成臟數(shù)據(jù)。由于 RocketMQ 單個節(jié)點上 Topic / Group 數(shù)量較多,通過日志的實現(xiàn)會導(dǎo)致持久化的數(shù)據(jù)量很大,在復(fù)雜場景下基于日志做回滾依賴 snapshot 機制也會增加計算開銷和恢復(fù)時間。這個問題和數(shù)據(jù)庫很像,MySQL 在執(zhí)行 DDL 修改元數(shù)據(jù)時通過會創(chuàng)建 MDL 鎖,阻塞用戶其他操作訪問表空間的訪問。備庫同步主庫也會加鎖,元數(shù)據(jù)修改開始點和結(jié)束點所代表的兩個日志并不是一個原子操作,這意味著主庫上在修改元數(shù)據(jù)的過程中如果宕機了,備庫上持有的 MDL 鎖就無法釋放。MySQL 的解決方案是在主庫每次崩潰恢復(fù)后,都寫一條特殊的日志,通知所有連接的備庫釋放其持有的所有 MDL 排他鎖。對所有操作都走日志流進行狀態(tài)機復(fù)制要求存儲層有多種日志類型,實現(xiàn)也更加復(fù)雜。RocketMQ 選擇以另一種同步的模式操作,即類似 ZAB 這樣二階段協(xié)議,例如位點更新時的可以選擇配置 LockInStrictMode 讓備都同步這條修改。事實上 RocketMQ 為了優(yōu)化上述位點跳躍的現(xiàn)象,客戶端在未重啟時,遇到服務(wù)端主備切換還會用優(yōu)先采納本地位點的方式獲取消息,進一步減少重復(fù)消費。
b. 同步復(fù)制與異步復(fù)制
同步復(fù)制的含義是用戶的一個操作在多個副本上都已經(jīng)提交。正常情況下,假設(shè)一個副本組中的 3 個副本都要對相同一個請求進行確認,相當(dāng)于數(shù)據(jù)寫透 3 個副本(簡稱 3-3 寫),3-3 寫提供了非常高的數(shù)據(jù)可靠性,但是把所有從節(jié)點都配置為同步復(fù)制時任何一個同步節(jié)點的中斷都會導(dǎo)致整個副本組處理請求失敗。當(dāng)?shù)谌齻€副本是跨可用區(qū)時,長尾也會帶來一定的延遲。
異步復(fù)制模式下,尚未復(fù)制到從節(jié)點的寫請求都會丟失。向客戶端確認的寫操作也無法保證被持久化。異步復(fù)制是一種故障時 RPO 不為 0的配置模式,由于不用考慮從節(jié)點上的狀態(tài),總是可以繼續(xù)響應(yīng)寫請求,系統(tǒng)的延遲更低,吞吐性能更好。為了權(quán)衡兩者,通常只有其中一個從節(jié)點是同步的,而其他節(jié)點是異步的模式。只要同步的從節(jié)點變得不可用或性能下降,則將另一個異步的從節(jié)點提升為同步模式。這樣可以保證至少有兩個節(jié)點(即主節(jié)點和一個同步從節(jié)點)擁有最新的數(shù)據(jù)副本。這種模式稱為 2-3 寫,能幫助避免抖動,提供更好的延遲穩(wěn)定性,有時候也叫稱為半同步。
在 RocketMQ 的場景中,異步復(fù)制也被廣泛應(yīng)用在消息讀寫比極高,從節(jié)點數(shù)量多或者異地多副本場景。同步復(fù)制和異步復(fù)制是通過 Broker 配置文件里的 brokerRole 參數(shù)進行設(shè)置的,這個參數(shù)可以被設(shè)置成 ASYNC_MASTER、SYNC_MASTER、SLAVE 三個值中的一個。實際應(yīng)用中要結(jié)合業(yè)務(wù)場景合理設(shè)置持久化方式和主從復(fù)制方式,通常,由于網(wǎng)絡(luò)的速度高于本地 IO 速度,采用異步持久化和同步復(fù)制是一個權(quán)衡性能與可靠性的設(shè)置。
c. 副本組自適應(yīng)降級
同步復(fù)制的含義是一條數(shù)據(jù)同時被主備確認才返回用戶操作成功,可以保證主宕機后消息還在備中,適合可靠性要求較高的場景,同步復(fù)制還可以限制未同步的數(shù)據(jù)量以減少 ha 鏈路的內(nèi)存壓力,缺點則是副本組中的某一個備出現(xiàn)假死就會影響寫入。異步復(fù)制無需等待備確認,性能高于同步復(fù)制,切換時未提交的消息可能會丟失(參考前文的日志分叉)。在三副本甚至五副本且對可靠性要求高的場景中無法采用異步復(fù)制,采用同步復(fù)制需要每一個副本確認后才會返回,在副本數(shù)多的情況下嚴重影響效率。關(guān)于一條消息需要被多少副本確認這個問題,RocketMQ 服務(wù)端會有一些數(shù)量上的配置來進行靈活調(diào)整:
因此,RocketMQ 提出了副本組在同步復(fù)制的模式下,也可以支持副本組的自適應(yīng)降級(參數(shù)名稱為 enableAutoInSyncReplicas)來適配消息的特殊場景。當(dāng)副本組中存活的副本數(shù)減少或日志流水位差距過大時進行自動降級,最小降級到 minInSyncReplicas 副本數(shù)。比如在兩副本下配置 。對于正常情況下,兩個副本會處于同步復(fù)制,當(dāng)備下線或假死時,會進行自適應(yīng)降級,保證主節(jié)點還能正常收發(fā)消息,這個功能為用戶提供了一個可用性優(yōu)先的選擇。
d. 輕量級心跳與快速隔離
在 RocketMQ v4.x 版本的實現(xiàn)中,Broker 周期性的(間隔 30 秒)將自身的所有 Topic 序列化并傳輸?shù)?NameServer 注冊進行?;睢S捎?Broker 上 Topic 的元數(shù)據(jù)規(guī)模較大,帶來了較大的網(wǎng)絡(luò)流量開銷,Broker 的注冊間隔不能設(shè)置的太短。同時 NameServer 對 Broker 是采取延遲隔離機制,防止 NameServer 網(wǎng)絡(luò)抖動時可能瞬間移除所有 Broker 的注冊信息,引發(fā)服務(wù)的雪崩。默認情況下異常主宕機時超過 2 分鐘,或者備切換為主重新注冊后才會替換。容錯設(shè)計的同時導(dǎo)致 Broker 故障轉(zhuǎn)移緩慢,RocketMQ v5.0 版本引入輕量級心跳(參數(shù)liteHeartBeat),將 Broker 的注冊行為與 NameServer 的心跳進行了邏輯拆分,將心跳間隔減小到 1 秒。當(dāng) NameServer 間隔 5 秒(可配置)沒有收到來自 Broker 的心跳請求就對 Broker 進行移除,使異常場景下自愈的時間從分鐘級縮短到了秒級。
最早的時候,RocketMQ 基于 Master-Slave 模式提供了主備部署的架構(gòu),這種模式提供了一定的高可用能力,在 Master 節(jié)點負載較高情況下,讀流量可以被重定向到備機。由于沒有選主機制,在 Master 節(jié)點不可用時,這個副本組的消息發(fā)送將會完全中斷,還會出現(xiàn)延遲消息、事務(wù)消息、Pop 消息等二級消息無法消費或者延遲。此外,備機在正常工作場景下資源使用率較低,造成一定的資源浪費。為了解決這些問題,社區(qū)提出了在一個 Broker 進程內(nèi)運行多個 BrokerContainer,這個設(shè)計類似于 Flink 的 slot,讓一個 Broker 進程上可以以 Container 的形式運行多個節(jié)點,復(fù)用傳輸層的連接,業(yè)務(wù)線程池等資源,通過單節(jié)點主備交叉部署來同時承擔(dān)多份流量,無外部依賴,自愈能力強。這種方式下隔離性弱于使用原生容器方式進行隔離,同時由于架構(gòu)的復(fù)雜度增加導(dǎo)致了自愈流程較為復(fù)雜。
另一條演進路線則是基于可切換的,RocketMQ 也嘗試過依托于 Zookeeper 的分布式鎖和通知機制進行 HA 狀態(tài)的管理。引入外部依賴的同時給架構(gòu)帶來了復(fù)雜性,不容易做小型化部署,部署運維和診斷的成本較高。另一種方式就是基于 Raft 在集群內(nèi)自動選主,Raft 中的副本身份被透出和復(fù)用到 Broker Role 層面去除外部依賴,然而強一致的 Raft 版本并未支持靈活的降級策略,無法在 C 和 A 之間靈活調(diào)整。兩種切換方案都是 CP 設(shè)計,犧牲高可用優(yōu)先保證一致性。主副本下線時選主和路由定時更新策略導(dǎo)致整個故障轉(zhuǎn)移時間依然較長,Raft 本身對三副本的要求也會面臨較大的成本壓力,RocketMQ 原生的 TransientPool,零拷貝等一些用來避免減少 IO 壓力的方案在 Raft 下無法有效使用。
RocketMQ DLedger 融合模式是 RocketMQ 5.0 演進中結(jié)合上述兩條路線后的一個系統(tǒng)的解決方案。核心的特性有以下幾點:
幾種實現(xiàn)對比表如下:
仔細閱讀 RocketMQ 的源碼,其實大家也會發(fā)現(xiàn) RocketMQ 在各種邊緣問題處理上細節(jié)滿滿,節(jié)點失效,網(wǎng)絡(luò)抖動,副本一致性,持久化,可用性與延遲之間存在各種細微的權(quán)衡,這也是 RocketMQ 多年來在生產(chǎn)環(huán)境下所積累的核心競爭力之一。隨著分布式技術(shù)的進一步發(fā)展,更多更有意思的技術(shù),如基于 RDMA 網(wǎng)絡(luò)的復(fù)制協(xié)議也呼之欲出。RocketMQ 將與社區(qū)協(xié)同進步,發(fā)展為 “消息,事件,流” 一體化的融合平臺。
參考文檔:
https://lamport.azurewebsites.net/pubs/paxos-simple.pdf
https://github.com/sofastack/sofa-jraft
https://pulsar.apache.org/zh-CN/docs/next/concepts-replication
https://pulsar.apache.org/zh-CN/docs/next/administration-metadata-store
https://kafka.apache.org/documentation/#persistence
https://kafka.apache.org/documentation/#basic_ops_leader_balancing
https://azure.microsoft.com/en-us/blog/sosp-paper-windows-azure-storage-a-highly-available-cloud-storage-service-with-strong-consistency/
https://www.cs.utah.edu/~lifeifei/papers/polardbserverless-sigmod21.pdf
RocketMQ 學(xué)習(xí)社區(qū)重磅上線!AI 互動,一秒了解 RocketMQ 功能源碼。RocketMQ 學(xué)習(xí)社區(qū)是國內(nèi)首個基于 AIGC 提供的知識服務(wù)社區(qū),旨在成為 RocketMQ 學(xué)習(xí)路上的“貼身小二”。
PS:RocketMQ 社區(qū)以 RocketMQ 5.0 資料為主要訓(xùn)練內(nèi)容,持續(xù)優(yōu)化迭代中,回答內(nèi)容均由人工智能模型生成,其準確性和完整性無法保證,且不代表 RocketMQ 學(xué)習(xí)社區(qū)的態(tài)度或觀點。
點擊此處,立即體驗 RocketMQ 學(xué)習(xí)社區(qū)(建議 PC 端體驗完整功能)
標簽: