这系列是根据极客时间《Kafka核心技术与实战》这个课程做的笔记
本篇目录
- 位移主题_consumer_offsets
- 位移提交
- CommitFailedException
- 重设消费组位移
位移主题 _consumer_offsets
诞生背景
- 老版本的Kafka会把位移信息保存在Zk中,当Consumer重启后,自动从Zk中读取位移信息。这种设计使Kafka Broker不需要保存位移数据,可减少Broker端需要持有的状态空间,有利于实现高伸缩性。
- 但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中有些许功能写死的是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
两种重设消费者组位移的方式
一、通过消费者API来实现
void seek(TopicPartition partition, long offset);void seek(TopicPartition partition, OffsetAndMetadata offsetAndMetadata);//调整多个主题分区void seekToBeginning(Collection<TopicPartition> partitions);void seekToEnd(Collection<TopicPartition> partitions);复制代码
- Earliest策略
这段代码中有几个比较关键的部分,你需要注意一下。
- 你要创建的消费者程序,要禁止自动提交位移。
- 组 ID 要设置成你要重设的消费者组的组 ID。
- 调用 seekToBeginning 方法时,需要一次性构造主题的所有分区对象。
- 最重要的是,一定要调用带长整型的 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复制代码