天天看點

Redis、Kafka 和 Pulsar 消息隊列對比

導語 | 市面上有非常多的消息中間件,rabbitMQ、kafka、rocketMQ、pulsar、 redis等等,多得令人眼花缭亂。它們到底有什麼異同,你應該選哪個?本文嘗試通過技術演進的方式,以redis、kafka和 pulsar為例,逐漸深入,講講它們架構和原理,幫助你更好地了解和學習消息隊列。文章作者:劉德恩,騰訊IEG研發工程師。

一、最基礎的隊列

最基礎的消息隊列其實就是一個雙端隊列,我們可以用雙向連結清單來實作,如下圖所示:

Redis、Kafka 和 Pulsar 消息隊列對比

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、Kafka 和 Pulsar 消息隊列對比

和redis list不同的是,首先kafka的topic(實際上是partion)是用的單向隊列來存儲資料的,新資料每次直接追加到隊尾。同時它維護了一個遊标cursor,從頭開始,每次指向即将被消費的資料的下标。每消費一條,cursor+1 。通過這種方式,kafka也能和redis list一樣實作先入先出的語義,但是kafka每次隻需要更新遊标,并不會去删資料。

這樣設計的好處太多了,尤其是性能方面,順序寫一直是最大化利用磁盤帶寬的不二法門。但我們主要講講遊标這種設計帶來功能上的優勢。

首先可以支援消息的ACK機制了。由于消息不會被删除,是以可以等消費者明确告知kafka這條消息消費成功以後,再去更新遊标。這樣的話,隻要kafka持久化存儲了遊标的位置,即使消費失敗程序崩潰,等它恢複時依然可以重新消費

第二是可以支援分組消費:

Redis、Kafka 和 Pulsar 消息隊列對比

這裡需要引入一個消費組的概念,這個概念非常簡單,因為消費組本質上就是一組遊标。對于同一個topic,不同的消費組有各自的遊标。監控組的遊标指向第二條,BI組的遊标指向第4條,trace組指向到了第10000條……各消費者遊标彼此隔離,互不影響。

通過引入消費組的概念,就可以非常容易地支援多業務方同時消費一個topic,也就是說所謂的1-N的“廣播”,一條消息廣播給N個訂閱方。

最後,通過遊标也很容易實作重新消費。因為遊标僅僅就是記錄目前消費到哪一條資料了,要重新消費的話直接修改遊标的值就可以了。你可以把遊标重置為任何你想要指定的位置,比如重置到0重新開始消費,也可以直接重置到最後,相當于忽略現有所有資料。

是以你可以看到,kafka這種資料結構相比于redis的雙向連結清單有了一個質的飛躍,不僅是性能上,同時也是功能上,全面的領先。

我們可以來看看kafka的一個簡單的架構圖:

Redis、Kafka 和 Pulsar 消息隊列對比

從這個圖裡我們可以看出,topic是一個邏輯上的概念,不是一個實體。一個topic包含多個partition,partition分布在多台機器上。這個機器,kafka中稱之為broker。(kafka叢集中的一個broker對應redis叢集中的一個執行個體)。對于一個topic,可以有多個不同的消費組同時進行消費。一個消費組内部可以有多個消費者執行個體同時進行消費,這樣可以提高消費速率。

但是這裡需要非常注意的是,一個partition隻能被消費組中的一個消費者執行個體來消費。換句話說,消費組中如果有多個消費者,不能夠存在兩個消費者同時消費一個partition的場景。

為什麼呢?其實kafka要在partition級别提供順序消費的語義,如果多個consumer消費一個partition,即使kafka本身是按順序分發資料的,但是由于網絡延遲等各種情況,consumer并不能保證按kafka的分發順序接收到資料,這樣達到消費者的消息順序就是無法保證的。是以一個partition隻能被一個consumer消費。kafka各consumer group的遊标可以表示成類似這樣的資料結構:

了解了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中消息是有序的,看是否都過期就看最後一條是否過期就行了。

topic的一個partition是一個邏輯上的數組,由多個segment組成,如下圖所示:

Redis、Kafka 和 Pulsar 消息隊列對比

這時候就有一個問題,如果我把遊标重置到一個任意位置,比如第2897條消息,我怎麼讀取資料呢?根據上面的檔案組織結構,你可以發現我們需要确定兩件事才能讀出對應的資料:

第2897條消息在哪個segment檔案裡;

第2897條消息在segment檔案裡的什麼位置。

為了解決上面兩個問題,kafka有一個非常巧妙的設計。首先,segment檔案的檔案名是以該檔案裡第一條消息的offset來命名的。一開始的segment檔案名是 0.log,然後一直寫直到寫了18234條消息後,發現達到了設定的檔案大小上限100M,然後就建立一個新的segment檔案,名字是18234.log……

