kafka消费者

消费者

消费方式

  • pull(拉)模式:kafka采用拉模式,消费者主动从broker拉取数据。拉模式的不足之处:如果kafka没有数据,那么消费者可能陷入循环中,一直返回空数据。
  • push(推)模式:kafka没有采用这种方式,因为有broker决定消息发送速率,很难适应所有的消费者。每个 消费者消费的速率不同,可能造成有的消费者非常忙碌,有的消费者很空闲。

消费者组原理

  1. 消费者组,有多个consumer组成。形成一个消费者组的条件,是所有消费者的groupid相同。消费组之间互不影响。所有的消费者都属于某个消费组。即消费者组是逻辑上的一个订阅者
  2. 当只创建一个消费者时候,未指定groupid的时候,其实kafka会自动分配一个groupid。
  3. 同一个分区只可以被一个消费者组内的消费者消费,如果组内只有一个消费者,那同一主题下所有分区都被该消费者消费。如果消费者组中的消费者超过分区数,那么就有一部分消费者闲置,不会接收任何消息。
  4. 每个消费者消费的offset有消费者提交到系统主题中保存。_consumer_offsets

消费者组初始化流程

  1. coordinator(协调员):辅助实现消费者组的初始化和分区的分配。每个broker都存在一个coordinator
    1. coordinator节点选择=groupid的hashcode值%50(50=_consumer_offsets的分区数量)。假如:groupid的hashcode值=1,1%50=1。那么_consumer_offsets主题的1号分区在那个leader broker上,就选择这个节点的coordinator作为这个消费组的老大。消费者下的所有消费者提交的offset的时候就往这个分区去提交offset.
  2. 确定完coordinator所在的broker。
    1. 每个consumer都发送JoinGroup请求到coordinator
    2. coordinator选出一个consumer作为Leader
    3. coordinator将要消费的topic情况发送给Leader消费者
    4. leader消费者负责制定消费方案
    5. 将制定完的消费方案发送给coordinator
    6. coordinator会将消费方案下发给各个消费者
    7. 每个消费者都会和coordinator保持心跳(heartbeat.interval.ms=3s),一旦超时(session.timeout.ms=45s),该消费者会被移除,并触发再平衡;或者消费者处理消息的时间过长(max.poll.interval.ms=5分钟),也会出发再平衡

协议

kafka提供了5个协议来处理与消费组协调相关的问题:

  1. Heartbeat请求:consumer需要定期给组协调器发送心跳来表明自己还活着
  2. LeaveGroup请求:主动告诉组协调器我要离开消费组
  3. SyncGroup请求:消费组Leader把分配方案告诉组内所有成员
  4. JoinGroup请求:成员请求加入组
  5. DescribeGroup请求:显示组的所有信息,包括成员信息,协议名称,分配方案,订阅信息等。通常该请求是给管理员使用

组协调器在再均衡的时候主要用到了前面4种请求。

消费者组消费流程

在这里插入图片描述

  1. 创建一个消费者网络连接客户端,跟kafka集群进行交互。
  2. 获取消费者分区分配策略。
  3. 创建消费者协调器(提交offset)
  4. 调用sendFetches用来初始化抓取数据。设置fetch.min.bytes、fetch.max.wait.ms、fetch.max.bytes
  5. 开始调用send方法,发送请求。通过回调方法onSuccess把对应的结果拉取回来。一批一批的放入一个队列中。
  6. 消费者一批次会拉取500条(max.poll.records)。经过反序列化 -》 过滤器 -》 正常的处理数据

消费者参数配置

