深度剖析 Kafka/RocketMQ 順序消息的一些坑

通常我們在說順序消費指的是生產者按照順序發送,消費者按照順序進行消費,聽起來簡單,但做起來卻非常困難。

我們都知道無論是 Kafka 還是 RocketMQ,每個主題下面都有若干分區(RocketMQ 叫隊列),如果消息被分配到不同的分區中,那麼 Kafka 是不能保證消息的消費順序的,因爲每個分區都分配到一個消費者,此時無法保證消費者的消費先後,因此如果需要進行消息具有消費順序性,可以在生產端指定這一類消息的 key,這類消息都用相同的 key 進行消息發送,kafka 就會根據 key 哈希取模選取其中一個分區進行存儲,由於一個分區只能由一個消費者進行監聽消費,因此這時候消息就具有消息消費的順序性了。

生產端

但以上情況只是在正常情況下可以保證順序消息,但發生故障後,就沒辦法保證消息的順序了,我總結以下兩點:

1、當生產端是異步發送時,此時有消息發送失敗,比如你異步發送了 1,2,3 消息,2 消息發送異常重試發送,這時候順序就亂了;

2、當 Broker 宕機出現問題,此時生產端有可能會把順序消息發送到不同的分區,這時會發生短暫消息順序不一致的現象。

針對以上兩點,生產端必須保證單線程同步發送,這還好解決,針對第二點,想要做到嚴格的消息順序,就要保證當集羣出現故障後集羣立馬不可用,或者主題做成單分區,但這麼做大大犧牲了集羣的高可用,單分區也會另集羣性能大大降低。

針對以上第二點,下面盤點一下 Kafka 集羣中有哪些意外情況會打亂消息的順序。

1、分區變更的情況

假設有集羣中有兩個分區的主題 A,生產端需要往分區 1 發送 3 條順序消息,我們都知道生產端是根據消息 Key 取模計算決定消息發往哪個分區的,如果此時生產端發送第三條消息前,主題 A 增加了一個分區,生產端根據 Key 取模得出的分區號就不一樣了,第三條消息路由到其它分區,結果就是這三條順序消息就不在同一個分區了,此時就不能保證這三條消息的消費順序了。

2、分區不變更

1.1、分區單副本

假設此時集羣有兩個分區的主題 A,副本因子爲 1,生產端需要往分區 1 發送 3 條順序消息,前兩條消息已成功發送到分區 1,此時分區 1 所在的 broker 掛了(由於副本因子只有 1,因此會導致分區 1 不可用),當生產端發送第三條消息時發現分區 1 不可用,就會導致發送失敗,然後嘗試進行重試發送,如果此時分區 1 還未恢復可用,這時生產端會將消息路由到其它分區,導致了這三條消息不在同一個分區。

1.2、分區多副本

針對分區單副本情況,我們自然會想到將分區設置爲多副本不就可以避免這種情況發生嗎?多副本情況下,發送端同步發送,acks = all,即保證消息都同步到全部副本後,才返回發送成功,保證了所有副本都處在 ISR 列表中,如果此時其中一個 broker 宕機了,也不會導致分區不可用的情況,看起來確實避免了分區單副本分區不可用導致消息路由到其它分區的情況發生。

但我想說的是,還有一種極端的現象會發生,當某個 broker 宕機了,處在這個 broker 上的 leader 副本就不可用了,此時 controller 會進行該分區的 leader 選舉,在選舉過程中分區 leader不可用,生產端會短暫報 no leader 警告,這時生產端也會出現消息被路由到其它分區的可能。

消費端

Kafka

kafka 的消費類 KafkaConsumer 是非線程安全的,因此用戶無法在多線程中共享一個 KafkaConsumer 實例,且 KafkaConsumer 本身並沒有實現多線程消費邏輯,如需多線程消費,還需要用戶自行實現,在這裏我會講到 Kafka 兩種多線程消費模型。

1、每個線程維護一個 KafkaConsumer

這樣相當於一個進程內擁有多個消費者,也可以說消費組內成員是有多個線程內的 KafkaConsumer 組成的。

但其實這個消費模型是存在很大問題的,從消費消費模型可看出每個 KafkaConsumer 會負責固定的分區,因此無法提升單個分區的消費能力,如果一個主題分區數量很多,只能通過增加 KafkaConsumer 實例提高消費能力,這樣一來線程數量過多,導致項目 Socket 連接開銷巨大,項目中一般不用該線程模型去消費。

2、單 KafkaConsumer 實例 + 多 worker 線程

針對第一個線程模型的缺點,我們可採取 KafkaConsumer 實例與消息消費邏輯解耦,把消息消費邏輯放入單獨的線程中去處理,線程模型如下:

從消費線程模型可看出,當 KafkaConsumer 實例與消息消費邏輯解耦後,我們不需要創建多個 KafkaConsumer 實例就可進行多線程消費,還可根據消費的負載情況動態調整 worker 線程,具有很強的獨立擴展性,在公司內部使用的多線程消費模型就是用的單 KafkaConsumer 實例 + 多 worker 線程模型。

但這個消費模型由於消費邏輯是利用多線程進行消費的,因此並不能保證其消息的消費順序,在這裏我們可以引入阻塞隊列的模型,一個 woker 線程對應一個阻塞隊列,線程不斷輪訓從阻塞隊列中獲取消息進行消費,對具有相同 key 的消息進行取模,並放入相同的隊列中,實現順序消費, 消費模型如下:

但是以上兩個消費線程模型,存在一個問題:

在消費過程中,如果 Kafka 消費組發生重平衡,此時的分區被分配給其它消費組了,如果拉取回來的消息沒有被消費,雖然 Kakfa 可以實現 ConsumerRebalanceListener 接口,在新一輪重平衡前主動提交消費偏移量,但這貌似解決不了未消費的消息被打亂順序的可能性?

因此在消費前,還需要主動進行判斷此分區是否被分配給其它消費者處理,並且還需要鎖定該分區在消費當中不能被分配到其它消費者中(但 kafka 目前做不到這一點)。

參考 RocketMQ 的做法:

在消費前主動調用 ProcessQueue#isDropped 方法判斷隊列是否已過期,並且對該隊列進行加鎖處理(向 broker 端請求該隊列加鎖)。

RocketMQ

RocketMQ 不像 Kafka 那麼“原生”,RocketMQ 早已爲你準備好了你的需求,它本身的消費模型就是單 consumer 實例 + 多 worker 線程模型,有興趣的小夥伴可以從以下方法觀摩 RocketMQ 的消費邏輯:

org.apache.rocketmq.client.impl.consumer.PullMessageService#run

RocketMQ 會爲每個隊列分配一個 PullRequest,並將其放入 pullRequestQueue,PullMessageService 線程會不斷輪詢從 pullRequestQueue 中取出 PullRequest 去拉取消息,接着將拉取到的消息給到 ConsumeMessageService 處理,ConsumeMessageService 有兩個子接口:

// 併發消息消費邏輯實現類
org.apache.rocketmq.client.impl.consumer.ConsumeMessageConcurrentlyService;
// 順序消息消費邏輯實現類
org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService;

其中,ConsumeMessageConcurrentlyService 內部有一個線程池,用於併發消費,同樣地,如果需要順序消費,那麼 RocketMQ 提供了 ConsumeMessageOrderlyService 類進行順序消息消費處理。

經過對 Kafka 消費線程模型的思考之後,從 ConsumeMessageOrderlyService 源碼中能夠看出 RocketMQ 能夠實現局部消費順序,我認爲主要有以下兩點:

1)RocketMQ 會爲每個消息隊列建一個對象鎖,這樣只要線程池中有該消息隊列在處理,則需等待處理完才能進行下一次消費,保證在當前 Consumer 內,同一隊列的消息進行串行消費。

2)向 Broker 端請求鎖定當前順序消費的隊列,防止在消費過程中被分配給其它消費者處理從而打亂消費順序。

總結

經過這篇文章的分析後,嘗試回答讀者的問題:

1、生產端:

1)生產端必須保證單線程同步發送,將順序消息發送到同一個分區(當然如果發生了文中所描述的 Kafka 集羣中意外情況,還是有可能會打亂消息的順序,因此無論是 Kafka 還是 RocketMQ 都無法保證嚴格的順序消息);

2、消費端:

2)多分區的情況下:

如果想要保證 Kafka 在消費時要保證消費的順序性,可以使用每個線程維護一個 KafkaConsumer 實例,並且是一條一條地去拉取消息並進行消費(防止重平衡時有可能打亂消費順序);對於能容忍消息短暫亂序的業務(話說回來, Kafka 集羣也不能保證嚴格的消息順序),可以使用單 KafkaConsumer 實例 + 多 worker 線程 + 一條線程對應一個阻塞隊列消費線程模型。

3)單分區的情況下:

由於單分區不存在重平衡問題,以上兩個線程模型的都可以保證消費的順序性。

另外如果是 RocketMQ,使用 MessageListenerOrderly 監聽消費可保證消息消費順序。

很多人也有這個疑問:既然 Kafka 和 RocketMQ 都不能保證嚴格的順序消息,那麼順序消費還有意義嗎?

一般來說普通的的順序消息能夠滿足大部分業務場景,如果業務能夠容忍集羣異常狀態下消息短暫不一致的情況,則不需要嚴格的順序消息。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章