kafka的位移

文章目录

概要

本文主要总结kafka的位移是如何管理的,在broker端如何通过命令行查看到位移信息,并从代码层面总结了位移的提交方式。

消费位移

对于 Kafka 中的分区而言,它的每条消息都有唯一offset ,用来表示消息在分区中对应位置;对于消费者来说,它也有 offset 的概念,消费者使用 offse 来表示消费到分区中某个消息所在的位置。可通过命令行在查看到一个群组,在topic中两者当前的位置
bin/kafka-consumer-groups.sh --bootstrap-server node1:9092 --describe --group kafka-boot

shell 复制代码
[root@node1 kafka_2.13-3.2.1]# bin/kafka-consumer-groups.sh --bootstrap-server node1:9092 --describe  --group kafka-boot

Consumer group 'kafka-boot' has no active members.

GROUP           TOPIC            PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG             CONSUMER-ID     HOST            CLIENT-ID
kafka-boot      test-error-topic 0          26              26              0               -               -               -
kafka-boot      normal-test      0          23              24              1               -               -               -

这里对offse 做些区分 对于消息在分区中的位置 CURRENT-OFFSET称为"偏移量" 或消息位移;对于消费者消费到的位置,LOG-END-OFFSET称为"位移 ,有时候也会更明确地称之为"消费位移"。

生产者位移跟消费者位移的关系可以用下图来说明:

总结几个需要注意的点:

  • 分区副本有两种类型
    领导者副本:生产者跟消费者的请求都只会经过领导者副本
    跟随者副本:首领之外的副本,不处理客户端请求,从领导者副本那里通过拉取的方式同步消息
  • 消费位移存储在Zookeeper或Kafka中,新消费者客户端,偏移量存储咋Kafka内部主题 __consumer_offsets
  • 消费者提交的位移是当前消费消息位移的下一个位置,即:lastConsumeedOffset+1

__consumer_offsets主题

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

老版本 Consumer 的位移是提交到 ZooKeeper 中保存的。当 Consumer 重启后,它能自动从 ZooKeeper 中读取位移数据,从而在上次消费截止的地方继续消费。这种设计使得Kafka Broker 不需要保存位移数据,减少了 Broker 端需要持有的状态空间,因而有利于实现高伸缩性。

但是,ZooKeeper 其实并不适用于这种高频的写操作,Kafka 社区自 0.8.2.x 版本开始推出了全新的位移管理

机制,将 Consumer 的位移数据作为一条条普通的 Kafka 消息,提交到 __consumer_offsets 中。可以这么说,

__consumer_offsets 的主要作用是保存 Kafka 消费者的位移信息。这种方式能够满足高频的写操作。

两个相关参数:
offsets.topic.num.partitions : 设置 __consumer_offsets主题的分区数,默认是50个分区
offsets.topic.replication.factor : 设置__consumer_offsets主题的副本数,默认是3(下载安装的包中此值可能为1 )

当Kafka 集群中的第一个 Consumer 程序启动时,Kafka 会自动创建位移主题

一共有50个分区,那么消费者将位移提交到了哪个分区呢?

通过如下公式可以选出consumer消费的offset要提交到__consumer_offsets的哪个分区,这个分区leader对应的broker

就是这个consumer group的coordinator

公式:Math.abs(groupID.hashCode()) % numPartitions

Kafka 1.0.2及以后提供了kafka_consumer_groups.sh脚本供用户查看consumer信息

1. 创建一个topic,分区数设置为1,副本数设置为1

shell 复制代码
[root@node1 kafka_2.13-3.2.1]# bin/kafka-topics.sh --bootstrap-server node1:9092 --create --topic test-offset --partitions 1 --replication-factor 1
Created topic test-offset.

[root@node1 kafka_2.13-3.2.1]# bin/kafka-topics.sh --bootstrap-server node1:9092 --describe --topic test-offset
Topic: test-offset      TopicId: in6gxQ5OQS6x9R8V3oJ7AQ PartitionCount: 1       ReplicationFactor: 1    Configs: segment.bytes=1073741824
        Topic: test-offset      Partition: 0    Leader: 0       Replicas: 0     Isr: 0

2. 向主题test-offset中发送消息

shell 复制代码
[root@node1 kafka_2.13-3.2.1]# bin/kafka-console-producer.sh --broker-list node1:9092 --topic test-offset
>hello

3. 创建一个消费组,并从头开始消费

shell 复制代码
[root@node1 kafka_2.13-3.2.1]# bin/kafka-console-consumer.sh --bootstrap-server node1:9092  --from-beginning --consumer-property group.id=testOffsetGroup   --topic test-offset
hello

4. 用代码根据上面的公式计算消费组testOffsetGroup提交位移的分区数

java 复制代码
@Test
void getCommitOffsetPartitionTest() {
    String groupId = "testOffsetGroup";
    // 运行结果为16
    System.out.println(Math.abs(groupId.hashCode() % 50));
}
  1. 将kafka配置文件consumer.properties中设置exclude.internal.topics=false,并重启服务
    6. 查看主题__consumer_offsets第16分区上的信息,可以看到消费组testOffsetGroup提交的位移确实保存在了16分区上