参数解释
fetch.min.bytes:该属性指定了消费者从服务器获取记录的最小字节数。broker 在收到消费者的数据请求时,如果可用的数据量小于 fetch.min.bytes 指定的大小,那么它会等到有足够的可用数据时才把它返回给消费者。这样可以降低消费者和 broker 的工作负载,因为它们在主题不是很活跃的时候(或者一天里的低谷时段)就不需要来来回回地处理消息。如果没有很多可用数据,但消费者的 CPU 使用率却很高,那么就需要把该属性的值设得比默认值大。如果消费者的数量比较多,把该属性的值设置得大一点可以降低 broker 的工作负载。 默认值为1 byte
fetch.max.bytes默认 Default: 52428800(50 m)。消费者获取服务器端一批消息最大的字节数。如果服务器端一批次的数据大于该值(50m)仍然可以拉取回来这批数据,因此,这不是一个绝对最大值。一批次的大小受 message.max.bytes (broker config)or max.message.bytes (topic config)影响。
max.poll.interval.ms消费者处理消息的最大时长,默认是 5 分钟。超过该值,该消费者被移除,消费者组执行再平衡。
fetch.max.wait.ms我们通过 fetch.min.bytes 告诉 Kafka,等到有足够的数据时才把它返回给消费者。而 feth.max.wait.ms 则用于指定 broker 的等待时间,默认是如果没有足够的数据流入Kafka,消费者获取最小数据量的要求就得不到满足,最终导致 500ms 的延迟。如果 fetch.max.wait.ms 被设为 100ms,并且 fetch.min.bytes 被设为 1MB,那么 Kafka 在收到消费者的请求后,要么返回 1MB 数据,要么在 100ms 后返回所有可用的数据,就看哪个条件先得到满足。 默认值为500ms
max.partition.fetch.bytes该属性指定了服务器从每个分区里返回给消费者的最大字节数。默认值是 1MB
session.timeout.ms 和heartbeat.interval.ms消费者多久没有发送心跳给服务器服务器则认为消费者死亡/退出消费者组 默认值:10000ms
heartbeat.interval.ms :消费者往kafka服务器发送心跳的间隔 一般设置为session.timeout.ms的三分之一 默认值:3000ms
auto.offset.reset:当消费者本地没有对应分区的offset时 会根据此参数做不同的处理 默认值为:latest
earliest当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费
latest当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据
nonetopic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常
enable.auto.commit该属性指定了消费者是否自动提交偏移量,默认值是 true。为了尽量避免出现重复数据和数据丢失,可以把它设为 false,由自己控制何时提交偏移量。如果把它设为 true,还可以通过配置 auto.commit.interval.ms 属性来控制提交的频率。
partition.assignment.strategyPartitionAssignor 根据给定的消费者和主题,决定哪些分区应该被分配给哪个消费者。Kafka 有两个默认的分配策略。1、• Range:该策略会把主题的若干个连续的分区分配给消费者。假设消费者 C1 和消费者 C2 同时订阅了主题 T1 和主题 T2,并且每个主题有 3 个分区。那么消费者 C1 有可能分配到这两个主题的分区 0 和分区 1,而消费者 C2 分配到这两个主题的分区2。因为每个主题拥有奇数个分区,而分配是在主题内独立完成的,第一个消费者最后分配到比第二个消费者更多的分区。只要使用了 Range 策略,而且分区数量无法被消费者数量整除,就会出现这种情况。2、• RoundRobin:该策略把主题的所有分区逐个分配给消费者。如果使用 RoundRobin 策略来给消费者 C1 和消费者 C2 分配分区,那么消费者 C1 将分到主题 T1 的分区 0 和分区 2 以及主题 T2 的分区 1,消费者 C2 将分配到主题 T1 的分区 1 以及主题 T2 的分区 0 和分区 2。一般来说,如果所有消费者都订阅相同的主题(这种情况很常见),RoundRobin 策略会给所有消费者分配相同数量的分区(或最多就差一个分区)。
max.poll.records单次调用 poll() 方法最多能够返回的记录条数 ,默认值 500
receive.buffer.bytes 和 send.buffer.bytesreceive.buffer.bytes 默认值 64k 单位 bytes
offsets.topic.num.partitions__consumer_offsets 的分区数,默认是 50 个分区
heartbeat.interval.ms Kafka消费者和 coordinator 之间的心跳时间,默认 3s。该条目的值必须小于 session.timeout.ms ,也不应该高于session.timeout.ms 的 1/3。

分区的分配及再平衡

​ 重平衡其实就是一个协议,它规定了如何让消费者组下的所有消费者来分配topic中的每一个分区。比如一个topic有100个分区,一个消费者组内有20个消费者,在协调者的控制下让组内每一个消费者分配到5个分区,这个分配的过程就是重平衡。

重平衡的触发条件主要有三个:

  1. 消费者组内成员发生变更,这个变更包括了增加和减少消费者,比如消费者宕机退出消费组。
  2. 主题的分区数发生变更,kafka目前只支持增加分区,当增加的时候就会触发重平衡
  3. 订阅的主题发生变化,当消费者组使用正则表达式订阅主题,而恰好又新建了对应的主题,就会触发重平衡

kafka的四种主流的分区分配策略:Range、RoundRobin、Sticky、CooperativeSticky。可以通过partitiin.assignment.strategy,修改分区的分配策略。默认的策略是Range+CooperativeSticky。kafka可以同时使用多个分区分配策略。

