Kafka節點重啓失敗,一波騷操作力挽數據丟失!

​以下文章來源於科技中通,作者張乘輝

 

背景

 

在 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.

 

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

 

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

 

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 中似乎找到了一些答案:
 

 

https://issues.apache.org/jira/browse/KAFKA-1112

https://issues.apache.org/jira/browse/KAFKA-1554

 

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

 

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

 

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

 

https://issues.apache.org/jira/browse/KAFKA-4972

https://issues.apache.org/jira/browse/KAFKA-3955

 

這些 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.x 版本;

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(確保發送時有一個副本是同步成功的,但這個是否有必要,因爲可能會造成性能損失)。

 

從源碼中定位到問題的根源

 

首先把導致 Kafka 進程退出的異常棧貼出來:
 

注:以下源碼基於 kafka 0.11.x 版本。

 

我們直接從 index 文件損壞警告日誌的位置開始:

 

 

kafka.log.Log#loadSegmentFiles



 

從前一篇文章中已經說到,Kafka 在啓動的時候,會檢查kafka是否爲 cleanshutdown,判斷依據爲 ${log.dirs} 目錄中是否存在 .kafka_cleanshutDown 的文件,如果非正常退出就沒有這個文件,接着就需要 recover log 處理,在處理中會調用 。

 

在 recover 前,會調用 sanityCheck() 方法用於檢驗每個 log sement 的 index 文件,確保索引文件的完整性 ,如果發現索引文件損壞,刪除並調用 recoverSegment() 方法進行索引文件的重構,最終會調用 recover() 方法:

 

 

kafka.log.LogSegment#recover



 

源碼中相關變量說明:

 

  • log:當前日誌 Segment 文件的對象;

  • batchs:一個 log segment 的消息壓縮批次;

  • batch:消息壓縮批次;

  • indexIntervalBytes:該參數決定了索引文件稀疏間隔打底有多大,由 broker 端參數 log.index.interval.bytes 決定,默認值爲 4 KB,即表示當前分區 log 文件寫入了 4 KB 數據後纔會在索引文件中增加一個索引項(entry);

  • validBytes:當前消息批次在 log 文件中的物理地址。

 

知道相關參數的含義之後,那麼這段代碼的也就容易解讀了:循環讀取 log 文件中的消息批次,並讀取消息批次中的 baseOffset 以及在 log 文件中物理地址,將其追加到索引文件中,追加的間隔爲 indexIntervalBytes 大小。

 

我們再來解讀下消息批次中的 baseOffset:

我們知道一批消息中,有最開頭的消息和末尾消息,所以一個消息批次中,分別有 baseOffset 和 lastOffset,源碼註釋如下:



 

其中最關鍵的描述是:它可以是也可以不是第一條記錄的偏移量。

 

 

kafka.log.OffsetIndex#append



 

以上是追加索引塊核心方法,在這裏可以看到 Kafka 異常棧的詳細信息,Kafka 進程也就是在這裏被異常中斷退出的。

 

這裏吐槽一下,爲什麼一個分區有損壞,要整個 broker 掛掉?寧錯過,不放過?就不能標記該分區不能用,然後讓 broker 正常啓動以提供服務給其他分區嗎?建議 Kafka 在日誌恢復期間加強異常處理,不知道後續版本有沒有優化,後面等我拿 2.x 版本源碼分析一波。

 

退出的條件是:

 

 

_entries == 0 || offset > _lastOffset = false

 

也就是說,假設索引文件中的索引條目爲 0,說明索引文件內容爲空,那麼直接可以追加索引,而如果索引文件中有索引條目了,需要消息批次中的 baseOffset 大於索引文件最後一個條目中的位移,因爲索引文件是遞增的,因此不允許比最後一個條目的索引還小的消息位移。

 

現在也就很好理解了,產生這個異常報錯的根本原因,是因爲後面的消息批次中,有位移比最後索引位移還要小(或者等於)。

 

