Kafka消費者剖析

該篇主要介紹Kafka消費者相關一些知識點,以及使用時需要注意的事項;

消費者組

消費者組(Consumer Group):是 Kafka 提供的可擴展且具有容錯性的消費者機制。其中可以有多個消費者或者消費者實例,他們共享一個公共ID(Group ID)。
每一個分區只能由同一個消費者組內的一個Consumer實例來消費;

特性

  • Consumer Group 下可以有一個或多個 Consumer 實例;
  • Group ID 是一個字符串,唯一標識一個 Consumer Group;
  • Consumer Group 訂閱主題的單個分區,只能分配給組內的某個 Consumer實例消費;
  • Consumer Group 可以訂閱多個主題;
  • Consumer Group 之間相互獨立,能夠訂閱相同的一組主題而互不影響;
    Consumer Group 實現了傳統消息引擎系統的兩大模型: 點對點模型,訂閱發佈模型。
一個Group下Consumer實例的理想數量:
	Consumer實例的數量等於該Group訂閱主題的分區總數;
如果,實例數小於分區數,則一個實例可能會消費多個分區;
如果,實例數大於分區數,則部分實例可能閒置,浪費系統資源;

消費者組位移管理

對於 Consumer Group: 位移(Offset)是一組KV對,K標識分區,V標識對應 Consumer 消費該分區的最新位移。

老版本:
	Consumer Group 把位移保存在Zookeeper中;
	好處:減少了 Kafka Broker 端的狀態保存開銷;保證服務器節點的無狀態,利於自由擴縮容,實現強伸縮性。
	缺點:位移的寫操作十分的頻繁,這種大吞吐量的寫操作會極大的拖慢 Zookeeper 集羣的性能。
	Zookeeper是一個分佈式協調服務框架,保證其性能及高可用十分重要,因此將位移保存在 Zookeeper中時不合適的做法;
新版本:
	Consumer Group 採用將位移保存在 Kafka 內部主題(__consumer_offsets)的方法來記錄位移;

消費者組的重平衡

Rebalance 本質是一種協議,規定一個 Consumer Group下的所有 Consumer 如何達成一致,來分配訂閱 Topic 的每個分區。

重平衡時機

  • 組成員數發生變更。比如有新的 Consumer 實例加入組或者離開組,抑或是有 Consumer 實例崩潰被“踢出”組。
  • 訂閱主題數發生變更。Consumer Group 可以使用正則表達式的方式訂閱主題,比如 consumer.subscribe(Pattern.compile(“t.*c”)) 就表明該 Group 訂閱所有以字母 t 開頭、字母 c 結尾的主題。在 Consumer Group 的運行過程中,你新創建了一個滿足這樣條件的主題,那麼該 Group 就會發生 Rebalance。
  • 訂閱主題的分區數發生變更。Kafka 當前只能允許增加一個主題的分區數。當分區數增加時,就會觸發訂閱該主題的所有 Group 開啓 Rebalance。

重平衡策略

當 Rebalance 發生時,Group下所有的生產者實例都會協調在一起共同參與,而具體的分配情況跟策略有關:詳細參見:https://blog.csdn.net/shenshouniu/article/details/84076930

  • Range 分區分配策略: 即平均分配,分區總數 % 實例數, 餘數分配給第一個實例;
  • Round-robin 分區分配策略:如果同一個消費組內所有的消費者的訂閱信息都是相同的,那麼RoundRobinAssignor策略的分區分配會是均勻的。(你一個我一個他一個,輪詢)
    使用RoundRobin策略有兩個前提條件必須滿足:
    1. 同一個Consumer Group裏面的所有消費者的num.streams必須相等;
    2. 每個消費者訂閱的主題必須相同。
  • StickyAssignor分區分配策略
    1. 分區的分配要儘可能的均勻;
    2. 分區的分配儘可能的與上次分配的保持相同。

當兩者發生衝突時,第一個目標優先於第二個目標。鑑於這兩個目標,StickyAssignor策略的具體實現要比RangeAssignor和RoundRobinAssignor這兩種分配策略要複雜很多。