Range

Range是对每个topic而言的。首先对同一个topic里面的分区按照序号进行排序,并对消费者组内的消费者按照字母顺序进行排序。假设现在有七个分区,三个消费者。排序后的分区将会是0,1,2,3,4,5,6;消费者排序之后将会是C0,C1,C2。通过分区数/消费者数来决定每个消费者应该消费几个分区,如果除不尽,那么前面几个消费者将会多消费一个分区。

例如,7/3=2余1。那么C0消费者就消费三个分区(0,1,2),C1(3,4)和C2(5,6)就消费两个分区。

**注意:**如果只是针对一个topic而言,那么C0多消费1个分区影响不是很多。但是如果有N多个topic,消费者C0都多消费一个分区,那么topic越多,C0消费的分区就比其他的消费者明细多消费N个分区。容易产生数据倾斜

分区的再均衡

当关闭消费者0的时候,在45S之内发送一大批消息,此时消费者1和消费者2正常消费原先分配的分区消息。等待45S之后,就会发现原先消费者0的分区消息,会分配给消费者1和消费者2进行消费。

消费者与broker的coordinator的超时时间是45S。超过45S会判断出消费者0退出了。会触发分区再均衡。

45秒之后在发送。此时已经触发了分区再均衡。按照Range分配方式,消费者1消费分区(0,1,2,3)。消费者2消费分区(4,5,6)。

RoundRobin

RoundRobin是针对集群中所有的topic而言的。按照轮询分区策略。把所有的partition和所有的consumer都列出来,然后按照hashcode进行排序。最后通过轮询算法来分配partition给到各个消费者。

// 修改分区分配策略
properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, "org.apache.kafka.clients.consumer.RoundRobinAssignor");

同样有七个分区,一个消费者组里有三个消费者。假设排序之后分区号是0,1,2,3,4,5,6,消费者是C0,C1,C2。那么按照轮询规则,C0消费的分区是0,3,6;C1消费的分区是1,4;C2消费的分区2,5。

分区再均衡

停掉消费者C0。45s之内发送一大批消息,C1和C2正常消费原先分配的分区数据。0 号消费者的任务会按照 RoundRobin 的方式,把数据轮询分成 0 、6 和 3 号分区数据,分别由 1 号消费者或者 2 号消费者消费。

等到45S秒之后,broker确定消费者0退出消费组之后。会进行分区分区再均衡。此时消费者1消费的分区是0,2,4,6;消费者2消费的分区是1,3,5。

Sticky

粘性分区定义:可以理解为分配的结果带有“粘性的”。即在执行一次新的分配之前,考虑上一次分配的结果,尽量少的调整分配的变动,可以节省大量的开销。粘性分区是 Kafka 从 0.11.x 版本开始引入这种分配策略,首先会尽量均衡的放置分区到消费者上面,在出现同一消费者组内消费者出现问题的时候,会尽量保持原有分配的分区不变化。

// 修改分区分配策略
properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, "org.apache.kafka.clients.consumer.StickyAssignor");

七个分区,三个消费者,尽量均衡有随机的分区策略。3:2:2

停掉消费者C0。45S内会将C0消费的分区按照粘性规则将分区给其他两个消费者消费。45S之后消费者C0被剔除消费者组之后,会重新按照粘性方式的分配。

offset位移

消费者offset默认维护的位置

_consumer_offsets 主题里面采用 key 和 value 的方式存储数据。key 是 group.id+topic+分区号,value 就是当前 offset 的值。每隔一段时间,kafka 内部会对这个 topic 进行compact(压缩),也就是每个 group.id+topic+分区号就保留最新数据。一直被覆盖更新的。

默认是无法查看系统主题里面的参数。需要在配置文件里面增加配置。

#config/consumer.properties  
#默认是true 表示不能消费系统主题。为了查看该系统主题数据,所以该参数修改为 false
exclude.internal.topics=false
#查看消费者消费主题 __consumer_offsets
bin/kafka-console-consumer.sh --topic __consumer_offsets --bootstrap-server node01:9092 --consumer.config config/consumer.properties --formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter" --from-beginning
[test,kafka_test,1]::OffsetAndMetadata(offset=7, 
leaderEpoch=Optional[0], metadata=, commitTimestamp=1622442520203, 
expireTimestamp=None)
[test,kafka_test,0]::OffsetAndMetadata(offset=8, 
leaderEpoch=Optional[0], metadata=, commitTimestamp=1622442520203, 
expireTimestamp=None)

