一. Producer API
消息發送流程
Kafka 的 Producer 發送消息採用的是異步發送的方式。在消息發送的過程中,涉及到了
兩個線程——main 線程和 Sender 線程,以及一個線程共享變量——RecordAccumulator。 main 線程將消息發送給 RecordAccumulator,Sender 線程不斷從 RecordAccumulator 中拉取 消息發送到 Kafka broker。
pom文件配置
2.2.1 爲kafka的版本
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.2.1</version>
</dependency>
1.2 異步發送普通生產者
代碼:
BATCH_SIZE_CONFIG = "batch.size":消息爲batch.size大小,生產者才發送消息
LINGER_MS_CONFIG = "linger.ms":如果消息大小遲遲不爲batch.size大小,則等待linger.ms時間後直接發送
package com.bigdata.study.kafka;
/**
* @author 只是甲
* @date 2021-10-29
* @remark kafka生產者 - 異步 - 不帶回調函數的API
*/
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
import org.apache.kafka.clients.producer.*;
public class CustomProducer {
public static void main(String[] args) throws Exception {
Properties props = new Properties();
// 設置集羣配置
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hp2:9092,hp3:9092,hp4:9092");
// ack機制
props.put(ProducerConfig.ACKS_CONFIG, "all");
// 重試次數
props.put(ProducerConfig.RETRIES_CONFIG, 1);
// 批次大小:消息大小爲16384才發送消息
props.put("batch.size", 16384);
// 等待時間:如果消息大小遲遲不爲batch.size大小,則等待linger.ms時間後直接發送
props.put(ProducerConfig.LINGER_MS_CONFIG, 1);
// ReadAccumulator緩衝區大小
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
// 序列化
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
// 構造Producer
Producer<String,String> producer=new KafkaProducer<>(props);
// 生產消費
for (int i = 0; i < 100; i++){
producer.send(new ProducerRecord<String, String>("kafka_test1", "test_" + Integer.toString(i), "test_" + Integer.toString(i)));
}
producer.close();
}
}
shell中查看消費者:
/opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin/kafka-console-consumer.sh --from-beginning --bootstrap-server hp2:9092 --topic kafka_test1
1.2 異步發送帶回調函數的生產者
回調函數會在producer收到ack時調用,爲異步調用,該方法有兩個參數,分別爲RecordMetaData和Exception,如果Exception爲null,說明消息發送成功,如果Exception不爲null,說明消息發送失敗。
消息發送失敗會啓動重試機制,但需要在回調函數中手動重試
代碼:
package com.bigdata.study.kafka;
/**
* @author 只是甲
* @date 2021-10-29
* @remark kafka生產者 - 異步 - 帶回調函數的API
*/
import java.util.Properties;
import java.util.concurrent.ExecutionException;
import org.apache.kafka.clients.producer.*;
public class CallBackProducer {
public static void main(String[] args) throws Exception {
Properties props = new Properties();
// 設置集羣配置
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hp2:9092,hp3:9092,hp4:9092");
// ack機制
props.put(ProducerConfig.ACKS_CONFIG, "all");
// 重試次數
props.put(ProducerConfig.RETRIES_CONFIG, 1);
// 批次大小:消息大小爲16384才發送消息
props.put("batch.size", 16384);
// 等待時間:如果消息大小遲遲不爲batch.size大小,則等待linger.ms時間後直接發送
props.put(ProducerConfig.LINGER_MS_CONFIG, 1);
// ReadAccumulator緩衝區大小
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
// 序列化
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
// 構造Producer
Producer<String,String> producer=new KafkaProducer<>(props);
for (int i = 0; i < 100; i++){
producer.send(new ProducerRecord<String, String>("kafka_test1", "test-" + Integer.toString(i),"test-" + Integer.toString(i)), new Callback() {
//回調函數,該方法會在 Producer 收到 ack 時調用,爲異步調用
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if ( e == null ) {
System.out.println("success->" + recordMetadata.offset());
} else {
e.printStackTrace();
}
}
});
}
producer.close();
}
}
1.3 生產者分區策略測試
public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value, Iterable<Header> headers) {};
public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value) {};
public ProducerRecord(String topic, Integer partition, K key, V value, Iterable<Header> headers) {};
public ProducerRecord(String topic, Integer partition, K key, V value) {};
public ProducerRecord(String topic, K key, V value) {};
public ProducerRecord(String topic, V value) {};
上面ProducerRecord中的partition參數即爲指定的分區(分區是有編號的,這是指定分區中的某一個,實際應該爲一個分區編號)。
這裏要注意,如果指定特定分區的話,消息是會發送到這個編號的特定分區,但是注意如果你的Topic分區只有默認的1個,而你卻要發送到分區1號,此時發送會失敗!因爲你只有1個分區,即0號分區。所以在構建的topic的時候需要注意。
默認分區構造
// 構造消息體,這裏加上具體的分區,其中的2是特定的分區編號
producer.send(new ProducerRecord<>("aaroncao",2, "test-" + i, "test-" + i), new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (e == null) {
System.out.println(recordMetadata.partition() + "-" + recordMetadata.offset());
} else {
e.printStackTrace();
}
}
});
二. API消費者
2.1 簡單消費者
Kafka提供了自動提交offset的功能enable.auto.commit=true;
代碼:
package com.bigdata.study.kafka;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.Properties;
/**
* @author 只是甲
* @date 2021-10-29
* @remark kafka消費者
*/
public class CustomConsumer {
public static void main(String[] args) throws Exception {
Properties props = new Properties();
// 設置集羣配置
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hp2:9092,hp3:9092,hp4:9092");
// 設置消費者組
props.put(ConsumerConfig.GROUP_ID_CONFIG, "consumer_group1");
// 設置offset的自動提交
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
// 設置offset自動化提交的間隔時間
props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
// 生產者是序列化,消費者則爲反序列化
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
// offset重置,需要設置自動重置爲earliest
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
// 這裏需要訂閱具體的topic
consumer.subscribe(Collections.singletonList("kafka_test1"));
// 一直處於監聽狀態中
while (true) {
// 因爲消費者是通過pull獲取消息消費的,這裏設置間隔100ms
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMillis(100));
// 對獲取到的結果遍歷
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.printf("offset=%d, key=%s, value=%s\n", consumerRecord.offset(),consumerRecord.key(),consumerRecord.value());
}
// 同步提交,會一直阻塞直到提交成功,這裏可以設置超時時間,如果阻塞超過超時時間則釋放
//consumer.commitSync();
consumer.commitAsync(new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
if (exception != null) {
System.out.println("Commit failed, offset = " + offsets);
}
}
});
}
}
}
測試記錄:
2.2 消費者重置offset
Consumer消費數據時的可靠性很容易保證,因爲數據在Kafka中是持久化的,不用擔心數據丟失問題。但由於Consumer在消費過程中可能遭遇斷電或者宕機等故障,Consumer恢復之後,需要從故障前的位置繼續消費,所以Consumer需要實時記錄自己消費的offset位置,以便故障恢復後可以繼續消費。
offset的維護是Consumer消費數據必須考慮的問題。
// offset重置,需要設置自動重置爲earliest
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");
將消費者組的id變換一下即可,否則由於一條消息只能夠被一個消費者組中的消費者消費一次,此時不會重新消費之前的消息,即使設置了offset重置也沒有作用。
注意
這裏的auto.offset.reset="earliest"的作用等同於在linux控制檯,消費者監聽的時候添加的--from-beginning命令。
auto.offset.reset取值
- earliest:重置offset到最早的位置
- latest:重置offset到最新的位置,默認值
- none:如果在消費者組中找不到前一個offset則拋出異常
- anything else:拋出異常給消費者
2.3 消費者保存offset讀取問題
enable.auto.commit=true即自動提交offset。默認是自動提交的。
2.4 消費者手動提交offset
自動提交offset十分便利,但是由於其實基於時間提交的,開發人員難以把握offset提交的時機,因此kafka提供了手動提交offset的API。
手動提交offset的方法主要有兩種:
- commitSync:同步提交
- commitAsync:異步提交
相同點: 兩種方式的提交都會將本次poll拉取的一批數據的最高的偏移量提交。
不同點: commitSync阻塞當前線程,持續到提交成功,失敗會自動重試(由於不可控因素導致,也會出現提交失敗);而commitAsync則沒有失敗重試機制,有可能提交失敗。
代碼:
同步提交
package com.bigdata.study.kafka;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Collections;
import java.util.Properties;
/**
* @author 只是甲
* @date 2021-10-29
* @remark kafka消費者 - 同步提交
*/
public class SyncCommitOffset {
public static void main(String[] args) throws Exception {
Properties props = new Properties();
// 設置集羣配置
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hp2:9092,hp3:9092,hp4:9092");
// 設置消費者組
props.put(ConsumerConfig.GROUP_ID_CONFIG, "consumer_group2");
// 設置offset的自動提交
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
// 設置offset自動化提交的間隔時間
props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
// 生產者是序列化,消費者則爲反序列化
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
// offset重置,需要設置自動重置爲earliest
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
// 這裏需要訂閱具體的topic
consumer.subscribe(Collections.singletonList("kafka_test1"));
// 一直處於監聽狀態中
while (true) {
// 因爲消費者是通過pull獲取消息消費的,這裏設置間隔100ms
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMillis(100));
// 對獲取到的結果遍歷
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.printf("offset=%d, key=%s, value=%s\n", consumerRecord.offset(),consumerRecord.key(),consumerRecord.value());
}
// 同步提交,會一直阻塞直到提交成功,這裏可以設置超時時間,如果阻塞超過超時時間則釋放
consumer.commitSync();
}
}
}
異步提交
consumer.commitAsync(new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
if (exception != null) {
System.out.println("Commit failed, offset = " + offsets);
}
}
});
2.5 數據漏消費和重複消費分析
無論是同步提交還是異步提交offset,都可能會造成數據的漏消費或者重複消費,先提交offset後消費,有可能造成數據的漏消費,而先消費再提交offset,有可能會造成數據的重複消費。
2.6 自定義存儲offset
Kafka0.9版本之前,offset存儲在zookeeper中,0.9版本及之後的版本,默認將offset存儲在Kafka的一個內置的topic中,除此之外,Kafka還可以選擇自定義存儲offset數據。offse的維護相當繁瑣,因爲需要考慮到消費者的rebalance過程:
當有新的消費者加入消費者組、已有的消費者退出消費者組或者訂閱的主體分區發生了變化,會觸發分區的重新分配操作,重新分配的過程稱爲Rebalance。
消費者發生Rebalace之後,每個消費者消費的分區就會發生變化,因此消費者需要先獲取到重新分配到的分區,並且定位到每個分區最近提交的offset位置繼續消費。(High Water高水位)
代碼:
package com.bigdata.study.kafka;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.*;
/**
* @author 只是甲
* @date 2021-10-29
* @remark Kafka自定義offset提交
*/
public class CustomOffsetCommit {
private static Map<TopicPartition, Long> currentOffset = new HashMap<>();
public static void main(String[] args) {
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hp2:9092,hp3:9092,hp4:9092");
// 設置消費者組
props.put(ConsumerConfig.GROUP_ID_CONFIG, "consumer_group4");
// 設置offset的自動提交爲false
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
// 這裏的意思是訂閱的時候同時定義Consumer重分配的監聽器接口
consumer.subscribe(Collections.singletonList("kafka_test1"), new ConsumerRebalanceListener() {
// rebalance發生之前調用
@Override
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
commitOffset(currentOffset);
}
// rebalance發生之後調用
@Override
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
currentOffset.clear();
for (TopicPartition partition : partitions) {
// 定位到最新的offset位置
consumer.seek(partition, getOffset(partition));
}
}
});
while (true) {
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.printf("offset=%d, key=%s, value=%s\n", consumerRecord.offset(), consumerRecord.key(), consumerRecord.value());
// 記錄下當前的offset
currentOffset.put(new TopicPartition(consumerRecord.topic(), consumerRecord.partition()), consumerRecord.offset());
}
}
}
// 獲取某分區最新的offset
private static long getOffset(TopicPartition topicPartition) {
return 0;
}
// 提交該消費者所有分區的offset
private static void commitOffset(Map<TopicPartition, Long> currentOffset) {
}
}
即自己記錄下需要提交的offset,利用Rebalance分區監聽器監聽rebalance事件,一旦發生rebalance,先將offset提交,分區之後則找到最新的offset位置繼續消費即可
三. 自定義攔截器
攔截器原理
Producer攔截器interceptor是在Kafka0.10版本引入的,主要用於Clients端的定製化控制邏輯。對於Producer而言,interceptor使得用戶在消息發送之前以及Producer回調邏輯之前有機會對消息做一些定製化需求,比如修改消息的展示樣式等,同時Producer允許用戶指定多個interceptor按序作用於同一條消息從而形成一個攔截鏈interceptor chain,Interceptor實現的接口爲ProducerInterceptor,主要有四個方法:
configure(Map<String, ?> configs):獲取配置信息和初始化數據時調用
onSend(ProducerRecord record):該方法封裝在KafkaProducer.send()方法中,運行在用戶主線程中,Producer確保在消息被序列化之前及計算分區前調用該方法,並且通常都是在Producer回調邏輯出發之前。
onAcknowledgement(RecordMetadata metadata, Exception exception):onAcknowledgement運行在Producer的IO線程中,因此不要再該方法中放入很重的邏輯,否則會拖慢Producer的消息發送效率。
close():關閉inteceptor,主要用於執行資源清理工作。
Inteceptor可能被運行到多個線程中,在具體使用時需要自行確保線程安全,另外倘若指定了多個interceptor,則producer將按照指定順序調用它們,並緊緊是捕獲每個interceptor可能拋出的異常記錄到錯誤日誌中而非向上傳遞。
自定義加入時間戳攔截器
TimeInterceptor
package com.bigdata.study.kafka;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import java.util.Map;
/**
* @author 只是甲
* @date 2021-10-29
* @remark Kafka 消息攔截器
*/
public class TimeInterceptor implements ProducerInterceptor<String, String> {
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
return new ProducerRecord(record.topic(), record.partition(), record.timestamp(), record.key(),
"TimeInterceptor:" + System.currentTimeMillis() + "," + record.value());
}
@Override
public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
}
}
CounterInterceptor
package com.bigdata.study.kafka;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import java.util.Map;
/**
* @author 只是甲
* @date 2021-10-29
* @remark Kafka 消息攔截器
*/
public class CounterInterceptor implements ProducerInterceptor<String, String> {
private int errorCounter = 0;
private int successCounter = 0;
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
return record;
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
if (exception == null) {
successCounter++;
} else {
errorCounter++;
}
}
@Override
public void close() {
// 輸出結果,結束輸出
System.out.println("Sent successful:" + successCounter);
System.out.println("Sent failed:" + errorCounter);
}
@Override
public void configure(Map<String, ?> map) {
}
}
在CustomProducer中加入攔截器
// 加入攔截器
List<Object> interceptors = new ArrayList<>();
interceptors.add(TimeInterceptor.class);
interceptors.add(CounterInterceptor.class);
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);
測試記錄:
/opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin/kafka-console-consumer.sh --from-beginning --bootstrap-server hp2:9092 --topic kafka_test1