記一次 Kafka 重啟失敗問題排查

後端進階發表於2020-03-16

背景

在 2 月10 號下午大概 1 點半左右,收到使用者方反饋,發現日誌 kafka 叢集 A 主題 的 34 分割槽選舉不了 leader,導致某些訊息傳送到該分割槽時,會報如下 no leader 的錯誤資訊:

In the middle of a leadership election, there is currently no leader for this partition and hence it is unavailable for writes.
複製程式碼

接下來運維在 kafka-manager 查不到 broker0 節點了處於假死狀態,但是程式依然還在,重啟了好久沒見反應,然後通過 kill -9 命令殺死節點程式後,接著重啟失敗了,導致瞭如下問題:

由於 A 主題 34 分割槽的 leader 副本在 broker0,另外一個副本由於速度跟不上 leader,已被踢出 ISR,0.11 版本的 kafka 的 unclean.leader.election.enable 引數預設為 false,表示分割槽不可在 ISR 以外的副本選舉 leader,導致了 A 主題傳送訊息持續報 34 分割槽 leader 不存在的錯誤,且該分割槽還未消費的訊息不能繼續消費了。

Kafka 日誌分析

檢視了 KafkaServer.log 日誌,發現 Kafka 重啟過程中,產生了大量如下日誌:

記一次 Kafka 重啟失敗問題排查

發現大量主題索引檔案損壞並且重建索引檔案的警告資訊,定位到原始碼處:

kafka.log.OffsetIndex#sanityCheck

記一次 Kafka 重啟失敗問題排查

按我自己的理解描述下:

Kafka 在啟動的時候,會檢查 kafka 是否為 cleanshutdown,判斷依據為 ${log.dirs} 目錄中是否存在 .kafka_cleanshutDown 的檔案,如果非正常退出就沒有這個檔案,接著就需要 recover log 處理,在處理中會呼叫 sanityCheck() 方法用於檢驗每個 log sement 的 index 檔案,確保索引檔案的完整性:

  • entries:由於 kafka 的索引檔案是一個稀疏索引,並不會將每條訊息的位置都儲存到 .index 檔案中,因此引入了 entry 模式,即每一批訊息只記錄一個位置,因此索引檔案的 entries = mmap.position / entrySize;
  • lastOffset:最後一塊 entry 的位移,即 lastOffset = lastEntry.offset;
  • baseOffset:指的是索引檔案的基偏移量,即索引檔名稱的那個數字。

索引檔案與日誌檔案對應關係圖如下:

記一次 Kafka 重啟失敗問題排查

判斷索引檔案是否損壞的依據是:

_entries == 0 || _lastOffset > baseOffset = false // 損壞
_entries == 0 || _lastOffset > baseOffset = true // 正常
複製程式碼

這個判斷邏輯我的理解是:

entries 索引塊等於零時,意味著索引沒有內容,此時可以認為索引檔案是沒有損壞的;當 entries 索引塊不等於 0,就需要判斷索引檔案最後偏移量是否大於索引檔案的基偏移量,如果不大於,則說明索引檔案被損壞了,需要用重新構建。

那為什麼會出現這種情況呢?

我在相關 issue 中似乎找到了一些答案:

記一次 Kafka 重啟失敗問題排查

issues.apache.org/jira/browse…

issues.apache.org/jira/browse…

總的來說,非正常退出在舊版本似乎會可能發生這個問題?

有意思的來了,導致開機不了並不是這個問題導致的,因為這個問題已經在後續版本修復了,從日誌可看出,它會將損壞的日誌檔案刪除並重建,我們接下來繼續看導致重啟不了的錯誤資訊:

記一次 Kafka 重啟失敗問題排查

問題就出在這裡,在刪除並重建索引過程中,就可能出現如上問題,在 issues.apache.org 網站上有很多關於這個 bug 的描述,我這裡貼兩個出來:

issues.apache.org/jira/browse…

issues.apache.org/jira/browse…

這些 bug 很隱晦,而且非常難復現,既然後續版本不存在該問題,當務之急還是升級 Kafka 版本,後續等我熟悉 scala 後,再繼續研究下原始碼,細節一定是會在原始碼中呈現。

解決思路分析

針對背景兩個問題,矛盾點都是因為 broker0 重啟失敗導致的,那麼我們要麼把 broker0 啟動成功,才能恢復 A 主題 34 分割槽。

由於日誌和索引檔案的原因一直啟動不起來,我們只需要將損壞的日誌和索引檔案刪除並重啟即可。

但如果出現 34 分割槽的日誌索引檔案也損壞的情況下,就會丟失該分割槽下未消費的資料,原因如下:

此時 34 分割槽的 leader 還處在 broker0 中,由於 broker0 掛掉了且 34 分割槽 isr 只有 leader,導致 34 分割槽不可用,在這種情況下,假設你將 broker0 中 leader 的資料清空,重啟後 Kafka 依然會將 broker0 上的副本作為 leader,那麼就需要以 leader 的偏移量為準,而這時 leader 的資料清空了,只能將 follower 的資料強行截斷為 0,且不大於 leader 的偏移量。

這似乎不太合理,這時候是不是可以提供一個操作的可能:

在分割槽不可用時,使用者可以手動設定分割槽內任意一個副本作為 leader?

後面我會單獨一篇文章對這個問題進行分析。

後續叢集的優化

  1. 制定一個升級方案,將叢集升級到 2.2 版本;
  2. 每個節點的伺服器將 systemd 的預設超時值為 600 秒,因為我發現運維在故障當天關閉 33 節點時長時間沒反應,才會使用 kill -9 命令強制關閉。但據我瞭解關閉一個 Kafka 伺服器時,Kafka 需要做很多相關工作,這個過程可能會存在相當一段時間,而 systemd 的預設超時值為 90 秒即可讓程式停止,那相當於非正常退出了。
  3. 將 broker 引數 unclean.leader.election.enable 設定為 true(確保分割槽可從非 ISR 中選舉 leader);
  4. 將 broker 引數 default.replication.factor 設定為 3(提高高可用,但會增大叢集的儲存壓力,可後續討論);
  5. 將 broker 引數 min.insync.replicas 設定為 2(這麼做可確保 ISR 同時有兩個,但是這麼做會造成效能損失,是否有必要?因為我們已經將 unclean.leader.election.enable 設定為 true 了);
  6. 傳送端傳送 acks=1(確保傳送時有一個副本是同步成功的,但這個是否有必要,因為可能會造成效能損失)。

作者簡介

張乘輝,目前就職於中通科技資訊中心技術平臺部,主要負責中通訊息平臺與全鏈路壓測專案的研發,熱愛分享技術,微信公眾號「後端進階」作者,技術部落格(objcoding.com/)博主,Seata Contributor,GitHub ID:objcoding。

公眾號「後端進階」,專注後端技術分享!

相關文章