背景
在 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.log.OffsetIndex#sanityCheck
按我自己的理解描述下:
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:指的是索引檔案的基偏移量,即索引檔名稱的那個數字。
索引檔案與日誌檔案對應關係圖如下:
判斷索引檔案是否損壞的依據是:
_entries == 0 || _lastOffset > baseOffset = false // 損壞
_entries == 0 || _lastOffset > baseOffset = true // 正常
複製程式碼
這個判斷邏輯我的理解是:
entries 索引塊等於零時,意味著索引沒有內容,此時可以認為索引檔案是沒有損壞的;當 entries 索引塊不等於 0,就需要判斷索引檔案最後偏移量是否大於索引檔案的基偏移量,如果不大於,則說明索引檔案被損壞了,需要用重新構建。
那為什麼會出現這種情況呢?
我在相關 issue 中似乎找到了一些答案:
issues.apache.org/jira/browse…
issues.apache.org/jira/browse…
總的來說,非正常退出在舊版本似乎會可能發生這個問題?
有意思的來了,導致開機不了並不是這個問題導致的,因為這個問題已經在後續版本修復了,從日誌可看出,它會將損壞的日誌檔案刪除並重建,我們接下來繼續看導致重啟不了的錯誤資訊:
問題就出在這裡,在刪除並重建索引過程中,就可能出現如上問題,在 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?
後面我會單獨一篇文章對這個問題進行分析。
後續叢集的優化
- 制定一個升級方案,將叢集升級到 2.2 版本;
- 每個節點的伺服器將 systemd 的預設超時值為 600 秒,因為我發現運維在故障當天關閉 33 節點時長時間沒反應,才會使用 kill -9 命令強制關閉。但據我瞭解關閉一個 Kafka 伺服器時,Kafka 需要做很多相關工作,這個過程可能會存在相當一段時間,而 systemd 的預設超時值為 90 秒即可讓程式停止,那相當於非正常退出了。
- 將 broker 引數 unclean.leader.election.enable 設定為 true(確保分割槽可從非 ISR 中選舉 leader);
- 將 broker 引數 default.replication.factor 設定為 3(提高高可用,但會增大叢集的儲存壓力,可後續討論);
- 將 broker 引數 min.insync.replicas 設定為 2(這麼做可確保 ISR 同時有兩個,但是這麼做會造成效能損失,是否有必要?因為我們已經將 unclean.leader.election.enable 設定為 true 了);
- 傳送端傳送 acks=1(確保傳送時有一個副本是同步成功的,但這個是否有必要,因為可能會造成效能損失)。
作者簡介
張乘輝,目前就職於中通科技資訊中心技術平臺部,主要負責中通訊息平臺與全鏈路壓測專案的研發,熱愛分享技術,微信公眾號「後端進階」作者,技術部落格(objcoding.com/)博主,Seata Contributor,GitHub ID:objcoding。