持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第17天,点击查看活动详情
消费者只从
Leader
分区批量拉取消息。
为了提高消费速度,多个消费者并行消费比不可少。
Kafka
允许创建消费组(唯一标识 group.id
),在同一个消费组的消费者共同消费数据。
举个栗子:
- 有两个
Kafka Broker
,即有 2个机子 - 有一个主题:
TOPICA
,有 3 个分区(0, 1, 2)
如上图,举例 4 中情况:
-
group.id = 1
,有一个消费者:这个消费者要处理所有数据,即 3 个分区的数据。 -
group.id = 2
,有两个消费者:consumer 1
消费者需处理 2个分区的数据,consumer2
消费者需处理 1个分区的数据 -
group.id = 3
,有三个消费者:消费者数量与分区数量相等,刚好每个消费者处理一个分区 -
group.id = 4
,有四个消费者:消费者数量 > 分区数量,第四个消费者则会处于空闲状态
(1)offset
位移提交
Consumer
需要向Kafka
汇报自己的位移数据,这个汇报过程被称为提交位移(Committing Offsets
)。
每个消费组都会存储 offset
,提交位移方式有两种:
-
自动提交
enable.auto.commit = true
:Consumer
在后台默默地为你定期提交位移,提交间隔由一个专属的参数auto.commit.interval.ms
来控制。存在问题:只要
consumer
一直启动设置,就会无期限地向主题写入消息。 -
手动提交
enable.auto.commit = false
开启自动提交,提交间隔给 2s:
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "test");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "2000");
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("foo", "bar"));
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());
}
手动提交位移 API
:enable.auto.commit = false
-
同步提交位移:
Consumer#commitSync()
-
异步提交位移:
Consumer#commitAsync()
-
更精细化位移管理:调用
commitSync(Map<TopicPartition, OffsetAndMetadata>)
和commitAsync(Map<TopicPartition, OffsetAndMetadata>)
1)自动提交 offset
导致消息丢失和重复消费问题
**消息丢失问题:**时间到自动提交了 offset
,但上批数据还没处理完就宕机了
poll
了一批数据:offset = 65510 ~ 65532
- 时间到了,自动提交了
offset = 65532
给Broker
- 这时候,消费者宕机了,实际数据还没处理完
- 下次重启时候,从
offset = 65533
位置开始消费
重复消费问题:poll
的消息处理完了,但还没提交 offset
就宕机了
poll
了一批数据:offset = 65510 ~ 65532
- 消费者很快处理完了
- 还没有提交
offset
,消费者就宕机了 - 下次重启时候,又从
offset = 65510
开始消费
2)CommitFailedException
异常处理
CommitFailedException
:Consumer
客户端在提交位移时出现了错误或异常,而且还是那种不可恢复的严重异常。
产生此异常的场景有二:
-
场景一:应用中设置相同
group.id
值的消费者组程序 和 独立消费者程序,当独立消费者程序手动提交位移时,Kafka
就会立即抛出CommitFailedException
异常因为
Kafka
无法识别这个具有相同group.id
的消费者实例,于是就向它返回一个错误,表明它不是消费者组内合法的成员。 -
场景二:当消息处理的总时间超过预设的
max.poll.interval.ms
参数值(默认值:5分钟)
举个栗子:
// 两次 poll 操作间隔为 5秒,处理一批数据需要 6秒
…
Properties props = new Properties();
…
// 两次poll操作间隔超过了这个时间,broker就会认为这个consumer处理能力太弱,
// 会将其踢出消费组,将分区分配给别的consumer消费 ,触发rebalance
props.put("max.poll.interval.ms", 5000);
consumer.subscribe(Arrays.asList("test-topic"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
// 使用Thread.sleep模拟真实的消息处理逻辑
Thread.sleep(6000L);
consumer.commitSync();
}
防止这种场景下抛出异常,有 4种方法:
- 缩短单条消息处理的时间
- 增加
Consumer
端允许下游系统消费一批消息的最大时长 - 减少下游系统一次性消费的消息总数
- 下游系统使用多线程来加速消费
(2)Coordinator
每个
Consumer group
都会选择一个Broker
作为自己的Coordinator
:负责监控这个消费组里的各个消费者的心跳,以及判断是否宕机,然后开启Rebalance
的。
Coordinator
如何进行选择呢?
根据
group.id
来进行选择,内部有一个选择机制,会挑选一个对应的Broker
,总会把各个消费组均匀分配给各个Broker
作为coordinator
来进行管理的。
-
对
group.id
进行hash
,得到数字 -
对
_consumer_offsets
的分区数量(默认 50)进行取模可以通过
offsets.topic.num.partition
来设置 -
找到这个
consumer group
的offset
要提交到_consumer_offsets
的分区 -
分区对应的
Leader
所在的Broker
,就是这个consumer group
的Coordinator
-
接着会维护一个
Socket
连接跟Broker
进行通信
Coordinator
如何与 Consumer Leader
制定分区方法?
- 每个
Consumer
都会发送joinGroup
请求到Coordinator
Coordinator
从Consumer Group
中选择一个Consumer
作为Leader
,并发送Consumer Group
情况Leader
会指定分区方案,通过SyncGroup
发送给Coordinator
Coordinator
会把分区方案发给各个Consumer