Rebalance注意事項(弊端)

  • Rebalance過程中, 所有 Consumer 實例都會停止消費,等待 Rebalance 完成;這會對Consumer的 TPS影響很大; 整個過程類似JVM的垃圾回收機制–萬物靜止(stop the world)
  • Rebalance 的設計是所有 Consumer 實例共同參與,全部重新分配所有分區。其實更高效的做法是儘量減少分配方案的變動。
  • Rebalance的效率極低,唯一的解決方案就是:避免Rebalance的發生。

避免 Rebalance

在 Rebalance 過程中,所有 Consumer 實例共同參與,在 協調者組件(Coordinator)的幫助下,完成訂閱主題分區的分配;
協調者組件(Coordinator):專門爲 Consumer Group服務,負責爲 Group 執行 Rebalance 以及提供位移管理和組成員管理等;
Consumer 端應用程序再提交位移時,是向 Coordinator 所在的 Broker 提交位移。
同樣地,當 Consumer 應用啓動時,也是向 Coordinator 所在的 Broker 發送各種請求,然後由 Coordinator 負責執行消費者組的註冊、成員管理記錄等元數據管理操作。
所有 Broker 在啓動時,都會創建和開啓相應的 Coordinator 組件。也就是說,所有 Broker 都有各自的 Coordinator 組件

當 Consumer Group 出現問題時,可以根據以下算法快速定位到正確的 Broker 端,可查看日誌:
	1. 確定由位移主題的哪個分區來保存該Group數據:根據groupId的hash值來確定
		partitionId=Math.abs(groupId.hashCode() % offsetsTopicPartitionCount)
	3. 找到該分區 Leader副本所在的 Broker, 該 Broker 即爲對應的 Coordinator。

Relalance 在 訂閱主題數量和分區數發生變化時發生,大多由運維主動操作產生,這類大多是無法避免的;
能避免的時機:組成員發生變化時

如果 Consumer Group 下的Consumer 實例數量發生變化時,一定會引發 Rebalance;
通常的,對於新增Consumer的操作都是計劃內的,可能是出於增加TPS或提高伸縮性的需要;
而在某些情況下, Consumer 實例會被 Coordinator 錯誤地認爲“已停止”從而被“踢出”Group。如果是這個原因導致的 Rebalance,那麼是可以避免的;

session.timeout.ms : Consumer端參數,表徵最大心跳間隔時間;默認 10秒
每個 Consumer 實例會定期的向 Coordinator 發送心跳請求,表示它還存活;
如果 Consumer沒有在以上配置項的時間內發送心跳,Coordinator會認爲該Consumer死掉,從而將其從 Group中移除,然後開始新的Rebalance;
heartbeat.interval.ms : Consumer端參數,表示心跳發送頻率;頻繁發送會額外消耗寬帶資源;
max.poll.interval.ms :限定了 Consumer 端應用程序兩次調用 poll 方法的最大時間間隔。
默認值是 5 分鐘,表示你的 Consumer 程序如果在 5 分鐘之內無法消費完 poll 方法返回的消息,
那麼 Consumer 會主動發起“離開組”的請求,Coordinator 也會開啓新一輪 Rebalance。

Coordinator通知Consumer開啓Rebalance的方法:將 REBALANCE_NEEDED 標誌封裝進心跳請求的響應體中。
不必要的Rebalance分類:

  1. 未能及時發送心跳
設置 session.timeout.ms = 6s。
設置 heartbeat.interval.ms = 2s。
要保證 Consumer 實例在被判定爲“dead”之前,能夠發送至少 3 輪的心跳請求,
即 session.timeout.ms = 3 * heartbeat.interval.ms。
  1. 消息消費時間太長
設置 max.poll.interval.ms爲一個較大的值,保證下游的業務邏輯能夠處理完;
  1. 其他
可以檢查下Consumer端的 GC 表現,是否是出現頻繁的 Full GC 導致的長時間停頓,從而引發的 Rebalance;
 這種情況需要調整 GC設置

位移概述

位移主題

位移主題(Offsets Topic): 主題名:__consumer_offsets,用於記錄消費者消費一個主題的進度;
自 0.8.2.x 版本開始修改,並在最終的新版本 Consumer (穩定版本:0.10.2.2及之後版本)中正式推出新的位移管理機制:通過位移主題管理;
位移主題機制:將 Consumer 的位移數據作爲一條條普通的Kafka消息,提交到 __consumer_offsets中;
位移主題也是普通的 Kafka 主題,不過他的消息格式是 Kafka 自己定義的,我們可以手動的創建、修改,甚至刪除;不過大部分情況下,我們可以不關注他;

