关于kafka消费者位移不得不知道的事

​ 对于kafka 中 的分区而言,它的每条消息都有唯一的offset,用来表示在分区中对应的位置。对于消费者而言,它也有一个offset的概念,消费者使用offset来表示消费到分区中某个消息所在的位置。这里所介绍的是消费者的位移,即第二种情况。

​ 在每次调用poll方法的时候,返回的是还没有消费过的消息集,要做到这一点,就需要记录上一次消费时候的消费位移,并且这个位移必须是做持久化的保存,而不是单单保存在内存中,否则消费者重启后就无法获取之前的消费位移。还有新的情况是,如果新增一个消费者,那么分区再均衡的时候,新的消费者无法获取消费位移。

​ 旧版的客户端中,消费位移的信息存储再zookeeper中,而新版的客户端,消费位移则存储在kafka内部的主题_consumer_offsets中。我将消费位移做持久化操作的动作称为提交,消费者在消费完消息之后需要执行消费位移的提交。
在这里插入图片描述

消费者位移

kafkaConsumer类中提供了position(TopicPartition)和committed(TopicPartition)两个方法来来获取上面所说的lastConsumerOffset和position。在消费者中还有一个committed offset的概念,我们来测试下这个三个值得关系:

 public static void testCommittedOffset(String topic,String brokerList){
        Properties properties = new Properties();
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,brokerList);
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"group.demo1");
        //开启手动提交消费位移
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);
        //实例化consumer
        JSONDeserializer<User> userJSONDeserializer = new JSONDeserializer<>();

        userJSONDeserializer.setClazz(User.class);
        KafkaConsumer<String,User> consumer = new KafkaConsumer<>(properties, new StringDeserializer(),userJSONDeserializer);


        long lastConsumerOffset = -1L;
        long position = -1L;
        long committedOffset = -1L;

        //获取topic主题对应 所有分区信息
        List<PartitionInfo> partitionInfos = consumer.partitionsFor(topic);
        PartitionInfo partitionInfo = partitionInfos.get(0);

        TopicPartition topicPartition = new TopicPartition(partitionInfo.topic(), partitionInfo.partition());



        try{
            consumer.assign(Collections.singleton(topicPartition));

            while(isRunning.get()){

                ConsumerRecords<String, User> records = consumer.poll(Duration.ofMillis(1000));
                Iterator<ConsumerRecord<String, User>> iterator = records.iterator();

                while(iterator.hasNext()){
                    ConsumerRecord<String, User> record = iterator.next();
                    System.out.printf(" key = %s, value = %s,offset = %d,%n", record.key(), record.value(), record.offset());
                    lastConsumerOffset = record.offset();
                }
                //这里我们只演示拉取一次消息
                break;
            }
            //同步提交位移信息
            consumer.commitSync();

            OffsetAndMetadata committed = consumer.committed(topicPartition);
            committedOffset =   committed.offset();

            //获取当前分区位置
            position = consumer.position(topicPartition);


            System.out.printf("lastConsumerOffset=%d,position=%d,committed offset = %d" ,lastConsumerOffset,position, committedOffset);

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            consumer.close();
        }
    }

运行该方法后,我们看到打印得消息如下:

lastConsumerOffset=6499,position=6500,committed offset = 6500

说明position = conmmitted offset == lastConsumerOffset + 1

在提交了消费位移后成立。

在kafka中默认消费位置的提交方式是自动提交的,这个由消费者客户端参数enable.auto.commit配置,默认值为true。当然默认的提交也不是每消费一条消息就提交一次,而是定期提交,这个定期的周期由客户端的参数auto.commit.interval.ms 来决定的。

    /**
     * <code>enable.auto.commit</code>
     */
    public static final String ENABLE_AUTO_COMMIT_CONFIG = "enable.auto.commit";
    private static final String ENABLE_AUTO_COMMIT_DOC = "If true the consumer's offset will be periodically committed in the background.";

    /**
     * <code>auto.commit.interval.ms</code>
     */
    public static final String AUTO_COMMIT_INTERVAL_MS_CONFIG = "auto.commit.interval.ms";
    private static final String AUTO_COMMIT_INTERVAL_MS_DOC = "The frequency in milliseconds that the consumer offsets are auto-committed to Kafka if <code>enable.auto.commit</code> is set to <code>true</code>.";

在kafka消费的编程逻辑中位移提交是一大难点。自动提交的方式简单快捷。但可能会引发重复消费和消息丢失的问题。

重复消费:当消费者拉取到消息后,在下一次位移提交前,消费者崩溃。重启后会再次拉取上一次消费的消息。再均衡的情况也会引起重复消费。

消息丢失:如果线程A负责从kafka拉取消息存入到BlockingQueue中,然后由其他的线程进行相应的逻辑处理,再BlockingQueue中还有消息的时候,消费者崩溃,那么会引起消息的丢失。

