Pulsar、Kafka和Redis消息隊列對比
一、最基礎的隊列
最基礎的消息隊列其實就是一個雙端隊列,我們可以用雙向鏈表來實現,如下圖所示:

push_front:添加元素到隊首;
pop_tail:從隊尾取出元素。
有了這樣的數據結構之后,我們就可以在內存中構建一個消息隊列,一些任務不停地往隊列里添加消息,同時另一些任務不斷地從隊尾有序地取出這些消息。添加消息的任務我們稱為producer,而取出并使用消息的任務,我們稱之為consumer。
要實現這樣的內存消息隊列并不難,甚至可以說很容易。但是如果要讓它能在應對海量的并發讀寫時保持高效,還是需要下很多功夫的。
二、Redis的隊列
redis剛好提供了上述的數據結構——list。redis list支持:
lpush:從隊列左邊插入數據;
rpop:從隊列右邊取出數據。
這正好對應了我們隊列抽象的push_front和pop_tail,因此我們可以直接把redis的list當成一個消息隊列來使用。而且redis本身對高并發做了很好的優化,內部數據結構經過了精心地設計和優化。所以從某種意義上講,用redis的list大概率比你自己重新實現一個list強很多。
但另一方面,使用redis list作為消息隊列也有一些不足,比如:
消息持久化:redis是內存數據庫,雖然有aof和rdb兩種機制進行持久化,但這只是輔助手段,這兩種手段都是不可靠的。當redis服務器宕機時一定會丟失一部分數據,這對于很多業務都是沒法接受的。
熱key性能問題:不論是用codis還是twemproxy這種集群方案,對某個隊列的讀寫請求最終都會落到同一臺redis實例上,并且無法通過擴容來解決問題。如果對某個list的并發讀寫非常高,就產生了無法解決的熱key,嚴重可能導致系統崩潰。
沒有確認機制:每當執行rpop消費一條數據,那條消息就被從list中永久刪除了。如果消費者消費失敗,這條消息也沒法找回了。你可能說消費者可以在失敗時把這條消息重新投遞到進隊列,但這太理想了,極端一點萬一消費者進程直接崩了呢,比如被kill -9,panic,coredump…
不支持多訂閱者:一條消息只能被一個消費者消費,rpop之后就沒了。如果隊列中存儲的是應用的日志,對于同一條消息,監控系統需要消費它來進行可能的報警,BI系統需要消費它來繪制報表,鏈路追蹤需要消費它來繪制調用關系……這種場景redis list就沒辦法支持了。
不支持二次消費:一條消息rpop之后就沒了。如果消費者程序運行到一半發現代碼有bug,修復之后想從頭再消費一次就不行了。
對于上述的不足,目前看來第一條(持久化)是可以解決的。很多公司都有團隊基于rocksdb leveldb進行二次開發,實現了支持redis協議的kv存儲。這些存儲已經不是redis了,但是用起來和redis幾乎一樣。它們能夠保證數據的持久化,但對于上述的其他缺陷也無能為力了。
其實redis 5.0開始新增了一個stream數據類型,它是專門設計成為消息隊列的數據結構,借鑒了很多kafka的設計,但是依然還有很多問題…直接進入到kafka的世界它不香嗎?
三、Kafka
從上面你可以看到,一個真正的消息中間件不僅僅是一個隊列那么簡單。尤其是當它承載了公司大量業務的時候,它的功能完備性、吞吐量、穩定性、擴展性都有非常苛刻的要求。kafka應運而生,它是專門設計用來做消息中間件的系統。
前面說redis list的不足時,雖然有很多不足,但是如果你仔細思考,其實可以歸納為兩點:
熱key的問題無法解決,即:無法通過加機器解決性能問題;
數據會被刪除:rpop之后就沒了,因此無法滿足多個訂閱者,無法重新從頭再消費,無法做ack。
這兩點也是kafka要解決的核心問題。
熱key的本質問題是數據都集中在一臺實例上,所以想辦法把它分散到多個機器上就好了。為此,kafka提出了partition的概念。一個隊列(redis中的list),對應到kafka里叫topic。kafka把一個topic拆成了多個partition,每個partition可以分散到不同的機器上,這樣就可以把單機的壓力分散到多臺機器上。因此topic在kafka中更多是一個邏輯上的概念,實際存儲單元都是partition。
其實redis的list也能實現這種效果,不過這需要在業務代碼中增加額外的邏輯。比如可以建立n個list,key1, key2, ..., keyn,客戶端每次往不同的key里push,消費端也可以同時從key1到keyn這n個list中rpop消費數據,這就能達到kafka多partition的效果。所以你可以看到,partition就是一個非常樸素的概念,用來把請求分散到多臺機器。
redis list中另一個大問題是rpop會刪除數據,所以kafka的解決辦法也很簡單,不刪就行了嘛。kafka用游標(cursor)解決這個問題。

