Pulsar 的共識機制和成員管理
參考:
Jack Vanlightly 的這篇博客雖然是講的BookKeeper错英,但實際上述吸,整體還是Pulsar的內容偏多。因為單純的BookKeeper本身并不涉及到共識機制和成員管理的內容签舞。
共識機制與存儲分離
對比一下 Raft 和 Kafka 的共識方式绢陌,如下圖,
Raft 和 Kafka 的參與共識的節(jié)點和數(shù)據(jù)存儲的節(jié)點是合一的,一個節(jié)點即參與共識,也完成數(shù)據(jù)存儲圾浅。數(shù)據(jù)寫入請求都是先發(fā)送到 Leader 節(jié)點,再同步到 Follower 節(jié)點憾朴。這樣的設計狸捕,成為內置(internal)共識機制。
這個設計的一個很大的問題就是集群的成員管理相對固定伊脓,博客的原文是府寒,
A byproduct of replication being performed by stateful fully integrated nodes is that cluster membership is relatively static.
如果想要增加或者刪除節(jié)點,就會比較受限报腔,非常麻煩株搔。Pulsar 的設計是不一樣的,分離了共識節(jié)點和存儲節(jié)點纯蛾。存儲節(jié)點是單純的存儲功能纤房,不需要處理自己在共識機制中的角色(實際上也沒有角色),也不需要處理存儲數(shù)據(jù)復制的問題翻诉。共識機制放在了客戶端炮姨,也就是 Pulsar Broker 上處理,如下碰煌,
對 Bookie 節(jié)點來說舒岸,Bookie 本身只是存儲,提供通用的 Entry 存儲功能芦圾。共識機制放在 Broker 處理蛾派,也就是說,Broker 在維護 Leader 的角色个少。
對 Raft 來說洪乍,如果節(jié)點故障一段時間后恢復,只需要將 Leader 節(jié)點上的數(shù)據(jù)復制過去就可以恢復節(jié)點夜焦,但 Pulsar 的 Leader 因為沒有存儲數(shù)據(jù),處理起來就會非常麻煩(后文詳述)。
但是因為分離了共識機制和存儲妈踊,也會帶來好處瓷炮,Broker 可以快速的改變成員,來應對數(shù)據(jù)節(jié)點的故障科平,這個特性稱為動態(tài)成員管理褥紫。那 Pulsar 是如何實現(xiàn)動態(tài)成員管理的特性的呢?
Commit Index
比較 Raft瞪慧、Kafka髓考、Pulsar,這三者都有 Commit Index 的概念弃酌,雖然在不同系統(tǒng)中的名稱不一樣氨菇。其基本特點是,都需要達到一定數(shù)量的寫入成功妓湘,才可以確認 Commit Index查蓉,比較一下,
- Raft 需要集群的大多數(shù)節(jié)點確認榜贴,才可以 Commit Entry豌研;
- Kafka 需要 min-insync-replicas 個確認妹田,才能確認持久化成功;
- Pulsar 需要 Ack Quorum (AQ) 個確認鹃共,才確認 Commit Entry鬼佣;
為了方便描述,這個數(shù)量在后文統(tǒng)一稱為提交數(shù)量(Commit Quorum)霜浴。
數(shù)據(jù)確認提交后晶衷,會返回 Entry Log 中提交成功的點,Raft 稱呼這個點叫 Commit Index阴孟;Kafka 稱之為高水位(High Watermark)晌纫;BookKeeper 稱之為 LAC(Last Add Confirmed)。這個機制的存在主要是保護各個節(jié)點數(shù)據(jù)的一致性永丝。
對于 Raft 和 Kafka锹漱,Commit Index 是和數(shù)據(jù)一起從 Leader 傳輸?shù)?Follower,最后慕嚷,所有的節(jié)點都更新到了 Commit Index 和數(shù)據(jù)凌蔬。Leader 對 Commit Index 的認知是略微超前 Follower 的,但通常情況下闯冷,這也沒問題砂心。如下圖,
對 Pulsar 來說蛇耀,存儲和共識機制是分離的辩诞,LAC 需要存儲在 Bookie 中,但這些信息對 Bookie 來說幾乎是沒有用的纺涤。寫入的過程也比較有特色译暂,順序是:寫入數(shù)據(jù) -> 更新 Leader Broker 的 LAC -> 更新 Bookie 的 LAC 存儲。相比 Raft 和 Kafka撩炊,要多了一個步驟外永,不過由于可以流水線組合處理,實際性能也不會差拧咳。如下圖伯顶,
Raft Commit Index 的部分似乎并不精確,Raft 會收到大多數(shù)節(jié)點的 Ack 之后骆膝,才會更新 Commit Index祭衩。Leader 當然會比 Follower 更早知道下一個 Commit Index,但只有收到 Follower 的 Ack阅签,才可以確認這個 Commit Index掐暮。
Kafka 的數(shù)據(jù)副本特性
動態(tài)成員管理可以解決什么樣的問題?主要是副本創(chuàng)建政钟、數(shù)據(jù)復制的靈活性路克,先看看 Raft 的例子樟结。
當部分節(jié)點緩慢,或者故障的時候精算,由于 Raft 選擇的是相對固定的成員管理方案狭吼,無法及時變更成員。為了提升系統(tǒng)整體的可用性殖妇,只能放棄一些安全性,選擇了在有少量節(jié)點破花,不超過一半谦趣,有故障的前提下,依然可以提供服務座每。
Kafka 的選擇也很類似前鹅,只要 min-insync-replicas 個節(jié)點能正常運行,就可以提供服務峭梳,不需要整個副本集合完全正常運行舰绘。
但是,這樣的選擇會破壞 liveness捂寿。liveness 是分布式算法領域的屬性孵运,用來衡量分布式算法的好壞,比如治笨,
- liveness: something good eventually happens. 或者說驳概,算法期望的結果最終將發(fā)生;
- safty: nothing bad happens. 或者說顺又,不會發(fā)生惡性結果等孵;
對 Raft 和 Kafka 來說稚照,liveness 要求,數(shù)據(jù)將在所有節(jié)點上最終復制成功俯萌。達到這個目標,有以下一些事實前提绳瘟,
- Entry 按時間順序追加在 Leader 的日志/Entry文件中;
- Leader 將日志/Entry發(fā)送給 Follower斤彼,要求 Follower 按同樣的順序進行存儲;
- 提交的日志/Entry達到大多數(shù)(Raft)嘲玫,或者 min-insync-replicas(Kafka) 個時并扇,就不考慮丟失;
- Follower 節(jié)點上的日志/Entry文件和 Leader 完全一致土陪,特指從 Commit Index 開始往前的部分肴熏;
對于這樣一個復制機制,從第一個節(jié)點寫入 Entry 開始源哩,到 Entry 復制到所有副本上鸦做,Entry 通常會處于3種狀態(tài),如下崩侠,
最開始坷檩,Entry 追加到頭部,但還沒有更新 Commit Index系瓢,可能丟失句灌;然后,Leader 收到足夠多的 Ack骗绕,更新 Commit Index资昧,Entry 就持久化到了足夠多的副本中,不會丟失了撤缴;最后,Entry 持久化到了所有副本中屈呕,處理完成。從上圖來看蟋软,在系統(tǒng)運行的任何一個時刻嗽桩,我們有,
Uncommit Index(Head) >= Commit Index >= Full-replicated Index
對系統(tǒng)使用來說,我們希望 Uncommit Index(Head)种樱、Commit Index俊卤、Full-replicated Index 盡量接近,Tail 部分盡量大岂昭。但事與愿違狠怨,如果幾個節(jié)點中佣赖,有一個比較慢,始終追趕不上其他節(jié)點同步副本的速度外傅,或者有一個節(jié)點故障后丟失數(shù)據(jù)俩檬,需要從零開始恢復,這個時候技竟,我們只能依賴副本復制機制屈藐。在復制期間,liveness 是受損的瓷患。
Pulsar 的數(shù)據(jù)副本特性
Pulsar 是通過寫入數(shù)量(Write Quorum)和響應數(shù)量(Ack Quorum)來管理副本的擅编,細節(jié)這里不介紹了。
對 Pulsar 來說谭贪,分離共識機制和存儲锦担,使得共識機制在無存儲的 Broker 上實現(xiàn),當發(fā)現(xiàn)一個 Bookie 節(jié)點不可用的時候套媚,就馬上選取另一個 Bookie 替代磁椒。如下浆熔,
對集群的整體改動,也就是修改一下 Zookeeper 中賬本的元數(shù)據(jù)慎皱,然后重新發(fā)送一下 Uncommitted Entry 去新 Bookie 就好了(因為已經 Commit 的 Entry 認為不需要再同步了)宝冕。這會導致 Ledger 的數(shù)據(jù)被分成多份邓萨,稱之為 Fragment,放在多個 Bookie 上宝剖。每次有 Bookie 故障歉甚,就發(fā)起成員變更,并切分一個 Fragment 出來赖钞,以應對這個問題。如下圖弓千,一個 Ledger 分為了4個 Fragment献起,
當仔細分析 Ledger 頭部的 Entry 存儲時谴餐,我們會發(fā)現(xiàn)結構和 Kafka 類似,如下汁展,
當成員變更發(fā)生時食绿,Bookie 會丟棄尚未達到 Ack Quorum 的 Entry年枕,只保留 Commit Index 之前的部分熏兄。新的 Bookie 從 Commit Index 之后開始树姨,如下圖,
成員變更時硝清,Pulsar 并不會處理 Full-replicated Index 和 Commit Index 之間的數(shù)據(jù)芦拿,于是導致 Bookie 上多個 Fragment 的存儲可能尾部出現(xiàn)空洞查邢,變成這樣,
Pulsar 不在成員變更的時候處理這個空洞的原因也是很自然的缓苛,因為很有可能此時 Bookie 處于故障階段未桥,Pulsar 即使想補足數(shù)據(jù)也是很難做到的∩嗖耍空洞的數(shù)據(jù)最終會由單獨的恢復程序補足淆党,但這并不是主流程的一部分染乌。對比一下 Kafka 和 Pulsar 的存儲結構圖,
雖然 Pulsar 的存儲中間會發(fā)生空洞台颠,但是當發(fā)現(xiàn)節(jié)點無法及時返回的時候勒庄,Pulsar 會立刻開始調整成員实蔽,恢復到正常的副本數(shù),所以每個空洞都不會很大坛吁;而 Kafka 的存儲雖然沒有空洞铐尚,但是可能出現(xiàn) Full-replicated Index 遠遠落后于 Commit Index 的情況∶蛋颍可見 Pulsar 的設計對系統(tǒng)整體的 liveness 有改進爹脾。
當 WQ=AQ 時
出于數(shù)據(jù)安全性的考慮,可能 WQ=AQ 是一個選擇解阅。假設 WQ=AQ=3闷串,如果有節(jié)點發(fā)生故障,我們可以得到下圖桨武,
可以看到锈津,由于 WQ=AQ琼梆,那么,就始終有 Commit Index == Full-replicated Index错览,不會有節(jié)點處于未被完全同步的狀態(tài)煌往。如果 Bookie 發(fā)生故障,還是從 Commit Index 開始新的 Fragment羞海,數(shù)據(jù)的空洞問題解決了曲管。當然院水,這個方法也有問題。
比如昧穿,必須準備有足夠多的 Bookie 節(jié)點橙喘,否則當節(jié)點故障的時候厅瞎,如果沒有新 Bookie 的加入初坠,就無法繼續(xù)運行。
還有锁保,可用性會有小幅度降低,因為在成員變更的時候吴菠,取決于 Zookeeper 操作的性能浩村,在此期間心墅,Broker是無法運行的。
注:這兩個原因有些似是而非瘫筐,在 AQ < WQ 的時候铐姚,也會有一樣的問題谦屑。當 AQ < WQ 時,如果發(fā)生故障酝枢,會延遲一段時間進行成員變更悍手,這一小段的可用性是可以保證的,但這個時間幾乎可以忽略不計竣付。還有古胆,因為 Ack 數(shù)量增多筛璧,系統(tǒng)延遲必然變大,但是如果 Bookie 的性能可以跟得上的話棺牧,其實延遲問題應該不顯著朗儒。
雖然,從系統(tǒng)整體上來看乏悄,我也認為 AQ < WQ 是最佳實踐纲爸。
后記
Pulsar 的動態(tài)成員管理功能是一個很好的功能,但是其來源并不是存儲與共識機制分離负蚊,而是單 Topic 多 Ledger家妆,單 Ledger 多 Fragment 等特性的一個具體的應用而已冕茅。