指定位移消费

上面我们已经谈论如何进行消费位移的提交,正式有了消费位移的持久化,才能使消费者再关闭、崩溃或者遇到再均衡的时候,可以让接替的消费者能够根据存储的消费位移继续进行消费。

当一个消费组建立时候,它根本就没有可以查找的消费位移。kafka中每当消费者找不到所记录的消费位移时,就会根据消费者客户端参数auto.offset.reset的配置来决定从何处开始消费,这个参数的值默认为"latest";表示消费从分区末尾开始进行消费。如下图分区中已经写入了6条消息,此时启动一个新的消费组来进行消费:

在这里插入图片描述

  • 1、默认时从消息开始写入的位置读取信息
  • 2、如果设置auto.offset.reset=earliest 将会从分区开始的位置进行消息的读取
  • 3、如果设置为NONE,将会抛NoOffsetForPartitionException异常

如果我们需要更加细粒度来决定从特定的位移处开始拉取消息,KafkaConsumer的seek()方法正好提供了这个功能,可以让我们往前消费。
在这里插入图片描述

第一个方法中,topicPartition表示分区,而offset参数用来指定从分区的哪个位置开始消费。

那下面我们实验下这个方法:

 Properties properties = new Properties();
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,brokerList);
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"group.demo1");

        //实例化consumer
        JSONDeserializer<User> userJSONDeserializer = new JSONDeserializer<>();

        userJSONDeserializer.setClazz(User.class);
        KafkaConsumer<String,User> consumer = new KafkaConsumer<>(properties, new StringDeserializer(),userJSONDeserializer);

        //获取topic主题对应 所有分区信息
        List<PartitionInfo> partitionInfos = consumer.partitionsFor(topic);
        PartitionInfo partitionInfo = partitionInfos.get(0);

        TopicPartition topicPartition = new TopicPartition(partitionInfo.topic(), partitionInfo.partition());



        try{
            consumer.subscribe(Collections.singleton(topic));
            consumer.seek(topicPartition,10);

            while(isRunning.get()){

                ConsumerRecords<String, User> records = consumer.poll(Duration.ofMillis(1000));
                Iterator<ConsumerRecord<String, User>> iterator = records.iterator();

                while(iterator.hasNext()){
                    ConsumerRecord<String, User> record = iterator.next();
                    System.out.printf(" key = %s, value = %s,offset = %d,%n", record.key(), record.value(), record.offset());

                }
                //这里我们只演示拉取一次消息
                break;
            }

这里我们订阅主题后,直接从第一topicPartition的位移消费调整为10。如果运行上面的代码,将会抛出如下异常:

java.lang.IllegalStateException: No current assignment for partition topic-demo3-0
	at org.apache.kafka.clients.consumer.internals.SubscriptionState.assignedState(SubscriptionState.java:323)
	at org.apache.kafka.clients.consumer.internals.SubscriptionState.seekUnvalidated(SubscriptionState.java:340)
	at org.apache.kafka.clients.consumer.KafkaConsumer.seek(KafkaConsumer.java:1550)

这个时当前还未分配到分区。我们时在poll()方法中分配分区的,所以需要将代码进行如下调整。

public static void testSeek(String topic,String brokerList){
        Properties properties = new Properties();
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,brokerList);
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"group.demo1");

        //实例化consumer
        JSONDeserializer<User> userJSONDeserializer = new JSONDeserializer<>();

        userJSONDeserializer.setClazz(User.class);
        KafkaConsumer<String,User> consumer = new KafkaConsumer<>(properties, new StringDeserializer(),userJSONDeserializer);



        Set<TopicPartition> assignment = new HashSet<>();

        try{
            consumer.subscribe(Collections.singleton(topic));
           //如果已经分配到分区,那么assignment.size()
            while(assignment.size() == 0 ){
                consumer.poll(Duration.ofMillis(100));
                assignment = consumer.assignment();
            }

            for(TopicPartition p:assignment){
                consumer.seek(p,10);
            }

            while(isRunning.get()){

                ConsumerRecords<String, User> records = consumer.poll(Duration.ofMillis(1000));
                Iterator<ConsumerRecord<String, User>> iterator = records.iterator();

                while(iterator.hasNext()){
                    ConsumerRecord<String, User> record = iterator.next();
                    System.out.printf(" key = %s, value = %s,offset = %d,%n", record.key(), record.value(), record.offset());

                }

            }

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            consumer.close();
        }
    }

我们可以通过org.apache.kafka.clients.consumer.KafkaConsumer#endOffsets(java.util.Collection<org.apache.kafka.common.TopicPartition>)方法来获取分区尾部消息位置。

在这里插入图片描述
通过offsetsForTimes()方法来查询一个分区的在某个具体时间点的分区位置

只要获取到消息的位置,我们就可以通过seek()方法来进行消费。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值