和redis list不同的是,首先kafka的topic(實際上是partion)是用的單向隊列來存儲數據的,新數據每次直接追加到隊尾。同時它維護了一個游標cursor,從頭開始,每次指向即將被消費的數據的下標。每消費一條,cursor+1 。通過這種方式,kafka也能和redis list一樣實現先入先出的語義,但是kafka每次只需要更新游標,并不會去刪數據。
這樣設計的好處太多了,尤其是性能方面,順序寫一直是最大化利用磁盤帶寬的不二法門。但我們主要講講游標這種設計帶來功能上的優勢。
首先可以支持消息的ACK機制了。由于消息不會被刪除,因此可以等消費者明確告知kafka這條消息消費成功以后,再去更新游標。這樣的話,只要kafka持久化存儲了游標的位置,即使消費失敗進程崩潰,等它恢復時依然可以重新消費
第二是可以支持分組消費:

這里需要引入一個消費組的概念,這個概念非常簡單,因為消費組本質上就是一組游標。對于同一個topic,不同的消費組有各自的游標。監控組的游標指向第二條,BI組的游標指向第4條,trace組指向到了第10000條……各消費者游標彼此隔離,互不影響。
通過引入消費組的概念,就可以非常容易地支持多業務方同時消費一個topic,也就是說所謂的1-N的“廣播”,一條消息廣播給N個訂閱方。
最后,通過游標也很容易實現重新消費。因為游標僅僅就是記錄當前消費到哪一條數據了,要重新消費的話直接修改游標的值就可以了。你可以把游標重置為任何你想要指定的位置,比如重置到0重新開始消費,也可以直接重置到最后,相當于忽略現有所有數據。
因此你可以看到,kafka這種數據結構相比于redis的雙向鏈表有了一個質的飛躍,不僅是性能上,同時也是功能上,全面的領先。
我們可以來看看kafka的一個簡單的架構圖:

從這個圖里我們可以看出,topic是一個邏輯上的概念,不是一個實體。一個topic包含多個partition,partition分布在多臺機器上。這個機器,kafka中稱之為broker。(kafka集群中的一個broker對應redis集群中的一個實例)。對于一個topic,可以有多個不同的消費組同時進行消費。一個消費組內部可以有多個消費者實例同時進行消費,這樣可以提高消費速率。
但是這里需要非常注意的是,一個partition只能被消費組中的一個消費者實例來消費。換句話說,消費組中如果有多個消費者,不能夠存在兩個消費者同時消費一個partition的場景。
為什么呢?其實kafka要在partition級別提供順序消費的語義,如果多個consumer消費一個partition,即使kafka本身是按順序分發數據的,但是由于網絡延遲等各種情況,consumer并不能保證按kafka的分發順序接收到數據,這樣達到消費者的消息順序就是無法保證的。因此一個partition只能被一個consumer消費。kafka各consumer group的游標可以表示成類似這樣的數據結構:
{"topic-foo": {"groupA": {"partition-0":0,"partition-1":123,"partition-2":78 },"groupB": {"partition-0":85,"partition-1":9991,"partition-2":772 }, }}
了解了kafka的宏觀架構,你可能會有個疑惑,kafka的消費如果只是移動游標并不刪除數據,那么隨著時間的推移數據肯定會把磁盤打滿,這個問題該如何解決呢?這就涉及到kafka的retention機制,也就是消息過期,類似于redis中的expire。
不同的是,redis是按key來過期的,如果你給redis list設置了1分鐘有效期,1分鐘之后redis直接把整個list刪除了。而kafka的過期是針對消息的,不會刪除整個topic(partition),只會刪除partition中過期的消息。不過好在kafka的partition是單向的隊列,因此隊列中消息的生產時間都是有序的。因此每次過期刪除消息時,從頭開始刪就行了。
看起來似乎很簡單,但仔細想一下還是有不少問題。舉例來說,假如topicA-partition-0的所有消息被寫入到一個文件中,比如就叫topicA-partition-0.log。我們再把問題簡化一下,假如生產者生產的消息在topicA-partition-0.log中一條消息占一行,很快這個文件就到200G了。現在告訴你,這個文件前x行失效了,你應該怎么刪除呢?非常難辦,這和讓你刪除一個數組中的前n個元素一樣,需要把后續的元素向前移動,這涉及到大量的CPU copy操作。假如這個文件有10M,這個刪除操作的代價都非常大,更別說200G了。
因此,kafka在實際存儲partition時又進行了一個拆分。topicA-partition-0的數據并不是寫到一個文件里,而是寫到多個segment文件里。假如設置的一個segment文件大小上限是100M,當寫滿100M時就會創建新的segment文件,后續的消息就寫到新創建的segment文件,就像我們業務系統的日志文件切割一樣。這樣做的好處是,當segment中所有消息都過期時,可以很容易地直接刪除整個文件。而由于segment中消息是有序的,看是否都過期就看最后一條是否過期就行了。
1. Kafka中的數據查找
topic的一個partition是一個邏輯上的數組,由多個segment組成,如下圖所示:

這時候就有一個問題,如果我把游標重置到一個任意位置,比如第2897條消息,我怎么讀取數據呢?
根據上面的文件組織結構,你可以發現我們需要確定兩件事才能讀出對應的數據:
第2897條消息在哪個segment文件里;
第2897條消息在segment文件里的什么位置。
為了解決上面兩個問題,kafka有一個非常巧妙的設計。首先,segment文件的文件名是以該文件里第一條消息的offset來命名的。一開始的segment文件名是 0.log,然后一直寫直到寫了18234條消息后,發現達到了設置的文件大小上限100M,然后就創建一個新的segment文件,名字是18234.log……
- /kafka/topic/order_create/partition-0-0.log-18234.log #segment file-39712.log-54101.log
當我們要找offset為x的消息在哪個segment時,只需要通過文件名做一次二分查找就行了。比如offset為2879的消息(第2880條消息),顯然就在0.log這個segment文件里。
定位到segment文件之后,另一個問題就是要找到該消息在文件中的位置,也就是偏移量。如果從頭開始一條條地找,這個耗時肯定是無法接受的!kafka的解決辦法就是索引文件。
就如mysql的索引一樣,kafka為每個segment文件創建了一個對應的索引文件。索引文件很簡單,每條記錄就是一個kv組,key是消息的offset,value是該消息在segment文件中的偏移量:
offsetposition 00 1124 2336
每個segment文件對應一個索引文件:
- /kafka/topic/order_create/partition-0-0.log-0.index -18234.log #segment file-18234.index #index file -39712.log-39712.index -54101.log-54101.index
有了索引文件,我們就可以拿到某條消息具體的位置,從而直接進行讀取。再捋一遍這個流程:
當要查詢offset為x的消息
利用二分查找找到這條消息在y.log
讀取y.index文件找到消息x的y.log中的位置
讀取y.log的對應位置,獲取數據
通過這種文件組織形式,我們可以在kafka中非常快速地讀取出任何一條消息。但這又引出了另一個問題,如果消息量特別大,每條消息都在index文件中加一條記錄,這將浪費很多空間。
可以簡單地計算一下,假如index中一條記錄16個字節(offset 8 + position 8),一億條消息就是16*10^8字節=1.6G。對于一個稍微大一點的公司,kafka用來收集日志的話,一天的量遠遠不止1億條,可能是數十倍上百倍。這樣的話,index文件就會占用大量的存儲。因此,權衡之下kafka選擇了使用”稀疏索引“。
所謂稀疏索引就是并非所有消息都會在index文件中記錄它的position,每間隔多少條消息記錄一條,比如每間隔10條消息記錄一條offset-position:
offsetposition 00 101852 204518 306006 408756 5010844
這樣的話,如果當要查詢offset為x的消息,我們可能沒辦法查到它的精確位置,但是可以利用二分查找,快速地確定離他最近的那條消息的位置,然后往后多讀幾條數據就可以讀到我們想要的消息了。
比如,當我們要查到offset為33的消息,按照上表,我們可以利用二分查找定位到offset為30的消息所在的位置,然后去對應的log文件中從該位置開始向后讀取3條消息,第四條就是我們要找的33。這種方式其實就是在性能和存儲空間上的一個折中,很多系統設計時都會面臨類似的選擇,犧牲時間換空間還是犧牲空間換時間。
到這里,我們對kafka的整體架構應該有了一個比較清晰的認識了。不過在上面的分析中,我故意隱去了kafka中另一個非常非常重要的點,就是高可用方面的設計。因為這部分內容比較晦澀,會引入很多分布式理論的復雜性,妨礙我們理解kafka的基本模型。在接下來的部分,將著重討論這個主題。
2. Kafka高可用
高可用(HA)對于企業的核心系統來說是至關重要的。因為隨著業務的發展,集群規模會不斷增大,而大規模集群中總會出現故障,硬件、網絡都是不穩定的。當系統中某些節點各種原因無法正常使用時,整個系統可以容忍這個故障,繼續正常對外提供服務,這就是所謂的高可用性。對于有狀態服務來說,容忍局部故障本質上就是容忍丟數據(不一定是永久,但是至少一段時間內讀不到數據)。
系統要容忍丟數據,最樸素也是唯一的辦法就是做備份,讓同一份數據復制到多臺機器,所謂的冗余,或者說多副本。為此,kafka引入 leader-follower的概念。topic的每個partition都有一個leader,所有對這個partition的讀寫都在該partition leader所在的broker上進行。partition的數據會被復制到其它broker上,這些broker上對應的partition就是follower:

producer在生產消息時,會直接把消息發送到partition leader上,partition leader把消息寫入自己的log中,然后等待follower來拉取數據進行同步。具體交互如下:

上圖中對producer進行ack的時機非常關鍵,這直接關系到kafka集群的可用性和可靠性。
如果producer的數據到達leader并成功寫入leader的log就進行ack
優點:不用等數據同步完成,速度快,吞吐率高,可用性高;
缺點:如果follower數據同步未完成時leader掛了,就會造成數據丟失,可靠性低。
如果等follower都同步完數據時進行ack
優點:當leader掛了之后follower中也有完備的數據,可靠性高;
缺點:等所有follower同步完成很慢,性能差,容易造成生產方超時,可用性低。
而具體什么時候進行ack,對于kafka來說是可以根據實際應用場景配置的。
其實kafka真正的數據同步過程還是非常復雜的,本文主要是想講一講kafka的一些核心原理,數據同步里面涉及到的很多技術細節,HW epoch等,就不在此一一展開了。最后展示一下kafka的一個全景圖:

最后對kafka進行一個簡要地總結:kafka通過引入partition的概念,讓topic能夠分散到多臺broker上,提高吞吐率。但是引入多partition的代價就是無法保證topic維度的全局順序性,需要這種特性的場景只能使用單個partition。在內部,每個partition以多個segment文件的方式進行存儲,新來的消息append到最新的segment log文件中,并使用稀疏索引記錄消息在log文件中的位置,方便快速讀取消息。當數據過期時,直接刪除過期的segment文件即可。為了實現高可用,每個partition都有多個副本,其中一個是leader,其它是follower,分布在不同的broker上。對partition的讀寫都在leader所在的broker上完成,follower只會定時地拉取leader的數據進行同步。當leader掛了,系統會選出和leader保持同步的follower作為新的leader,繼續對外提供服務,大大提高可用性。在消費端,kafka引入了消費組的概念,每個消費組都可以互相獨立地消費topic,但一個partition只能被消費組中的唯一一個消費者消費。消費組通過記錄游標,可以實現ACK機制、重復消費等多種特性。除了真正的消息記錄在segment中,其它幾乎所有meta信息都保存在全局的zookeeper中。
3. 優缺點
(1)優點:kafka的優點非常多
高性能:單機測試能達到 100w tps;
低延時:生產和消費的延時都很低,e2e的延時在正常的cluster中也很低;
可用性高:replicate + isr + 選舉 機制保證;
工具鏈成熟:監控 運維 管理 方案齊全;
生態成熟:大數據場景必不可少 kafka stream.
(2)不足
無法彈性擴容:對partition的讀寫都在partition leader所在的broker,如果該broker壓力過大,也無法通過新增broker來解決問題;
擴容成本高:集群中新增的broker只會處理新topic,如果要分擔老topic-partition的壓力,需要手動遷移partition,這時會占用大量集群帶寬;
消費者新加入和退出會造成整個消費組rebalance:導致數據重復消費,影響消費速度,增加e2e延遲;
partition過多會使得性能顯著下降:ZK壓力大,broker上partition過多讓磁盤順序寫幾乎退化成隨機寫。
在了解了kafka的架構之后,你可以仔細想一想,為什么kafka擴容這么費勁呢?其實這本質上和redis集群擴容是一樣的!當redis集群出現熱key時,某個實例扛不住了,你通過加機器并不能解決什么問題,因為那個熱key還是在之前的某個實例中,新擴容的實例起不到分流的作用。
kafka類似,它擴容有兩種:新加機器(加broker)以及給topic增加partition。給topic新加partition這個操作,你可以聯想一下mysql的分表。比如用戶訂單表,由于量太大把它按用戶id拆分成1024個子表user_order_{0..1023},如果到后期發現還不夠用,要增加這個分表數,就會比較麻煩。因為分表總數增多,會讓user_id的hash值發生變化,從而導致老的數據無法查詢。所以只能停服做數據遷移,然后再重新上線。
kafka給topic新增partition一樣的道理,比如在某些場景下msg包含key,那producer就要保證相同的key放到相同的partition。但是如果partition總量增加了,根據key去進行hash,比如 hash(key) % parition_num,得到的結果就不同,就無法保證相同的key存到同一個partition。當然也可以在producer上實現一個自定義的partitioner,保證不論怎么擴partition相同的key都落到相同的partition上,但是這又會使得新增加的partition沒有任何數據。
其實你可以發現一個問題,kafka的核心復雜度幾乎都在存儲這一塊。數據如何分片,如何高效的存儲,如何高效地讀取,如何保證一致性,如何從錯誤中恢復,如何擴容再平衡……
上面這些不足總結起來就是一個詞:scalebility。通過直接加機器就能解決問題的系統才是大家的終極追求。Pulsar號稱云原生時代的分布式消息和流平臺,所以接下來我們看看pulsar是怎么樣的。
四、Pulsar
kafka的核心復雜度是它的存儲,高性能、高可用、低延遲、支持快速擴容的分布式存儲不僅僅是kafka的需求,應該是現代所有系統共同的追求。而apache項目底下剛好有一個專門就是為日志存儲打造的這樣的系統,它叫bookeeper!
有了專門的存儲組件,那么實現一個消息系統剩下的就是如何來使用這個存儲系統來實現feature了。pulsar就是這樣一個”計算-存儲 分離“的消息系統:

pulsar利用bookeeper作為存儲服務,剩下的是計算層。這其實是目前非常流行的架構也是一種趨勢,很多新型的存儲都是這種”存算分離“的架構。比如tidb,底層存儲其實是tikv這種kv存儲。tidb是更上層的計算層,自己實現sql相關的功能。還有的例子就是很多"持久化"redis產品,大部分底層依賴于rocksdb做kv存儲,然后基于kv存儲關系實現redis的各種數據結構。
在pulsar中,broker的含義和kafka中的broker是一致的,就是一個運行的pulsar實例。但是和kafka不同的是,pulsar的broker是無狀態服務,它只是一個”API接口層“,負責處理海量的用戶請求,當用戶消息到來時負責調用bookeeper的接口寫數據,當用戶要查詢消息時從bookeeper中查數據,當然這個過程中broker本身也會做很多緩存之類的。同時broker也依賴于zookeeper來保存很多元數據的關系。
由于broker本身是無狀態的,因此這一層可以非常非常容易地進行擴容,尤其是在k8s環境下,點下鼠標的事兒。至于消息的持久化,高可用,容錯,存儲的擴容,這些都通通交給bookeeper來解決。
但就像能量守恒定律一樣,系統的復雜性也是守恒的。實現既高性能又可靠的存儲需要的技術復雜性,不會憑空消失,只會從一個地方轉移到另一個地方。就像你寫業務邏輯,產品經理提出了20個不同的業務場景,就至少對應20個if else,不論你用什么設計模式和架構,這些if else不會被消除,只會從從一個文件放到另一個文件,從一個對象放到另一個對象而已。所以那些復雜性一定會出現在bookeeper中,并且會比kafka的存儲實現更為復雜。
但是pulsar存算分離架構的一個好處就是,當我們在學習pulsar時可以有一個比較明確的界限,所謂的concern segregation。只要理解bookeeper對上層的broker提供的API語義,即使不了解bookeeper內部的實現,也能很好的理解pulsar的原理。
接下來你可以思考一個問題:既然pulsar的broker層是無狀態的服務,那么我們是否可以隨意在某個broker進行對某個topic的數據生產呢?
看起來似乎沒什么問題,但答案還是否定的——不可以。為什么呢?想一想,假如生產者可以在任意一臺broker上對topic進行生產,比如生產3條消息a b c,三條生產消息的請求分別發送到broker A B C,那最終怎么保證消息按照a b c的順序寫入bookeeper呢?這是沒辦法保證,只有讓a b c三條消息都發送到同一臺broker,才能保證消息寫入的順序。
既然如此,那似乎又回到和kafka一樣的問題,如果某個topic寫入量特別特別大,一個broker扛不住怎么辦?所以pulsar和kafka一樣,也有partition的概念。一個topic可以分成多個partition,為了每個partition內部消息的順序一致,對每個partition的生產必須對應同一臺broker。