shell 复制代码
[root@node1 kafka_2.13-3.2.1]# bin/kafka-console-consumer.sh --topic __consumer_offsets --partition 16 --bootstrap-server node1:9092 --formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter" --from-beginning
[testOffsetGroup,test-offset,0]::OffsetAndMetadata(offset=1, leaderEpoch=Optional.empty, metadata=, commitTimestamp=1691896116191, expireTimestamp=None)
[testOffsetGroup,test-offset,0]::OffsetAndMetadata(offset=1, leaderEpoch=Optional.empty, metadata=, commitTimestamp=1691896121189, expireTimestamp=None)
[testOffsetGroup,test-offset,0]::OffsetAndMetadata(offset=1, leaderEpoch=Optional.empty, metadata=, commitTimestamp=1691896126188, expireTimestamp=None)
[testOffsetGroup,test-offset,0]::OffsetAndMetadata(offset=1, leaderEpoch=Optional.empty, metadata=, commitTimestamp=1691896131188, expireTimestamp=None)
[testOffsetGroup,test-offset,0]::OffsetAndMetadata(offset=1, leaderEpoch=Optional.empty, metadata=, commitTimestamp=1691896133573, expireTimestamp=None)
[testOffsetGroup,test-offset,0]::OffsetAndMetadata(offset=1, leaderEpoch=Optional.empty, metadata=, commitTimestamp=1691896162124, expireTimestamp=None)
[testOffsetGroup,test-offset,0]::OffsetAndMetadata(offset=1, leaderEpoch=Optional.empty, metadata=, commitTimestamp=1691896167124, expireTimestamp=None)
[testOffsetGroup,test-offset,0]::OffsetAndMetadata(offset=1, leaderEpoch=Optional.empty, metadata=, commitTimestamp=1691896172123, expireTimestamp=None)
[testOffsetGroup,test-offset,0]::OffsetAndMetadata(offset=1, leaderEpoch=Optional.empty, metadata=, commitTimestamp=1691896177124, expireTimestamp=None)
[testOffsetGroup,test-offset,0]::OffsetAndMetadata(offset=1, leaderEpoch=Optional.empty, metadata=, commitTimestamp=1691896178781, expireTimestamp=None)

从上面也可看出__consumer_offsets topic的每一日志项的格式都是:
[Group, Topic, Partition]::[OffsetMetadata[Offset, Metadata], CommitTime, ExpirationTime]

客户端提交消费位移是使用OffsetCommitRequest 请求实现的,其结构如下

__consumer_offsets这个主题中的消息格式为KV对,key为[Group, Topic, Partition],value可以简单理解为记录了偏移量;这样的记录方式,使得broker端不需要关系group下有多少个消费者,新增消费者或者减少消费者发生重平衡时,都能准确地定位到对应地分区应该从哪个位置开始消费。

位移提交

鉴于位移提交甚至是位移管理对 Consumer 端的巨大影响,Kafka,特别是KafkaConsumer API,提供了多种提交位移的方法。从用户的角度来说,位移提交分为自动提交和手动提交;从 Consumer 端的角度来说,位移提交分为同步提交和异步提交。

自动提交

自动提交,就是指 Kafka Consumer 在后台默默地为你提交位移

两个重要的参数

  • enable.auto.commit设置是否自动提交位移,默认是true
  • auto.commit.interval.ms:设置自动提交为true时,该参数生效,标识多久提交一次位移,默认5s,
java 复制代码
public static void main(String[] args) {
      Map<String, Object> configs = new HashMap<>();
      configs.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092");
      configs.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
      configs.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, UserDeserializer.class);
      configs.put(ConsumerConfig.GROUP_ID_CONFIG, "consumer1");
      configs.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
      configs.put(ConsumerConfig.CLIENT_ID_CONFIG, "con1");
      // 设置偏移量自动提交。自动提交是默认值。这里做示例。
      configs.put("enable.auto.commit", "true");
      // 偏移量自动提交的时间间隔
      configs.put("auto.commit.interval.ms", "2000");
      KafkaConsumer<String, String> consumer = new KafkaConsumer<String,String>(configs);
      consumer.subscribe(Collections.singleton("tp_demo_01"));

      while (true) {
          ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
          for (ConsumerRecord<String, String> record : records) {
              System.out.println(record.topic()
                      + "\t" + record.partition()
                      + "\t" + record.offset()
                      + "\t" + record.key()
                      + "\t" + record.value());
          }
      }
 }

设置了 enable.auto.commit 为 true,Kafka 会保证在开始调用 poll 方法时,提交上次 poll 返回的所有消息。从顺序上来说,poll 方法的逻辑是先提交上一批消息的位移,再处理下一批消息,因此它能保证不出现消费丢失的情况。但是会出现消息重复消费

在默认情况下,Consumer 每 5 秒自动提交一次位移。现在,我们假设提交位移之后的 3秒发生了 Rebalance 操作。在 Rebalance 之后,所有 Consumer 从上一次提交的位移处继续消费,但该位移已经是 3 秒前的位移数据了,故在Rebalance 发生前 3 秒消费的所有数据都要重新再消费一次。虽然你能够通过减少 auto.commit.interval.ms 的值来提高提交频率,但这么做只能缩小重复消费的时间窗口,不可能完全消除它。这是自动提交机制的一个缺陷。

