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