這里看起來似乎和kafka沒區別,也是每個partition對應一個broker,但是其實差別很大。為了保證對partition的順序寫入,不論kafka還是pulsar都要求寫入請求發送到partition對應的broker上,由該broker來保證寫入的順序性。然而區別在于,kafka同時會把消息存儲到該broker上,而pulsar是存儲到bookeeper上。這樣的好處是,當pulsar的某臺broker掛了,可以立刻把partition對應的broker切換到另一個broker,只要保證全局只有一個broker對topic-partition-x有寫權限就行了,本質上只是做一個所有權轉移而已,不會有任何數據的搬遷。
當對partition的寫請求到達對應broker時,broker就需要調用bookeeper提供的接口進行消息存儲。和kafka一樣,pulsar在這里也有segment的概念,而且和kafka一樣的是,pulsar也是以segment為單位進行存儲的(respect respect respect)。
為了說清楚這里,就不得不引入一個bookeeper的概念,叫ledger,也就是賬本。可以把ledger類比為文件系統上的一個文件,比如在kafka中就是寫入到xxx.log這個文件里。pulsar以segment為單位,存入bookeeper中的ledger。
在bookeeper集群中每個節點叫bookie(為什么集群的實例在kafka叫broker在bookeeper又叫bookie……無所謂,名字而已,作者寫了那么多代碼,還不能讓人開心地命個名啊)。在實例化一個bookeeper的writer時,就需要提供3個參數:
節點數n:bookeeper集群的bookie數;
副本數m:某一個ledger會寫入到n個bookie中的m個里,也就是說所謂的m副本;
確認寫入數t:每次向ledger寫入數據時(并發寫入到m個bookie),需要確保收到t個acks,才返回成功。
bookeeper會根據這三個參數來為我們做復雜的數據同步,所以我們不用擔心那些副本啊一致性啊的東西,直接調bookeeper的提供的append接口就行了,剩下的交給它來完成。

如上圖所示,parition被分為了多個segment,每個segment會寫入到4個bookie其中的3個中。比如segment1就寫入到了bookie1,2,4中,segment2寫入到bookie1,3,4中…
這其實就相當于把kafka某個partition的segment均勻分布到了多臺存儲節點上。這樣的好處是什么呢?在kafka中某個partition是一直往同一個broker的文件系統中進行寫入,當磁盤不夠用了,就需要做非常麻煩的擴容+遷移數據的操作。而對于pulsar,由于partition中不同segment可以保存在bookeeper不同的bookies上,當大量寫入導致現有集群bookie磁盤不夠用時,我們可以快速地添加機器解決問題,讓新的segment尋找最合適的bookie(磁盤空間剩余最多或者負載最低等)進行寫入,只要記住segment和bookies的關系就好了。

