Kafka技術知識總結之四——Kafka 再均衡

接上篇《Kafka技術知識總結之三——Kafka 高效文件存儲設計》

四. Kafka 再均衡原理

4.1 消費者再均衡

Kafka 通過 消費組協調器 (GroupCoordinator)消費者協調器 (ConsumerCoordinator),實現消費者再均衡操作。

  • 消費組協調器 (GroupCoordinator):Kafka 服務端中,用於管理消費組的組件;
  • 消費者協調器 (ConsumerCoordinator):Consumer 客戶端中,負責與 GroupCoordinator 進行交互;

注:新版消費者客戶端將全部消費組分成多個子集,每個消費組的子集在服務端對應一個 GroupCoordinator 進行管理。

ConsumerCoordinator 與 GroupCoordinator 之間最重要的職責就是負責執行消費者再均衡操作。導致消費者再均衡的操作:

  • 新的消費者加入消費組;
  • 消費者宕機下線(不一定是真的下線,令消費組以爲消費者宕機下線的本質原因是消費者長時間未向 GroupCoordinator 發送心跳包);
  • 消費者主動退出消費組;
  • 消費組對應的 GroupCoordinator 節點發生了變更;
  • 任意主題或主題分區數量發生變化;

4.2 再均衡策略

參考地址:
《kafka消費者分組消費的再平衡策略》
《深入理解 Kafka 核心設計與實踐原理》7.1 章節

Kafka 提供了三種再均衡策略(即分區分配策略),默認使用 RangeAssignor

注:Kafka 提供消費者客戶端參數 partition.assignment.strategy 設置 Consumer 與訂閱 Topic 之間的分區分配策略。

4.2.1 RangeAssignor

RangeAssignor 分配策略,原理是按照消費者總數和分區總數進行整除運算,獲得一個跨度,然後將分區按照跨度進行平均分配。
對於分區數可以整除消費組內消費者數量的情況(比如一個消費組內有 2 個消費者,某個 Topic 中有 4 個分區),這種方法的分配特性較好。但如果分區數除以消費組的消費者數量有餘數(比如一個消費組內有 2 個消費者,某個 Topic 有 3 個分區),則會分配不均。這種情況下,如果類似情形擴大,可能會出現消費者過載情況。

注:算法如下:

  1. 將目標 Topic 下的所有 Partirtion 排序,存於 TP
  2. 對某 Consumer Group 下所有 Consumer 按照名字根據字典排序,存於 CG;此外,第 i 個 Consumer 記爲 Ci
  3. N = size(TP) / size(CG)
  4. R = size(TP) % size(CG)
  5. Ci 獲取的分區起始位置:N * i + min(i, R)
  6. Ci 獲取的分區總數:N + (if (i + 1 > R) 0 else 1)

4.2.2 RoundRobinAssignor

RoundRobinAssignor 分配策略,原理是對某個消費組的所有消費者訂閱的所有 Topic 的所有分區進行字典排序,然後用輪詢方式將分區逐個分配給各消費者。
合理使用這種分配策略,最主要的要求是:消費組內所有消費者都有相同的訂閱 Topic 集合。如果消費組內消費者訂閱信息不同,則執行分區分配的時候就不能實現完全的輪詢,可能導致分區分配不均的情況。

注:算法如下:

  1. 對所有 Topic 的所有分區按照 Topic + Partition 轉 String 後的 Hash 計算,進行排序;
  2. 對消費者按照字典排序;
  3. 輪詢方式,將所有分區分配給消費者;

4.2.3 StickyAssignor

StickyAssignor 分配策略注重兩點:

  • 分配儘量均勻;
  • 分配儘量與上一次分配的相同;

從 StickyAssignor 的名稱可以看出,該分配策略儘可能的保持“黏性”。在發生分區重分配後,儘可能讓前後兩次分配相同,減少系統的損耗。雖然該策略的代碼實現很複雜,但通常從結果上看通常比其他兩種分配策略更優秀。

4.3 消費者再均衡階段

4.3.1 階段一:尋找 GroupCoordinator

消費者需要確定它所述消費組對應 GroupCoordinator 所在 broker,並創建網絡連接。向集羣中負載最小的節點發送 FindCoordinatorRequest
Kafka 收到 FindCoordinatorRequest 後,根據請求中包含的 groupId 查找對應的 GroupCoordinator 節點。

4.3.2 階段二:加入消費組

消費者找到消費組對應的 GroupCoordinator 之後,進入加入消費組的階段。消費者會向 GroupCoordinator 發送 JoinGroupRequest 請求。每個消費者發送的 GroupCoordinator 中,都攜帶了各自提案的分配策略與訂閱信息

Kafka Broker 收到請求後進行處理。

  1. GroupCoordinator 爲消費組內的消費者,選舉該消費組的 Leader;
    • 如果消費組內還沒有 Leader,那麼第一個加入消費組的消費者會成爲 Leader;對於普通的選舉情況,選舉消費組 Leader 的算法很隨意,基本上可以認爲是隨機選舉;
  2. 選舉分區分配策略
    • Kafka 服務端收到各個消費者支持的分配策略,構成候選集,所有的消費者從候選集中找到第一個分配策略進行投票,最後票數最多的策略成爲當前消費組的分配策略。

注:如果有消費者不支持選出的分配策略,會報出異常。

Kafka 處理完數據後,將響應 JoinGroupResponse 返回給各個消費者。JoinGroupResponse 回執中包含着 GroupCoordinator 投票選舉的結果,在這些分別給各個消費者的結果中,只有給 leader 消費者的回執中包含各個消費者的訂閱信息

4.3.3 階段三:同步階段