當我們要找offset為x的消息在哪個segment時,隻需要通過檔案名做一次二分查找就行了。比如offset為2879的消息(第2880條消息),顯然就在0.log這個segment檔案裡。定位到segment檔案之後,另一個問題就是要找到該消息在檔案中的位置,也就是偏移量。如果從頭開始一條條地找,這個耗時肯定是無法接受的!kafka的解決辦法就是索引檔案。就如mysql的索引一樣,kafka為每個segment檔案建立了一個對應的索引檔案。索引檔案很簡單,每條記錄就是一個kv組,key是消息的offset,value是該消息在segment檔案中的偏移量:

offset

position

1

124

2

336

每個segment檔案對應一個索引檔案:

有了索引檔案,我們就可以拿到某條消息具體的位置,進而直接進行讀取。再捋一遍這個流程:

當要查詢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:

10

1852

20

4518

30

6006

40

8756

50

10844

這樣的話,如果當要查詢offset為x的消息,我們可能沒辦法查到它的精确位置,但是可以利用二分查找,快速地确定離他最近的那條消息的位置,然後往後多讀幾條資料就可以讀到我們想要的消息了。比如,當我們要查到offset為33的消息,按照上表,我們可以利用二分查找定位到offset為30的消息所在的位置,然後去對應的log檔案中從該位置開始向後讀取3條消息,第四條就是我們要找的33。這種方式其實就是在性能和存儲空間上的一個折中,很多系統設計時都會面臨類似的選擇,犧牲時間換空間還是犧牲空間換時間。到這裡,我們對kafka的整體架構應該有了一個比較清晰的認識了。不過在上面的分析中,我故意隐去了kafka中另一個非常非常重要的點,就是高可用方面的設計。因為這部分内容比較晦澀,會引入很多分布式理論的複雜性,妨礙我們了解kafka的基本模型。在接下來的部分,将着重讨論這個主題。

高可用(HA)對于企業的核心系統來說是至關重要的。因為随着業務的發展,叢集規模會不斷增大,而大規模叢集中總會出現故障,硬體、網絡都是不穩定的。當系統中某些節點各種原因無法正常使用時,整個系統可以容忍這個故障,繼續正常對外提供服務,這就是所謂的高可用性。對于有狀态服務來說,容忍局部故障本質上就是容忍丢資料(不一定是永久,但是至少一段時間内讀不到資料)。系統要容忍丢資料,最樸素也是唯一的辦法就是做備份,讓同一份資料複制到多台機器,所謂的備援,或者說多副本。為此,kafka引入 leader-follower的概念。topic的每個partition都有一個leader,所有對這個partition的讀寫都在該partition leader所在的broker上進行。partition的資料會被複制到其它broker上,這些broker上對應的partition就是follower:

Redis、Kafka 和 Pulsar 消息隊列對比

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

Redis、Kafka 和 Pulsar 消息隊列對比

 上圖中對producer進行ack的時機非常關鍵,這直接關系到kafka叢集的可用性和可靠性。

如果producer的資料到達leader并成功寫入leader的log就進行ack

優點:不用等資料同步完成,速度快,吞吐率高,可用性高;

缺點:如果follower資料同步未完成時leader挂了,就會造成資料丢失,可靠性低。

如果等follower都同步完資料時進行ack優點:當leader挂了之後follower中也有完備的資料,可靠性高;

缺點:等所有follower同步完成很慢,性能差,容易造成生産方逾時,可用性低。

而具體什麼時候進行ack,對于kafka來說是可以根據實際應用場景配置的。其實kafka真正的資料同步過程還是非常複雜的,本文主要是想講一講kafka的一些核心原理,資料同步裡面涉及到的很多技術細節,HW epoch等,就不在此一一展開了。最後展示一下kafka的一個全景圖:

Redis、Kafka 和 Pulsar 消息隊列對比

最後對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中。

(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就是這樣一個”計算-存儲 分離“的消息系統:

Redis、Kafka 和 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。

Redis、Kafka 和 Pulsar 消息隊列對比

這裡看起來似乎和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接口就行了,剩下的交給它來完成。

Redis、Kafka 和 Pulsar 消息隊列對比

如上圖所示,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的關系就好了。

Redis、Kafka 和 Pulsar 消息隊列對比

由于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種消費方式:

Redis、Kafka 和 Pulsar 消息隊列對比

 可以把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的邏輯,也是未來大型分布式系統的基本邏輯,所謂的“雲原生”。

繼續閱讀