KAFKA實踐:【二十】如何保證消息順序?消息不丟失?消息不重複?

大家好,這是一個爲了夢想而保持學習的博客。這個專題會記錄我對於KAFKA的學習和實戰經驗,希望對大家有所幫助,目錄形式依舊爲問答的方式,相當於是模擬面試。

前言

我們在前面幾個文章,知道了kafka的生產者/消費者的基本原理,這裏就讓我們來思考一些常見的生產問題,例如標題中的那些。
在討論這些問題之前,我需要強調一下:
消息交付是一個端到端的問題,所以我們需要進行全鏈路的分析和思考才能得到問題的答案。


如何保證消息順序?

首先我們需要知道:kafka只保證分區內的消息是順序的,並不保證Topic維度的消息順序。
其實我們聯繫存儲架構來思考,就很容易理解。我們的分區文件是追加寫入的,那麼對於一個分區而言,它保證消息順序的特性是天然自帶的。但是Topic是一個邏輯概念,是由多個分區文件組成的,因此想要做到Topic維度的數據順序代價是非常大的,所以kafka並不保證Topic維度的消息順序。

有了上面的那個前提,我們就知道了如果想要實現順序消費,那麼對於生產/服務/消費三端就需要以下動作:

  • 生產端,保證將消息寫入一個分區內。
  • 服務端,創建只有一個分區的Topic。
  • 消費端,保證一個線程消費一個分區。

對於生產端而言,如何保證消息寫入到一個分區內呢?這得分情況討論。
1、如果Topic有多個分區,那麼我們可以通過設置key的方式讓一些特定的消息通過key.hash寫入到特定的分區中,例如我們想讓某個訂單的消息都寫到一個分區中,可以設置orderId爲key即可實現。
2、如果Topic有多個分區,我們還可以手動指定分區,在創建「ProducerRecord」時。
3、如果Topic只有一個分區,那麼顯然,我們使用默認的生產方式即可達到目的。
除此之外呢,還需要關閉重試機制,也就是把重試次數設置爲0,否則當出現可重試異常的時候,客戶端會自動重試會導致亂序。
很多同學可能還對max.in.flight.requests.per.connection有疑問,不知道是否需要設置爲1(默認值是5)。首先我們瞭解下這個參數的含義是控制發出給某個Node的請求還有未收到響應的個數,不超過5個。內部的數據結構類似於是Map<Node, List<Request>>,這個參數就是控制這個List.size() <= 5
這麼分析下來,顯然是不需要設置爲1的,因爲只要你關閉了重試,你哪怕是前面一個請求失敗了,後面一個請求成功了,只要是前一個請求不重試,那麼也是符合順序的要求的。
那如果說業務連這種場景都無法忍受的話,那麼可以考慮設置爲1,就相當於一條一條的發。其實真有這麼嚴格的要求的話,乾脆同步發送,一條條的發,如果失敗了就報錯終止發送。

對於服務端而言,保證消息順序最簡單的就是創建一個只有一個分區的Topic即可。但是我們知道分區是用於橫向擴展的,單分區的話可能整體的吞吐量比較一般。如果是多分區的話呢?那麼就需要配合生產端進行了,要麼指定分區,要麼設置key,也可以達到同樣的效果,不過這種方式又可能存在數據/流量傾斜的情況,也就是造成某個分區的消息積壓非常多,或者某個分區的流量特別大,導致整體的負載不均衡。這種情況的話,就需要考慮好業務場景以及搭配好對應的監控。

對於消費端而言,也需要分情況討論。
1、如果是單線程消費,那麼所消費到的所有消息都是順序的,不需要做什麼額外的處理,但是這種消費方式往往消費速率跟不上,導致消息積壓。
2、如果是多線程消費,比如經典的拿到消息之後丟入線程池,這種方式呢顯然就無法保證消息的有序性了。那麼我們就需要思考一下如何讓一個分區的消息只被一個線程消費呢?一種簡單的實現方式就是使用內存隊列 —— 將消費出來的消息,根據一些策略丟入對應的內存隊列,隊列的下游再用單線程的方式從隊列中拉取數據進行消費。最基本的策略有:

  • 基於record.partition()拿到對應的分區ID,然後丟入對應的內存隊列。
  • 基於record.key()拿到上游設置的key,進行hash後丟入對應的內存隊列。

這兩種策略的選擇呢,也是根據業務場景去進行選擇的,還需要配合上遊生產者的生產策略進行選擇。
除此之外呢,對應的消息提交還會變得比較麻煩,需要進一步的設計,這裏只是提供一個簡單的思路。

