spring-kafka中ContainerProperties.AckMode詳解

發表於2023-09-19

  近期,我們線上遇到了一個效能問題,幾乎快引起線上故障,後來僅僅是修改了一行程式碼,效能就提升了幾十倍。一行程式碼幾十倍,資料聽起來很誇張,不過這是真實的資料,線上錯誤的配置的確有可能導致效能有數量級上的差異,等我說完我們這個效能問題你就清楚了。

  我們線上是對接了騰訊雲的IOT平臺,任何iot裝置的上傳事件都是透過騰訊雲的CKafka傳遞給我們的,隨著裝置量以及事件資料量的增加,我們消費騰訊雲CKafka出現了效能瓶頸,資料高峰期會有資料擁堵,從而因資料處理延遲導致業務的問題。解決最簡單的方案就是擴partition和consumer,實際上半年前我們發生效能問題的時候就是這麼做的,擴了一倍的partition提升了一倍的效能,然而半年後的今天又到了瓶頸。

  經過排查發現,單條kafka訊息處理需要6ms,拆分所有執行邏輯後發現這6ms的延遲主要是向騰訊雲傳送ack的時間,我們機房到騰訊雲的rtt恰好就是6ms左右,所以幾乎所有的事件都耗費在訊息的網路傳輸上面了。然而這個是受物理距離所限制,無法減減少的。後來偶然發現我們在程式碼中使用了spring-kafka的AckMode中的MANUAL_IMMEDIATE,這個模式下kafka的consumer會向服務端手動確認每一條訊息,後來我們將這個配置調整成了AckMode.MANUAL,單條訊息的處理時長從原來的6ms降低到不到0.2ms,提升了30多倍,這下即便不擴容我們的效能冗餘也足夠支援很多年了。 為什麼簡簡單單改個配置就會有如此的提升? 是否還有其他的配置型別?

  實際上在spring-kafka中並不是隻提供了MANUAL和MANUAL_IMMEDIATE兩種ack模式,而是有以下七種,每種都有各種的作用和適合的場景。

  • RECORD:每處理一條記錄後就立即進行確認。
  • BATCH:每次呼叫poll()方法後,只確認返回的最後一條記錄。
  • TIME:每次過了設定的時間間隔後,確認最後一條在這段時間內處理的記錄。
  • COUNT:每處理設定數量的記錄後,確認最後一條處理的記錄。
  • COUNT_TIME:組合了TIME和COUNT,即滿足任意一個條件時,確認最後一條處理的記錄。
  • MANUAL:使用者需要手動呼叫acknowledgement.acknowledge()批次來確認訊息。
  • MANUAL_IMMEDIATE:使用者需要手動呼叫acknowledgement.acknowledge()來確認訊息,每條訊息都會確認一次。

  以上7種模式如果分類的話可以分成兩種,手動確認和自動確認,其中MANUAL和MANUAL_IMMEDIATE是手動確認,其餘的都是自動確認。手動確認和自動確定的核心區別就在於你是否需要在程式碼中顯示呼叫Acknowledgment.acknowledge(),我們挨個來看下。

手動確認

MANUAL:

  在此模式下,消費者需要在處理完訊息後手動呼叫Acknowledgment.acknowledge()方法來確認訊息。確認操作會被批次進行,即確認操作被延遲到一批訊息都處理完畢後再傳送給Kafka。這種模式的優點是可以提高效率,因為減少了與Kafka伺服器的互動次數。但缺點是如果一批訊息消費了一半,consumer突然異常當機,因為資料沒有及時向kafka服務端確認,下次就會重複拉取到訊息,導致部分資料被重複消費。

MANUAL_IMMEDIATE:

  在此模式下,消費者需要在處理完訊息後手動呼叫Acknowledgment.acknowledge()方法來確認訊息。不過,與MANUAL模式不同的是,一旦呼叫了acknowledge()方法,確認資訊會立即傳送給Kafka,而不是等待一批訊息都處理完畢後再傳送。這種模式可能會增加與Kafka伺服器的互動次數,在網路延遲較大的情況下會出現顯著的效能消費瓶頸,但可以儘快將確認資訊傳送給Kafka,即便是consumer異常當機,也只是會導致單條訊息被重複消費。

  手動確認的優勢在於consumer可以在程式碼邏輯中自行判斷資料是否消費成功,未消費成功的資料不確認,這樣可以保證資料不丟失,手動模式可以保證資料的完整性,也就是分散式資料系統中所說的at least once。而這兩種模式的核心差異就是單條確認和批次確認,批次的方式可以顯著提升效能, 我在上個月的部落格IO密集型服務提升效能的三種方法詳細介紹過,有興趣可以看下。

自動確認

  RECORD、BATCH、TIME、COUNT、TIME_COUNT這5種都是屬於自動確認,也就是你不需要在程式碼中顯式呼叫Acknowledgment.acknowledge(),只要consumer拉到訊息就是自動確認,才不管是否真的消費成功,所以自動確認的模式可能會導致資料丟失,但要注意相對於手動確認,自動確認即可能導致資料丟失,也可能導致資料重複,所以它也不是at most once語義級別的。 雖然同為自動確認,但其實這5種模式還有自己的差異。

RECORD和BATCH

  首先我們先來看下RECORD、BATCH,這兩種模式其實就是上文中MANUAL和MANUAL_IMMEDIATE對應的自動版本。RECORD是一條就確認一次,同樣如果是在網路延遲較大的情況下也會出現效能問題。BATCH是批次確認,每次poll()後會確認這一批的訊息,同樣的如果consumer異常當機也會導致未成功確認訊息,從而導致訊息被重複拉取到。當然如果是consumer因其他原因導致資料處理失敗,但正常確認了,這種情況下會丟失訊息。

TIME

  TIME模式是定時確認,比如你設定了確認時間間隔為5S,consumer就會每5s向kafka確認這5s內消費完的訊息,這裡有個問題是如果是高頻資料流且時間間隔設定較大,可能導致堆積大量訊息未被確認,然後異常當機後重復拉取到這些訊息,我們接下來要說的COUNT模式可以避免這種情況。

COUNT

  COUNT模式確認的時機是由消費資料條數觸發的,比如每消費100條就確認一次,完美的避免了堆積大量未確認資料的情況。但是,如果是極低頻的資料流,比如幾分鐘才一條資料,攢夠100條得好幾個小時,資料消費後長時間得不到確認,甚至可能導致kafka認為資料消費超時失敗,從而導致資料被重複消費。

TIME_COUNT

  針對於TIME和COUNT的優缺點,TIME_COUNT結合了兩者的特點,只要是時間間隔或者訊息條數滿足其一就確認,具有更強的適應性,所以當你想從TIME、COUT、TIME_COUNT三者中選一個的話,我個人覺得可以盲選TIME_COUNT,除非你特別清楚你資料的特徵,知道那種更合適。

總結

  簡單總結下以上幾種模式,如果是不能容忍資料丟失那一定要選手動模式,如果是網路延時比較高,可以選MANUAL(批處理)的模式,但是注意即便是手動模式它也不能保證資料不重複,要想做到完全冪等還得依賴其他的方式,比如資料庫事務。 如果可以接受部分資料丟失(例:監控資料),那就可以考慮自動模式了,但我個人還是不推薦RECORD模式,因為這種模式會在高網路延遲的情況下啊產生比較嚴重的效能問題,剩下的幾種模式可以根據自己的資料量、網路情況選取,不同的情況用不同的模式可能會有明顯的效能差異。

相關文章