Kafka 筆記
1概念
- Producer:消息生產者,向Kafka中發布消息的角色。
- Consumer:消息消費者,即從Kafka中拉取消息消費的客戶端。
- Consumer Group:消費者組,消費者組則是一組中存在多個消費者,消費者消費Broker中當前Topic的不同分區中的消息,消費者組之間互不影響,所有的消費者都屬于某個消費者組,即消費者組是邏輯上的一個訂閱者。某一個分區中的消息只能夠一個消費者組中的一個消費者所消費
- Broker:經紀人,一臺Kafka服務器就是一個Broker,一個集群由多個Broker組成,一個Broker可以容納多個Topic。
- Topic:主題,可以理解為一個隊列,生產者和消費者都是面向一個Topic
- Partition:分區,為了實現擴展性,一個非常大的Topic可以分布到多個Broker上,一個Topic可以分為多個Partition,每個Partition是一個有序的隊列(分區有序,不能保證全局有序)
- Replica:副本Replication,為保證集群中某個節點發生故障,節點上的Partition數據不丟失,Kafka可以正常的工作,Kafka提供了副本機制,一個Topic的每個分區有若干個副本,一個Leader和多個Follower
- Leader:每個分區多個副本的主角色,生產者發送數據的對象,以及消費者消費數據的對象都是Leader。
- Follower:每個分區多個副本的從角色,實時的從Leader中同步數據,保持和Leader數據的同步,Leader發生故障的時候,某個Follower會成為新的Leader。
上述一個Topic會產生多個分區Partition,分區中分為Leader和Follower,消息一般發送到Leader,Follower通過數據的同步與Leader保持同步,消費的話也是在Leader中發生消費,如果多個消費者,則分別消費Leader和各個Follower中的消息,當Leader發生故障的時候,某個Follower會成為主節點,此時會對齊消息的偏移量
工作流程
2 .1工作流程
Kafka中消息是以topic進行分類的,Producer生產消息,Consumer消費消息,都是面向topic的。

Topic是邏輯上的改變,Partition是物理上的概念,每個Partition對應著一個log文件,該log文件中存儲的就是producer生產的數據
Producer生產的數據會被不斷的追加到該log文件的末端,且每條數據都有自己的offset,consumer組中的每個consumer,都會實時記錄自己消費到了哪個offset,以便出錯恢復的時候,可以從上次的位置繼續消費。
2.2 文件存儲
Kafka文件存儲也是通過本地落盤的方式存儲的,主要是通過相應的log與index等文件保存具體的消息文件。

生產者不斷的向log文件追加消息文件,為了防止log文件過大導致定位效率低下,Kafka的log文件以1G為一個分界點,當.log文件大小超過1G的時候,此時會創建一個新的.log文件,同時為了快速定位大文件中消息位置,Kafka采取了分片和索引的機制來加速定位。
在kafka的存儲log的地方,即文件的地方,會存在消費的偏移量以及具體的分區信息,分區信息的話主要包括.index和.log文件組成,
2.3 生產者分區策略
分區的原因
-
方便在集群中擴展:每個partition通過調整以適應它所在的機器,而一個Topic又可以有多個partition組成,因此整個集群可以適應適合的數據
-
可以提高并發:以Partition為單位進行讀寫。類似于多路。
分區的原則
- 指明partition(這里的指明是指第幾個分區)的情況下,直接將指明的值作為partition的值
- 沒有指明partition的情況下,但是存在值key,此時將key的hash值與topic的partition總數進行取余得到partition值
- 值與partition均無的情況下,第一次調用時隨機生成一個整數,后面每次調用在這個整數上自增,將這個值與topic可用的partition總數取余得到partition值,即round-robin算法。
2.4 生產者ISR
為保證producer發送的數據能夠可靠的發送到指定的topic中,topic的每個partition收到producer發送的數據后,都需要向producer發送ackacknowledgement,如果producer收到ack就會進行下一輪的發送,否則重新發送數據。

