目錄
一 生產端僵尸
1.1 原因
1.2 問題
1.3解決辦法
1.4 案例
1.4.1 案例1:生產者崩潰后重啟 (同一 transactional.id)
1.4.2?案例2:短暫網絡分區導致的腦裂
1.4.3?案例3:正確 - 解決僵尸
1.4.4?案例4:錯誤 - 無法解決僵尸
1.5 結論
二? 消費端僵尸
2.1 原因
2.2 問題
2.3 解決辦法
一 生產端僵尸
1.1 原因
一個生產者實例(producer)在發送消息過程中發生故障(如進程崩潰、網絡隔離),但可能未被外部系統及時檢測到。過一段時間后,該生產的新實例可能被重啟(例如,在容器化環境中被調度器重啟,或者運維手動重啟)
1.2 問題
問題1:如果故障前的生產者實例在崩潰前可能已經成功發送了部分消息(但未完成事務提交),而新實例并不知道,它可能會重新發送這些消息,導致重復數據。
問題2:腦裂問題: 在短暫的網絡分區期間,可能出現兩個都認為自己是“活動”的生產者實例同時向同一個分區寫入數據,造成數據混亂。
1.3解決辦法
1.kafka使用transaction id和producer epoch來解決生產者僵尸問題;epoch它本質上是該transactional.id的一個單調遞增的序列號。
2.Epoch比較只在相同 transactional.id 內有效:只有配置了相同 transactional.id 的生產者實例之間,它們的 Epoch 值才有比較的意義。Transaction Coordinator 會為新實例分配更高的 Epoch,并通知 Broker 拒絕任何攜帶舊 Epoch 的寫入請求(針對該 transactional.id)。
也即:新啟動的實例必須使用與舊實例完全相同的 transactional.id。這是 Epoch 比較和隔離機制生效的前提條件。
3.不同 transactional.id = 完全獨立:?如果兩個生產者實例使用不同的 transactional.id,無論它們的 Epoch 值是多少(即使其中一個為 1,另一個為 100),它們都會被 Kafka 視為兩個完全獨立、互不相干的生產者。Broker 不會對它們進行Epoch比較或相互隔離。它們可以同時向同一個分區發送消息(盡管可能因 Leader 處理順序導致消息交叉),但 Kafka 不保證它們之間的順序或原子性。
1.4 案例
1.4.1 案例1:生產者崩潰后重啟 (同一 transactional.id)
舊實例崩潰,事務可能處于中間狀態(消息已發送但未提交)。
新實例啟動,向 Transaction Coordinator 初始化事務。
Transaction Coordinator 分配一個更高的新 Epoch (e.g., old=1, new=2),并隔離舊Epoch=1。
Transaction Coordinator 中止由舊實例啟動的未完成事務(標記為 ABORT),確保那些部分寫入的消息不會被消費者讀取(在 read_committed 隔離級別下)。
新實例(Epoch=2)開始新的事務。它不會重復發送舊實例可能已發送過的消息,因為應用邏輯知道事務未提交,需要重新處理業務邏輯并發送新消息。
如果舊實例“僵尸復活”并嘗試發送消息,Broker 會檢查其 Epoch=1 < 當前最新 Epoch=2,拒絕寫入。
1.4.2?案例2:短暫網絡分區導致的腦裂
兩個實例(可能是由于網絡分區誤判)都認為自己是活躍的,使用同一個 transactional.id。
其中一個實例(假設是重啟后的新實例)成功聯系到 Transaction Coordinator,獲得了更高的 Epoch。
Transaction Coordinator 隔離了舊 Epoch。
當網絡恢復時,持有舊 Epoch 的實例的任何寫入請求都會被 Broker 拒絕(InvalidProducerEpoch)。
只有持有最新 Epoch 的實例的寫入有效,避免了數據沖突。
1.4.3?案例3:正確 - 解決僵尸
生產者 A (Instance 1): transactional.id = "prod-app-1", Epoch = 1 (由 TC 分配)。
生產者 A 崩潰。
生產者 A (Instance 2 - 新實例) 啟動,配置 transactional.id = "prod-app-1"。
Transaction Coordinator (TC) 檢測到 "prod-app-1" 的新會話。
TC 為 "prod-app-1" 分配 新的 Epoch = 2。
TC 通知相關 Broker:對于 "prod-app-1",最新 Epoch 是 2,拒絕 Epoch <= 1 的請求。
如果舊實例 (Instance 1) 的進程“僵尸復活”并嘗試發送消息 (攜帶 Epoch=1),Broker 會檢查:"prod-app-1" 的當前最新 Epoch=2 > 請求中的 Epoch=1 → 拒絕寫入 (InvalidProducerEpoch)。
新實例 (Instance 2) 使用 Epoch=2 可以正常寫入。
1.4.4?案例4:錯誤 - 無法解決僵尸
生產者 A (Instance 1): transactional.id = "prod-app-1", Epoch = 1。
生產者 A 崩潰。
生產者 A (Instance 2 - 新實例) 啟動,但錯誤地配置了 transactional.id = "prod-app-1-backup" (一個不同的 ID)。
TC 將 "prod-app-1-backup" 視為一個全新的、獨立的生產者。
TC 為 "prod-app-1-backup" 分配 新的 Epoch = 1 (或其他初始值,與 "prod-app-1" 的 Epoch 無關)。
Broker 記錄:
對于 "prod-app-1": 最新 Epoch = 1 (對應舊僵尸實例)。
對于 "prod-app-1-backup": 最新 Epoch = 1 (對應新實例)。
如果舊實例 (Instance 1) 的進程“僵尸復活”并嘗試發送消息 (攜帶 transactional.id="prod-app-1", Epoch=1),Broker 檢查:"prod-app-1" 的最新 Epoch=1 == 請求中的 Epoch=1 → 允許寫入。
新實例 (Instance 2) 使用 "prod-app-1-backup", Epoch=1 也可以寫入。
結果:兩個實例同時寫入相同的分區,造成數據混亂。Fencing 機制完全失效,因為新實例沒有使用相同的 transactional.id 來“聲明接管”。其實本質上是兩個不同的transaction id,是兩個獨立的事務,并不相關,至于寫入相同分區的相同內容可以使用去重,冪等機制來解決。
1.5 結論
transactional.id, Producer Epoch, 和 Kafka 事務協議共同構成了 Kafka 保障生產者高可用、防止僵尸實例破壞數據一致性的基石。
DeepSeek
二? 消費端僵尸
2.1 原因
已經從物理上或邏輯上失效(如進程崩潰、網絡隔離、長時間 GC 停頓、負載過高無響應)但 Kafka 集群(特別是 Group Coordinator)暫時還未將其識別為失效并從消費者組中移除的消費者實例?
2.2 問題
分區分配不均衡/浪費: 新啟動的健康消費者無法分配到這些僵尸實例"霸占"的分區,導致部分分區無人消費,消息堆積。
2.3 解決辦法
1.心跳機制和超時:健康的消費者會按照 心跳機制(heartbeat.interval.ms )的間隔定期向消費組內的協調者( Group Coordinator) 發送心跳。如果連續兩次發送的心跳的間隔超時(超過session.timeout.ms),協調者它就判定該消費者已經死亡(失效)。
2.再平衡:再平衡會將原本分配給僵尸實例的分區重新分配給組內存活的其他健康消費者,解決消息堆積問題。
3.給拉取消費消息的方法poll()處理消費卡頓問題設置規定時間( max.poll.interval.ms)
如果消費者在規定的時間內( max.poll.interval.ms) 時間內沒有再次調用 poll(),Group Coordinator 會認為該消費者處理能力不足或卡住了。Group Coordinator 會主動將該消費者從組中移除,觸發再平衡
4.監控告警:及時進行人工干預。
https://chat.deepseek.com/a/chat/s/6e3234e3-fd76-4000-9326-01780a2fdb48