这系列是根据极客时间《Kafka核心技术与实战》这个课程做的笔记

本篇目录

  • 位移主题_consumer_offsets
  • 位移提交
  • CommitFailedException
  • 重设消费组位移

位移主题 _consumer_offsets

诞生背景

  1. 老版本的Kafka会把位移信息保存在Zk中,当Consumer重启后,自动从Zk中读取位移信息。这种设计使Kafka Broker不需要保存位移数据,可减少Broker端需要持有的状态空间,有利于实现高伸缩性。
  2. 但zk不适用于高频的写操作,这令zk集群性能严重下降,在新版本中将消费者的位移数据作为一条条普通的Kafka消息,提交至内部主题_consumer_offsets中保存。实现高持久性和高频写操作。

特点

  • 位移主题是一个普通主题,同样可以被手动创建,修改,删除。。
  • 位移主题的消息格式是kafka定义的,不可以被手动修改,若修改格式不正确,kafka将会崩溃。
  • 位移主题保存了三部分内容:Group ID,主题名,分区号。

创建:

  • 当Kafka集群中的第一个Consumer程序启动时,Kafka会自动创建位移主题。也可以手动创建
  • 分区数依赖于Broker端的offsets.topic.num.partitions的取值,默认为50
  • 副本数依赖于Broker端的offsets.topic.replication.factor的取值,默认为3

使用:

  • 当Kafka提交位移消息时会使用这个主题
  • 位移提交得分方式有两种:手动和自动提交位移。
    • Consumer 端有个参数叫 enable.auto.commit,如果值是 true,则 Consumer 在后台默默地为你定期提交位移,提交间隔由一个专属的参数 auto.commit.interval.ms 来控制。
    • 手动提交位移,即设置enable.auto.commit = false。
  • 推荐使用手动提交位移,自动提交位移会存在问题:只要 Consumer 一直启动着,它就会无限期地向位移主题写入消息。

清理:

  • Kafka使用Compact策略来删除位移主题中的过期消息,避免位移主题无限膨胀。
  • kafka提供专门的后台线程定期巡检待compcat的主题,查看是否存在满足条件的可删除数据。这个后台线程叫 Log Cleaner。

Kafka核心技术与实战 <四>_Kafka

注意事项:

  • 建议不要修改默认分区数,在kafka中有些许功能写死的是50个分区
  • 建议不要使用自动提交模式,采用手动提交,避免消费者无限制的写入消息。
  • 后台定期巡检线程叫Log Cleaner,若线上遇到位移主题无限膨胀占用过多磁盘,应该检查此线程的工作状态。

消费位移的提交

概念区分

  • Consumer端的位移概念和消息分区的位移概念不是一回事。
  • Consumer的消费位移,记录的是Consumer要消费的下一条消息的位移。

提交位移

Consumer 需要向 Kafka 汇报自己的位移数据,这个汇报过程被称为提交位移 (Committing Offsets)。因为 Consumer 能够同时消费多个分区的数据,所以位移的提 交实际上是在分区粒度上进行的,Consumer 需要为分配给它的每个分区提交各自的位移数据。

提交位移主要是为了表征Consumer的消费进度,这样当Consumer发生故障重启后,能够从kafka中读取之前提交的位移值,从相应的位置继续消费,避免从头在消费一遍。

位移提交方式

从用户的角度讲,位移提交分为自动提交和手动提交;
从Consumer端的角度而言,位移提交分为同步提交和异步提交。

  • 自动提交:由Kafka consumer在后台默默的执行提交位移,用户不用管。开启简单,使用方便,但可能会出现重复消费。

  • 手动提交:好处在更加灵活,完全能够把控位移提交的时机和频率。

    • 同步提交:在调用commitSync()时,Consumer程序会处于阻塞状态,直到远端Broker返回提交结果,这个状态才会结束。对TPS影响显著
    • 异步提交:在调用commitAsync()时,会立即给响应,但是出问题了它不会自动重试。
    • 手动提交最好是同步和异步结合使用,正常用异步提交,如果异步提交失败,用同步提交方式补偿提交。
