Kafka学习三-消费者

目录

 

消费者和消费组

消费者群组和分区再均衡

消费者的相关配置

消费者如何提交偏移量

KafkaConsumer API 提供的偏移量的提交方式

再均衡监听器

从特定偏移量处开始处理记录

如何优雅的退出


消费者和消费组

Kafk的消费者从属于消费群组,一个群组里面的消费者订阅的是同一个主题。每个消费者接受主题的一部分分区消息。关系如下: 

消费者群组和分区再均衡

(1)什么是分区再均衡?
分区的所有权从一个消费者转移到另一个消费者,叫做分区再均衡。
(2)什么情况下会导致分区再均衡?
a.一个新的消费者加入群组时,它读取的是原本其他消费者读取的消息
b.当一个消费者关闭或者发生崩溃时,会离开群组,此时原本又它读取的分区将有群组中其他的消费者读取
c.在主题发生变化时,比如添加了新的分区
(3)发生分区再均衡的影响?
再均衡期间,消费者无法读取消息,造成整个群组一小段时间的不可用。并且当分区被从新分配给另一个消费者时,消费者当前的读取状态丢失。
(4)消费者通过向被指派为群组协调器的broker发送心跳来维持他们和群组的从属关系以及他们对分区的所有权关系。只有消费者在正常时间间隔内发送了心跳,就被认为是活跃的。如果消费者停止发送心跳的时间足够长,会话就会过期,群组协调器会认为该消费者已经死亡,触发一次再均衡。
(5)如何进行分配分区?
当有消费者加入到群组,会先向群组协调器发送一个JoinGroup的请求,第一个加入群组的消费者会成功”群主“。群主会从协调器获取到群组的成员信息(包括所有最近发送过心跳的消费者)并负责给每一个消费者分配分区。

消费者的相关配置

(1)fetch.min.bytes:指定了消费者从服务器获取记录的最小字节数。broker在收到消费者的数据请求时,如果可用数据量小于该值,那么broker会等到有足够多的数据量时再返回给消费者。
(2)fetch.max.wait.ms:用于指定broker的等待时间。默认500ms。如果么有足够的数据流入kafka,消费者获取最小数据量的要求无法满足,最终达到500ms的延迟,会将此时topic小于fetch.min.bytes的数据量返回给消费者。
(3)max.partiton.fetch.bytes:指定服务器从每个分区中返回给消费者的最大字节数。默认值1M,也就是KafkaConsumer.poll()方法从每个分区里返回的记录最多不能超过该值。在实际分配消费者内存时,需要多分配一些,需要防止有的消费者挂掉,其他消费者需要处理更多的分区。
(4)session.timeout.ms:指定消费者在被认为死亡之前可以与服务器断开连接的时间,默认是3s。也就是3s之内没有发送心跳检测,就会被认为已死亡,协调器会再均衡。该参数与heartbeat.interval.ms相关。heartbeat.interval.ms指定了发送心跳的频率。所以heartbeat.interval.ms要比session.timeout.ms小。一般是session.timeout.ms的三分之一。
(5)auto.offset.reset:消费者在读取一个没有偏移量的分区或者偏移量无效如何处理。
a.latest:偏移量无效的时候,消费者读取最新的记录
b.earliest:偏移量无效的时候,消费者会从起始位置读取分区记录
(6)enable.auto.commit:消费者是否自动提交偏移量,默认true。一般为了避免出现重复数据,我们会自行进行偏移量的提交。
(7)partition.assignment.strategy:设置分区分配策略。
a.Range:该策略会把主题的若干连续的分区分配给消费者。

 b.RoundRobin:该策略会把主题的所有分区逐个分配给消费者 
(8)client.id:标识从客户端发送过来的消息
(9)max.poll.records:控制单次调用call()方法能够返回的记录数据

消费者如何提交偏移量

消费者会向一个特殊主题:consumeroffset发送消息,消息包括每个分区的偏移量。当有新的消费者加入群组或者消费者崩溃,会发生再均衡。再均衡之后每个消费者可能分配到新的分区,为了能够继续之前的工作,消费者读取每个分区最后提交的偏移量,从该该偏移量处进行读取。 

那么如果提交的偏移量不正确,就会引发两种情况:(1)提交的偏移量小于客户端最后处理的消息的偏移量,此时消息会被重复处理;(2)提交的偏移量大于客户端最后处理的消息偏移量,此时消息就会有丢失。 

KafkaConsumer API 提供的偏移量的提交方式

