第一章 初始kafka
參考書籍: 朱小廝--深入理解Kafka 核心設計與實踐原理
Kafka體系結構
-
Kafka體系架構包含若干Producer, 若干Broker , 若干Consumer,以及一個Zookeeper集群。
- Zookeeper是Kafka用來負責集群元數據的管理、控制器的選舉等操作。
- Producer:生產者,即發送消息的一方。生產者負責創建消息,然后將其投遞到Kafka中
- Broker:一個獨立的Kafka服務節點。 一個或多個Broker組成了一個Kafka集群
- Consumer: 消費者,也就是接收消息的一方。消費者連接到Kafka上并拉取消息,進行相應的業務邏輯處理
kafka體系架構
主題和分區
Kafka的每條消息都屬于一個主題,生產者負責將消息發送到特定的主題,而消費者負責訂閱主題并消費
-
一個主題可以細分為多個分區,一個分區屬于單個主題。 分區可以看成是一個可追加的日志文件, 消息在被追加到分區日志文件時會分配一個
偏移量
, 偏移量是消息在分區中的唯一標識,Kafka保證了 偏移量在分區中是有序的。消息寫入 每一條消息發送時會根據分區規則選擇存儲到哪一個分區,在主題創建之后可以通過修改分區的數量實現水平擴展。
多副本(Replica機制)
- 多副本機制是通過增加副本數量進行數據冗余,從而提高容災能力。 副本之間是“一主多從”的關系。其中leader副本負責處理讀寫請求,follower副本只負責與leader副本的消息同步 (區別于讀寫分離) , 當leader副本宕機時通過leader選舉和失效轉移,保證了Kafka的高可用性。
- folower副本的消息相對于leader副本具有一定的滯后性
幾個重要名詞概念
AR (Assigned Replicas): 分區中的所有副本
ISR (In-Sync-Replicas): 與leader副本保持一定程度同步的副本
-
OSR(Out-of-Sync-Replicas): 與leader副本滯后過多的副本
即 AR = ISR + OR;
HW(High WaterMark): 高水位, 用來標記一個特定的消息偏移量,消費者只能拉取到這個offset之前的消息(可見性)
LEO( Log End Offset) : 標志著當前日志文件中下一條待寫入消息的offset 。 分區ISR集合中的每個副本都維護自身的LEO,而ISR集合中的最小LEO為分區的HW,對消費者而言只能消費HW之前的消息。
第二章 生產者
KafkaProducer是線程安全的
public class KafkaProducerAnalysis {
public static final String brokerList = "localhost:9092";
public static final String topic = "topic-demo";
public static Properties initConfig() {
Properties props = new Properties();
props.put("bootstrap.servers", brokerList);
props.put("key.serializer",
"org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer",
"org.apache.kafka.common.serialization.StringSerializer");
props.put("client.id", "producer.client.id.demo");
return props;
}
public static void main(String[] args) {
Properties props = initConfig();
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
ProducerRecord<String, String> record = new ProducerRecord<>(topic, "hello, Kafka!");
producer.send(record);
}
}
- 生產邏輯的幾個步驟
- 配置生產者客戶端參數并創建生產者實例
- 構建待發送消息
- 發送消息
- 關閉生產者實例
發送消息的三種模式
public Future<RecordMetadata> send(ProducerRecord<K, V> record);
public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback);
- 發后即忘
- 只管發送消息,不關心信息是否正確到達。
- 優點:性能最高,吞吐量大 缺點:會造成數據丟失,可靠性低
- 同步
- 發送消息后返回Future對象,調用get()方法時阻塞等待,直到發送成功或出現異常
- 優點:可靠性高,如有異常可處理或進行消息重發 缺點:性能低,造成阻塞
- 異步
- 發送消息時指定回調函數,Kafka在返回響應時會調用該函數實現異步的發送確認。
- 在同一個分區中,如果消息record1比record2先發送,那么它會保證callback1在callback2之前調用。
序列化器
- 生產者使用序列化器將對象轉換為字節數組,才能通過網絡發送給Kafka
- 消費者使用反序列化其把Kafka中收到的字節數組轉換為相應的對象。
- 因此生產者的序列化器和消費者使用的反序列化器要一一對應。
分區器
分區器 是根據key這個字段來計算partition值。它的作用是為消息分配分區
生產者攔截器
生產者攔截器既可用來在消息發送前做一些準備工作如 按照某個規則過濾掉不符合要求的消息,修改消息內容等。也可以用來在發送回調邏輯前做一些定制化需求,如統計工作。 還可以指定多個攔截器形成攔截器鏈
生產者整體架構
- 整個生產者客戶端由 主線程和Sender線程構成
- 在④中,是用于緩存消息,以便Sender線程進行批量發送,進而減少網絡傳輸
- 在⑤中,是將 <分區,消息集合> 轉化為 <brokerId, 消息集合>。 即邏輯地址到物理地址的轉化
- 在⑦中,用于緩存尚未收到回應的消息,以便異常時可進行重發
- 重要參數 max.in.flight.requests.per 默認值為5,即每個連接最多只能緩存5個未響應的請求。 可類比于TCP連接中的滑動窗口大小
元數據的更新
元數據是指Kafka集群中的元數據,這些元數據記錄了集群中有哪些主題,這些主題有哪些分區,每個分區的leader副本分配在哪個節點上,follwer副本分配在哪些節點上,哪些副本在AR,ISR集合中,集群有哪些節點,控制節點又是哪一個等信息。
元數據更新會挑選 InFlightRequests中當前負載最小的節點發送更新元數據請求。 由于Sender線程需要更新,而主線程需要讀取。因此數據同步問題也要考慮。使用synchronized和final保證。
幾個重要的參數
- acks : 用來指定分區中必須要有多少個副本收到這條消息,這樣生產者才認為消息寫入成功
- 取值為1 : 只要leader副本成功寫入消息,就會收到kafka的成功響應
- 取值為0: 不需要等待任何服務器響應,寫入就認為成功
- 取值為-1或all:需要等待ISR中的所有副本都成功寫入消息,才會收到kafka的成功響應
- max.request.size
- 限制生產者客戶端能發送消息最大值
- retires 、retry.backoff.ms
- 配置生產者重試次數 、 兩次重試的時間間隔
- max.in.flight.requests.per.connection
- 默認值為5,即每個連接最多只能緩存5個未響應的請求。
- 當此參數 > 1 ,則會因為重發而出現錯序的問題
第三章 消費者
kafkaConsumer是線程不安全的
消費者和消費者組
每個消費者只能消費所分配到的分區中的消息,即每一個分區只能被一個消費者組中的一個消費者所消費
-
當消費組內的消費者個數變化時對應的分區分配演變如下:(默認的RangeAssinor為例)
消費者與消費者組 消費者與消費者組的模型讓整體消費能力具有了伸縮性。可以增加消費者個數來提高(或降低)整體消費能力
-
當消費者過多,出現消費者分配不到任何分區時,那么這些消費者將無法消費消息,造成浪費
消費組內有過多的消費者 -
Kafka基于消費者和消費者組模型 支持了 點對點和發布/訂閱兩種模式
- 如果所有消費者都隸屬于同一個消費組,那么所有的消息都會被均衡地投遞給每一個消費者,即每條消息只會被一個消費者處理,相當于點對點模式
- 如果所有的消費者都隸屬于不同的消費組,那么所有的消息都會被廣播給所有的消費者,即每條消息會被所有的消費者處理,相當于發布/訂閱模式的應用
每一個消費者只隸屬于一個消費組。消息發送時可指定消費者組, 消費者客戶端通過group.id配置消費者組名稱,默認為空字符串。
消費者客戶端開發
KafkaConsumer是非線程安全的
public class KafkaConsumerAnalysis {
public static final String brokerList = "localhost:9092";
public static final String topic = "topic-demo";
public static final String groupId = "group.demo";
public static final AtomicBoolean isRunning = new AtomicBoolean(true);
public static Properties initConfig() {
Properties props = new Properties();
props.put("key.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer");
props.put("bootstrap.servers", brokerList);
props.put("group.id", groupId);
props.put("client.id", "consumer.client.id.demo");
return props;
}
public static void main(String[] args) {
Properties props = initConfig();
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList(topic));
try {
while (isRunning.get()) {
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
System.out.println("topic = " + record.topic()
+ ", partition = " + record.partition()
+ ", offset = " + record.offset());
System.out.println("key = " + record.key()
+ ", value = " + record.value());
//do something to process record.
}
}
} catch (Exception e) {
log.error("occur exception ", e);
} finally {
consumer.close();
}
}
}
-
Kafka的消費邏輯
- 配置消費者客戶端參數及創建相應消費者實例
- 訂閱主題
- 拉取消息并消費
- 提交消費位移(后面會講)
- 關閉消費者實例
-
訂閱主題和分區的細節
-
有三種訂閱的方式。 集合訂閱的方式subscribe(Collection)、正則表達式訂閱方式subscribe(Pattern)、
指定分區的訂閱方式assign(Collection)
subscribe訂閱主題時具有再平衡(后面會講)的功能,而assign沒有。
-
反序列化器
將字節數組轉化為對象,與生產者的序列化器要一一對應
消息消費
幾個常用API
#KafkaConsumer
public ConsumerRecords<K, V> poll(Duration timeout);
#ConsumerRecords
public List<ConsumerRecord<K, V>> records(TopicPartition partition);
public Iterable<ConsumerRecord<K, V>> records(String topic);
- timeout參數用于控制阻塞時間,再消費者緩沖區里沒有數據時會發生阻塞
- 按照分區的維度,獲取拉取消息中 某個分區的所有記錄
- 按照主題的維度,獲取拉取消息中 某個主題的所有記錄
位移提交
-
Consumer會記錄上一次的消費位移,并進行持久化保存。 存儲到Kafka的內部主題__consumer_offset中
消費位移 -
位移的提交時機也有講究,可能會造成重復消費和消息丟失的現象
拉取到消息之后就進行位移提交, 若消費到一半時宕機,則造成消失丟失現象
-
消費完所有消息后在進行位移提交, 若消費到一半時宕機,則造成重復消費現象
消費位移的提交位置
Kafka默認的消費位移提交方式是自動提交(定期)。
enable.auto.commit
默認為true;auto.commit.interval.ms
配置提交的周期。自動提交的動作是在poll()方法的邏輯中完成的,會在每次拉取請求之間檢查是否可以進行位移提交。-
自動提交會造成重復消費和消息丟失的現象
重復消費: 消費到一半時宕機,而尚未提交,則造成重復消費
-
消息丟失:如圖線程A進行拉取消息到緩存,線程B從緩存中處理邏輯。 若線程B處理到一半時宕機,那么下次恢復時又從【X+7】開始拉取,造成了【x+4】-【X+7】消息的丟失
自動位移提交中消息丟失的情況
可以看出自動提交編碼簡單但會出現消息丟失和重復消費現象,并且無法做到精確的位移管理,因此Kafka還提供了 手動提交的方式。通常不是拉取到消息就算消費完成了,而是當我們通過這條消息完成一系列業務處理后,才認為消息被成功消費。開啟手動提交需要
enable.auto.commit
設置為false-
手動提交可分為 同步提交和異步提交。 即commitSync()和commitAsync()兩種方式
以下是同步提交和異步提交的一些案例
#拉取所有消息并處理后進行同步提交 while (running.get()) { ConsumerRecords<String, String> records = consumer.poll(1000); for (ConsumerRecord<String, String> record : records) { //do some logical processing. } consumer.commitSync(); } #批量處理+批量提交 int minBatchSize = 200; while (running.get()) { ConsumerRecords<String, String> records = consumer.poll(1000); for (ConsumerRecord<String, String> record : records) { buffer.add(record); } if (buffer.size() >= minBatchSize) { //do some logical processing with buffer. consumer.commitSync(); buffer.clear(); } } #帶參數的同步位移提交,可控制提交的offset,該案例為每消費一條就提交一次 while (running.get()) { ConsumerRecords<String, String> records = consumer.poll(1000); for (ConsumerRecord<String, String> record : records) { //do some logical processing. long offset = record.offset(); TopicPartition partition = new TopicPartition(record.topic(), record.partition()); consumer.commitSync(Collections .singletonMap(partition, new OffsetAndMetadata(offset + 1))); } } #按分區粒度同步提交消費位移,每處理完一個分區就提交一次 while (running.get()) { ConsumerRecords<String, String> records = consumer.poll(1000); for (TopicPartition partition : records.partitions()) { List<ConsumerRecord<String, String>> partitionRecords = records.records(partition); for (ConsumerRecord<String, String> record : partitionRecords) { //do some logical processing. } long lastConsumedOffset = partitionRecords .get(partitionRecords.size() - 1).offset(); consumer.commitSync(Collections.singletonMap(partition, new OffsetAndMetadata(lastConsumedOffset + 1))); } } #異步提交,可指定提交完成后的回調函數 public void commitAsync(); public void commitAsync(OffsetCommitCallback callback); public void commitAsync(Map<TopicPartition, OffsetAndMetadata> offsets, OffsetCommitCallback callback)
異步提交也存在重復消費的問題。如果先提交了【X+2】,再提交【X+8】。如果后者提交成功而前者提交失敗。 如果此時前者進行重試提交,那么成功后會造成數據的重復消費。
對于異步提交可以設置一個遞增的序號維護異步提交的順序,如當位移提交失敗需要重試提交時,對比所提交的位移和維護的序號大小,如果前者小于后者,就不需要再重復提交了。
控制或關閉消費
- KafkaConsumer提供了暫停pause()和恢復resume()某些分區的消費;以及關閉close()的方法'
指定位移消費
- 如用一個新的消費者組來消費主題時,由于沒有可查找的消費偏移,因此會按照
auto.offset.reset
配置來決定從何處開始消費消息。 - seek()方法為我們提供了從特定位置讀取消息的能力,通過這個方法來向前跳過若干消息,也可以通過這個方法來向后回溯若干消息
再均衡
- 再均衡是指分區所屬權從一個消費者轉移到另一個消費者的行為。它為消費者組具備高可用性和伸縮性提供了保障。
- 在再均衡期間,消費組內的消費者是無法讀取消息的。即在再均衡發生期間的一段時間內,消費者會變得不可用
- 當一個分區被重新分配給另一個消費者時,消費者當前的狀態也會丟失。比如消費者消費完某個分區的一部分信息但還沒來得及提交消費位移就發生了再均衡操作
- 調用subscribe()方法時可以提供再均衡監聽器來設置 消費者停止讀取消息 和 開始讀取消費 時的回調函數
消費者攔截器
- 在消費者poll()方法返回之前 、 提交完消息位移之后 、關閉消費者之前 調用攔截器中的方法
- 多個消費者攔截器也能組成攔截器鏈
多線程的實現
-
一個消費線程消費一個或多個分區
一個消費線程消費一個或多個分區 多個消費線程同時消費同一個分區 (會使得提交偏移量異常復雜)
-
類似IO多路復用,一個線程拉取消息,拉取消息后提交到線程池中。 (因為拉取消息會比處理業務邏輯快)
- 會出現消息丟失的情況,線程A消費0-99 ,線程B消費100-199 后提交。 線程A未提交而掛掉后,那么0-99這一段數據就丟失了
第三種方式