持续创作,加速成长!这是我参与「掘金日新计划 · 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发送给CoordinatorCoordinator会把分区方案发给各个Consumer