(1)自动提交:设置enable.auto.commit=true,但是在发生再均衡的时候极易丢失数据。 (2)提交当前偏移量:设置enable.auto.commit=false,让应用程序自行提交偏移量。可以使用commitSync()或者commitASync()进行提交。 

    Properties properties = new Properties();
    properties.put("bootstrap.servers","broker1:port");
    properties.put("key.serializer","xxxxx");
    properties.put("value.serializer","xxxx");

    KafkaConsumer<String,String> consumer = new KafkaConsumer<String, String>(properties);
    while (true){
        Duration duration = Duration.of(100, ChronoUnit.MILLIS);
        ConsumerRecords<String, String> records = consumer.poll(duration);

        for (ConsumerRecord<String, String> record : records) {
            System.out.println("topic:"+record.topic()+",partition:"+record.partition()+",offset:"+record.offset());
        }
        try {
            //同步提交
            consumer.commitSync();
            //异步提交
            consumer.commitAsync();
        }catch (CommitFailedException e){
            System.out.println("commit failed:"+e);
        }
    }

同步提交:会一直尝试提交直至成功,如果提交失败,我们也是能把日志记录下来
异步提交:提交最后一个偏移量之后,继续做其他的事情。如果提交失败,不会进行重试,原因:在收到服务器相应的时候,可能有一个更大的偏移量已经提交成功。
同步+异步结合: 

    try {
            //异步提交
            consumer.commitAsync();
        }catch (CommitFailedException e){
            System.out.println("commit failed:"+e);
        }finally {
            try{
                //同步提交
                consumer.commitSync(); 
            }finally {
                consumer.close();
            }
        }

如果一切正常,我们使用异步提交的方式。如果直接关闭了消费者,就没有下一次提交了,使用同步提交,一直进行重试。 (3)提交特定的偏移量:
场景:我们通过poll()方法获取到大批数据,我们并不想在数据都处理完了,才返回一个偏移量。我们想在处理的中间就返回偏移量,那么不管是同步的方式还是异步的方式都不满足我们的需求。消费者API中提供了 consumer.commitSync()和consumer.commitAsync()方法时传入希望提交的分区以及相关偏移量 

    KafkaConsumer<String,String> consumer = new KafkaConsumer<String, String>(properties);
    Map<TopicPartition,OffsetAndMetadata> currentOffSets = new HashMap<>();
    int count =0;
    while (true){
        Duration duration = Duration.of(100, ChronoUnit.MILLIS);
        ConsumerRecords<String, String> records = consumer.poll(duration);

        for (ConsumerRecord<String, String> record : records) {
            System.out.println("topic:"+record.topic()+",partition:"+record.partition()+",offset:"+record.offset());
            currentOffSets.put(new TopicPartition(record.topic(),record.partition()),new OffsetAndMetadata(record.offset()+1));
            if (count%100==0){
                consumer.commitSync(currentOffSets);
            }
            count++;
        }
    }

再均衡监听器

消费者在退出和进行分区再均衡之前,会做一些清理工作,我们可以在分区之前做一些操作,或者在分区之后做一些操作。可以在调用subscribe()方法的时候传入一个ConsumerRebalanceListener实例,该实例有如下两个方法:
(1)void onPartitionsRevoked(Collection partitions)该方法会在再均衡开始之前和消费者停止读取消息之后被调用。
(2)void onPartitionsAssigned(Collection partitions)该方法会在重新分配分区之后的消费者开始读取消息之前被调用。

从特定偏移量处开始处理记录

我们使用poll()方法目前都是从各个分区的最新偏移量开始处理数据,我们也可以从特定的偏移量处进行获取。
(1)void seekToBeginning(Collection partitions):从分区的开始位置获取
(2) void seekToEnd(Collection partitions):从分区的末尾开始获取
(3) void seek(TopicPartition partition, OffsetAndMetadata offsetAndMetadata) 重某个分区的特定位置进行获取。这里我们可以在我们处理数据的时间,保存本次处理的offset到本地数据库,那么下次获取就可以使用这个方法,从而减少消息丢失。

如何优雅的退出

我们消费者都是在一个无线循环中进行消息轮询,可以使用consumer.wakeup()退出poll(),并抛出WakeUpException异常,我们不需要处理该异常。consumer.wakeup()是消费者唯一一个可以从其他线程里安全调用的方法。同时在退出线程之前,我们要调用consumer.close()方法,该方法会提交任何还没有提交的东西,并告知协调器触发再均衡,不需要等待会话超时。

某些场景下,可能出现一个消费者从一个主题的所有分区获取特定的分区读取数据,此时不在需要消费者群组和再均衡,值需要把主题和分区分配给消费者,消费者提交偏移量就可以。

    List<PartitionInfo> topic = consumer.partitionsFor("topic");
    List<TopicPartition> partitions = Lists.newArrayList();
    if (topic!=null){
        for (PartitionInfo partitionInfo : topic) {
            TopicPartition partition  =  new TopicPartition(partitionInfo.topic(),partitionInfo.partition());
            partitions.add(partition);
        }
    }
    consumer.assign(partitions);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章