try {            while (true) {
                        ConsumerRecords<String, String> records =
                                     consumer.poll(Duration.ofSeconds(1));
                        process(records); // 处理消息commitAysnc(); // 使用异步提交规避阻塞} 
} catch (Exception e) {
            handle(e); // 处理异常
 } finally {try {
                        consumer.commitSync(); // 最后一次提交使用同步阻塞式提交 } finally {
                          consumer.close(); 
            } 
}复制代码
  • 批次提交:对于一次要处理很多消费的Consumer而言,将一个大事务分割成若干个小事务分别提交。这可以有效减少错误恢复的时间,避免大批量的消息重新消费。
    • 使用commitSync(Map<TopicPartition,Offset>)和commitAsync(Map<TopicPartition,OffsetAndMetadata>)

CommitFailedException

所谓CommitFailedException,是指Consumer客户端在提交位移时出现了错误或异常,并且并不可恢复的严重异常。

导致原因:

  • 消费者端处理的总时间超过预设的max.poll.interval.ms参数值
  • 出现一个Standalone Consumerd的独立消费者,配置的group.id重名冲突。

解决方案:

  • 减少单条消息处理的时间
  • 增加Consumer端允许下游系统消费一批消息的最大时长,这取决于 Consumer 端参 数 max.poll.interval.ms 的值。在新版的 Kafka 中,该参数的默认值是 5 分钟。
  • 减少下游系统一次性消费的消息总数。。这取决于 Consumer 端参数 max.poll.records 的值。当前该参数的默认值是 500 条,表明调用一次 KafkaConsumer.poll 方法,多 返回 500 条消息。
  • 下游使用多线程加速消费

重设消费组位移

Kafka的消费者读取消息是可以重演的。 Kafka与其他消息队列对比(延伸阅读Kafka、RabbitMQ、RocketMQ 之间的区别是什么 ? - 不才陈某的回答 - 知乎):

  • RabbitMQ ActiveMQ 这样传统的消息中间件,处理和响应消息的方式,一旦消息被成功处理就会从Broker上删除
  • kafka是基于日志结构的消息引擎,消费者在消费消息时,仅仅是从磁盘上读取数据而已,只是读的操作,因此消费者不会删除消息数据。同时,由于位移数据是由消费者控制的,因此它能够很容易地修改位移的值,实现重复消费历史数据的功能
  • 选择Kafka:如果你的场景时较高的通吐量,每条消息的处理时间都很短,同时很在意消息的顺序

重设位移策略

  • 位移维度:根据位移值重设,直接吧消费者的位移值重设成我们给定的位移值
    • Earliest: 将位移调整到主题当前最早位移处。可以实现重新消费主题的所有消息
    • Latest: 把位移重设成最新末端位移。可以跳过所有历史消息,从最新消息开始消费
    • Current: 调整到消费者当前提交的最新位移。你修改了消费者程序代码,并重启了消费者,结果发现代码有问题,需要回滚之前的代码变更,同时也要把位移重设到消费者重启时的位置。
    • Specified-Offset: 是比较通用的策略,表示消费者把位移值调整到你指定的位移处。这个策略的典型使用场景是,消费者程序在处理某条错误消息时,你可以手动地“跳过”此消息的处理。
    • Shift-By-N: 制定位移的相对数值  可以向前跳也可以向后跳
  • 时间维度:定一个时间维度,让消费者把位移调整成大于该时间的最小位移;也可以给出一段时间比如30分钟前,然后让消费者直接将位移调回到30分钟之前的位移值
    • DateTime: 允许制定一个时间 然后将位移重置到该时间之后的最早位移处。常见的使用场景是,你想重新消费昨天的而数据,可以使用该策略将位移移到昨天0点
    • Duration: 是指给定的时间间隔,如果想将位移调回到15分钟之前,那么指定PT0H15M0S

Kafka核心技术与实战 <四>_Kafka_02

两种重设消费者组位移的方式

一、通过消费者API来实现

void seek(TopicPartition partition, long offset);void seek(TopicPartition partition, OffsetAndMetadata offsetAndMetadata);//调整多个主题分区void seekToBeginning(Collection<TopicPartition> partitions);void seekToEnd(Collection<TopicPartition> partitions);复制代码
  • Earliest策略

这段代码中有几个比较关键的部分,你需要注意一下。

  1. 你要创建的消费者程序,要禁止自动提交位移。
  2. 组 ID 要设置成你要重设的消费者组的组 ID。
  3. 调用 seekToBeginning 方法时,需要一次性构造主题的所有分区对象。
  4. 最重要的是,一定要调用带长整型的 poll 方法,而不要调用consumer.poll(Duration.ofSecond(0))。
Properties consumerProperties = new Properties();
consumerProperties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
consumerProperties.put(ConsumerConfig.GROUP_ID_CONFIG, groupID);
consumerProperties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
consumerProperties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
consumerProperties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
consumerProperties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList);