位移主題消息格式

位移主題的 Key 由三部分組成:<Group ID, 主題名, 分區號>;
位移主題的 Value,主要保存了位移值;當然還會保存其他一些元數據(時間戳,用戶定義的數據),主要用於幫助Kafka執行各種各樣的後續操作;

其他格式:

  1. 用於保存 Consumer Group 信息的消息;
該格式非常神祕,幾乎無法在搜索引擎中搜到他的信息,主要是用來註冊 Consumer Group的
  1. 用於刪除 Group 過期位移甚至是刪除 Group的信息
專屬名:tombstone消息 --- 墓碑消息(delete mark)
這些消息只出現在源碼中而不會對外暴露,主要特點是他的消息體是 空消息體(null)
寫入時機: 一旦某個 Consumer Group 下的所有Consumer 實例都停止,而且他們的位移數據都已被刪除時, 
Kafka 會向位移主題的對應分區寫入 tombstone消息,表明要徹底刪除這個Group的信息。

位移主題的創建

通常, 當 Kafka 集羣中的第一個 Consumer 程序啓動時, Kafka會自動創建位移主題。
在位移主題自動創建時,會根據 Broker端參數 offsets.topic.num.partitions來設置分區數,默認值爲50;即在不修改配置的情況下,位移主題默認有50個分區;
對於副本,由另一個Broker端參數控制:offsets.topic.replication.factor, 默認值:3; 即每個位移主題的分區有3個副本;

**位移主題也可以手動創建:**在 Kafka 集羣尚未啓動任何 Consumer 之前,使用 Kafka API創建它;手動創建好處就是,可以根據資源情況自由控制分區副本數量;(不推薦,目前源碼中有部分地方硬編碼了50分區,因此可能可能出現一些奇怪的問題,該社區bug已修復,但仍在審覈)

位移主題的使用

當 Kafka Consumer 提交位移時,會寫入該主題; 提交方式有兩種:

  • 自動提交
enable.auth.commit : Consumer 端參數,爲 true時, Consumer在後臺默默地定期提交位移;
auth.commit.interval.ms : Consumer 端參數,控制提交時間間隔;
當啓動自動提交時,使用者可以不用關注位移這個概念,但正因爲完全交給 Kafka 去完成,
因此無法做到精確把控位移;靈活性和可控性很低;
  • 手動提交
通常,很多與Kafka基層的大數據框架都是禁用自動提交位移的:
	enable.auth.commit = false
此時, Consumer應用開發就需要承擔起位移提交的責任。Kafka Consumer API 爲你提供了位移提交的方法,如 consumer.commitSync

位移主題消息刪除策略

當 Consumer消費到某個主題的最新一條消息時,之後沒有新的消息產生;在自動提交位移的情況的,會不斷向位移主題寫入最新位移的消息,這會導致重複消息存在;之前的消息應該進行清理;否則可能會撐爆磁盤;
Compact 策略:刪除位移主題中過期消息的策略

大概原理:對於同一個 Key 的兩條消息 M1 和 M2,如果 M1 的發送時間早於 M2,那麼 M1 就是過期消息。Compact 的過程就是掃描日誌的所有消息,剔除那些過期的消息,然後把剩下的消息整理在一起。在這裏貼一張來自官網的圖片,來說明 Compact 過程。
在這裏插入圖片描述
Kafka 提供了專門的後臺線程定期地巡檢待 Compact 的主題,看看是否存在滿足條件的可刪除數據。這個後臺線程叫 Log Cleaner。很多實際生產環境中都出現過位移主題無限膨脹佔用過多磁盤空間的問題,如果你的環境中也有這個問題,我建議你去檢查一下 Log Cleaner 線程的狀態,通常都是這個線程掛掉了導致的。

位移提交