由于partition以segment為粒度均勻的分散到bookeeper上的節點上,這使得存儲的擴容變得非常非常容易。這也是Pulsar一直宣稱的存算分離架構的先進性的體現:
broker是無狀態的,隨便擴容;
partition以segment為單位分散到整個bookeeper集群,沒有單點,也可以輕易地擴容;
當某個bookie發生故障,由于多副本的存在,可以另外t-1個副本中隨意選出一個來讀取數據,不間斷地對外提供服務,實現高可用。
其實在理解kafka的架構之后再來看pulsar,你會發現pulsar的核心就在于bookeeper的使用以及一些metadata的存儲。但是換個角度,正是這個恰當的存儲和計算分離的架構,幫助我們分離了關注點,從而能夠快速地去學習上手。
消費模型
Pulsar相比于kafka另一個比較先進的設計就是對消費模型的抽象,叫做subscription。通過這層抽象,可以支持用戶各種各樣的消費場景。還是和kafka進行對比,kafka中只有一種消費模式,即一個或多個partition對一個consumer。如果想要讓一個partition對多個consumer,就無法實現了。pulsar通過subscription,目前支持4種消費方式:

可以把pulsar的subscription看成kafka的consumer group,但subscription更進一步,可以設置這個”consumer group“的消費類型:
exclusive:消費組里有且僅有一個consumer能夠進行消費,其它的根本連不上pulsar;
failover:消費組里的每個消費者都能連上每個partition所在的broker,但有且僅有一個consumer能消費到數據。當這個消費者崩潰了,其它的消費者會被選出一個來接班;
shared:消費組里所有消費者都能消費topic中的所有partition,消息以round-robin的方式來分發;
key-shared:消費組里所有消費者都能消費到topic中所有partition,但是帶有相同key的消息會保證發送給同一個消費者。
這些消費模型可以滿足多種業務場景,用戶可以根據實際情況進行選擇。通過這層抽象,pulsar既支持了queue消費模型,也支持了stream消費模型,還可以支持其它無數的消費模型(只要有人提pr),這就是pulsar所說的統一了消費模型。
其實在消費模型抽象的底下,就是不同的cursor管理邏輯。怎么ack,游標怎么移動,怎么快速查找下一條需要重試的msg……這都是一些技術細節,但是通過這層抽象,可以把這些細節進行隱藏,讓大家更關注于應用。
五、存算分離架構
其實技術的發展都是螺旋式的,很多時候你會發現最新的發展方向又回到了20年前的技術路線了。
在20年前,由于普通計算機硬件設備的局限性,對大量數據的存儲是通過NAS(Network Attached Storage)這樣的“云端”集中式存儲來完成。但這種方式的局限性也很多,不僅需要專用硬件設備,而且最大的問題就是難以擴容來適應海量數據的存儲。
數據庫方面也主要是以Oracle小型機為主的方案。然而隨著互聯網的發展,數據量越來越大,Google后來又推出了以普通計算機為主的分布式存儲方案,任意一臺計算機都能作為一個存儲節點,然后通過讓這些節點協同工作組成一個更大的存儲系統,這就是HDFS。
然而移動互聯網使得數據量進一步增大,并且4G 5G的普及讓用戶對延遲也非常敏感,既要可靠,又要快,又要可擴容的存儲逐漸變成了一種企業的剛需。而且隨著時間的推移,互聯網應用的流量集中度會越來越高,大企業的這種剛需訴求也越來越強烈。
因此,可靠的分布式存儲作為一種基礎設施也在不斷地完善。它們都有一個共同的目標,就是讓你像使用filesystem一樣使用它們,并且具有高性能高可靠自動錯誤恢復等多種功能。然而我們需要面對的一個問題就是CAP理論的限制,線性一致性(C),可用性(A),分區容錯性(P),三者只能同時滿足兩者。因此不可能存在完美的存儲系統,總有那么一些“不足”。我們需要做的其實就是根據不同的業務場景,選用合適的存儲設施,來構建上層的應用。這就是pulsar的邏輯,也是tidb等newsql的邏輯,也是未來大型分布式系統的基本邏輯,所謂的“云原生”。