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

- 20歲的民生保險 一個向光而行的追夢者2023年6月18日,是民生保險開業20周年的生日。民生保險從源起...
- 業之峰橋牌隊又雙叒叕拿冠軍了!6月16-18日,浙江省第十七屆南潯杯橋牌公開賽暨第二屆水晶晶...
- 傳謠者公開道歉,“DR鉆戒購買記錄可刪”被證實是謠言很多正在崛起的品牌,都有過被頻繁地造謠的經歷。其中,DR鉆...
- 臺鈴營銷勢能再加碼:王一博成為臺鈴首位全球代言人6月20日,臺鈴集團正式官宣代言人,全能藝人王一博成為臺鈴品牌...
- 尋味成都之宮廷糕點--三十年老字號 成都人鐘愛的味道說起成都,除了千年古城、神獸大熊貓、神奇川劇變臉 ...
- ipo上市是什么意思?ipo和直接上市有什么區別?
2023-06-21 11:47:33
- 出水芙蓉最佳買入形態?出水芙蓉上漲概率大嗎?
2023-06-20 16:14:53
- visa信用卡是什么?不出國visa信用卡有什么用?
2023-06-16 16:10:22
- 跌停能賣出嗎?漲停跌停還能交易嗎?
2023-06-14 15:46:11
- 按揭轉抵押有什么優缺點?按揭轉抵押有什么風險?
2023-06-09 16:31:22