kafka消费者
kafka消费方式
kafka采用pull(拉)模式,consumer从broker中拉取数据
pull模式的不足:如果Kafka没有数据,消费者可能会陷入循环中,一直返回空数据
kafka不采用push(推)模式,因为由broker决定消息发送速率,很难适应所有消费者的消费速率
Kafka 消费者工作流程
![](https://img-blog.csdnimg.cn/img_convert/a83c666e40044325b3eb77c18f00759f.png)
offset记录的是当前消费者消费到了哪一条数据,在系统主题中持久化存储可以使得消费者重新上线后继续消费
注意两点:
1.每个分区的数据只能由消费者组中一个消费者消费
2.一个消费者可以消费多个分区数据
消费者组
消费者组,由多个consumer组成。形成一个消费者组的条件,是所有消费者的groupid相同
消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费
消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者
如果向消费组中添加更多的消费者,超过主题分区数量,则有一部分消费者就会闲置,不会接收任何消息
消费者组初始化流程
![](https://img-blog.csdnimg.cn/img_convert/db16fa91c1e04585adbd41e89f258015.png)
消费者组的groupid是由用户在代码中手动创建的;
消费者组详细消费流程
![](https://img-blog.csdnimg.cn/img_convert/284cfcefbd584189bd61a54b63af418a.png)
相关参数:
Fetch.min.bytes:每批次最小抓取大小,默认1字节
fetch.max.wait.ms:一批数据最小值未达到的超时时间,默认500ms
Fetch.max.bytes:每批次最大抓取大小,默认50m
消费者重要参数
![](https://img-blog.csdnimg.cn/img_convert/7c5f8349965f47d5abcb85e5d9525431.png)
![](https://img-blog.csdnimg.cn/img_convert/3a1ca5b3cd26460788c3f354a7e59f70.png)
![](https://img-blog.csdnimg.cn/img_convert/ccf7f1b223f44400ae9cef5b92fff77f.png)
消费者 API
配置文件:
Properties properties = new Properties();
//消费者相关配置
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop102:9092,hadoop103:9092");
//配置序列化(必须)
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
//消费者组id(组名任意起名,必须)
properties.put(ConsumerConfig.GROUP_ID_CONFIG,"test");
独立消费者
订阅主题(topic)
subscribe方法订阅主题
//创建消费者对象
Consumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
//注册要消费的主题(可以同时消费多个主题)
ArrayList<String> topics = new ArrayList<>();
topics.add("first");
consumer.subscribe(topics);
//消费数据
while(true) //一直拉取
{
//设置1s消费一批数据
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
//打印数据
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.println(consumerRecord);
}
}
订阅分区(partition)
assign方法注册主题和分区
Consumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
//注册要消费的主题和分区
ArrayList<TopicPartition> topicPartition = new ArrayList<>();
topicPartition.add(new TopicPartition("first",0));
consumer.assign(topicPartition);
while(true) //一直拉取
{
//拉取数据
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
//打印数据
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.println(consumerRecord);
}
}
消费者组
同时启动两份消费者代码
然后启动生产者代码进行数据发送(如果只发送到一个分区,可以在发送时增加延迟代码 Thread.sleep(2);)
![](https://img-blog.csdnimg.cn/img_convert/a03ddc97eea248c696891248b2d11c21.png)
![](https://img-blog.csdnimg.cn/img_convert/e675b5a8c5c644acbfa88e853bd3fbca.png)
可以看到,消费者组中不同的消费者消费不同分区的数据
生产经验
分区的分配以及再平衡
分区分配策略:
一个consumer group中有多个consumer组成,一个 topic有多个partition组成,现在的问题是,到底由哪个consumer来消费哪个partition的数据
Kafka有四种主流的分区分配策略: Range、RoundRobin、Sticky、CooperativeSticky
可以通过配置参数partition.assignment.strategy,修改分区的分配策略。默认策略是Range + CooperativeSticky。Kafka可以同时使用多个分区分配策略
Range
![](https://img-blog.csdnimg.cn/img_convert/d478110cf3eb49cb8ceef22bb9d16d6e.png)
RoundRobin
![](https://img-blog.csdnimg.cn/img_convert/9e7a42194fae42c6aa89aa09985b80cb.png)
如何在代码中修改分区分配策略:
// 修改分区分配策略
properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, "org.apache.kafka.clients.consumer.RoundRobinAssignor");
Sticky
粘性分区:在执行一次新的分配之前,考虑上一次分配的结果,尽量少的调整分配的变动,可以节省大量的开销
粘性分区是 Kafka 从 0.11.x 版本开始引入这种分配策略,首先会尽量均衡的放置分区到消费者上面,在出现同一消费者组内消费者出现问题的时候,会尽量保持原有分配的分区不变化
举例:
1.初始消费者分配到的分区情况:
0 号消费者:消费到 0、1 号分区数据。
1 号消费者:消费到 2、5、3 号分区数据。
2 号消费者:消费到 4、6 号分区数据。
2.关闭0号消费者:
0 号消费者的任务会按照粘性规则,尽可能均衡的随机分成 0 和 1 号分区数据,分别由1号消费者或者2号消费者消费。
说明:0 号消费者挂掉后,消费者组需要按照超时时间 45s 来判断它是否退出,所以需要等待,时间到了 45s 后,判断它真的退出就会把任务分配给其他 broker 执行。
3.再次重新发送消息观看结果(45s 以后)
1 号消费者:消费到 2、3、5 号分区数据。
2 号消费者:消费到 0、1、4、6 号分区数据。
说明:消费者 0 已经被踢出消费者组,所以重新按照粘性方式分配
数据积压(消费者如何提高吞吐量)
1.如果是Kafka消费能力不足,则可以考虑增加Topic的分区数,并且同时提升消费组的消费者数量,消费者数 = 分区数。(两者缺一不可)
2.如果是下游的数据处理不及时:提高每批次拉取的数量。批次拉取数据过少(拉取数据/处理时间 < 生产速度),使处理的数据小于生产的数据,也会造成数据积压
相关参数:
fetch.max.bytes:消费者获取服务器端一批消息最大的字节数,默认是52428800(50 m);如果服务器端一批次的数据大于该值(50m)仍然可以拉取回来这批数据,因此,这不是一个绝对最大值。一批次的大小受 message.max.bytes (broker config)or max.message.bytes (topic config)影响
max.poll.records:一次 poll 拉取数据返回消息的最大条数,默认是 500 条
offset 位移
从0.9版本开始,consumer默认将offset保存在Kafka一个内置的topic中,该topic为__consumer_offsets
__consumer_offsets 主题里面采用 key 和 value 的方式存储数据。key 是 group.id+topic+分区号,value 就是当前 offset 的值。每隔一段时间,kafka 内部会对这个 topic 进行compact,也就是每个group.id+topic+分区号就保留最新数据
key的含义:记录某一个消费者组中的消费者消费某一个主题某一分区的数据的offset位置
查看系统主题数据
首先在配置文件 config/consumer.properties 中添加配置 exclude.internal.topics=false
默认是 true,表示不能消费系统主题。为了查看该系统主题数据,所以该参数修改为 false
指令:
bin/kafka-console-consumer.sh --topic __consumer_offsets --bootstrap-server hadoop102:9092 --consumer.config config/consumer.properties --formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter" --from-beginning
![](https://img-blog.csdnimg.cn/img_convert/0555dd0ec39d42cda0df792c795694f6.png)
自动提交offset
为了使我们能够专注于自己的业务逻辑,Kafka提供了自动向__consumer_offsets提交offset的功能
相关参数:
enable.auto.commit:是否开启自动提交offset功能,默认是true
auto.commit.interval.ms:自动提交offset的时间间隔,默认是5s
![](https://img-blog.csdnimg.cn/img_convert/0322deb5a6ac49699185e77968f2e003.png)
代码中进行设置:
// 是否自动提交 offset
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
// 提交 offset 的时间周期 1000ms,默认 5s
properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 1000);
手动提交 offset
虽然自动提交offset十分简单便利,但由于其是基于时间提交的,开发人员难以把握offset提交的时机。因此Kafka还提供了手动提交offset的API
方法:commitSync(同步提交)和commitAsync(异步提交)
相同点:都会将本次提交的一批数据最高的偏移量提交
不同点:同步提交阻塞当前线程,一直到提交成功,并且会自动失败重试(由不可控因素导致,也会出现提交失败);而异步提交则没有失败重试机制,故有可能提交失败。
commitSync(同步提交):必须等待offset提交完毕,再去消费下一批数据。
commitAsync(异步提交) :发送完提交offset请求后,就开始消费下一批数据了
相关代码:
// 设置自动提交为false
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
消费数据之后通过consumer.commitSync()方法同步提交offset
//消费数据
while (true){
// 读取消息
ConsumerRecords<String, String> consumerRecords =
consumer.poll(Duration.ofSeconds(1));
// 输出消息
for (ConsumerRecord<String, String> consumerRecord :
consumerRecords) {
System.out.println(consumerRecord.value());
}
// 同步提交 offset
consumer.commitSync();
}
异步提交和同步提交相同,只是把方法换成了consumer.commitAsync();