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