String topic = "test";  // 要重设位移的 Kafka 主题 try (final KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerProperties)) {
         consumer.subscribe(Collections.singleton(topic));
         consumer.poll(0);
         consumer.seekToBeginning(
    consumer.partitionsFor(topic).stream().map(partitionInfo ->          new TopicPartition(topic, partitionInfo.partition()))
    .collect(Collectors.toList()));
} 
复制代码

Latest 策略

consumer.seekToEnd(
    consumer.partitionsFor(topic).stream().map(partitionInfo ->          new TopicPartition(topic, partitionInfo.partition()))
    .collect(Collectors.toList()));复制代码

Current策略

consumer.partitionsFor(topic).stream().map(info -> new TopicPartition(topic, info.partition()))
    .forEach(tp -> {long committedOffset = consumer.committed(tp).offset();
    consumer.seek(tp, committedOffset);
});复制代码

Specified-Offset策略

long targetOffset = 1234L;for (PartitionInfo info : consumer.partitionsFor(topic)) {
    TopicPartition tp = new TopicPartition(topic, info.partition());
    consumer.seek(tp, targetOffset);
}复制代码

Shift-By-N策略

for (PartitionInfo info : consumer.partitionsFor(topic)) {
         TopicPartition tp = new TopicPartition(topic, info.partition());// 假设向前跳 123 条消息 long targetOffset = consumer.committed(tp).offset() + 123L; 
         consumer.seek(tp, targetOffset);
}复制代码

DateTime策略

long ts = LocalDateTime.of(2019, 6, 20, 20, 0).toInstant(ZoneOffset.ofHours(8)).toEpochMilli();
Map<TopicPartition, Long> timeToSearch = 
         consumer.partitionsFor(topic).stream().map(info -> new TopicPartition(topic, info.partition()))
    .collect(Collectors.toMap(Function.identity(), tp -> ts));for (Map.Entry<TopicPartition, OffsetAndTimestamp> entry : 
    consumer.offsetsForTimes(timeToSearch).entrySet()) {
consumer.seek(entry.getKey(), entry.getValue().offset());
}复制代码

Duration策略代码

Map<TopicPartition, Long> timeToSearch = consumer.partitionsFor(topic).stream()
         .map(info -> new TopicPartition(topic, info.partition()))
         .collect(Collectors.toMap(Function.identity(), tp -> System.currentTimeMillis() - 30 * 1000  * 60));for (Map.Entry<TopicPartition, OffsetAndTimestamp> entry : 
         consumer.offsetsForTimes(timeToSearch).entrySet()) {
         consumer.seek(entry.getKey(), entry.getValue().offset());
}复制代码

二、通过Kafka-consumer-groups命令行脚本来实现

通过Kafka-consumer-groups命令行脚本来实现,这个功能是在 Kafka 0.11 版本中新引入的。这就是说,如果你使用的 Kafka 是 0.11 版本 之前的,那么你只能使用 API 的方式来重设位移。

Earliest 策略直接指定–to-earliest

bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --all-topics --to-earliest –execute复制代码

Latest 策略直接指定–to-latest。

bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --all-topics --to-latest --execute复制代码

Current 策略直接指定–to-current。

bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --all-topics --to-current --execute复制代码

Specified-Offset 策略直接指定–to-offset

bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --all-topics --to-offset <offset> --execute复制代码

Shift-By-N 策略直接指定–shift-by N。

bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --shift-by <offset_N> --execute复制代码

DateTime 策略直接指定–to-datetime。

bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --to-datetime 2019-06-20T20:00:00.000 --execute复制代码

Duration 策略,我们直接指定–by-duratio

bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --by-duration PT0H30M0S --execute复制代码