加入消費者的結果通過響應返回給各個消費者,消費者接收到響應後,開始準備實施具體的分區分配。上一步中只有 leader 消費者收到了包含各消費者訂閱結果的回執信息,所以需要 leader 消費者主導轉發同步分配方案。轉發同步分配方案的過程,就是同步階段
同步階段,leader 消費者是通過“中間人” GroupCoordinator 進行的。各個消費者向 GroupCoordinator 發送 SyncGroupRequest 請求,其中只有 leader 消費者發送的請求中包含相關的分配方案。Kafka 服務端收到請求後交給 GroupCoordinator 處理。處理過程有:

  1. 主要是將消費組的元數據信息存入 Kafka 的 __consumer_offset 主題中;
  2. 最後 GroupCoordinator 將各自所屬的分配方案發送給各個消費者。

各消費者收到分配方案後,會開啓 ConsumerRebalanceListener 中的 onPartitionAssigned() 方法,開啓心跳任務,與 GroupCoordinator 定期發送心跳請求 HeartbeatRequest,保證彼此在線。

4.3.4 階段四:心跳階段

進入該階段後的消費者,已經屬於進入正常工作狀態了。消費者通過向 GroupCoordinator 發送心跳,來維持它們與消費組的從屬關係,以及對 Partition 的所有權關係。
心跳線程是一個獨立的線程,可以在輪詢消息空檔發送心跳。如果一個消費者停止發送心跳的時間比較長,那麼整個會話被判定爲過期,GroupCoordinator 會認爲這個消費者已經死亡,則會觸發再均衡行爲

觸發再均衡行爲的情況:

  1. 停止發送心跳請求;(包括消費者發生崩潰的情況)
  2. 參數 max.poll.interval.ms 是 poll() 方法調用之間的最大延遲,如果在該時間範圍內,poll() 方法沒有調用,那麼消費者被視爲失敗,觸發再均衡;
  3. 消費者可以主動發送 LeaveGroupRequest 請求,主動退出消費組,也會觸發再均衡。

4.4 頻繁再均衡

參考地址:《記一次線上kafka一直rebalance故障》

由前面章節可知,有多種可能觸發再均衡的原因。下述記錄一次 Kafka 的頻繁再均衡故障。平均間隔 2 到 3 分鐘就會觸發一次再均衡,分析日誌發現比較嚴重。主要日誌內容如下:

commit failed
org.apache.kafka.clients.consumer.CommitFailedException: Commit cannot be completed since the group has already rebalanced and assigned the partitions to another member. This means that the time between subsequent calls to poll() was longer than the configured max.poll.interval.ms, which typically implies that the poll loop is spending too much time message processing. You can address this either by increasing the session timeout or by reducing the maximum size of batches returned in poll() with max.poll.records.

這個錯誤意思是消費者在處理完一批 poll 的消息之後,同步提交偏移量給 Broker 時報錯,主要原因是當前消費者線程消費的分區已經被 Broker 節點回收了,所以 Kafka 認爲這個消費者已經死了,導致提交失敗。
導致該問題的原因,主要涉及構建消費者的一個屬性 max.poll.interval.ms。這個屬性的意思是消費者兩次 poll() 方法調用之間的最大延遲。如果超過這個時間 poll 方法沒有被再次調用,則認爲該消費者已經死亡,觸發消費組的再平衡。該參數的默認值爲 300s,但我們業務中設置了 5s。

查詢 Kafka 拉取日誌後,發現有幾條日誌由於邏輯問題,單條數據處理時間超過了一分鐘,所以在處理一批消息之後,總時間超過了該參數的設置值 5s,導致消費者被踢出消費組,導致再均衡。

解決方法:

  1. 增加 max.poll.interval.ms 值的大小:將該參數調大至合理值,比如默認的 300s;
  2. 設置分區拉取閾值:通過用外部循環不斷拉取的方式,實現客戶端的持續拉取效果。消費者每次調用 poll 方法會拉取一批數據,可以通過設置 max.poll.records 消費者參數,控制每次拉取消息的數量,從而減少每兩次 poll 方法之間的拉取時間。

此外,再均衡可能會導致消息的重複消費現象。消費者每次拉取消息之後,都需要將偏移量提交給消費組,如果設置了自動提交,則這個過程在消費完畢後自動執行偏移量的提交;如果設置手動提交,則需要在程序中調用 consumer.commitSync() 方法執行提交操作。
反過來,如果消費者沒有將偏移量提交,那麼下一次消費者重新與 Broker 相連之後,該消費者會從已提交偏移量處開始消費。問題就在這裏,如果處理消息時間較長,消費者被消費組剔除,那麼提交偏移量出錯。消費者踢出消費組後觸發了再均衡,分區被分配給其他消費者,其他消費者如果消費該分區的消息時,由於之前的消費者已經消費了該分區的部分消息,所以這裏出現了重複消費的問題。

解決該問題的方式在於拉取後的處理。poll 到消息後,消息處理完一條就提交一條,如果出現提交失敗,則馬上跳出循環,Kafka 觸發再均衡。這樣的話,重新分配到該分區的消費者也不會重複消費之前已經處理過的消息。代碼如下:

        while (isRunning) {
            ConsumerRecords<KEY, VALUE> records = consumer.poll(100);
            if (records != null && records.count() > 0) {

                for (ConsumerRecord<KEY, VALUE> record : records) {
                    // 處理一條消息
                    dealMessage(bizConsumer, record.value());
                    try {
                        // 消息處理完畢後,就直接提交
                        consumer.commitSync();
                    } catch (CommitFailedException e) {
                        // 如果提交失敗,則日誌記錄,並跳出循環
                        // 跳出循環後,Kafka Broker 端會觸發再均衡
                        logger.error("commit failed, will break this for loop", e);
                        break;
                    }
                }
            }
        }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章