自动提交offset

设置自动提交offset

// 自动提交  `enable.auto.commit` 默认值为 true,消费者会自动周期性地向服务器提交偏移量。
 properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
// 提交时间间隔  `auto.commit.interval.ms` 如果设置了 `enable.auto.commit` 的值为 true, 则该值定义了消费者偏移量向 Kafka 提交的频率,默认 5s。
properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,1000);
自动提交带来的问题

​ 默认情况下消费者每五秒钟会提交一次改动的偏移量, 这样做虽然说吞吐量上来了, 但是可能会出现重复消费的问题。

​ 因为可能在下一次提交偏移量之前消费者本地消费了一些消息,然后发生了分区再均衡。假设上次提交的偏移量是 2000 在下一次提交之前 其实消费者又消费了500条数据 也就是说当前的偏移量应该是2500 但是这个2500只在消费者本地, 也就是说其他消费者去消费这个分区的时候拿到的偏移量是2000,那么又会从2000开始消费消息 那么 2000到2500之间的消息又会被消费一遍,这就是重复消费的问题.

手动提交offset

手动提交有两种。commitSync(同步提交)和commitASync(异步提交)。相同点,都会将本次提交的一批数据最高的偏移量提交;不同点是,同步提交阻塞当前线程,一直到提交成功,并且会自动失败重试异步提交没有失败重试机制,可能会提交失败

同步提交offset

必须等待offset提交完毕之后再去消费下一批数据。由于存在失败重试机制,可靠性比较高。但是因为需要等待提交结果,所以效率比较低。

// 设置手动提交
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);

 // 消费数据
        while (true){
            ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.println(consumerRecord);
            }
            // 手动提交
            kafkaConsumer.commitSync();
        }
异步提交offset

发送完提交offset请求后,就开始消费下一批数据了。异步提交offset无需等待,效率比较高

// 设置手动提交
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);

 // 消费数据
        while (true){
            ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.println(consumerRecord);
            }
            // 手动提交
            consumer.commitAsync();
        }

指定offset消费

auto.offset.reset : earliest | latest | none 默认是 latest。

当kafak中没有初始偏移量。或者服务器上不存在当前偏移量时这三个配置项是怎么处理的?

  1. earliest: 自动将偏移量重置为最早的偏移量,–from-beginning。(一个新的消费组,是消费不到之前的消费的,可以设置这个,从头开始消费,后面会使用已提交的offset来消费)
  2. latest: 自动将偏移量(topic中最大的)重置为最新偏移量。默认值
  3. none: 如果未找到消费者组的先前偏移量,则向消费者抛出异常。
       // 指定位置进行消费      第一次尝试有可能拿到的是空的分区信息,因为消费者要和broker进行大量的交互
        Set<TopicPartition> assignment = kafkaConsumer.assignment(); // 获取到对应的分区信息
        // 保证分区分配方案已经指定完毕
        while (assignment.size() == 0){
            kafkaConsumer.poll(Duration.ofSeconds(1));
            assignment = kafkaConsumer.assignment();
        }
        for (TopicPartition topicPartition : assignment) {   // 拿到了所有的分区信息
            // 指定offset进行消费
            kafkaConsumer.seek(topicPartition,600);
        }

指定时间消费

在生产环境中,会遇到最近消费的几个小时数据异常,想重新按照时间消费。例如要求按照时间消费前一天的数据,怎么处理?

  // 指定位置进行消费      第一次尝试有可能拿到的是空的分区信息,因为消费者要和broker进行大量的交互
        Set<TopicPartition> assignment = kafkaConsumer.assignment(); // 获取到对应的分区信息
        // 保证分区分配方案已经指定完毕
        while (assignment.size() == 0){
            kafkaConsumer.poll(Duration.ofSeconds(1));
            assignment = kafkaConsumer.assignment();
        }
        // 把时间转换对对应的offset
        HashMap<TopicPartition, Long> topicPartitionHashMap = new HashMap<>();
        // 封装集合存储,每个分区对应一天前的数据
        for (TopicPartition topicPartition : assignment) {
            topicPartitionHashMap.put(topicPartition, System.currentTimeMillis() - 1*24*3600*1000);
        }

        // 获取从 1 天前开始消费的每个分区的 offset
        Map<TopicPartition, OffsetAndTimestamp> topicPartitionOffsetAndTimestampMap = kafkaConsumer.offsetsForTimes(topicPartitionHashMap);

        //  遍历每个分区,对每个分区设置消费时间。
        for (TopicPartition topicPartition : assignment) {   // 拿到了所有的分区信息
            // 指定offset进行消费
            OffsetAndTimestamp offsetAndTimestamp = topicPartitionOffsetAndTimestampMap.get(topicPartition);
            kafkaConsumer.seek(topicPartition,offsetAndTimestamp.offset());
            
        }

