從演進式角度看消息隊列

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

一、最基礎的隊列

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

有了這樣的數據結構之後,我們就可以在內存中構建一個消息隊列,一些任務不停地往隊列裏添加消息,同時另一些任務不斷地從隊尾有序地取出這些消息。添加消息的任務我們稱爲 producer,而取出並使用消息的任務,我們稱之爲 consumer。

要實現這樣的內存消息隊列並不難,甚至可以說很容易。但是如果要讓它能在應對海量的併發讀寫時保持高效,還是需要下很多功夫的。

二、Redis 的隊列

redis 剛好提供了上述的數據結構——list。redis list 支持:

這正好對應了我們隊列抽象的 push_front 和 pop_tail,因此我們可以直接把 redis 的 list 當成一個消息隊列來使用。而且 redis 本身對高併發做了很好的優化,內部數據結構經過了精心地設計和優化。所以從某種意義上講,用 redis 的 list 大概率比你自己重新實現一個 list 強很多。

但另一方面,使用 redis list 作爲消息隊列也有一些不足,比如:

對於上述的不足,目前看來第一條(持久化)是可以解決的。很多公司都有團隊基於 rocksdb leveldb 進行二次開發,實現了支持 redis 協議的 kv 存儲。這些存儲已經不是 redis 了,但是用起來和 redis 幾乎一樣。它們能夠保證數據的持久化,但對於上述的其他缺陷也無能爲力了。

其實 redis 5.0 開始新增了一個 stream 數據類型,它是專門設計成爲消息隊列的數據結構,借鑑了很多 kafka 的設計,但是依然還有很多問題… 直接進入到 kafka 的世界它不香嗎?

三、Kafka

從上面你可以看到,一個真正的消息中間件不僅僅是一個隊列那麼簡單。尤其是當它承載了公司大量業務的時候,它的功能完備性、吞吐量、穩定性、擴展性都有非常苛刻的要求。kafka 應運而生,它是專門設計用來做消息中間件的系統。

前面說 redis list 的不足時,雖然有很多不足,但是如果你仔細思考,其實可以歸納爲兩點:

這兩點也是 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 條消息,我怎麼讀取數據呢?

根據上面的文件組織結構,你可以發現我們需要確定兩件事才能讀出對應的數據:

爲了解決上面兩個問題,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 文件中的偏移量:

BHxhlL

每個 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

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

通過這種文件組織形式,我們可以在 kafka 中非常快速地讀取出任何一條消息。但這又引出了另一個問題,如果消息量特別大,每條消息都在 index 文件中加一條記錄,這將浪費很多空間。

可以簡單地計算一下,假如 index 中一條記錄 16 個字節(offset 8 + position 8),一億條消息就是 16*10^8 字節 = 1.6G。對於一個稍微大一點的公司,kafka 用來收集日誌的話,一天的量遠遠不止 1 億條,可能是數十倍上百倍。這樣的話,index 文件就會佔用大量的存儲。因此,權衡之下 kafka 選擇了使用” 稀疏索引 “。

所謂稀疏索引就是並非所有消息都會在 index 文件中記錄它的 position,每間隔多少條消息記錄一條,比如每間隔 10 條消息記錄一條 offset-position:

5t10zz

這樣的話,如果當要查詢 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 集羣的可用性和可靠性。

而具體什麼時候進行 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 的優點非常多

(2)不足

在瞭解了 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 個參數:

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 一直宣稱的存算分離架構的先進性的體現:

其實在理解 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“的消費類型:

這些消費模型可以滿足多種業務場景,用戶可以根據實際情況進行選擇。通過這層抽象,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 的邏輯,也是未來大型分佈式系統的基本邏輯,所謂的 “雲原生”。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/2NoRkIKG0IFcoI-nZienAQ