这里写目录标题
1.Kafka消费者
1.1 kafka消费方式
消息队列的两种消费方式
- pull(拉)模式
consumer采用从broker中主动拉取数据。Kafka采用这种方式 - push(推)模式
broker主动将消息推给consumer
一、为什么Kafka没有采用push模式?
因为在push模式下, broker决定消息的发送速率, 很难使用所有消费者的消费速率,例如推送的速度是50m/s,Consumer1、Consumer2就来不及处理消息。
二、pull的不足之处
pull模式不足之处是,如果Kafka没有数据,消费者可能会陷入循环中,一直返回空数据。
1.2 Kafka消费者工作流程
1.2.1 消费者总体工作流程
生产者工作流程:
- 生产者向每一个分区的leader发送消息(一批一批发送, 触发Deque阈值)
- follower主动跟leader进行数据同步, 保证数据的可靠性
一个消费者可以消费多个分区的数据, 例如consumer可以消费topicA下的partition0、partition1、partition2
1.2.2 消费者组原理
消费者组(consumer group), 简称CG, 由多个消费者构成
一、消费者组形成的条件
形成一个消费者组的条件,是所有消费者的groupid相同。
- 消费者组中每个消费者负责消费不同分区的数据, 一个分区只能由一个组内的消费者消费, 例如有两个消费者, 但是有4个分区, 那么两个消费者会分别消费其中两个分区
- 消费者组之间互不影响, 所有的消费者都属于某个消费者组, 消费者组是逻辑上的一个订阅者
如果向消费组中添加更多的消费者, 超过主题分区数量(组中消费者数量 > 主题分区数), 则有一部分消费者会闲置, 不会接收任何消息
消费者组之间互不影响, 所有的消费者都属于某个消费者组, 即消费者组是逻辑上的一个订阅者
1.2.3 消费者组初始化流程
一、coordinator
辅助实现消费者组的初始化和分区的分配
coordinator节点选择 = groupid的hashcode值 % 50( __consumer_offsets的分区数量)
例如: groupid的hashcode值 = 1,1% 50 = 1,那么__consumer_offsets 主题的1号分区,在哪个broker上,就选择这个节点的coordinator作为这个消费者组的老大。消费者组下的所有的消费者提交offset的时候就往这个分区去提交offset。
- kafka集群根据consumer组的groupId计算出选择哪个broker的coordinator作为消费者组初始化以及分区分配的协调者
- 组中所有消费者都会主动向coordinator发送joinGroup请求. 然后从所有消费者中选出一个leader
- 将要消费的topic的情况发送给leader消费者
- leader消费者负责制定消费策略
- 将消费策略发送给coordinator
- coordinator把消费方案下发给所有consumer
- 每个消费者都会和coordinator保持心跳(
默认3s
), 一旦超时(session.timeout.ms=45s
),该消费者会被移除,并触发再平衡;或者消费者处理消息的时间过长(max.poll.interval.ms5分钟
),也会触发再平衡
1.2.4 消费者组详细消费流程
1.3 消费者 API
1.3.1 独立消费者案例(订阅主题)
创建一个独立消费者, 消费First主题中的数据
注意点:在消费者API中必须配置消费组id(groupId), 命令行启动消费者时不填写消费者id, 会自动填写随机的消费组id
一、通过java实现consumer
public class CustomConsumer {
public static void main(String[] args) {
// 1.创建消费者的配置对象
Properties properties = new Properties();
// 2.给消费者配置对象添加参数
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.202.130:9092");
// 3.配置序列化(必须)
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
// 4.配置消费者组(组名任意起名) 必须
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
// 5.创建消费者对象
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(properties);
// 6.注册要消费的主题(可以消费多个主题)
ArrayList<String> topics = new ArrayList<>();
topics.add("first");
kafkaConsumer.subscribe(topics);
// 拉取数据打印
while (true) {
// 设置 1s 中消费一批数据
ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
// 打印消费到的数据
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.println(consumerRecord);
}
}
}
}
二、在Kafka集群控制台,创建Kafka生产者,并输入数据
bin/kafka-console-producer.sh --bootstrap-server 192.168.202.130:9092 --topic first
> hello
三、在IDEA控制台观察收到的数据
ConsumerRecord(topic = first, partition = 1, leaderEpoch = 6, offset = 36, CreateTime = 1679573070488, serialized key size = -1, serialized value size = 5, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = hello)
1.3.2 独立消费者案例(订阅分区)
创建一个独立消费者,消费 first 主题 0 号分区的数据。
一、通过java实现consumer
public class CustomConsumerPartition {
public static void main(String[] args) {
// 1.创建消费者的配置对象
Properties properties = new Properties();
// 2.给消费者配置对象添加参数
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.202.130:9092");
// 3.配置序列化(必须)
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
// 4.配置消费者组(必须), 名字可以任意起
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties);
// 消费某个主题的某个分区数据
ArrayList<TopicPartition> topicPartitions = new ArrayList<>();
topicPartitions.add(new TopicPartition("first", 0));
kafkaConsumer.assign(topicPartitions);
while (true) {
ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.println(consumerRecord);
}
}
}
}
二、在 IDEA 中执行生产者程序 CustomProducerCallback()在控制台观察生成几个 0 号分区的数据。
first 0 381
first 0 382
first 2 168
first 1 165
first 1 166
三、在 IDEA 控制台,观察接收到的数据,只能消费到 0 号分区数据表示正确
ConsumerRecord(topic = first, partition = 0, leaderEpoch = 14, offset = 381, CreateTime = 1636791331386, serialized key size = -1, serialized value size = 9, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = atguigu 0)
ConsumerRecord(topic = first, partition = 0, leaderEpoch = 14, offset = 382, CreateTime = 1636791331397, serialized key size = -1, serialized value size = 9, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = atguigu 1)
1.3.3 消费者组案例
测试同一个主题的分区数据,只能由一个消费者组中的一个消费。
一、复制一份CustomConsumer, 在IDEA中同时启动
两个消费者的groupId需要一致
二、启动代码中的生产者发送消息
主题:first->分区:2
主题:first->分区:2
主题:first->分区:1
主题:first->分区:1
主题:first->分区:2
三、在 IDEA 控制台即可看到两个消费者在消费不同分区的数据
1.CustomConsumer1
ConsumerRecord(topic = first, partition = 1, leaderEpoch = 6, offset = 48, CreateTime = 1679574836068, serialized key size = -1, serialized value size = 9, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = atguigu 2)
ConsumerRecord(topic = first, partition = 1, leaderEpoch = 6, offset = 49, CreateTime = 1679574836072, serialized key size = -1, serialized value size = 9, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = atguigu 3)
2.CustomConsumer2
ConsumerRecord(topic = first, partition = 2, leaderEpoch = 8, offset = 22, CreateTime = 1679574836054, serialized key size = -1, serialized value size = 9, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = atguigu 0)
ConsumerRecord(topic = first, partition = 2, leaderEpoch = 8, offset = 23, CreateTime = 1679574836065, serialized key size = -1, serialized value size = 9, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = atguigu 1)
ConsumerRecord(topic = first, partition = 2, leaderEpoch = 8, offset = 24, CreateTime = 1679574836076, serialized key size = -1, serialized value size = 9, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = atguigu 4)
1.4 生产经验—分区的分配以及再平衡
- 一个consumer group中有多个consumer组成,一个 topic有多个partition组成,现在的问题是,到底由哪个consumer来消费哪个partition的数据。
- Kafka有四种主流的分区分配策略
- Range
- RoundRobin
- Sticky
- CooperativeSticky
可以通过配置参数partition.assignment.strategy
,修改分区的分配策略。默认策略是Range + CooperativeSticky
。Kafka可以同时使用多个分区分配策略。
1.4.1 Range 以及再平衡(todo)
Range是对每个topic而言的
-
首先对同一个 topic 里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序。
假如现在有 7 个分区,3 个消费者,排序后的分区将会是0,1,2,3,4,5,6;消费者排序完之后将会是C0,C1,C2。 -
通过 partitions数/consumer数 来决定每个消费者应该消费几个分区。如果除不尽,那么前面几个消费者将会多消费 1 个分区。
例如,7/3 = 2 余 1 ,除不尽,那么 消费者 C0 便会多消费 1 个分区。 8/3=2余2,除不尽,那么C0和C1分别多消费一个。
注意:如果只是针对 1 个 topic 而言,C0消费者多消费1个分区影响不是很大。但是如果有 N 多个 topic,那么针对每个 topic,消费者 C0都将多消费 1 个分区,topic越多,C0消费的分区会比其他消费者明显多消费 N 个分区。
容易产生数据倾斜!