漏消费和重复消费

重复消费

已经消费了数据,但是 offset 没提交。自动提交offset引起的

消费者每5s提交offset。如果提交后的offset的2s。消费者挂了。再次重启消费者,则从上一次提交的offset处继续消费。导致重复消费。

或者是消费者挂了,触发再均衡之后所有的消费者从上次提交的offset除继续消费,也会重复消费。

漏消息

先提交 offset 后消费,有可能会造成数据的漏消费。手动提交offset引起的。

当手动提交offset的时候,数据还在内存中未落盘,此时消费者正好挂掉。那么offset已经提交,但是数据未处理,导致这部分内存的数据丢失。

消费者位移管理

API说明
public void assign(Collection partitions)给当前消费者手动分配一系列主题分区。手动分配分区不支持增量分配,如果先前有分配分区,则该操作会覆盖之前的分配。如果给出的主题分区是空的,则等价于调用unsubscribe方法。手动分配主题分区的方法不使用消费组管理功能。当消费组成员变了,或者集群或主题的元数据改变了,不会触发分区分配的再平衡。手动分区分配assign(Collection)不能和自动分区分配subscribe(Collection,ConsumerRebalanceListener)一起使用。
public Set assignment()获取给当前消费者分配的分区集合。如果订阅是通过调用assign方法直接分配主题分区,则返回相同的集合。如果使用了主题订阅,该方法返回当前分配给该消费者的主题分区集合。如果分区订阅还没开始进行分区分配,或者正在重新分配分区,则会返回none。
public Map<String, List> listTopics()获取对用户授权的所有主题分区元数据。该方法会对服务器发起远程调用。
public List partitionsFor(String topic)获取指定主题的分区元数据。如果当前消费者没有关于该主题的元数据,就会对服务器发起远程调用。
public Map<TopicPartition, Long> beginningOffsets(Collection partitions)对于给定的主题分区,列出它们第一个消息的偏移量。注意,如果指定的分区不存在,该方法可能会永远阻塞。该方法不改变分区的当前消费者偏移量。
public void seekToEnd(Collection partitions)将偏移量移动到每个给定分区的最后一个。该方法延迟执行,只有当调用过poll方法或position方法之后才可以使用。如果没有指定分区,则将当前消费者分配的所有分区的消费者偏移量移动到最后。如果设置了隔离级别为:isolation.level=read_committed,则会将分区的消费偏移量移动到最后一个稳定的偏移量,即下一个要消费的消息现在还是未提交状态的事务消息。
public void seek(TopicPartition partition, long offset)将给定主题分区的消费偏移量移动到指定的偏移量,即当前消费者下一条要消费的消息偏
移量。若该方法多次调用,则最后一次的覆盖前面的。如果在消费中间随意使用,可能会丢失数据。
public long position(TopicPartition partition)检查指定主题分区的消费偏移量
public void seekToBeginning(Collection partitions)将给定每个分区的消费者偏移量移动到它们的起始偏移量。该方法懒执行,只有当调用过
poll方法或position方法之后才会执行。如果没有提供分区,则将所有分配给当前消费者的分区消费偏移量移动到起始偏移量。

消息积压

如果kafka消费能力不足了,则可以考虑增加topic的分区数,并且同时提升消费组的消费者数量,消费者数=分区数。

如果是下游的数据处理不及时,提高每批次拉去的数据。批次拉取的数据过少(拉取数据/处理时间<生产速度),使处理的数据小于生产的数据,也会造成数据的积压

  • **fetch.max.bytes: **Default52428800(50M)。消费者获取服务器端一批消息最大的字节数。如果服务器端一批次的数据大于该值(50m)仍然可以拉取回来这批数据,因此,这不是一个绝对最大值。一批次的大小受 message.max.bytes (broker config)``or max.message.bytes (topic config)影响。
  • max.poll.records: 一次 poll 拉取数据返回消息的最大条数,默认是 500 条
  • 28
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值