往往排查很久的問題,最後發現都非常簡單。。。
之前線上發生了一個很詭異的異常,網上各種搜尋、排查,都沒有找到問題,給大家分享一下。
大概在 2 月份的時候,我們的某個應用整合了中介軟體的 kafka 客戶端,釋出到灰度和藍節點進行觀察,然後就發現線上某個 Topic 發生了大量的RetriableCommitException
,並且集中在灰度機器上。
E20:21:59.770 RuntimeException org.apache.kafka.clients.consumer.RetriableCommitFailedException ERROR [Consumer clientId=xx-xx.4-0, groupId=xx-xx-consumer_[gray]] Offset commit with offsets {xx-xx-xx-callback-1=OffsetAndMetadata{offset=181894918, leaderEpoch=4, metadata=''}, xx-xx-xx-callback-0=OffsetAndMetadata{offset=181909228, leaderEpoch=5, metadata=''}} failed org.apache.kafka.clients.consumer.RetriableCommitFailedException: Offset commit failed with a retriable exception. You should retry committing the latest consumed offsets.
Caused by: org.apache.kafka.common.errors.TimeoutException: Failed to send request after 30000 ms.
排查
檢查了這個 Topic 的流量流入、流出情況,發現並不是很高,至少和 QA 環境的壓測流量對比,連零頭都沒有達到。
但是從發生異常的這個 Topic 的歷史流量來看的話,發生問題的那幾個時間點的流量又確實比平時高出了很多。
同時我們檢查 Broker 叢集的負載情況,發現那幾個時間點的 CPU 負載也比平時也高出很多(也只是比平時高,整體並不算高)。
對Broker叢集的日誌排查,也沒發現什麼特殊的地方。
然後我們對這個應用在QA上進行了模擬,嘗試復現,遺憾的是,儘管我們在QA上把生產流量放大到很多倍並嘗試了多次,問題還是沒能出現。
此時,我們把問題歸於當時的網路環境,這個結論在當時其實是站不住腳的,如果那個時刻網路環境發生了抖動的話,其它應用為什麼沒有這類異常?
可能其它的服務例項網路情況是好的,只是發生問題的這個灰例項網路發生了問題。
那問題又來了,為什麼這個例項的其它 Topic 沒有報出異常,偏偏問題只出現在這個 Topic 呢?。。。。。。。。。
至此,陷入了僵局,無從下手的感覺。
從這個客戶端的開發、測試到壓測,如果有 bug 的話,不可能躲過前面那麼多環節,偏偏爆發在了生產環境。
沒辦法了,我們再次進行了一次灰度釋出,如果過了一夜沒有事情發生,我們就把問題劃分到環境問題,如果再次出現問題的話,那就只能把問題劃分到我們實現的 Kafka 客戶端的問題了。
果不其然,釋出後的第二天凌晨1點多,又出現了大量的 RetriableCommitFailedException
,只是這次換了個 Topic,並且異常的原因又多出了其它Caused by 。
org.apache.kafka.clients.consumer.RetriableCommitFailedException: Offset commit failed with a retriable exception. You should retry committing the latest consumed offsets.
Caused by: org.apache.kafka.common.errors.DisconnectException
...
...
E16:23:31.640 RuntimeException org.apache.kafka.clients.consumer.RetriableCommitFailedException ERROR
...
...
org.apache.kafka.clients.consumer.RetriableCommitFailedException: Offset commit failed with a retriable exception. You should retry committing the latest consumed offsets.
Caused by: org.apache.kafka.common.errors.TimeoutException: The request timed out.
分析
這次出現的異常與之前異常的不同之處在於:
- 1. Topic 變了
- 2. 異常Cause變了
而與之前異常又有相同之處:
- 1. 只發生在灰度消費者組
- 2. 都是
RetriableCommitFailedException
RetriableCommitFailedException
意思很明確了,可以重試提交的異常,網上搜了一圈後僅發現StackOverFlow上有一問題描述和我們的現象相似度很高,遺憾的是沒人回覆這個問題:StackOverFlow。
我們看下 RetriableCommitFailedException 這個異常和產生這個異常的呼叫層級關係。
除了產生異常的具體 Cause 不同,剩下的都是讓我們再 retry,You should retry Commiting the lastest consumed offsets。
從呼叫層級上來看,我們可以得到幾個關鍵的資訊,commit 、 async。
再結合異常發生的例項,我們可以得到有用關鍵資訊: 灰度、commit 、async。
在灰度訊息的實現上,我們確實存在著管理位移和手動提交的實現。
看程式碼的第 62 行,如果當前批次訊息經過 filter 的過濾後一條訊息都不符合當前例項消費,那麼我們就把當前批次進行手動非同步提交位移。結合我們在生產的實際情況,在灰度例項上我們確實會把所有的訊息都過濾掉,並非同步提交位移。
為什麼我們封裝的客戶端提交就會報大量的報錯,而使用 spring-kafka 的沒有呢?
我們看下Spring對提交位移這塊的核心實現邏輯。
可以同步,也可以非同步提交,具體那種提交方式就要看 this.containerProperties.isSyncCommits() 這個屬性的配置了,然而我們一般也不會去配置這個東西,大部分都是在使用預設配置。
人家預設使用的是同步提交方式,而我們使用的是非同步方式。
同步提交和非同步提交有什麼區別麼?
先看下同步提交的實現:
只要遇到了不是不可恢復的異常外,在 timer 引數過期時間範圍內重試到成功(這個方法的描述感覺不是很嚴謹的樣子)。
我們在看下非同步提交方式的核心實現:
我們不要被第 645 行的 RequestFuture future = sendOffsetCommitRequest(offsets) 所迷惑,它其實並不是傳送位移提交的請求,它內部只是把當前請求包裝好,放到 private final UnsentRequests unsent = new UnsentRequests(); 這個屬性中,同時喚醒真正的傳送執行緒來發送的。
這裡不是重點,重點是如果我們的非同步提交發生了異常,它只是簡單的使用 RetriableCommitFailedException 給我們包裝了一層。
重試呢?為什麼非同步傳送產生了可重試異常它不給我們自動重試?
如果我們對多個非同步提交進行重試的話,很大可能會導致位移覆蓋,從而引發重複消費的問題。
正好,我們遇到的所有異常都是 RetriableCommitException 型別的,也就是說,我們把灰度位移提交的方式修改成同步可重試的提交方式,就可以解決我們遇到的問題了。
- 往往排查很久的問題,最後發現都非常簡單。。。
- 都在用 AI 生成美少女,而我卻。。。
- 炸了!3年圖片都沒了!
- chatGPT辣麼火,你卻不會註冊
- SpringBoot3正式版將於11月24日釋出:都有哪些新特性?
- NB的Github專案,看到最後一個我驚呆了!
- 10分鐘教你寫一個數據庫
- 程式設計師最容易讀錯的單詞,聽到status我炸了
- 從-1開始實現一箇中間件
- 搭建個人部落格,Docsify Github webhook JGit解決方案
- IDEA 2022全新UI體驗
- Sentinel叢集限流探索
- 又碰到一個奇葩的BUG
- 面試官:你說說限流的原理?
- MySQL最高每秒57萬寫入,帶你裝X,帶你飛!
- 面試八股文
- 一個csdn 大佬
- 你知道CSDN排名第一的人是誰嗎?
- Talk is cheap,show me the code!
- 男人要慢,SQL要快:記一次慢SQL優化