總結一下
生產端:設置Key或者指定分區,關閉重試。
服務端:單分區、多分區Topic都可以,取決於上游生產者的生產策略。
消費端:單線程消費都是順序的,如果想要多線程消費可以考慮使用內存隊列構造一個內部的生產/消費模型。


如何實現消息不丟失

我們首先來思考一下哪些場景下會丟失消息?

  • 生產端,acks參數設置爲0(默認是1),發完就不管了,可能壓根消息沒發送到服務端從而導致消息丟失。
  • 生產端,retries參數設置爲0(默認是0),當收到異常之後不重試,直接丟棄發送的消息從而導致消息丟失。
  • 服務端,如果Topic的副本數爲1,那麼對應的機器如果損壞就會直接導致消息丟失。
  • 服務端,如果沒有設置數據強制刷盤,那麼可能機器重啓導致寫入OS的那部分消息丟失。
  • 服務端,如果ISR中的副本數只有1的情況下如果還允許生產消息,那麼當這個副本所在的機器出問題後對應的消息就會丟失。
  • 服務端,如果允許非ISR中的副本選舉爲Leader,那麼也可能導致消息丟失。
  • 消費端,如果在業務處理完這條消息之前,該消息被提交位移了,那麼當業務處理出現問題後就可能丟失消息。

以上就我們想到的端到端可能存在消息丟失的所有場景,那麼我們一個個來回答應該怎麼做

  • 生產端,acks參數設置爲all,強制要求寫入所有ISR中的副本成功後才認爲是成功。
  • 生產端,retries參數設置爲Integer.MAX_VALUE,在出現一條消息發送失敗之後,就一直重試直到成功爲止。
  • 服務端,設置Topic的副本數至少大於等於2,通常情況下是默認爲3。
  • 服務端,設置log.flush.interval.messages參數爲1,也就是每寫入一條消息就強制刷盤。默認情況下kafka是不控制刷盤的,交給OS去控制。
  • 服務端,設置min.insync.replicas參數大於等於2,也就是要求ISR中的副本數不得小於2,否則不再提供生產服務,拒絕生產請求。
  • 服務端,設置unclean.leader.election.enable參數爲false,也就是當不會選擇ISR之外的副本成爲Leader。
  • 消費端,關閉自動提交,即設置enable.auto.commit爲false,同時使用同步提交及在代碼中使用commitOffsetsSync函數按照offset的維度進行消息提交。

通過以上的一系列參數設置,是可以保證全鏈路的消息不丟失的,但是同時吞吐量會下降到一個令人髮指的程度。
最後,補充一個鏈接,大家可以在官網上查到某個版本的kafka所有參數詳情。
https://kafka.apache.org/documentation/#brokerconfigs


如何實現消息不重複?

上面的兩個問題呢,其實很少真的有業務有這種嚴格的需求,出現的最多可能就是面試了。
但是這個消息不重複卻是很多服務都需要保證的,它還有個名字,叫做消息冪等。

這裏需要說明的一點是,我們通常說的消息冪等,默認是指消費端如何保證不重複消費消息。
因此,這個問題我們就不需要進行全鏈路端到端的進行分析了。

其實有兩種常見的策略:

  • 基於DB的唯一鍵,我們可以通過消息的內容拼成一個唯一的key。然後創建一個冪等表,其中可以就兩列<id, key>,其中設置key列爲唯一鍵。每次進行消息的業務處理前,進行冪等判斷,也就是朝表中插入一個key,如果報了對應的違反唯一性的異常,那麼就跳過該消息的處理。
  • 基於緩存,實現原理跟用DB基本一致,不過可以修改爲判斷key是否存在於緩存中,如果存在則跳過否則存入後再進行業務處理。

瞭解了策略之後,我們再思考下,哪些情況下會出現消息重複呢?
首先是生產端,如果遇到了網絡抖動,服務端已經寫入消息成功了,但是客戶端卻以爲超時了,因此進行重試,此時就會出現重複消息。
然後服務端,服務端是不管那麼多的,只要你上游的生產請求到了我這裏我就會往對應的Log裏面寫數據。而服務端本身呢,是不會產生重複數據的。
接着是消費端,如果我一次性拉取了500條消息,我業務處理了其中200條,然後消費端Crash了,由於上次消費位移沒有提交,因此消費者重新拉取原先的500條進行消費,那麼原來處理過的200條消息就被重複消費了。


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