前面也說過了,消息批次中的 baseOffset 不一定是第一條記錄的偏移量,那麼問題是不是出在這裏?我的理解是這裏有可能會造成兩個消息批次獲取到的 baseOffset 有相交的值?

 

對此我並沒有繼續研究下去了,但我確定的是,在 kafka 2.x版本中,append() 方法中的 offset 已經改成 消息批次中的 lastOffset 了:



 

這裏我也需要吐槽一下,如果出現這個 bug,意味着這個問題除非是將這些故障的日誌文件和索引文件刪除,否則該節點永遠啓動不了,這也太暴力了吧?

 

我花了非常多時間去專門看了很多相關 issue,目前還沒看到有解決這個問題的方案?

 

或者我需要繼續尋找?我把相關 issue 貼出來:

 

https://issues.apache.org/jira/browse/KAFKA-1211

https://issues.apache.org/jira/browse/KAFKA-3919

https://issues.apache.org/jira/browse/KAFKA-3955

 

嚴重建議各位儘快把 Kafka 版本升級到 2.x 版本,舊版本太多問題了,後面我着重研究 2.x 版本的源碼。

 

下面我從日誌文件結構中繼續分析。

 

從日誌文件結構中看到問題的本質

 

我們用 Kafka 提供的 DumpLogSegments 工具打開 log 和 index 文件:

 

 

$ ~/kafka_2.1x-0.11.x/bin/kafka-run-class.sh kafka.tools.DumpLogSegments 

--files {log_path}/secxxx-2/00000000000110325000.log > secxxx.log

 

$ ~/kafka_2.1x-0.11.x/bin/kafka-run-class.sh kafka.tools.DumpLogSegments 

--files {log_path}/secxxx-2/00000000000110325000.index > secxxx-index.log

 

用 less -Nm 命令查看,log 和 index 對比:



 

如上圖所示,index最後記錄的 offset = 110756715,positioin=182484660,與異常棧顯示的一樣,說明在進行追加下一個索引塊的時候,發現下一個索引塊的 offset 索引不大於最後一個索引塊的 offset,因此不允許追加,報異常並退出進程,那麼問題就出現在下一個消息批次的 baseOffset,根據 log.index.interval.bytes 默認值大小爲 4 KB(4096),而追加的條件前面也說了,需要大於 log.index.interval.bytes,因此我們 DumpLogSegments 工具查詢:

 

 

從 dump 信息中可知,在 positioin=182484660 往後的幾個消息批次中,它們的大小加起來大於 4096 的消息批次的 offset=110756804,postion=182488996,它的 baseOffset 很可能就是 110756715,與索引文件最後一個索引塊的 Offset 相同,因此出現錯誤。

 

接着我們繼續用 DumpLogSegments 工具查看消息批次內容:

我們先查看 offset = 110756715,positioin=182484660 的消息塊詳情:

 

 

接着尋找 offset = 110756715,的消息批次塊:

 

 

終於找到你了,跟我預測的一樣!postion=182488996,在將該消息批次追加到索引文件中,發生 offset 混亂了。

 

如果還是沒找到官方的處理方案,就只能刪除這些錯誤日誌文件和索引文件,然後重啓節點?

 

非常遺憾,我在查看了相關的 issue 之後,貌似還沒看到官方的解決辦法,所幸的是該集羣是日誌集羣,數據丟失也沒有太大問題。

 

我也嘗試發送郵件給 Kafka 維護者,期待大佬的迴應:

 

 

不過呢,0.11.x 版本屬於很舊的版本了,因此,升級 Kafka 版本纔是長久之計啊!我已經迫不及待地想擼 kafka 源碼了!

 

經過以上問題分析與排查之後,我專門對分區不可用進行故障重現,並給出我的一些騷操作來儘量減少數據的丟失。

 

故障重現

 

