【Kafka】消费者

148 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第17天,点击查看活动详情

消费者只从 Leader分区批量拉取消息。

为了提高消费速度,多个消费者并行消费比不可少。 Kafka 允许创建消费组(唯一标识 group.id),在同一个消费组的消费者共同消费数据。

举个栗子:

  • 有两个 Kafka Broker,即有 2个机子
  • 有一个主题:TOPICA,有 3 个分区(0, 1, 2)

2022-06-1212-40-07.png

如上图,举例 4 中情况:

  1. group.id = 1,有一个消费者:这个消费者要处理所有数据,即 3 个分区的数据。

  2. group.id = 2,有两个消费者:consumer 1消费者需处理 2个分区的数据,consumer2 消费者需处理 1个分区的数据

  3. group.id = 3,有三个消费者:消费者数量与分区数量相等,刚好每个消费者处理一个分区

  4. group.id = 4,有四个消费者:消费者数量 > 分区数量,第四个消费者则会处于空闲状态

(1)offset 位移提交

Consumer 需要向 Kafka 汇报自己的位移数据,这个汇报过程被称为提交位移(Committing Offsets)。

每个消费组都会存储 offset,提交位移方式有两种:

  1. 自动提交 enable.auto.commit = trueConsumer 在后台默默地为你定期提交位移,提交间隔由一个专属的参数 auto.commit.interval.ms 来控制。

    存在问题:只要 consumer 一直启动设置,就会无期限地向主题写入消息。

  2. 手动提交 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());
}

手动提交位移 APIenable.auto.commit = false

  • 同步提交位移:Consumer#commitSync()

  • 异步提交位移:Consumer#commitAsync()

  • 更精细化位移管理:调用 commitSync(Map<TopicPartition, OffsetAndMetadata>)commitAsync(Map<TopicPartition, OffsetAndMetadata>)

1)自动提交 offset 导致消息丢失和重复消费问题

**消息丢失问题:**时间到自动提交了 offset,但上批数据还没处理完就宕机了

  1. poll 了一批数据:offset = 65510 ~ 65532
  2. 时间到了,自动提交了 offset = 65532Broker
  3. 这时候,消费者宕机了,实际数据还没处理完
  4. 下次重启时候,从 offset = 65533 位置开始消费

重复消费问题:poll 的消息处理完了,但还没提交 offset就宕机了

  1. poll 了一批数据:offset = 65510 ~ 65532
  2. 消费者很快处理完了
  3. 还没有提交 offset,消费者就宕机了
  4. 下次重启时候,又从 offset = 65510 开始消费

2)CommitFailedException 异常处理

CommitFailedExceptionConsumer 客户端在提交位移时出现了错误或异常,而且还是那种不可恢复的严重异常。

产生此异常的场景有二:

  1. 场景一:应用中设置相同 group.id 值的消费者组程序 和 独立消费者程序,当独立消费者程序手动提交位移时,Kafka 就会立即抛出 CommitFailedException 异常

    因为 Kafka 无法识别这个具有相同 group.id 的消费者实例,于是就向它返回一个错误,表明它不是消费者组内合法的成员。

  2. 场景二:当消息处理的总时间超过预设的 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种方法:

  1. 缩短单条消息处理的时间
  2. 增加 Consumer 端允许下游系统消费一批消息的最大时长
  3. 减少下游系统一次性消费的消息总数
  4. 下游系统使用多线程来加速消费

(2)Coordinator

每个 Consumer group 都会选择一个 Broker 作为自己的 Coordinator:负责监控这个消费组里的各个消费者的心跳,以及判断是否宕机,然后开启 Rebalance 的。

Coordinator 如何进行选择呢?

根据 group.id 来进行选择,内部有一个选择机制,会挑选一个对应的 Broker,总会把各个消费组均匀分配给各个 Broker 作为 coordinator 来进行管理的。

  1. group.id 进行 hash,得到数字

  2. _consumer_offsets 的分区数量(默认 50)进行取模

    可以通过 offsets.topic.num.partition 来设置

  3. 找到这个 consumer groupoffset 要提交到 _consumer_offsets 的分区

  4. 分区对应的 Leader 所在的 Broker,就是这个 consumer groupCoordinator

  5. 接着会维护一个 Socket 连接跟 Broker 进行通信

Coordinator 如何与 Consumer Leader 制定分区方法?

  1. 每个 Consumer 都会发送 joinGroup 请求到 Coordinator
  2. CoordinatorConsumer Group 中选择一个 Consumer 作为 Leader ,并发送 Consumer Group 情况
  3. Leader 会指定分区方案,通过 SyncGroup 发送给 Coordinator
  4. Coordinator 会把分区方案发给各个 Consumer