Consumer 的消費位移:記錄 Consumer 要消費的下一跳消息的位移,而不是目前最新消費消費的位移;
Consumer 需要向 Kafka 彙報自己的位移數據,彙報過程被稱爲提交位移(Commiting Offsets);Consumer 可以同時消費多個分區的數據,所以位移的提交實際上是在分區粒度上進行的(Consumer 需要爲分配給他的每個分區提交各自的位移數據);
位移提交時 Kafka 提供的一個工具或語義保障,由使用者維持這個語義保障,如果提交了位移X,那麼 Kafka會認爲位移值小於 X 的消息均已成功消費;
從用戶角度,位移提交分爲: 自動提交手動提交
從Consumer端角度,位移提交分爲: 同步提交異步提交

自動提交

設置:
enable.auto.commit = true , 默認情況下Kafka自動提交是打開的;
auto.commit.interval.ms = 5000 , 默認情況下該值爲 5 秒;表示 Kafka 每5秒回自動提交一次位移;

Properties props = new Properties();
    props.put("bootstrap.servers", "localhost:9092");
    props.put("group.id", "test");
    props.put("enable.auto.commit", "true"); // 開啓自動提交
    props.put("auto.commit.interval.ms", "2000"); // 設置指定提交間隔爲2秒
    props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
    props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
    KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
    consumer.subscribe(Arrays.asList("foo", "bar"));
    while (true) {
         ConsumerRecords<String, String> records = consumer.poll(100);
         for (ConsumerRecord<String, String> record : records)
             System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
     }

自動提交開啓後,Kafka 會保證在開始調用 poll 方法時,提交上次poll 返回的所有信息。因此保證不出現消息不丟失的情況。但可能存在重複消費:當在時間間隔內發生重平衡時,在上次時間到重平衡時間段的消費消息會再次被消費;

手動提交

設置:enable.auto.commit = false
調用API: KafkaConsumer#commitSync(), 該方法會自動提交 KafkaConsumer#poll() 返回的位移;爲同步提交;

while (true) {
	ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
	process(records); // 處理消息
	try {
		consumer.commitSync();
	} catch (CommitFailedException e) {
	 handle(e); // 處理提交失敗異常
	}
}

同步提交缺陷:影響整個應用的 TPS;在任何系統中,因爲程序而非自願限制而導致的阻塞都可能是系統的瓶頸。
異步API: KafkaConsumer#commitAsync(), 調用該方法後會立即返回,不會阻塞;通過回調函數來實現提交後的邏輯;

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
    process(records); // 處理消息
    consumer.commitAsync((offsets, exception) -> {
	if (exception != null)
		handle(exception);
	});
}

異步提交異常重試毫無意義,因爲可能重試時已經消費到更大位移處。
手動提交最佳實踐:

try {
	while(true) {
		ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
		process(records); // 處理消息
		commitAysnc(); // 使用異步提交規避阻塞
	}
} catch(Exception e) {
	handle(e); // 處理異常
} finally {
	try {
		consumer.commitSync(); // 最後一次提交使用同步阻塞式提交
	} finally {
		consumer.close();
	}
}

在正常處理流程中,我們使用異步提交來提高性能,但最後使用同步提交來保證位移提交成功。
上述方法,都是提交 poll 方法返回的所有消息的位移,即直接提交這一批消息中最新一條消息的位移;
Kafka提供了更細粒度的位移提交API:
commitSync(Map<TopicPartition, OffsetAndMetadata>)
commitAsync(Map<TopicPartition, OffsetAndMetadata>)
它們的參數是一個 Map 對象,鍵就是 TopicPartition,即消費的分區,而值是一個 OffsetAndMetadata 對象,保存的主要是位移數據。

private Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();
int count = 0;
……
while (true) {
	ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
	for (ConsumerRecord<String, String> record: records) {
		process(record);  // 處理消息
		offsets.put(new TopicPartition(record.topic(), record.partition()), new OffsetAndMetadata(record.offset() + 1)if(count % 100 == 0)
			consumer.commitAsync(offsets, null); // 回調處理邏輯是 null
		count++;
	}
}

問題1:對於手動同步和異步提交結合的場景,如果poll出來的消息是500條,而業務處理200條的時候,業務拋異常了,後續消息根本就沒有被遍歷過,finally裏手動同步提交的是201還是000,還是501?
答:如果調用沒有參數的commit,那麼提交的是500

CommitFailedException異常

Consumer 客戶端在提交位移時出現的不可恢復的嚴重錯誤或異常;如果異常時可恢復的瞬時錯誤,API大多會自動錯誤重試;
異常原因:提交位移失敗,原因是消費者組已經開啓了 Rebalance 過程,並且將要提交位移的分區分配給了另一個消費者實例。出現這個情況的原因是,你的消費者實例連續兩次調用 poll 方法的時間間隔超過了期望的 max.poll.interval.ms 參數值。這通常表明,你的消費者實例花費了太長的時間進行消息處理,耽誤了調用 poll 方法。

解決方案:

  • 增加期望的時間間隔 max.poll.interval.ms 參數值。
  • 減少 poll 方法一次性返回的消息數量,即減少 max.poll.records 參數值。

異常場景

  1. 消息處理的總時間超過預設的 max.poll.interval.ms 參數值
    a. 縮短單挑消息處理時間;
    b. 增加 Consumer 端允許下游系統消費一批消息的最大時長, max.poll.interval.ms, 默認值爲5分鐘; 0.10.1.0之前版本需要設置 session.timeout.ms, 但需要注意該參數還有其他作用;
    c. 減少下游系統一次性消費的消息總數;
    d. 下游系統使用多線程來加速消費;(實現難度大,主要是位移提交)
  2. 獨立消費需要指定 group.id纔可以手動提交位移;當一個消費者組合獨立消費者同時存在時,如果group.id相同,那麼當獨立消費者手動提交位移時,也會拋出該異常。表明它不是消費者組中合法的成員。

多線程開發

Kafka消費者進行多線程開發,可以大大提高系統下游的處理速度;同時能夠更充分的利用系統資源;

Kafka Java Consumer 設計原理

0.10.1.0 之後, KafkaConsumer 包含兩個線程:用戶主線程, 心跳線程;
心跳線程(Heartbeat Thread)只負責定期給對應的 Broker 機器發送心跳請求,以標識消費者應用的存活性(liveness);同時解耦真實的消息處理邏輯與消費者組成員存活性管理;

對於消息處理來說,Consumer 端是單線程設計,這很好的把消息處理的多線程管理策略從 Consumer 端代碼中剝離出去;更有利於其他編程語言移植;

多線程方案

**KafkaConsumer類不是線程安全的,多個線程中不能共享同一個 KafkaConsumer 實例,否則拋出 **ConcurrentModificationException異常。但 KafkaConsumer.wakeup()可以安全的在其他線程中調用,用來喚醒Consumer。

多個線程同時消費 + 邏輯處理

在消費者程序中啓動多個線程,每個線程維護專屬的 KafkaConsumer 實例,負責完整的消息獲取、消息處理流程。

  • 優勢:
  1. 實現簡單;在每個線程中創建KafkaConsumer實例即可。
  2. 線程間無交互,可減少保障線程安全方面的開銷。
  3. 由於同一個消費者組中,一個分區僅會被一個Consumer消費,因此可以很容易可以保障分區內的消息消費順序。對於有時間先後順序保證的場景,這尤爲重要。
  • 缺點:
  1. 佔用更多的系統資源(內存、TCP連接等)。
  2. 受限於Consumer訂閱主題的總分區數;同一個消費者組中,一個分區僅會被一個Consumer消費。可以多啓動線程,但線程會閒置。
  3. 消費和邏輯在同一線程,當業務阻塞時,消費會被影響,容易出現不必要的 rebalance

單個或多個線程消費 + 多個線程邏輯處理

從Kafka中獲取消息的線程是一個或多個,每個線程維護專屬的 KafkaConsumer 實例,但 對於邏輯處理部分移交特定線程池來完成, 實現消息消費與業務邏輯的解耦;

  • 優勢:
    具有更高的伸縮性,不用考慮業務對消息消費的影響;
  • 缺陷:
  1. 實現難度相對較大;
  2. 無法保證分區內的消費順序;同一分區的消息可能被多個線程消費;
  3. 消費位移的正確提交異常困難,可能導致消息重複消費;

TCP連接管理

和生產者不同, 構建 KafkaConsumer 實例時不會創建任何TCP 連接,而是在調用 KafkaConsumer.poll 方法時被創建的。(構造函數中啓動線程,會造成this指針逃逸)
poll 中創建TCP連接的時機:

  1. 發起 FindCoordinator 請求時
  2. 連接協調者時
  3. 消費數據時
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章