下面我用一個例子重現現分區不可用且 leader 副本被損壞的例子:

 

  • 使用 unclean.leader.election.enable = false 參數啓動 broker0;

  • 使用 unclean.leader.election.enable = false 參數啓動 broker1;

  • 創建 topic-1,partition=1,replica-factor=2;

  • 將消息寫入 topic-1;

  • 此時,兩個 broker 上的副本都處於 ISR 中,broker0 的副本爲 leader 副本;

  • 停止 broker1,此時 topic-1 的 leader 依然是 broker0 的副本,而 broker1 的副本從 ISR 中剔除;

  • 停止 broker0,並且刪除 broker0 上的日誌數據;

  • 重啓 broker1,topic-1 嘗試連接 leader 副本,但此時 broker0 已經停止運行,此時分區處於不可用狀態,無法寫入消息;

  • 恢復 broker0,broker0 上的副本恢復 leader 職位,此時 broker1 嘗試加入 ISR,但此時由於 leader 的數據被清除,即偏移量爲 0,此時 broker1 的副本需要截斷日誌,保持偏移量不大於 leader 副本,此時分區的數據全部丟失。

 

向Kafka官方提的建議

 

在遇到分區不可用時,是否可以提供一個選項,讓用戶可以手動設置分區內任意一個副本作爲 leader?

 

因爲集羣一旦設置了 unclean.leader.election.enable = false,就無法選舉 ISR 以外的副本作爲 leader,在極端情況下僅剩 leader 副本還在 ISR 中,此時 leader 所在的 broker 宕機了。

 

那如果此時 broker 數據發生損壞這麼辦?在這種情況下,能不能讓用戶自己選擇 leader 副本呢?儘管這麼做也是會有數據丟失,但相比整個分區的數據都丟失而言,情況還是會好很多的。

 

如何儘量減少數據丟失

 

首先你得有一個不可用的分區(並且該分區 leader 副本數據已損失),如果是測試,可以以上故障重現 1-8 步驟實現一個不可用的分區(需要增加一個 broker):

 

 

此時 leader 副本在 broker0,但已經掛了,且分區不可用,此時 broker2 的副本由於掉出 ISR ,不可選爲 leader,且 leader 副本已損壞清除,如果此時重啓 broker0,follower 副本會進行日誌截斷,將會丟失該分區所有數據。

 

經過一系列的測試與實驗,我總結出了以下騷操作,可以強行把 broker2 的副本選爲 leader,儘量減少數據丟失:

 

1、使用 kafka-reassign-partitions.sh 腳本對該主題進行分區重分配,當然你也可以使用 kafka-manager 控制檯對該主題進行分區重分配,重分配之後如下:

 

 

此時 preferred leader 已經改成 broker2 所在的副本了,但此時的 leader 依然還是 broker0 的副本。需要注意的是,分區重分配之後的 preferred leader 一定要之前那個踢出 ISR 的副本,而不是分區重分配新生成的副本。因爲新生成的副本偏移量爲 0,如果自動重分配不滿足,那麼需要編寫 json 文件,手動更改分配策略。

 

2、進入 zk,查看分區狀態並修改它的內容:

 

 

修改 node 內容,強行將 leader 改成 2(與重分配之後的 preferred leader 一樣),並且將 leader_epoch 加 1 處理,同時 ISR 列表改成 leader,改完如下:

 

 

此時,kafka-manager 控制檯會顯示成這樣:

 

 

 

但此時依然不生效,記住這時需要重啓 broker 0。

 

3、重啓 broker0,發現分區的 lastOffset 已經變成了 broker2 的副本的 lastOffset:

 

 

成功挽回了 46502 條消息數據,儘管依然丟失了 76053 - 46502 = 29551 條消息數據,但相比全部丟失相對好吧!

 

以上方法的原理其實很簡單,就是強行把 Kafka 認定的 leader 副本改成自己想要設置的副本,然後 lastOffset 就會以我們手動設置的副本 lastOffset 爲基準了。

 

作者丨張乘輝 來源丨公衆號:科技中通(ID:gh_2b0467268990) dbaplus社羣歡迎廣大技術人員投稿,投稿郵箱:[email protected]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章