發送ack的時機
確保有follower與leader同步完成,leader在發送ack,這樣可以保證在leader掛掉之后,follower中可以選出新的leader(主要是確保follower中數據不丟失)
follower同步完成多少才發送ack
- 半數以上的follower同步完成,即可發送ack
- 全部的follower同步完成,才可以發送ack
2.4.1 副本數據同步策略
半數follower同步完成即發送ack
優點是延遲低
缺點是選舉新的leader的時候,容忍n臺節點的故障,需要2n+1個副本(因為需要半數同意,所以故障的時候,能夠選舉的前提是剩下的副本超過半數),容錯率為1/2
全部follower同步完成完成發送ack
優點是容錯率搞,選舉新的leader的時候,容忍n臺節點的故障只需要n+1個副本即可,因為只需要剩下的一個人同意即可發送ack了
缺點是延遲高,因為需要全部副本同步完成才可
kafka選擇的是第二種,因為在容錯率上面更加有優勢,同時對于分區的數據而言,每個分區都有大量的數據,第一種方案會造成大量數據的冗余。雖然第二種網絡延遲較高,但是網絡延遲對于Kafka的影響較小。
2.4.2 ISR(同步副本集)
猜想
采用了第二種方案進行同步ack之后,如果leader收到數據,所有的follower開始同步數據,但有一個follower因為某種故障,遲遲不能夠與leader進行同步,那么leader就要一直等待下去,直到它同步完成,才可以發送ack,此時需要如何解決這個問題呢?
解決
leader中維護了一個動態的ISR(in-sync replica set),即與leader保持同步的follower集合,當ISR中的follower完成數據的同步之后,給leader發送ack,如果follower長時間沒有向leader同步數據,則該follower將從ISR中被踢出,該之間閾值由replica.lag.time.max.ms參數設定。當leader發生故障之后,會從ISR中選舉出新的leader。
2.5 生產者ack機制
對于某些不太重要的數據,對數據的可靠性要求不是很高,能夠容忍數據的少量丟失,所以沒有必要等到ISR中所有的follower全部接受成功。
Kafka為用戶提供了三種可靠性級別,用戶根據可靠性和延遲的要求進行權衡選擇不同的配置。
ack參數配置
0:producer不等待broker的ack,這一操作提供了最低的延遲,broker接收到還沒有寫入磁盤就已經返回,當broker故障時有可能丟失數據1:producer等待broker的ack,partition的leader落盤成功后返回ack,如果在follower同步成功之前leader故障,那么將丟失數據。(只是leader落盤-![img]()
-1(all):producer等待broker的ack,partition的leader和ISR的follower全部落盤成功才返回ack,但是如果在follower同步完成后,broker發送ack之前,如果leader發生故障,會造成數據重復。(這里的數據重復是因為沒有收到,所以繼續重發導致的數據重復)![img]()
producer返ack,0無落盤直接返,1只leader落盤然后返,-1全部落盤然后返
2.6 數據一致性問題

- LEO(Log End Offset):每個副本最后的一個offset
- HW(High Watermark):高水位,指代消費者能見到的最大的offset,ISR隊列中最小的LEO。
follower故障和leader故障
- follower故障:follower發生故障后會被臨時提出ISR(InSyncRepli),等待該follower恢復后,follower會讀取本地磁盤記錄的上次的HW,并將log文件高于HW的部分截取掉,從HW開始向leader進行同步,等待該follower的LEO大于等于該partition的HW,即follower追上leader之后,就可以重新加入ISR了。
- leader故障:leader發生故障之后,會從ISR中選出一個新的leader,為了保證多個副本之間的數據的一致性,其余的follower會先將各自的log文件高于HW的部分截掉,然后從新的leader中同步數據。
這只能保證副本之間的數據一致性,并不能保證數據不丟失或者不重復
2.7 ExactlyOnce
將服務器的ACK級別設置為-1(all),可以保證producer到Server之間不會丟失數據,即At Least Once至少一次語義。將服務器ACK級別設置為0,可以保證生產者每條消息只會被發送一次,即At Most Once至多一次。
At Least Once可以保證數據不丟失,但是不能保證數據不重復,而At Most Once可以保證數據不重復,但是不能保證數據不丟失,對于重要的數據,則要求數據不重復也不丟失,即Exactly Once即精確的一次。
在0.11版本的Kafka之前,只能保證數據不丟失,在下游對數據的重復進行去重操作,多余多個下游應用的情況,則分別進行全局去重,對性能有很大影響。
0.11版本的kafka,引入了一項重大特性:冪等性,冪等性指代Producer不論向Server發送了多少次重復數據,Server端都只會持久化一條數據。冪等性結合At Least Once語義就構成了Kafka的Exactly Once語義。
啟用冪等性,即在Producer的參數中設置enable.idempotence=true即可,Kafka的冪等性實現實際是將之前的去重操作放在了數據上游來做,開啟冪等性的Producer在初始化的時候會被分配一個PID,發往同一個Partition的消息會附帶Sequence Number,而Broker端會對<PID,Partition,SeqNumber>做緩存,當具有相同主鍵的消息的時候,Broker只會持久化一條。
但PID在重啟之后會發生變化,同時不同的Partition也具有不同的主鍵,所以冪等性無法保證跨分區跨會話的Exactly Once。
3.Kafka 與 Zookeeper
Kafka集群中有一個broker會被選舉為Controller,負責管理集群broker的上下線、所有topic的分區副本分配和leader的選舉等工作。Controller的工作管理是依賴于zookeeper的。
3.1 Zookeeper 協調控制
-
管理broker與consumer的動態加入與離開。(Producer不需要管理,隨便一臺計算機都可以作為Producer向Kakfa Broker發消息)
-
觸發負載均衡,當broker或consumer加入或離開時會觸發負載均衡算法,使得一
個consumer group內的多個consumer的消費負載平衡。(因為一個comsumer消費一個或多個partition,一個partition只能被一個consumer消費)
-
維護消費關系及每個partition的消費信息。
3.2 Zookeeper上的細節:
-
每個broker啟動后會在zookeeper上注冊一個臨時的broker registry,包含broker的ip地址和端口號,所存儲的topics和partitions信息。
-
每個consumer啟動后會在zookeeper上注冊一個臨時的consumer registry:包含consumer所屬的consumer group以及訂閱的topics。
-
每個consumer group關聯一個臨時的owner registry和一個持久的offset registry。對于被訂閱的每個partition包含一個owner registry,內容為訂閱這個partition的consumer id;同時包含一個offset registry,內容為上一次訂閱的offset。
用了磁盤,還速度快
沒錯,kafka就是速度無敵,本文將探究kafka無敵性能背后的秘密。
首先要有個概念,kafka高性能的背后,是多方面協同后、最終的結果,kafka從宏觀架構、分布式partition存儲、ISR數據同步、以及“無孔不入”的高效利用磁盤/操作系統特性,這些多方面的協同,是kafka成為性能之王的必然結果。
本文將從kafka零拷貝,探究其是如何“無孔不入”的高效利用磁盤/操作系統特性的。
先說說零拷貝
零拷貝并不是不需要拷貝,而是減少不必要的拷貝次數。通常是說在IO讀寫過程中。
實際上,零拷貝是有廣義和狹義之分,目前我們通常聽到的零拷貝,包括上面這個定義減少不必要的拷貝次數都是廣義上的零拷貝。其實了解到這點就足夠了。
我們知道,減少不必要的拷貝次數,就是為了提高效率。那零拷貝之前,是怎樣的呢?
聊聊傳統IO流程
比如:讀取文件,再用socket發送出去
傳統方式實現:
先讀取、再發送,實際經過1~4四次copy。
buffer = File.read
Socket.send(buffer)
1、第一次:將磁盤文件,讀取到操作系統內核緩沖區;
2、第二次:將內核緩沖區的數據,copy到application應用程序的buffer;
3、第三步:將application應用程序buffer中的數據,copy到socket網絡發送緩沖區(屬于操作系統內核的緩沖區);
4、第四次:將socket buffer的數據,copy到網卡,由網卡進行網絡傳輸。

傳統方式,讀取磁盤文件并進行網絡發送,經過的四次數據copy是非常繁瑣的。實際IO讀寫,需要進行IO中斷,需要CPU響應中斷(帶來上下文切換),盡管后來引入DMA來接管CPU的中斷請求,但四次copy是存在“不必要的拷貝”的。
重新思考傳統IO方式,會注意到實際上并不需要第二個和第三個數據副本。應用程序除了緩存數據并將其傳輸回套接字緩沖區之外什么都不做。相反,數據可以直接從讀緩沖區傳輸到套接字緩沖區。
顯然,第二次和第三次數據copy 其實在這種場景下沒有什么幫助反而帶來開銷,這也正是零拷貝出現的意義。
這種場景:是指讀取磁盤文件后,不需要做其他處理,直接用網絡發送出去。試想,如果讀取磁盤的數據需要用程序進一步處理的話,必須要經過第二次和第三次數據copy,讓應用程序在內存緩沖區處理。
為什么Kafka這么快
kafka作為MQ也好,作為存儲層也好,無非是兩個重要功能,一是Producer生產的數據存到broker,二是 Consumer從broker讀取數據;我們把它簡化成如下兩個過程:
1、網絡數據持久化到磁盤 (Producer 到 Broker)
2、磁盤文件通過網絡發送(Broker 到 Consumer)
下面,先給出“kafka用了磁盤,還速度快”的結論
1、順序讀寫
磁盤順序讀或寫的速度400M/s,能夠發揮磁盤最大的速度。
隨機讀寫,磁盤速度慢的時候十幾到幾百K/s。這就看出了差距。
kafka將來自Producer的數據,順序追加在partition,partition就是一個文件,以此實現順序寫入。
Consumer從broker讀取數據時,因為自帶了偏移量,接著上次讀取的位置繼續讀,以此實現順序讀。
順序讀寫,是kafka利用磁盤特性的一個重要體現。

2、零拷貝 sendfile(in,out)
數據直接在內核完成輸入和輸出,不需要拷貝到用戶空間再寫出去。
kafka數據寫入磁盤前,數據先寫到進程的內存空間。
3、mmap文件映射
虛擬映射只支持文件;
在進程 的非堆內存開辟一塊內存空間,和OS內核空間的一塊內存進行映射,
kafka數據寫入、是寫入這塊內存空間,但實際這塊內存和OS內核內存有映射,也就是相當于寫在內核內存空間了,且這塊內核空間、內核直接能夠訪問到,直接落入磁盤。
這里,我們需要清楚的是:內核緩沖區的數據,flush就能完成落盤。
我們來重點探究 kafka兩個重要過程、以及是如何利用兩個零拷貝技術sendfile和mmap的。
網絡數據持久化到磁盤 (Producer 到 Broker)
傳統方式實現:
data = socket.read()// 讀取網絡數據
File file = new File()
file.write(data)// 持久化到磁盤
file.flush()
先接收生產者發來的消息,再落入磁盤。
實際會經過四次copy,如下圖的四個箭頭。

數據落盤通常都是非實時的,kafka生產者數據持久化也是如此。Kafka的數據并不是實時的寫入硬盤,它充分利用了現代操作系統分頁存儲來利用內存提高I/O效率。
對于kafka來說,Producer生產的數據存到broker,這個過程讀取到socket buffer的網絡數據,其實可以直接在OS內核緩沖區,完成落盤。并沒有必要將socket buffer的網絡數據,讀取到應用進程緩沖區;在這里應用進程緩沖區其實就是broker,broker收到生產者的數據,就是為了持久化。
在此特殊場景下:接收來自socket buffer的網絡數據,應用進程不需要中間處理、直接進行持久化時。——可以使用mmap內存文件映射。
Memory Mapped Files
簡稱mmap,簡單描述其作用就是:將磁盤文件映射到內存, 用戶通過修改內存就能修改磁盤文件。
它的工作原理是直接利用操作系統的Page來實現文件到物理內存的直接映射。完成映射之后你對物理內存的操作會被同步到硬盤上(操作系統在適當的時候)。
通過mmap,進程像讀寫硬盤一樣讀寫內存(當然是虛擬機內存),也不必關心內存的大小有虛擬內存為我們兜底。
使用這種方式可以獲取很大的I/O提升,省去了用戶空間到內核空間復制的開銷。
mmap也有一個很明顯的缺陷——不可靠,寫到mmap中的數據并沒有被真正的寫到硬盤,操作系統會在程序主動調用flush的時候才把數據真正的寫到硬盤。Kafka提供了一個參數——producer.type來控制是不是主動flush;如果Kafka寫入到mmap之后就立即flush然后再返回Producer叫同步(sync);寫入mmap之后立即返回Producer不調用flush叫異步(async)。
磁盤文件通過網絡發送(Broker 到 Consumer)
傳統方式實現:
先讀取磁盤、再用socket發送,實際也是進過四次copy。
buffer = File.read
Socket.send(buffer)
而 Linux 2.4+ 內核通過 sendfile 系統調用,提供了零拷貝。磁盤數據通過 DMA 拷貝到內核態 Buffer 后,直接通過 DMA 拷貝到 NIC Buffer(socket buffer),無需 CPU 拷貝。這也是零拷貝這一說法的來源。除了減少數據拷貝外,因為整個讀文件 - 網絡發送由一個 sendfile 調用完成,整個過程只有兩次上下文切換,因此大大提高了性能。零拷貝過程如下圖所示。

相比于文章開始,對傳統IO 4步拷貝的分析,sendfile將第二次、第三次拷貝,一步完成。
其實這項零拷貝技術,直接從內核空間(DMA的)到內核空間(Socket的)、然后發送網卡。
應用的場景非常多,如Tomcat、Nginx、Apache等web服務器返回靜態資源等,將數據用網絡發送出去,都運用了sendfile。
簡單理解 sendfile(in,out)就是,磁盤文件讀取到操作系統內核緩沖區后、直接扔給網卡,發送網絡數據。
注: transferTo 和 transferFrom 并不保證一定能使用零拷貝。實際上是否能使用零拷貝與操作系統相關,如果操作系統提供 sendfile 這樣的零拷貝系統調用,則這兩個方法會通過這樣的系統調用充分利用零拷貝的優勢,否則并不能通過這兩個方法本身實現零拷貝。
Kafka總結
- partition順序讀寫,充分利用磁盤特性,這是基礎,順序讀寫是指的文件的順序追加,減少了磁盤尋址的開銷,相比隨機寫速度提升很多;
- Producer生產的數據持久化到broker,采用mmap文件映射,實現順序的快速寫入;
- Customer從broker讀取數據,采用sendfile,將磁盤文件讀到OS內核緩沖區后,直接轉到socket buffer進行網絡發送。
- Kafka天生的分布式架構
- 使用了零拷貝技術,不需要切換到用戶態,在內核態即可完成讀寫操作,且數據的拷貝次數也更少。
- 對log文件進行了分segment,并對segment建立了索引
mmap 和 sendfile總結
1、都是Linux內核提供、實現零拷貝的API;
2、sendfile 是將讀到內核空間的數據,轉到socket buffer,進行網絡發送;
3、mmap將磁盤文件映射到內存,支持讀和寫,對內存的操作會反映在磁盤文件上。
RocketMQ 在消費消息時,使用了 mmap。kafka 使用了 sendFile。
Kafka 和 RabbitMQ
有個 xx 需求,我應該用 Kafka 還是 RabbitMQ ?
這個問題很常見,而且很多人對二者的選擇也把握不好。
所以我決定寫篇文章來詳細說一下:Kafka 和 RabbitMQ 的區別,適用于什么場景?
同時,這個問題在面試中也經常問到。
下面我會通過 6 個場景,來對比分析一下 Kafka 和 RabbitMQ 的優劣。
一、消息的順序
有這樣一個需求:當訂單狀態變化的時候,把訂單狀態變化的消息發送給所有關心訂單變化的系統。
訂單會有創建成功、待付款、已支付、已發貨的狀態,狀態之間是單向流動的。

好,現在我們把訂單狀態變化消息要發送給所有關心訂單狀態的系統上去,實現方式就是用消息隊列。

在這種業務下,我們最想要的是什么?
- 消息的順序:對于同一筆訂單來說,狀態的變化都是有嚴格的先后順序的。
- 吞吐量:像訂單的業務,我們自然希望訂單越多越好。訂單越多,吞吐量就越大。
在這種情況下,我們先看看 RabbitMQ 是怎么做的。
首先,對于發消息,并廣播給多個消費者這種情況,RabbitMQ 會為每個消費者建立一個對應的隊列。也就是說,如果有 10 個消費者,RabbitMQ 會建立 10 個對應的隊列。然后,當一條消息被發出后,RabbitMQ 會把這條消息復制 10 份放到這 10 個隊列里。

當 RabbitMQ 把消息放入到對應的隊列后,我們緊接著面臨的問題就是,我們應該在系統內部啟動多少線程去從消息隊列中獲取消息。
如果只是單線程去獲取消息,那自然沒有什么好說的。但是多線程情況,可能就會有問題了……
RabbitMQ 有這么個特性,它在官方文檔就聲明了自己是不保證多線程消費同一個隊列的消息,一定保證順序的。而不保證的原因,是因為多線程時,當一個線程消費消息報錯的時候,RabbitMQ 會把消費失敗的消息再入隊,此時就可能出現亂序的情況。

T0 時刻,隊列中有四條消息 A1、B1、B2、A2。其中 A1、A2 表示訂單 A 的兩個狀態:待付款、已付款。B1、B2 也同理,是訂單 B 的待付款、已付款。
到了 T1 時刻,消息 A1 被線程 1 收到,消息 B1 被線程 2 收到。此時,一切都還正常。
到了 T3 時刻,B1 消費出錯了,同時呢,由于線程 1 處理速度快,又從消息隊列中獲取到了 B2。此時,問題開始出現。
到了 T4 時刻,由于 RabbitMQ 線程消費出錯,可以把消息重新入隊的特性,此時 B1 會被重新放到隊列頭部。所以,如果不湊巧,線程 1 獲取到了 B1,就出現了亂序情況,B2 狀態明明是 B1 的后續狀態,卻被提前處理了。
所以,可以看到了,這個場景用 RabbitMQ,出現了三個問題:
- 為了實現發布訂閱功能,從而使用的消息復制,會降低性能并耗費更多資源
- 多個消費者無法嚴格保證消息順序
- 大量的訂單集中在一個隊列,吞吐量受到了限制
那么 Kafka 怎么樣呢?Kafka 正好在這三個問題上,表現的要比 RabbitMQ 要好得多。
首先,Kafka 的發布訂閱并不會復制消息,因為 Kafka 的發布訂閱就是消費者直接去獲取被 Kafka 保存在日志文件中的消息就好。無論是多少消費者,他們只需要主動去找到消息在文件中的位置即可。
其次,Kafka 不會出現消費者出錯后,把消息重新入隊的現象。
最后,Kafka 可以對訂單進行分區,把不同訂單分到多個分區中保存,這樣,吞吐量能更好。
所以,對于這個需求 Kafka 更合適。
二、消息的匹配
我曾經做過一套營銷系統。這套系統中有個非常顯著的特點,就是非常復雜非常靈活地匹配規則。
比如,要根據推廣內容去匹配不同的方式做宣傳。又比如,要根據不同的活動去匹配不同的渠道去做分發。
總之,數不清的匹配規則是這套系統中非常重要的一個特點。

首先,先看看 RabbitMQ 的,你會發現 RabbitMQ 是允許在消息中添加 routing_key 或者自定義消息頭,然后通過一些特殊的 Exchange,很簡單的就實現了消息匹配分發。開發幾乎不用成本。
而 Kafka 呢?如果你要實現消息匹配,開發成本高多了。
首先,通過簡單的配置去自動匹配和分發到合適的消費者端這件事是不可能的。
其次,消費者端必須先把所有消息不管需要不需要,都取出來。然后,再根據業務需求,自己去實現各種精準和模糊匹配。可能因為過度的復雜性,還要引入規則引擎。
這個場景下 RabbitMQ 扳回一分。
三、消息的超時
在電商業務里,有個需求:下單之后,如果用戶在 15 分鐘內未支付,則自動取消訂單。
你可能奇怪,這種怎么也會用到消息隊列的?
我來先簡單解釋一下,在單一服務的系統,可以起個定時任務就搞定了。
但是,在 SOA 或者微服務架構下,這樣做就不行了。因為很多個服務都關心是否支付這件事,如果每種服務,都自己實現一套定時任務的邏輯,既重復,又難以維護。
在這種情況下,我們往往會做一層抽象:把要執行的任務封裝成消息。當時間到了,直接扔到消息隊列里,消息的訂閱者們獲取到消息后,直接執行即可。
希望把消息延遲一定時間再處理的,被稱為延遲隊列。
對于訂單取消的這種業務,我們就會在創建訂單的時候,同時扔一個包含了執行任務信息的消息到延遲隊列,指定 15 分鐘后,讓訂閱這個隊列的各個消費者,可以收到這個消息。隨后,各個消費者所在的系統就可以去執行相關的掃描訂單的任務了。

RabbitMQ 和 Kafka 消息隊列如何選?
先看下 RabbitMQ 的。
RabbitMQ 的消息自帶手表,消息中有個 TTL 字段,可以設置消息在 RabbitMQ 中的存放的時間,超時了會被移送到一個叫死信隊列的地方。
所以,延遲隊列 RabbitMQ 最簡單的實現方式就是設置 TTL,然后一個消費者去監聽死信隊列。當消息超時了,監聽死信隊列的消費者就收到消息了。
不過,這樣做有個大問題:假設,我們先往隊列放入一條過期時間是 10 秒的 A 消息,再放入一條過期時間是 5 秒的 B 消息。 那么問題來了,B 消息會先于 A 消息進入死信隊列嗎?
答案是否定的。B 消息會優先遵守隊列的先進先出規則,在 A 消息過期后,和其一起進入死信隊列被消費者消費。
在 RabbitMQ 的 3.5.8 版本以后,官方推薦的 rabbitmq delayed message exchange 插件可以解決這個問題。
- 用了這個插件,我們在發送消息的時候,把消息發往一個特殊的 Exchange。
- 同時,在消息頭里指定要延遲的時間。
- 收到消息的 Exchange 并不會立即把消息放到隊列里,而是在消息延遲時間到達后,才會把消息放入。

再看下 Kafka 的:

Kafka 要實現延遲隊列就很麻煩了。
- 你先需要把消息先放入一個臨時的 topic。
- 然后得自己開發一個做中轉的消費者。讓這個中間的消費者先去把消息從這個臨時的 topic 取出來。
- 取出來,這消息還不能馬上處理啊,因為沒到時間呢。也沒法保存在自己的內存里,怕崩潰了,消息沒了。所以,就得把沒有到時間的消息存入到數據庫里。
- 存入數據庫中的消息需要在時間到了之后再放入到 Kafka 里,以便真正的消費者去執行真正的業務邏輯。
- ……
想想就已經頭大了,這都快搞成調度平臺了。再高級點,還要用時間輪算法才能更好更準確。
這次,RabbitMQ 上那一條條戴手表的消息,才是最好的選擇。
四、消息的保持
在微服務里,事件溯源模式是經常用到的。如果想用消息隊列實現,一般是把事件當成消息,依次發送到消息隊列中。
事件溯源有個最經典的場景,就是事件的重放。簡單來講就是把系統中某段時間發生的事件依次取出來再處理。而且,根據業務場景不同,這些事件重放很可能不是一次,更可能是重復 N 次。
假設,我們現在需要一批在線事件重放,去排查一些問題。
RabbitMQ 此時就真的不行了,因為消息被人取出來就被刪除了。想再次被重復消費?對不起。
而 Kafka 呢,消息會被持久化一個專門的日志文件里。不會因為被消費了就被刪除。
所以,對消息不離不棄的 Kafka 相對用過就拋的 RabbitMQ,請選擇 Kafka。
五、消息的錯誤處理
很多時候,在做記錄數據相關業務的時候,Kafka 一般是不二選擇。不過,有時候在記錄數據吞吐量不大時,我自己倒是更喜歡用 RabbitMQ。
原因就是 Kafka 有一個我很不喜歡的設計原則:
當單個分區中的消息一旦出現消費失敗,就只能停止而不是跳過這條失敗的消息繼續消費后面的消息。即不允許消息空洞。
只要消息出現失敗,不管是 Kafka 自身消息格式的損壞,還是消費者處理出現異常,是不允許跳過消費失敗的消息繼續往后消費的。
所以,在數據統計不要求十分精確的場景下選了 Kafka,一旦出現了消息消費問題,就會發生項目不可用的情況。這真是徒增煩惱。
而 RabbitMQ 呢,它由于會在消息出問題或者消費錯誤的時候,可以重新入隊或者移動消息到死信隊列,繼續消費后面的,會省心很多。
壞消息就像群眾中的壞蛋那樣,Kafka 處理這種壞蛋太過殘暴,非得把壞蛋揪出來不行。相對來說,RabbitMQ 就溫柔多了,群眾是群眾,壞蛋是壞蛋,分開處理嘛。
六、消息的吞吐量
Kafka 是每秒幾十萬條消息吞吐,而 RabbitMQ 的吞吐量是每秒幾萬條消息。
其實,在一家公司內部,有必須用到 Kafka 那么大吞吐量的項目真的很少。大部分項目,像 RabbitMQ 那樣每秒幾萬的消息吞吐,已經非常夠了。
在一些沒那么大吞吐量的項目中引入 Kafka,我覺得就不如引入 RabbitMQ。
為什么呢?
因為 Kafka 為了更好的吞吐量,很大程度上增加了自己的復雜度。而這些復雜度對項目來說,就是麻煩,主要體現在兩個方面:
1、配置復雜、維護復雜
Kafka 的參數配置相對 RabbitMQ 是很復雜的。比如:磁盤管理相關參數,集群管理相關參數,ZooKeeper 交互相關參數,Topic 級別相關參數等,都需要一些思考和調優。
另外,Kafka 本身集群和參與管理集群的 ZooKeeper,這就帶來了更多的維護成本。Kafka 要用好,你要考慮 JVM,消息持久化,集群本身交互,以及 ZooKeeper 本身和它與 Kafka 之間的可靠和效率。
2、用好,用對存在門檻
Kafka 的 Producer 和 Consumer 本身要用好用對也存在很高的門檻。
比如,Producer 消息可靠性保障、冪等性、事務消息等,都需要對 KafkaProducer 有深入的了解。
而 Consumer 更不用說了,光是一個日志偏移管理就讓一大堆人掉了不少頭發。
相對來說,RabbitMQ 就簡單得多。你可能都不用配置什么,直接啟動起來就能很穩定可靠地使用了。就算配置,也是寥寥幾個參數設置即可。
所以,大家在項目中引入消息隊列的時候,真的要好好考慮下,不要因為大家都鼓吹 Kafka 好,就無腦引入。
總結
可以看到,如果我們要做消息隊列選型,有兩件事是必須要做好的:
- 列出業務最重要的幾個特點
- 深入到消息隊列的細節中去比較
等我們對這些中間件的特點非常熟悉之后,甚至可以把業務分解成不同的子業務,再根據不同的子業務的特征,引入不同的消息隊列,即消息隊列混用。這樣,我們就可能會最大化我們的獲益,最小化我們的成本。
說了這么多,其實還有很多 Kafka 和 RabbitMQ 的比較沒有說,比如二者集群的區別,占用資源多少的比較等。以后有機會可以再提提。
總之,期待大家看完這篇文章后,能對 Kafka 和 RabbitMQ 的區別有了更細節性的了解。
最后,分享一個網上的比較全的對比圖:




浙公網安備 33010602011771號