手动同步提交

开启手动提交位移的方法就是设置enable.auto.commit 为 false。但是,仅仅设置它为 false 还不够,因为你只是告诉

Kafka Consumer 不要自动提交位移而已,你还需要调用相应的 API 手动提交位移。

java 复制代码
public static void main(String[] args) {
    Map<String, Object> configs = new HashMap<>();
    configs.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092");
    configs.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);

    configs.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, UserDeserializer.class);
    configs.put(ConsumerConfig.GROUP_ID_CONFIG, "consumer1");
    configs.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
    configs.put(ConsumerConfig.CLIENT_ID_CONFIG, "con1");

   configs.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
    KafkaConsumer<String, String> consumer = new KafkaConsumer<String,String>(configs);
    consumer.subscribe(Collections.singleton("tp_demo_01"));
    while (true) {
        ConsumerRecords<String, String> records =
                consumer.poll(Duration.ofSeconds(1));
        process(records); // 处理消息
        try {
            consumer.commitSync();
        } catch (CommitFailedException e) {
           handle(e); // 处理提交失败异常
        }
    }
}

调用 commitSync() 时,Consumer 程序会处于阻塞状态,直到远端的 Broker 返回提交结果,这个状态才会结束,这样就会影响TPS。

鉴于此问题,还有另外一个提交方式

手动异步提交

java 复制代码
public static void main(String[] args) {
   Map<String, Object> configs = new HashMap<>();
   configs.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092");
   configs.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);

   configs.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, UserDeserializer.class);
   configs.put(ConsumerConfig.GROUP_ID_CONFIG, "consumer1");
   configs.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
   configs.put(ConsumerConfig.CLIENT_ID_CONFIG, "con1");
   configs.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
   KafkaConsumer<String, String> consumer = new KafkaConsumer<String,String>(configs);
   while (true) {
       ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
       process(records); // 处理消息
       consumer.commitAsync((offsets, exception) -> {
           if (exception != null) {
               handle(exception);
           }
       });
   }
}

commitAsync 是否能够替代 commitSync 呢?答案是不能。commitAsync 的问题在于,出现问题时它不会自动重试。因为它是异步操作,倘若提交失败后自动重试,那么它重试时提交的位移值可能早已经"过期"或不是最新值了。因此,异步提交的重试其实没有意义,所以 commitAsync 是不会重试的。

是手动提交,需要将 commitSync 和 commitAsync 组合使用才能到达最理想的效果,原因有两个:

  1. 利用 commitSync 的自动重试来规避那些瞬时错误,比如网络的瞬时抖动,Broker 端 GC 等。这些问题都是短暂的,自动重试通常都会成功。
  2. 不希望程序总处于阻塞状态,影响 TPS。
java 复制代码
public static void main(String[] args) {
   Map<String, Object> configs = new HashMap<>();
   configs.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092");
   configs.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);

   configs.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, UserDeserializer.class);
   configs.put(ConsumerConfig.GROUP_ID_CONFIG, "consumer1");
   configs.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
   configs.put(ConsumerConfig.CLIENT_ID_CONFIG, "con1");
   configs.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
   KafkaConsumer<String, String> consumer = new KafkaConsumer<String,
           String>(configs);
   consumer.subscribe(Collections.singleton("tp_demo_01"));
   try {
       while (true) {
           ConsumerRecords<String, String> records =
                   consumer.poll(Duration.ofSeconds(1));
           consumer.commitAsync();
           process(records); // 处理消息
           consumer.commitAsync(); // 异步提交
       }
   } catch (Exception e) {
       handle(e); // 处理异常
   } finally {
       try {
           consumer.commitSync();// 最后一次提交使用同步阻塞式提交
       } finally {
           consumer.close();
       }

   }
}
相关推荐
架构师老Y15 小时前
011、消息队列应用:RabbitMQ、Kafka与Celery
python·架构·kafka·rabbitmq·ruby
talen_hx29619 小时前
《kafka核心源码解读》学习笔记 Day 02
笔记·学习·kafka
lifallen19 小时前
如何保证 Kafka 的消息顺序性?
java·大数据·分布式·kafka
真实的菜19 小时前
Kafka 2.x vs 3.x,我为什么选择升级?
kafka
时光追逐者19 小时前
分享四款开源且实用的 Kafka 管理工具
分布式·kafka·开源
Rick199319 小时前
rabbitmq, rocketmq, kafka这三种消息如何分别保住可靠性,顺序性,以及应用场景?
kafka·rabbitmq·rocketmq
☞遠航☜1 天前
kafka快速上手
分布式·kafka·linq
工具罗某人1 天前
docker compose部署kafka集群搭建
docker·容器·kafka
qq_297574672 天前
【Kafka 系列・入门第六篇】Kafka 集群部署(3 节点)+ 负载均衡配置
分布式·kafka·负载均衡