kafka学习路线
1、kafka的概述
kafka的定义
kafka是一个分布式的基于发布/订阅模式的消息队列(Message Queue),主要应用于大数据实时处理领域。
使用消息队列的好处
1、解耦;允许你独立的扩展或修改两边的处理过程,只要确保他们遵循同样的接口约束
2、可恢复性;系统的每一部分组件失效时,不会影响到整个系统,消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后处理
3、缓冲;有助于控制数据流经过系统的速度,解决生产消息和消费消息的处理速度不一致的问题(主要解决生产大于消费的问题)
4、灵活性&峰值处理能力;在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见,如果以处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费,使用消息队列能够使关键组件顶住突发压力 ,而不会因为突发的超负荷的请求而全部崩溃。
5、异步通信;很多时候,用户不想也不需要立即处理消息。消息队列提供了异步通信的机制,允许用户把一个消息放入队列,但不立即处理它,想向队列中放入多少消息就放多少,然后在需要的时候再去处理他们。
消息队列的两种模式
1、点对点模式(一对一,消费者主动拉取数据,消息收到后消息清除)
消息生产者生产消息到Queue中,然后消息消费者从Queue中提取出并且消费消息,消息被消费以后,queue中不再储存,所以消息消费者不可能消费到已经消费的消息,queue支持存在多个消费者,但是对一个消息而言,只会有一个消费者消费。
2、发布订阅模式(一对多,消费者消费数据之后不会清除消息)
消息生产者(发布)将消息发布到topic中,同时有多个消息消费者(订阅)消费该消息,和点对点方式不同,发布到topic之后的消息会被所有订阅者消费
发布订阅模式有两种(推送和拉取)
kafka是基于拉取的发布订阅模式
推送的优点:由topic推送给消费者,只要有新消息就会推送给订阅该topic的消费者
推送的缺点:消费者的消费能力可能不一致,消费者有可能处理不了推送的消息
拉取的优点:拉取是根据消费者的处理能力去拉取的,所以不会存在处理不了的情况
拉取的缺点:要一直轮询topic是否有新的消息,是一个长轮询的过程,比较浪费资源。
kafka的架构
1、Producer:消息生产者,就是向kafka broker发消息的客户端
2、Consumer:消息消费者,向kafka broker去消息的客户端
3、Consumer Group:消费者组,由多个consumer组成,消费者组内的每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费,消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
4、Broker:一台kafka服务器就是一个broker,一个集群由多个broker组成,一个broker可以容纳多个topic
5、Topic:可以理解为一个队列,生产者和消费者面向的都是一个topic
6、Partition:为了实现扩展性,一个非常大的topic可以分布到多台broker(服务器)上,一个topic可以分为多个partition,每个partition是一个有序的队列
7、Replica:副本,为了保证集群中的某个节点发生故障时,该节点上的partition数据不丢失,且kafka仍能够继续工作,kafka提供了副本机制,一个topic的每个分区都有若干个副本,一个leader和若干个follower
8、leader:每个分区多个副本的“主”,生产者发送数据的对象,以及消费者消费数据的对象都是leader
9、follower:每个分区多个副本的“从”,实时从leader中同步数据,保持和leader数据的同步,leader发生故障时,某个follower会成为新的leader
2、kafka命令行操作
1、查看当前服务器中所有的topic
bin/kafka-topics.sh --zookeeper hadoop102:2181 --list
2、创建topic
bin/kafka-topics.sh --zookeeper hadoop102:2181 --create --topic first --partitions 2 --replication-factor 2
选项说明:
--topic 定义topic的名
--partitions 定义分区数
--replication-factor 定义副本数
3、删除topic
bin/kafka-topics.sh --zookeeper hadoop102:2181 --delete --topic first
这个需要在server.properties中设置delete.topic.enable=true,否则只是标记删除
4、发送消息
bin/kafka-console-producer.sh --broker-list hadoop102:9092 --topic first
5、消费消息
bin/kafka-console-consumer.sh --zookeeper hadoop102:2181 --topic first
//现在版本一般用下面这个,上面已经过时了
bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first
bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first --from-beginning
–from-beginning:会把主题中以往的数据都读取出来
6、查看某个topic的详情
bin/kafka-topics.sh --zookeeper hadoop102:2181 --describe --topic first
7、修改分区数
bin/kafka-topics.sh --zookeeper hadoop102:2181 --alter --topic first --partitions 6
3、kafka架构深入
kafka中消息是以topic进行分类的,生产者生产消息,消费者消费消息,都是面向topic的
topic是逻辑上的概念,而partition是物理上的概念,每个partition对应于一个log文件,而log文件中存储的就是producer生产的数据。producer生产的数据会被不断追加到该log文件末端,且每条数据都有自己的offset。消费者组中的每个消费者都会实时的记录自己消费到哪个offset,以便出错恢复时,从上次的位置在继续消费。kafka只能保证局部有序,不能保证全局有序。
1、kafka的文件存储机制
由于生产者生产的消息会不断的追加到log末尾,为了防止log文件过大导致数据定位效率低下,kafka采用了分片和索引的机制,将每个partition分为多个segment。每个segment对应两个文件–".index"文件和".log"文件。这些文件位于一个文件夹下,该文件夹的命名规则为:topic名称+分区序号。例如:first这个主题有三个分区,则其对应的文件夹为first-0,first-1,first-2
2、生产者的分区策略
分区的原因:
(1)方便在集群中扩展,每个partition可以通过调整以适应它所在的机器,而每一个topic又可以由多个partition组成,因此整个集群就可以适应任意大小的数据了
(2)可以提高并发,因为可以以partition为单位读写
分区的原则:
(1)指明partition的情况下,直接将指明的值作为partition值
(2)没有指明partition值但是由key的情况下,将key的hash值与topic的partition数进行取余得到partition值
(3)既没有partition也没有key的情况,第一次调用会随机生成一个随机整数(后面每次调用在这个整数上自增),将这个值与topic的可用partitino总数取余得到partition值,也就是常说的round-robin(轮询)算法
3、数据可靠性保证
为了保证producer发送的数据能可靠的发送到指定的topic,topic的每个partition收到producer发送的数据后都需要向producer发送ack(acknowledgement确认收到),如果producer收到ack,就会进行下一轮的发送,否则重新发送数据。
kafka选择了第二种方案,原因如下:
1、同样为了容忍n台节点的故障,第一种方案需要2n+1个副本,而第二种方案中只需要n+1个副本,而kafka的每个分区都有大量的数据,第一种方案会造成大量数据冗余。
2、虽然第二种方案延迟比较高,但是网络延迟对于kafka来说影响较小
3、ISR
采用第二种方案之后,设想一下场景,leader收到数据之后,所有的follower都开始同步数据,但是有一个follower因为某种原因迟迟不能与leader进行同步,那leader就要一直等下去,直到它完成同步,才能发送ack,这个问题怎么解决呢?
leader维护了一个动态的in-sync-replica set(ISR),意为和leader保持同步的follower集合。当ISR中的follower完成数据同步之后,leader就会给follower发送ack,如果follower长时间未向leader同步数据,则该follower将被踢出ISR,该阈值由replica.lag.time.max.ms参数设定,leader发生故障时,就会从follower中选举新的leader
4、ack应答机制
对于某些不太重要的数据,对数据的可靠性要求不是很高,能够容忍数据的少量丢失,所以没必要等ISR中的所有follower全部接收成功
所以kafka为用户提供了三种可靠性级别,用户根据可靠性和延迟的要求进行权衡,选择一下配置
acks参数配置
0:producer不等待broker的ack,这一操作提供了一个最低的延迟,broker一接收到还没有写入磁盘就已经返回,当borker故障时,可能丢失数据
1:producer等待broker的ack,partition的leader落盘成功后返回ack,如果在follower同步成功之前leader发生故障,那么将会丢失数据
-1(all):producer等待broker的ack,partition的leader和follower全部落盘成功后,才返回ack,但是如果在follower同步完成之后,broker发送ack之前,leader发生故障,会造成数据重复。
5、故障处理细节
LEO:指的是每个副本最大的offset
HW:指的是消费者能见到的最大的offset,ISR队列中最小的LEO
(1)follower故障
follower发生故障后会被临时踢出ISR,待该follower恢复后,follower会读取本地磁盘记录的上次HW,并将log文件高于HW的部分截取掉,从HW开始向leader进行同步。等待follower的LEO大于或等于该partition的HW,即follower追上leader之后,就可以重新加入ISR了。
(2)leader故障
leader发生故障之后,会从ISR中选出一个新的leader,之后,为了保证多个副本之间的数据一致性,其余的follower会先将各自的log文件高于HW的部分截掉,然后从新从leader中同步数据
注意 这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复
6、Exactly Once定义
将服务器的ACK级别设置为-1,可以保证Producer到Server之间的数据不会丢失,即At Least Once语义,相对的,将服务器ACK级别设置为0,可以保证生产者每条消息只会被发送一次,即At Most Once语义。
At Least Once可以保证数据不丢失,但是不能保证数据不重复;相对的,At Most Once可以保证数据不重复,但是不能保证数据不丢失。但是对于一些重要的消息,要求既不能丢失也不能重复发送,即Exactly Once语义。
0.11版本之前是做不到的,但是在0.11之后引入了幂等性。所谓的幂等性就是指Producer不论向Server发送多少数据,server只会持久化一条。幂等性结合At Least Once 语义就构成了Kafka的Exactly Once语义
即 At Least Once + 幂等性 = Eaxatly Once
要启用幂等性,只需要将Producer的参数中enable.idompotence设置为true即可。Kafka的幂等性实现其实就是将原来下游需要做的去重放在了数据上游。开启幂等性的Producer在初始化的时候会分配一个PID,发送同一Partition的消息会附带Sequence Number。而Broker端会对<PID,Partition,Sequence NUmber>做缓存,当具有相同主键的消息提交时,Broker只会持久化一条。
但是PID重启就会变化,同时不同的Partition也具有不同的主键,所以幂等性无法保证在跨分区会话的Exactly Once。
4、kafka的消费者
消费方式:consumer采用pull(拉)模式从Broker中读取数据
push(推)模式很难适应消费速率不同的消费者,因为消息发送速率是由broker决定的
他的目标是尽可能以最快的速度传递消息,但是这样很容易造成consumer来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而pull模式则会根据consumer的消费能力以适应的速度消费消息
pull模式的不足之处是,如果kafka没有数据,消费者可能会陷入循环中,一直返回空数据。针对这一点,kafka的消费者在消费数据时会传入一个时长参数timeout,如果当前没有数据可供消费,consumer会等待一段时间之后再返回,这段时长即为timeout
分区策略
一个consumer group中有多个consumer,一个topic有多个partition,所以必然会涉及到partition的分配问题,即确定哪个partition由哪个consumer来消费
kafka由两种分配策略,一个是RoundRobin,另一个是Range(默认)
RoundRobin(轮询)是以consumer group划分的,即消费者组内的一个消费者订阅的Partition会被整个消费者组轮询
range(范围),kafka的消息分配策略默认就是这种范围的,range是以topic划分的,即订阅topic的同一消费者组是按范围划分partition的消息,这个组内没有订阅这个topic的不会分消息,其它组也是按同样的道理划分消息,组之间互不干扰。
5、kafka高效读写数据
1、顺序写磁盘,Kafka的producer生产数据,要写入到log文件中,写的过程是一直追加到文件末端,为顺序写。官网有数据表明,同样的磁盘,顺序写能写到600M/s,而随机写只有100M/s。这与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间。
2、零复制技术,零拷贝
6、kafka事务
Producer事务
为了实现跨分区跨会话的事务,需要引入一个全局唯一的TransactionID,并将Producer获得的pid和TransactionID绑定,这样当producer重启之后就可以通过正在进行的TransactionID获得原来的PID。
为了管理TransactionID,kafka引入了一个新的组件Transaction Coordinator。producer就是通过和Transaction Coordinator 交互获得TransactionID对应的任务状态,Transaction Coordinator还负责将事务所有写入Kafka的一个内部Topic,这样即使整个服务重启,由于事务状态得到保存,进行中的事务状态可以得到恢复,从而继续进行。
Consumer事务
上述事务机制主要是从Producer方面考虑,对于Consumer而言,事务的保证就会相对较弱,尤其是无法保证commit的信息被精确消费。这是由于consumer可以通过offset访问任何信息,而且不同的segment file声明周期不同,同一事务的消息可能会出现重启之后被删除的情况。
4、kafka API
kafka的producer发送消息采用的是异步发送的方式。在消息发送的过程中,涉及到了两个线程,main线程和sender线程,以及一个线程共享变量–RecordAccumulator。main线程将消息发送给RecordAccumulator,sender线程不断从RecordAccumulator中拉取消息发送到kafka broker。
相关参数:
batch.size:只有数据积累到batch.size之后,sender才会发送数据
linger.ms:如果数据迟迟未达到batch.size,sender等待linger.time之后就会发送数据。
导入依赖:
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.11.0.0</version>
</dependency>
ProviderAPI
需要用到的类
KafkaProducer:需要创建一个生产者对象,用来发送数据
ProducerConfig:获取所需的一系列配置参数
ProducerRecord:每条数据都要封装成一个ProducerRecord对象
不带回调的API(异步)
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
public class CustomProducer {
public static void main(String[] args) throws ExecutionException,InterruptedException {
Properties props = new Properties();
//kafka 集群,broker-list
props.put("bootstrap.servers", "hadoop102:9092");
props.put("acks", "all");
//重试次数
props.put("retries", 1);
//批次大小
props.put("batch.size", 16384);
//等待时间
props.put("linger.ms", 1);
//RecordAccumulator 缓冲区大小
props.put("buffer.memory", 33554432);
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
Producer<String,String> producer=new KafkaProducer<>(props);
for (int i = 0; i < 100; i++) {
producer.send(new ProducerRecord<String, String>("first", Integer.toString(i), Integer.toString(i)));
}
producer.close();
}
}
带回调的API(异步)
回调函数会在 producer 收到 ack 时调用,为异步调用,该方法有两个参数,分别是 RecordMetadata 和 Exception,如果 Exception 为 null,说明消息发送成功,如果 Exception 不为 null,说明消息发送失败。
注意:消息发送失败会自动重试,不需要我们在回调函数中手动重试。
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
public class CustomProducer {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Properties props = new Properties();
props.put("bootstrap.servers", "hadoop102:9092");//kafka 集 群,broker-list
props.put("acks", "all");
props.put("retries", 1);//重试次数
props.put("batch.size", 16384);//批次大小
props.put("linger.ms", 1);//等待时间
props.put("buffer.memory", 33554432);//RecordAccumulator 缓冲区大小
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
Producer<String,String> producer=new KafkaProducer<>(props);
for (int i = 0; i < 100; i++) {
producer.send(new ProducerRecord<String, String>("first", Integer.toString(i), Integer.toString(i)), new Callback() {
//回调函数,该方法会在 Producer 收到 ack 时调用,为异步调用
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception == null) { System.out.println("success->" +
metadata.offset());
} else {
exception.printStackTrace();
}
}
});
}
producer.close();
}
}
同步发送API
同步发送的意思就是,一条消息发送之后,会阻塞当前线程,直至返回 ack。
由于 send 方法返回的是一个 Future 对象,根据 Futrue 对象的特点,我们也可以实现同步发送的效果,只需在调用 Future 对象的 get 方发即可
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
public class CustomProducer {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Properties props = new Properties();
props.put("bootstrap.servers", "hadoop102:9092");//kafka 集 群,broker-list
props.put("acks", "all");
props.put("retries", 1);//重试次数
props.put("batch.size", 16384);//批次大小
props.put("linger.ms", 1);//等待时间
props.put("buffer.memory", 33554432);//RecordAccumulator 缓 冲区大小
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
Producer<String,String> producer=new KafkaProducer<>(props);
for (int i = 0; i < 100; i++) {
producer.send(new ProducerRecord<String, String>("first", Integer.toString(i), Integer.toString(i))).get();
}
producer.close();
}
}
ConsumerAPI
Consumer消费数据时的可靠性是很容易保证的,因为数据在kafka中是持久化的,故不用担心数据丢失的问题。
由于consumer在消费过程中可能会出现断电宕机等故障,conusmer恢复后,需要从故障前的位置继续消费,所以consumer需要实时记录自己消费到哪个offset了,以便故障恢复后继续消费,所以offset的维护是consumer消费数据是必须考虑的问题。
导入依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.11.0.0</version>
</dependency>
要用到的类:
KafkaConsumer:需要创建一个消费者对象,用来消费数据
ConsumerConfig:获取所需的一系列配置参数
ConsumerRecord:每条数据都要封装成一个ConsumerRecord对象
为了使我们能够专注于自己的业务逻辑,kafka提供了自动提交offset的功能。
自动提交offset的相关参数:
enable.auto.commit:是否开启自动提交功能
auto.commit.interval.ms:自动提交offset的时间间隔
以下为自动提交的代码
import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; import org.apache.kafka.clients.consumer.KafkaConsumer;
import java.util.Arrays; import java.util.Properties;
public class CustomConsumer {
public static void main(String[] args) {
Properties props = new Properties();
props.put("bootstrap.servers", "hadoop102:9092");
props.put("group.id", "test");
props.put("enable.auto.commit", "true"); // 开启自动提交
props.put("auto.commit.interval.ms", "1000");
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("first"));
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());
}
}
}
}
手动提交offset
虽然自动提交offset十分简单便利,但由于其是基于时间提交的,开发人员难以把握offset提交的时机。因此kafka还提供了手动提交offset的API
手动提交offset的方法有两种,分别是commitSync(同步提交)和commitAsync(异步提交)两者的相同点都会将本次poll的一批数据最高的偏移量提交;不同点是,commitsync阻塞当前线程,一直到提交成功,并且会自动失败重试(由于不可控因素导致,也会出现提交失败),而commitAsync则没有失败重试机制,故有可能提交失败。
1、同步提交offset
由于同步提交offset有失败重试机制,故而更加可靠,以下为同步提交offset的示例
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import java.util.Arrays;
import java.util.Properties;
public class CustomComsumer {
public static void main(String[] args) {
Properties props = new Properties();
//Kafka 集群
props.put("bootstrap.servers", "hadoop102:9092");
//消费者组,只要 group.id 相同,就属于同一个消费者组
props.put("group.id", "test");
props.put("enable.auto.commit", "false");//关闭自动提交 offset
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("first")); //消费者订阅主题
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());
}
//同步提交,当前线程会阻塞直到 offset 提交成功
consumer.commitSync();
}
}
}
异步提交offset
虽然同步提交offset更可靠一些,但是由于其会阻塞当前线程,直到其提交成功,因此吞吐量会受到很大的影响,因此在很多情况下,会选择用异步提交的方式
以下为异步提交的示例
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import java.util.Arrays;
import java.util.Properties;
public class CustomComsumer {
public static void main(String[] args) {
Properties props = new Properties();
//Kafka 集群
props.put("bootstrap.servers", "hadoop102:9092");
//消费者组,只要 group.id 相同,就属于同一个消费者组
props.put("group.id", "test");
props.put("enable.auto.commit", "false");//关闭自动提交 offset
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("first")); //消费者订阅主题
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());
}
//异步提交
consumer.commitAsync(new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
if (exception != null) {
System.err.println("Commit failed for"+offsets);
}
}
});
}
}
}
数据漏消费和重复消费分析
无论是同步提交还是异步提交offset,都有可能造成数据的漏消费或者重复消费。先提交offset后消费,有可能造成数据的漏消费,而先消费后提交offset有可能造成数据的重复消费。
3、自定义存储offset
kafka0.9版本之前,offset存储在zookeeper,0.9之后默认将offset存储在kafka的内置的一个topic中,除此之外,kafka还可以选择自定义存储offset
offset的维护是相当频繁的,因为要考虑到消费者的offset
当有新的消费者加入消费者组,已有的消费者退出消费者组或者所订阅的主题发生变化,就会触发到分区的重新分配,重新分配的过程叫做Rebalace。
消费者发生Rebalance之后,每个消费者消费的分区就会发生变化。因此消费者首先要获取到自己被重新分配的分区,并且定位到每个分区最近提交的offset位置继续消费
要实现自定义存储的offset,需要借助ConsumerrebalanceListener,以下为示例代码,其中的提交和获取offset方法需要根据所选择的offset存储系统自行实现
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import java.util.*;
public class CustomConsumer {
private static Map<TopicPartition, Long> currentOffset = new HashMap<>();
public static void main(String[] args) {
//创建配置信息
Properties props = new Properties();
//Kafka 集群
props.put("bootstrap.servers", "hadoop102:9092");
//消费者组,只要 group.id 相同,就属于同一个消费者组
props.put("group.id", "test");
//关闭自动提交 offset
props.put("enable.auto.commit", "false");
//Key 和 Value 的反序列化类
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("first"), 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) {
consumer.seek(partition, getOffset(partition));//定位到最近提交的 offset 位置继续消费
}
}
});
while (true) {
ConsumerRecords<StringString> 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());
currentOffset.put(new TopicPartition(record.topic(),
record.partition()), record.offset());
}
commitOffset(currentOffset);//异步提交
}
}
//获取某分区的最新 offset
private static long getOffset(TopicPartition partition) {
return 0;
}
//提交该消费者所有分区的 offset
private static void commitOffset(Map<TopicPartition,Long> currentOffset) {
}
}
5、自定义拦截器
producer拦截器(interceptor)是在kafka0.10版本引入的,主要用于实现clients端的定制化控制逻辑
对于producer而言,interceptor使得用户在消息发送前以及producer回调逻辑前有机会对消息做一些定制化需求,比如修改消息等。同时,producer允许用户指定多个interceptor按顺序作用于同一条消息从而形成一个拦截链(interceptor chain)。interceptor的实现接口是org.apache.kafka.clients.producer.ProducerInterceptor,其定义的方法包括:
(1)configure(config)获取配置信息和初始化数据时调用
(2)onSend(ProduceRecord)该方法封装进KafkaProducer.send()方法中,即它运行在用户主线程中。Producer确保在消息被序列化以及计算分区前调用该方法。用户可以在该方法中对消息做任何操作,但最好保证不要修改消息所属的topic和分区,否则会影响目标分区的计算
(3)onAcknowledgement(RecordMetadata,Exception)该方法会在消息从RecordAccumulator成功发送到kafka broker之后,或者在发送过程中失败时调用。并且通常都是在producer回调逻辑触发之前。onAcknowledgement运行在producer的io线程中,因此不要在该方法中放入很重要的逻辑,否则会拖慢producer的消息发送效率
(4)close()关闭interceptor,主要用于执行一些资源的清理工作
如前所述,interceptor可能被运行在多个线程中,因此在具体实现时用户需要自行确保线程安全,另外倘若指定了多个interceptor,则producer将按照指定的顺序调用他们,不仅仅是捕获每个interceptor可能抛出的异常记录到错误日志中而非在向上传递,这在使用过程中要特备留意
案例:
需求:
实现一个简单的双 interceptor 组成的拦截链。第一个 interceptor 会在消息发送前将时间 戳信息加到消息 value 的最前部;第二个 interceptor 会在消息发送后更新成功发送消息数或 失败发送消息数。
(1)增加时间戳拦截器
import java.util.Map;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
public class TimeInterceptor implements ProducerInterceptor<String,String>{
@Override
public void configure(Map<String, ?> configs) {
}
@Override
public ProducerRecord<String,String> onSend (ProducerRecord<String, String> record) {
// 创建一个新的 record,把时间戳写入消息体的最前部
return new ProducerRecord(record.topic(), record.partition(), record.timestamp(), record.key(),System.currentTimeMillis()+","+record.value().toString());
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
}
@Override
public void close() {
}
(2)统计发送消息成功和发送失败消息数,并在 producer 关闭时打印这两个计数器
import java.util.Map;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
public class CounterInterceptor implements ProducerInterceptor<String,String>{
private int errorCounter = 0;
private int successCounter = 0;
@Override
public void configure(Map<String, ?> configs) {
}
@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("Successful sent: " + successCounter);
System.out.println("Failed sent: " + errorCounter);
}
}
(3)producer 主程序
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
public class InterceptorProducer {
public static void main(String[] args) throws Exception {
// 1 设置配置信息
Properties props = new Properties();
props.put("bootstrap.servers", "hadoop102:9092");
props.put("acks", "all");
props.put("retries", 3);
props.put("batch.size", 16384);
props.put("linger.ms", 1);
props.put("buffer.memory", 33554432);
props.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
// 2 构建拦截链
List<String> interceptors = new ArrayList<>();
interceptors.add("com.atguigu.kafka.interceptor.TimeInterceptor");
interceptors.add("com.atguigu.kafka.interceptor.CounterInterceptor");
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);
String topic = "first";
Producer<String,String> producer = new KafkaProduce<>(props);
// 3 发送消息
for (int i = 0; i < 10; i++) {
ProducerRecord<String,String> record = new ProducerRecord<>(topic,"message"+i);
producer.send(record);
}
// 4 一定要关闭 producer,这样才会调用 interceptor 的 close 方法
producer.close();
}
}