kafka的消费位移

1,051 阅读8分钟

Group和消费者offset

消费者在消费的过程中需要记录自己消费了多少数据,即消费位置信息。在 Kafka 中,这个位置信息有个专门的术语:消费者位移(Offset)。

位移 和 消费者位移 之间的区别。通常所说的位移是指 Topic Partition 在 Broker 端的存储偏移量,而消费者位移则是指某个 Consumer Group 在不同 Topic Partition 上面的消费偏移量(也可以理解为消费进度),它记录了 Consumer 要消费的下一条消息的位移

位移提交

Consumer 需要向 Kafka 上报自己的位移数据信息,我们将这个上报过程叫做提交位移(Committing Offsets) 。它是为了保证 Consumer的消费进度正常,当 Consumer 发生故障重启后, 可以直接从之前提交的 Offset 位置开始进行消费而不用重头再来一遍(Kafka 认为小于提交的 Offset 的消息都已经成功消费了),Kafka 设计了这个机制来保障消费进度。我们知道 Consumer 可以同时去消费多个分区的数据,所以位移提交是按照分区的粒度进行上报的,也就是说 Consumer 需要为分配给它的每个分区提交各自的位移数据。

提交方式

Kafka Consumer 提供了多种提交方式,从用户角度来说:位移提交可以分为自动提交和手动提交,但从 Consumer 的角度来说,位移提交可以分为同步提交和异步提交, 接下来我们就来聊聊自动提交和手动提交方式:

1.自动提交方式

自动提交是指 Kafka Consumer 在后台默默地帮我们提交位移,用户不需要关心这个事情。 启用自动提交位移,在 初始化 KafkaConsumer 的时候,通过设置参数 enable.auto.commit = true (默认为true),开启之后还需要另外一个参数进行配合即 auto.commit.interval.ms,这个参数表示 Kafka Consumer 每隔 X 秒自动提交一次位移,这个值默认是5秒。

自动提交看起来是挺美好的, 那么自动提交会不会出现消费数据丢失的情况呢? 在设置了 enable.auto.commit = true 的时候,Kafka 会保证在开始调用 Poll() 方法时,提交上一批消息的位移,再处理下一批消息, 因此它能保证不出现消费丢失的情况。但自动提交位移也有设计缺陷,那就是它可能会出现重复消费。就是在自动提交间隔之间发生 Rebalance 的时候,此时 Offset 还未提交,待 Rebalance 完成后, 所有 Consumer 需要将发生 Rebalance 前的消息进行重新消费一次。

2.手动提交方式

与自动提交相对应的就是手动提交了。开启手动提交位移的方法就是在初始化KafkaConsumer 的时候设置参数 enable.auto.commit = false, 但是只设置为 false 还不行,它只是告诉 Kafka Consumer 不用自动提交位移了,你还需要在处理完消息之后调用相应的  Consumer API 手动进行提交位移,对于手动提交位移,又分为同步提交和异步提交。

1)、同步提交API:

KafkaConsumer#commitSync(), 该方法会提交由 KafkaConsumer#poll() 方法返回的最新位移值,它是一个同步操作,会一直阻塞等待直到位移被成功提交才返回,如果提交的过程中出现异常,该方法会将异常抛出。这里我们知道在调用 commitSync() 方法的时机是在处理完 Poll() 方法返回所有消息之后进行提交,如果过早的提交了位移就会出现消费数据丢失的情况。 下面这段代码展示了 commitSync() 的使用方法:

while (true) {
            ConsumerRecords<String, String> records =
                        consumer.poll(Duration.ofSeconds(1));
            process(records); // 处理消息
            try {
                        consumer.commitSync();
            } catch (CommitFailedException e) {
                        handle(e); // 处理提交失败异常
            }
}

2)、异步提交API:

KafkaConsumer#commitAsync(), 该方法是异步方式进行提交的,调用 commitAsync() 之后,它会立即返回,并不会阻塞,因此不会影响 Consumer 的 TPS(每秒接收的消息数)。另外 Kafka 针对它提供了callback,方便我们来实现提交之后的逻辑,比如记录日志或异常处理等等。由于它是一个异步操作, 假如出现问题是不会进行重试的,这时候重试位移值可能已不是最新值,所以重试无意义。

下面这段代码展示了调用 commitAsync() 的方法:


while (true) {
            ConsumerRecords<String, String> records = 
  consumer.poll(Duration.ofSeconds(1));
            process(records); // 处理消息
            consumer.commitAsync((offsets, exception) -> {
  if (exception != null)
  handle(exception);
  };
}

3)、混合提交模式:

从上面分析可以得出 commitSync 和 commitAsync 都有自己的缺陷, 我们需要将 commitSync 和 commitAsync 组合使用才能到达最理想的效果,既不影响 Consumer TPS,又能利用 commitSync 的自动重试功能来避免一些瞬时错误(网络抖动,GC,Rebalance 问题),在生产环境中建议大家使用混合提交模式来提高 Consumer的健壮性。 我们来看一下下面这段代码,它展示的是如何将两个 API 方法结合使用进行手动提交。


   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();
}
}

这段代码同时使用了 commitSync() 和 commitAsync()。对于常规性、阶段性的手动提交,我们调用 commitAsync() 避免程序阻塞,而在 Consumer 要关闭前,我们调用 commitSync() 方法执行同步阻塞式的位移提交,以确保 Consumer 关闭前能够保存正确的位移数据。将两者结合后,我们既实现了异步无阻塞式的位移管理,也确保了 Consumer 位移的正确性,所以,如果你需要自行编写代码开发一套 Kafka Consumer 应用,那么我推荐你使用上面的代码范例来实现手动的位移提交。

Consumer之__consumer_offsets存储

Consumer 消费完数据后需要进行位移提交, 那么提交的位移数据究竟存储在哪里, 又是以何种方式进行存储的,接下来我们就看看新旧版本 Kafka 对于 Offset 存储方式。

我们知道 Kafka 旧版本(0.8版本之前)是重度依赖 Zookeeper 来实现各种各样的协调管理,当然旧版本的 Consumer Group 是把位移保存在 ZooKeeper 中,减少 Broker 端状态存储开销,鉴于 Zookeeper 的存储架构设计来说, 它不适合频繁写更新,而 Consumer Group 的位移提交又是高频写操作,这样会拖慢 ZooKeeper 集群的性能, 于是在新版 Kafka 中, 社区重新设计了 Consumer Group 的位移管理方式,采用了将位移保存在 Kafka 内部(这是因为 Kafka Topic 天然支持高频写且持久化),这就是所谓大名鼎鼎的__consumer_offsets

__consumer_offsets:用来保存 Kafka Consumer 提交的位移信息,另外它是由 Kafka 自动创建的,和普通的 Topic 相同,它的消息格式也是 Kafka 自己定义的,我们无法进行修改。这里我们很好奇它的消息格式究竟是怎么样的,让我们来一起分析并揭开它的神秘面纱吧。

__consumer_offsets 消息格式分析揭秘
  1. 所谓的消息格式我们可以简单理解为是一个 KV 对。Key 和 Value 分别表示消息的键值和消息体。
  2. 那么 Key 存什么呢?既然是存储 Consumer 的位移信息,在 Kafka 中,Consumer 数量会很多,那么必须有字段来标识这个位移数据是属于哪个 Consumer的,怎么来标识 Consumer 字段呢?前面在讲解 Consumer Group 的时候我们知道它共享一个公共且唯一的Group ID,那么只保存它就可以了吗?我们知道 Consumer 提交位移是在分区的维度进行的,很显然,key中还应该保存 Consumer 要提交位移的分区。
  3. 总结:位移主题的 Key 中应该保存 3 部分内容:<Group ID,主题名,分区号 >
  4. value 可以简单认为存储的是offset值,当然底层还存储其他一些元数据,帮助 Kafka 来完成一些其他操作,比如删除过期位移数据等。

__consumer_offsets 消息格式示意图:

image.png

image.png

image.png

__consumer_offsets创建过程

__consumer_offsets 是怎么被创建出来的呢? 当 Kafka 集群中的第一个 Consumer 启动时,Kafka 会自动创建 __consumer_offsets。前面说过,它就是普通的 Topic, 它也有对应的分区数,如果由 Kafka 自动创建的,那么分区数又是怎么设置的呢? 这个依赖 Broker 端参数 offsets.topic.num.partitions (默认值是50),因此 Kafka 会自动创建一个有 50 个分区的__consumer_offsets 。这就是我们在 Kafka 日志路径下看到有很多 __consumer_offsets-xxx 这样的目录的原因。既然有分区数,必然就会有对应的副本数,这个是依赖 Broker 端另一个参数  offsets.topic.replication.factor(默认值为3)。总结一下,如果 __consumer_offsets 由 Kafka 自动创建的,那么该 Topic 的分区数是 50,副本数是 3,而具体 Group 的消费情况要存储到哪个 Partition ,根据abs(GroupId.hashCode()) % NumPartitions 来计算的, 这样就可以保证 Consumer Offset 信息与 Consumer Group 对应的 Coordinator 处于同一个 Broker 节点上。

//5.脚本执行后输出的元数据信息 
//格式:[消费者组 : 消费的topic : 消费的分区] :: [offset位移], [offset提交时间], [元数据过期时间]
[order-group-1,topic-order,0]::[OffsetMetadata[36672,NO_METADATA],CommitTime 1633694193000,ExpirationTime 1633866993000]