有個(gè) xx 需求,我應(yīng)該用 Kafka 還是 RabbitMQ 畜眨?
下面我會(huì)通過 6 個(gè)場景昼牛,來對比分析一下 Kafka 和 RabbitMQ 的優(yōu)劣。
一康聂、消息的順序
有這樣一個(gè)需求:當(dāng)訂單狀態(tài)變化的時(shí)候贰健,把訂單狀態(tài)變化的消息發(fā)送給所有關(guān)心訂單變化的系統(tǒng)。
訂單會(huì)有創(chuàng)建成功恬汁、待付款伶椿、已支付、已發(fā)貨的狀態(tài),狀態(tài)之間是單向流動(dòng)的脊另。
好导狡,現(xiàn)在我們把訂單狀態(tài)變化消息要發(fā)送給所有關(guān)心訂單狀態(tài)的系統(tǒng)上去,實(shí)現(xiàn)方式就是用消息隊(duì)列偎痛。
在這種業(yè)務(wù)下旱捧,我們最想要的是什么?
*消息的順序:對于同一筆訂單來說踩麦,狀態(tài)的變化都是有嚴(yán)格的先后順序的枚赡。
*吞吐量:像訂單的業(yè)務(wù),我們自然希望訂單越多越好谓谦。訂單越多贫橙,吞吐量就越大。
**在這種情況下反粥,我們先看看 RabbitMQ 是怎么做的卢肃。
首先,對于發(fā)消息星压,并廣播給多個(gè)消費(fèi)者這種情況践剂,RabbitMQ 會(huì)為每個(gè)消費(fèi)者建立一個(gè)對應(yīng)的隊(duì)列。也就是說娜膘,如果有 10 個(gè)消費(fèi)者逊脯,RabbitMQ 會(huì)建立 10 個(gè)對應(yīng)的隊(duì)列。然后竣贪,當(dāng)一條消息被發(fā)出后军洼,RabbitMQ 會(huì)把這條消息復(fù)制 10 份放到這 10 個(gè)隊(duì)列里。
當(dāng) RabbitMQ 把消息放入到對應(yīng)的隊(duì)列后演怎,我們緊接著面臨的問題就是匕争,我們應(yīng)該在系統(tǒng)內(nèi)部啟動(dòng)多少線程去從消息隊(duì)列中獲取消息。
如果只是單線程去獲取消息爷耀,那自然沒有什么好說的甘桑。但是多線程情況,可能就會(huì)有問題了……
RabbitMQ 有這么個(gè)特性歹叮,它在官方文檔就聲明了自己是不保證多線程消費(fèi)同一個(gè)隊(duì)列的消息跑杭,一定保證順序的。而不保證的原因咆耿,是因?yàn)槎嗑€程時(shí)德谅,當(dāng)一個(gè)線程消費(fèi)消息報(bào)錯(cuò)的時(shí)候,RabbitMQ 會(huì)把消費(fèi)失敗的消息再入隊(duì)萨螺,此時(shí)就可能出現(xiàn)亂序的情況窄做。
T0 時(shí)刻愧驱,隊(duì)列中有四條消息 A1、B1椭盏、B2组砚、A2。其中 A1庸汗、A2 表示訂單 A 的兩個(gè)狀態(tài):待付款惫确、已付款。B1蚯舱、B2 也同理,是訂單 B 的待付款掩蛤、已付款枉昏。
到了 T1 時(shí)刻,消息 A1 被線程 1 收到揍鸟,消息 B1 被線程 2 收到兄裂。此時(shí),一切都還正常阳藻。
到了 T3 時(shí)刻晰奖,B1 消費(fèi)出錯(cuò)了,同時(shí)呢腥泥,由于線程 1 處理速度快匾南,又從消息隊(duì)列中獲取到了 B2。此時(shí)蛔外,問題開始出現(xiàn)蛆楞。
到了 T4 時(shí)刻,由于 RabbitMQ 線程消費(fèi)出錯(cuò)夹厌,可以把消息重新入隊(duì)的特性豹爹,此時(shí) B1 會(huì)被重新放到隊(duì)列頭部。所以矛纹,如果不湊巧臂聋,線程 1 獲取到了 B1,就出現(xiàn)了亂序情況或南,B2 狀態(tài)明明是 B1 的后續(xù)狀態(tài)孩等,卻被提前處理了。
所以迎献,可以看到了瞎访,這個(gè)場景用 RabbitMQ,出現(xiàn)了三個(gè)問題:
*為了實(shí)現(xiàn)發(fā)布訂閱功能吁恍,從而使用的消息復(fù)制扒秸,會(huì)降低性能并耗費(fèi)更多資源
*多個(gè)消費(fèi)者無法嚴(yán)格保證消息順序
*大量的訂單集中在一個(gè)隊(duì)列播演,吞吐量受到了限制
那么 Kafka 怎么樣呢?Kafka 正好在這三個(gè)問題上伴奥,表現(xiàn)的要比 RabbitMQ 要好得多写烤。
首先,Kafka 的發(fā)布訂閱并不會(huì)復(fù)制消息拾徙,因?yàn)?Kafka 的發(fā)布訂閱就是消費(fèi)者直接去獲取被 Kafka 保存在日志文件中的消息就好洲炊。無論是多少消費(fèi)者,他們只需要主動(dòng)去找到消息在文件中的位置即可尼啡。
其次暂衡,Kafka 不會(huì)出現(xiàn)消費(fèi)者出錯(cuò)后,把消息重新入隊(duì)的現(xiàn)象崖瞭。
最后狂巢,Kafka 可以對訂單進(jìn)行分區(qū),把不同訂單分到多個(gè)分區(qū)中保存书聚,這樣唧领,吞吐量能更好。
所以雌续,對于這個(gè)需求 Kafka 更合適斩个。
二、消息的匹配
我曾經(jīng)做過一套營銷系統(tǒng)驯杜。這套系統(tǒng)中有個(gè)非常顯著的特點(diǎn)受啥,就是非常復(fù)雜非常靈活地匹配規(guī)則。
比如艇肴,要根據(jù)推廣內(nèi)容去匹配不同的方式做宣傳腔呜。又比如,要根據(jù)不同的活動(dòng)去匹配不同的渠道去做分發(fā)再悼。
總之核畴,數(shù)不清的匹配規(guī)則是這套系統(tǒng)中非常重要的一個(gè)特點(diǎn)。
首先冲九,先看看 RabbitMQ 的谤草,你會(huì)發(fā)現(xiàn) RabbitMQ 是允許在消息中添加 routing_key 或者自定義消息頭,然后通過一些特殊的 Exchange莺奸,很簡單的就實(shí)現(xiàn)了消息匹配分發(fā)丑孩。開發(fā)幾乎不用成本。
而 Kafka 呢灭贷?如果你要實(shí)現(xiàn)消息匹配温学,開發(fā)成本高多了胸竞。
首先柿祈,通過簡單的配置去自動(dòng)匹配和分發(fā)到合適的消費(fèi)者端這件事是不可能的颤芬。
其次擎析,消費(fèi)者端必須先把所有消息不管需要不需要,都取出來轧拄。然后揽祥,再根據(jù)業(yè)務(wù)需求,自己去實(shí)現(xiàn)各種精準(zhǔn)和模糊匹配檩电≈舴幔可能因?yàn)檫^度的復(fù)雜性,還要引入規(guī)則引擎俐末。
這個(gè)場景下 RabbitMQ 扳回一分料按。
三、消息的超時(shí)
在電商業(yè)務(wù)里鹅搪,有個(gè)需求:下單之后站绪,如果用戶在 15 分鐘內(nèi)未支付,則自動(dòng)取消訂單丽柿。
你可能奇怪,這種怎么也會(huì)用到消息隊(duì)列的魂挂?
我來先簡單解釋一下甫题,在單一服務(wù)的系統(tǒng),可以起個(gè)定時(shí)任務(wù)就搞定了涂召。
但是坠非,在 SOA 或者微服務(wù)架構(gòu)下,這樣做就不行了果正。因?yàn)楹芏鄠€(gè)服務(wù)都關(guān)心是否支付這件事炎码,如果每種服務(wù),都自己實(shí)現(xiàn)一套定時(shí)任務(wù)的邏輯秋泳,既重復(fù)潦闲,又難以維護(hù)。
在這種情況下迫皱,我們往往會(huì)做一層抽象:把要執(zhí)行的任務(wù)封裝成消息歉闰。當(dāng)時(shí)間到了,直接扔到消息隊(duì)列里卓起,消息的訂閱者們獲取到消息后和敬,直接執(zhí)行即可。
希望把消息延遲一定時(shí)間再處理的戏阅,被稱為延遲隊(duì)列昼弟。
對于訂單取消的這種業(yè)務(wù),我們就會(huì)在創(chuàng)建訂單的時(shí)候奕筐,同時(shí)扔一個(gè)包含了執(zhí)行任務(wù)信息的消息到延遲隊(duì)列舱痘,指定15分鐘后变骡,讓訂閱這個(gè)隊(duì)列的各個(gè)消費(fèi)者,可以收到這個(gè)消息衰粹。隨后锣光,各個(gè)消費(fèi)者所在的系統(tǒng)就可以去執(zhí)行相關(guān)的掃描訂單的任務(wù)了。
RabbitMQ 和 Kafka 消息隊(duì)列如何選铝耻?
先看下 RabbitMQ 的誊爹。
RabbitMQ 的消息自帶手表,消息中有個(gè) TTL 字段瓢捉,可以設(shè)置消息在 RabbitMQ 中的存放的時(shí)間频丘,超時(shí)了會(huì)被移送到一個(gè)叫死信隊(duì)列的地方。
所以泡态,延遲隊(duì)列 RabbitMQ 最簡單的實(shí)現(xiàn)方式就是設(shè)置 TTL搂漠,然后一個(gè)消費(fèi)者去監(jiān)聽死信隊(duì)列。當(dāng)消息超時(shí)了某弦,監(jiān)聽死信隊(duì)列的消費(fèi)者就收到消息了桐汤。
不過,這樣做有個(gè)大問題:假設(shè)靶壮,我們先往隊(duì)列放入一條過期時(shí)間是 10 秒的 A 消息怔毛,再放入一條過期時(shí)間是 5 秒的 B 消息。 那么問題來了腾降,B 消息會(huì)先于 A 消息進(jìn)入死信隊(duì)列嗎拣度?
答案是否定的。B 消息會(huì)優(yōu)先遵守隊(duì)列的先進(jìn)先出規(guī)則螃壤,在 A 消息過期后抗果,和其一起進(jìn)入死信隊(duì)列被消費(fèi)者消費(fèi)。
在 RabbitMQ 的 3.5.8 版本以后奸晴,官方推薦的 rabbitmq delayed message exchange 插件可以解決這個(gè)問題冤馏。
*用了這個(gè)插件,我們在發(fā)送消息的時(shí)候蚁滋,把消息發(fā)往一個(gè)特殊的 Exchange宿接。
*同時(shí),在消息頭里指定要延遲的時(shí)間辕录。
*收到消息的 Exchange 并不會(huì)立即把消息放到隊(duì)列里睦霎,而是在消息延遲時(shí)間到達(dá)后,才會(huì)把消息放入走诞。
再看下 Kafka 的:
Kafka 要實(shí)現(xiàn)延遲隊(duì)列就很麻煩了副女。
*你先需要把消息先放入一個(gè)臨時(shí)的 topic。
*然后得自己開發(fā)一個(gè)做中轉(zhuǎn)的消費(fèi)者蚣旱。讓這個(gè)中間的消費(fèi)者先去把消息從這個(gè)臨時(shí)的 topic 取出來碑幅。
*取出來戴陡,這消息還不能馬上處理啊,因?yàn)闆]到時(shí)間呢沟涨。也沒法保存在自己的內(nèi)存里恤批,怕崩潰了,消息沒了裹赴。所以喜庞,就得把沒有到時(shí)間的消息存入到數(shù)據(jù)庫里。
存入數(shù)據(jù)庫中的消息需要在時(shí)間到了之后再放入到 Kafka 里棋返,以便真正的消費(fèi)者去執(zhí)行真正的業(yè)務(wù)邏輯延都。
……
想想就已經(jīng)頭大了,這都快搞成調(diào)度平臺(tái)了睛竣。再高級點(diǎn)晰房,還要用時(shí)間輪算法才能更好更準(zhǔn)確。
這次射沟,RabbitMQ 上那一條條戴手表的消息殊者,才是最好的選擇。
四验夯、消息的保持
在微服務(wù)里幽污,事件溯源模式是經(jīng)常用到的。如果想用消息隊(duì)列實(shí)現(xiàn)簿姨,一般是把事件當(dāng)成消息,依次發(fā)送到消息隊(duì)列中簸搞。
事件溯源有個(gè)最經(jīng)典的場景扁位,就是事件的重放。簡單來講就是把系統(tǒng)中某段時(shí)間發(fā)生的事件依次取出來再處理趁俊。而且域仇,根據(jù)業(yè)務(wù)場景不同,這些事件重放很可能不是一次寺擂,更可能是重復(fù) N 次暇务。
假設(shè),我們現(xiàn)在需要一批在線事件重放怔软,去排查一些問題垦细。
RabbitMQ 此時(shí)就真的不行了,因?yàn)橄⒈蝗巳〕鰜砭捅粍h除了挡逼。想再次被重復(fù)消費(fèi)括改?對不起。
而 Kafka 呢家坎,消息會(huì)被持久化一個(gè)專門的日志文件里嘱能。不會(huì)因?yàn)楸幌M(fèi)了就被刪除吝梅。
所以,對消息不離不棄的 Kafka 相對用過就拋的 RabbitMQ惹骂,請選擇 Kafka苏携。
五、消息的錯(cuò)誤處理
很多時(shí)候对粪,在做記錄數(shù)據(jù)相關(guān)業(yè)務(wù)的時(shí)候右冻,Kafka 一般是不二選擇。不過衩侥,有時(shí)候在記錄數(shù)據(jù)吞吐量不大時(shí)国旷,我自己倒是更喜歡用 RabbitMQ。
原因就是 Kafka 有一個(gè)我很不喜歡的設(shè)計(jì)原則:
當(dāng)單個(gè)分區(qū)中的消息一旦出現(xiàn)消費(fèi)失敗茫死,就只能停止而不是跳過這條失敗的消息繼續(xù)消費(fèi)后面的消息跪但。即不允許消息空洞。
只要消息出現(xiàn)失敗峦萎,不管是 Kafka 自身消息格式的損壞屡久,還是消費(fèi)者處理出現(xiàn)異常,是不允許跳過消費(fèi)失敗的消息繼續(xù)往后消費(fèi)的爱榔。
所以被环,在數(shù)據(jù)統(tǒng)計(jì)不要求十分精確的場景下選了 Kafka,一旦出現(xiàn)了消息消費(fèi)問題详幽,就會(huì)發(fā)生項(xiàng)目不可用的情況筛欢。這真是徒增煩惱。
而 RabbitMQ 呢唇聘,它由于會(huì)在消息出問題或者消費(fèi)錯(cuò)誤的時(shí)候版姑,可以重新入隊(duì)或者移動(dòng)消息到死信隊(duì)列,繼續(xù)消費(fèi)后面的迟郎,會(huì)省心很多剥险。
壞消息就像群眾中的壞蛋那樣,Kafka 處理這種壞蛋太過殘暴宪肖,非得把壞蛋揪出來不行表制。相對來說,RabbitMQ 就溫柔多了控乾,群眾是群眾么介,壞蛋是壞蛋,分開處理嘛阱持。
六夭拌、消息的吞吐量
Kafka 是每秒幾十萬條消息吞吐,而 RabbitMQ 的吞吐量是每秒幾萬條消息。
其實(shí)鸽扁,在一家公司內(nèi)部蒜绽,有必須用到 Kafka 那么大吞吐量的項(xiàng)目真的很少。大部分項(xiàng)目桶现,像 RabbitMQ 那樣每秒幾萬的消息吞吐躲雅,已經(jīng)非常夠了。
在一些沒那么大吞吐量的項(xiàng)目中引入 Kafka骡和,我覺得就不如引入 RabbitMQ相赁。
為什么呢?
因?yàn)?Kafka 為了更好的吞吐量慰于,很大程度上增加了自己的復(fù)雜度钮科。而這些復(fù)雜度對項(xiàng)目來說,就是麻煩婆赠,主要體現(xiàn)在兩個(gè)方面:
1绵脯、配置復(fù)雜、維護(hù)復(fù)雜
Kafka 的參數(shù)配置相對 RabbitMQ 是很復(fù)雜的休里。比如:磁盤管理相關(guān)參數(shù)蛆挫,集群管理相關(guān)參數(shù),ZooKeeper 交互相關(guān)參數(shù)妙黍,Topic 級別相關(guān)參數(shù)等悴侵,都需要一些思考和調(diào)優(yōu)。
另外拭嫁,Kafka 本身集群和參與管理集群的 ZooKeeper可免,這就帶來了更多的維護(hù)成本。Kafka 要用好做粤,你要考慮 JVM巴元,消息持久化,集群本身交互驮宴,以及 ZooKeeper 本身和它與 Kafka 之間的可靠和效率。
2呕缭、用好堵泽,用對存在門檻
Kafka 的 Producer 和 Consumer 本身要用好用對也存在很高的門檻。
比如恢总,Producer 消息可靠性保障迎罗、冪等性、事務(wù)消息等片仿,都需要對 KafkaProducer 有深入的了解纹安。
而 Consumer 更不用說了,光是一個(gè)日志偏移管理就讓一大堆人掉了不少頭發(fā)。
相對來說厢岂,RabbitMQ 就簡單得多光督。你可能都不用配置什么,直接啟動(dòng)起來就能很穩(wěn)定可靠地使用了塔粒。就算配置结借,也是寥寥幾個(gè)參數(shù)設(shè)置即可。
所以卒茬,大家在項(xiàng)目中引入消息隊(duì)列的時(shí)候船老,真的要好好考慮下,不要因?yàn)榇蠹叶脊拇?Kafka 好圃酵,就無腦引入柳畔。
總結(jié)
可以看到,如果我們要做消息隊(duì)列選型郭赐,有兩件事是必須要做好的:
*列出業(yè)務(wù)最重要的幾個(gè)特點(diǎn)
*深入到消息隊(duì)列的細(xì)節(jié)中去比較
等我們對這些中間件的特點(diǎn)非常熟悉之后薪韩,甚至可以把業(yè)務(wù)分解成不同的子業(yè)務(wù),再根據(jù)不同的子業(yè)務(wù)的特征堪置,引入不同的消息隊(duì)列躬存,即消息隊(duì)列混用。這樣舀锨,我們就可能會(huì)最大化我們的獲益岭洲,最小化我們的成本。
說了這么多坎匿,其實(shí)還有很多 Kafka 和 RabbitMQ 的比較沒有說盾剩,比如二者集群的區(qū)別,占用資源多少的比較等替蔬。以后有機(jī)會(huì)可以再提提告私。
總之,期待大家看完這篇文章后承桥,能對 Kafka 和 RabbitMQ 的區(qū)別有了更細(xì)節(jié)性的了解驻粟。
最后,分享一個(gè)網(wǎng)上的比較全的對比圖: