RocketMQ 是什么?它的架構是怎么樣的?和 Kafka 又有什么區(qū)別?
作為一個程序員,假設你有 A、B 兩個服務,A 服務發(fā)出消息后,不想讓 B 服務立馬處理到。而是要過半小時才讓 B 服務處理到,該怎么實現(xiàn)?
這類延遲處理消息的場景非常常見,舉個例子,比如我每天早上到公司后都會點個外賣,我希望外賣能在中午送過來,而不是立馬送過來,這就需要將外賣消息經(jīng)過延時后,再投遞到商家側。
那么問題就來了,有沒有優(yōu)雅的解決方案?當然有,沒有什么是加一層中間層不能解決的,如果有,那就再加一層。這次我們要加的中間層是消息隊列 RocketMQ。
RocketMQ 是什么?
RocketMQ 是阿里自研的國產(chǎn)消息隊列,目前已經(jīng)是 Apache 的頂級項目。和其他消息隊列一樣,它接受來自生產(chǎn)者的消息,將消息分類,每一類是一個 topic,消費者根據(jù)需要訂閱 topic,獲取里面的消息。
是不是很像我們上篇文章里提到的消息隊 Kafka,那么問題很自然就來了,既然都是消息隊列,那它們之間有什么區(qū)別呢?
RocketMQ 和 Kafka 的區(qū)別
RocketMQ 的架構其實參考了 Kafka 的設計思想,同時又在 Kafka 的基礎上做了一些調(diào)整。
這些調(diào)整,用一句話總結就是,"和 Kafka 相比,RocketMQ 在架構上做了減法,在功能上做了加法"。我們來看下這句話的含義。
在架構上做減法
我們來簡單回顧下消息隊列 Kafka 的架構。kakfa 也是通過多個 topic 對消息進行分類。
- 為了提升單個 topic 的并發(fā)性能,將單個 topic 拆為多個 partition。
- 為了提升系統(tǒng)擴展性,將多個 partition 分別部署在不同 broker 上。
- 為了提升系統(tǒng)的可用性,為 partition 加了多個副本。
- 為了協(xié)調(diào)和管理 Kafka 集群的數(shù)據(jù)信息,引入Zookeeper作為協(xié)調(diào)節(jié)點。
Kafka 已經(jīng)是非常強的消息隊列了,我們來看下 RocketMQ 在 Kafka 架構的基礎上,還能玩出什么花樣來。
簡化協(xié)調(diào)節(jié)點
Zookeeper 在 Kafka 架構中會和 broker 通信,維護 Kafka 集群信息。一個新的 broker 連上 Zookeeper 后,其他 broker 就能立馬感知到它的加入,像這種能在分布式環(huán)境下,讓多個實例同時獲取到同一份信息的服務,就是所謂的分布式協(xié)調(diào)服務。
但 Zookeeper 作為一個通用的分布式協(xié)調(diào)服務,它不僅可以用于服務注冊與發(fā)現(xiàn),還可以用于分布式鎖、配置管理等場景。Kafka 其實只用到了它的部分功能,多少有點殺雞用牛刀的味道。太重了。
所以 RocketMQ 直接將 Zookeeper 去掉,換成了 nameserver,用一種更輕量的方式,管理消息隊列的集群信息。生產(chǎn)者通過 nameserver 獲取到 topic 和 broker 的路由信息,然后再與 broker 通信,實現(xiàn)服務發(fā)現(xiàn)和負載均衡的效果。
當然,開發(fā) Kafka 的大佬們后來也意識到了 Zookeeper 過重的問題,所以從 2.8.0 版本就支持將 Zookeeper 移除,通過 在 broker 之間加入一致性算法 raft 實現(xiàn)同樣的效果,這就是所謂的 KRaft 或 Quorum 模式。
簡化分區(qū)
我們知道,Kafka 會將 topic 拆分為多個 partition,用來提升并發(fā)性能。
在 RocketMQ 里也一樣,將 topic 拆分成了多個分區(qū),但換了個名字,叫 Queue,也就是"隊列"。
Kafka 中的 partition 會存儲完整的消息體,而 RocketMQ 的 Queue 上卻只存一些簡要信息,比如消息偏移 offset,而消息的完整數(shù)據(jù)則放到"一個"叫 commitlog 的文件上,通過 offset 我們可以定位到 commitlog 上的某條消息。Kafka 消費消息,broker 只需要直接從 partition 讀取消息返回就好,也就是讀第一次就夠了。
而在 RocketMQ 中,broker 則需要先從 Queue 上讀取到 offset 的值,再跑到 commitlog 上將完整數(shù)據(jù)讀出來,也就是需要讀兩次。
那么問題就來了,看起來 Kafka 的設計更高效?為什么 RocketMQ 不采用 Kafka 的設計?
這就不得說一下 Kafka 的底層存儲了。
Kafka 的底層存儲
Kafka 的 partition 分區(qū),其實在底層由很多段(segment)組成,每個 segment 可以認為就是個小文件。將消息數(shù)據(jù)寫入到 partition 分區(qū),本質(zhì)上就是將數(shù)據(jù)寫入到某個 segment 文件下。
我們知道,操作系統(tǒng)的機械磁盤,順序?qū)懙男阅軙入S機寫快很多,差距高達幾十倍。為了提升性能,Kafka 對每個小文件都是順序?qū)憽?br>如果只有一個 segment 文件,那寫文件的性能會很好。
但當 topic 變多之后,topic 底下的 partition 分區(qū)也會變多,對應的 partition 底下的 segment 文件也會變多。同時寫多個 topic 底下的 partition,就是同時寫多個文件,雖然每個文件內(nèi)部都是順序?qū)?,但多個文件存放在磁盤的不同地方,原本順序?qū)懘疟P就可能劣化變成了隨機寫。于是寫性能就降低了。
那問題又又來了,究竟多少 topic 才算多?這個看實際情況,但打太極從來不是我的風格。
我給一個經(jīng)驗值僅供參考,8 個分區(qū)的情況下,超過 64 topic, Kafka 性能就會開始下降。
RocketMQ 的底層存儲
為了緩解同時寫多個文件帶來的隨機寫問題,RocketMQ 索性將單個 broker 底下的多個 topic 數(shù)據(jù),全都寫到"一個"邏輯文件 CommitLog 上,這就消除了隨機寫多文件的問題,將所有寫操作都變成了順序?qū)憽4蟠筇嵘?RocketMQ 在多 topic 場景下的寫性能。
注意上面提到的"一個"是帶引號的,雖然邏輯上它是一個大文件,但實際上這個 CommitLog 由多個小文件組成。每個文件的大小是固定的,當一個文件被寫滿后,會創(chuàng)建一個新的文件來繼續(xù)存儲新的消息。這種方式可以方便地管理和清理舊的消息。
簡化備份模型
我們知道,Kafka 會將 partiton 分散到多個 broker 中,并為 partiton 配置副本,將 partiton 分為 leader和 follower,也就是主和從。broker 中既可能有 A topic 的主 partiton,也可能有 B topic 的從 partiton。主從 partiton 之間會建立數(shù)據(jù)同步,本質(zhì)上就是同步 partiton 底下的 segment 文件數(shù)據(jù)
RocketMQ 將 broker 上的所有 topic 數(shù)據(jù)到寫到 CommitLog 上。如果還像 Kafka 那樣給每個分區(qū)單獨建立同步通信,就還得將 CommitLog 里的內(nèi)容拆開,這就還是退化為隨機讀了。
于是 RocketMQ 索性以 broker 為單位區(qū)分主從,主從之間同步 CommitLog 文件,保持高可用的同時,也大大簡化了備份模型。
好了,到這里,我們熟悉的 Kafka 架構,就成了 RocketMQ 的架構。
是不是跟 Kafka 的很像但又簡化了不少?
在功能上做加法
雖然 RocketMQ 的架構比 Kafka 的簡單,但功能卻比 Kafka 要更豐富,我們來看下。
消息過濾
我們知道,Kafka 支持通過 topic 將數(shù)據(jù)進行分類,比如訂單數(shù)據(jù)和用戶數(shù)據(jù)是兩個不同的 topic,但如果我還想再進一步分類呢?比如同樣是用戶數(shù)據(jù),還能根據(jù) vip 等級進一步分類。假設我們只需要獲取 vip6 的用戶數(shù)據(jù),在 Kafka 里,消費者需要消費 topic 為用戶數(shù)據(jù)的所有消息,再將 vip6 的用戶過濾出來。
而 RocketMQ 支持對消息打上標記,也就是打 tag,消費者能根據(jù) tag 過濾所需要的數(shù)據(jù)。比如我們可以在部分消息上標記 tag=vip6,這樣消費者就能只獲取這部分數(shù)據(jù),省下了消費者過濾數(shù)據(jù)時的資源消耗。
相當于 RocketMQ 除了支持通過 topic 進行一級分類,還支持通過 tag 進行二級分類。
支持事務
我們知道 Kafka 支持事務,比如生產(chǎn)者發(fā)三條消息 ABC,這三條消息要么同時發(fā)送成功,要么同時發(fā)送失敗。
是,這確實也叫事務,但跟我們要的不太一樣。
寫業(yè)務代碼的時候,我們更想要的事務是,"執(zhí)行一些自定義邏輯"和"生產(chǎn)者發(fā)消息"這兩件事,要么同時成功,要么同時失敗。
而這正是 RocketMQ 支持的事務能力。
加入延時隊列
如果我們希望消息投遞出去之后,消費者不能立馬消費到,而是過個一定時間后才消費,也就是所謂的延時消息,就像文章開頭的定時外賣那樣。如果我們使用 Kafka, 要實現(xiàn)類似的功能的話,就會很費勁。
但 RocketMQ 天然支持延時隊列,我們可以很方便實現(xiàn)這一功能。
加入死信隊列
消費消息是有可能失敗的,失敗后一般可以設置重試。如果多次重試失敗,RocketMQ 會將消息放到一個專門的隊列,方便我們后面單獨處理。這種專門存放失敗消息的隊列,就是死信隊列。
Kafka 原生不支持這個功能,需要我們自己實現(xiàn)。
消息回溯
Kafka 支持通過調(diào)整 offset 來讓消費者從某個地方開始消費,而 RocketMQ,除了可以調(diào)整 offset, 還支持調(diào)整時間。
所以不那么嚴謹?shù)恼f, RocketMQ 本質(zhì)就是在架構上做了減法,在功能上做了加法的 Kafka。
這個總結是不是特別精辟。
現(xiàn)在大家通了嗎?
最后遺留一個問題。
現(xiàn)在看起來,RocketMQ 好像各方面都比 Kafka 更能打。
但 Kafka 卻一直沒被淘汰,說明 RocketMQ 必然是有著不如 Kafka 的地方。
是啥呢?
性能,嚴格來說是吞吐量。
這就很奇怪了,為什么 RocketMQ 參考了 Kafka 的架構,性能卻還不如 Kafka?
這個問題,我們下期聊聊。
總結
- RocketMQ 和 Kafka 相比,在架構上做了減法,在功能上做了加法
- 跟 Kafka 的架構相比,RocketMQ 簡化了協(xié)調(diào)節(jié)點和分區(qū)以及備份模型。同時增強了消息過濾、消息回溯和事務能力,加入了延遲隊列,死信隊列等新特性。