Kafka消费者-概念速览|群组|心跳机制|轮询|提交|偏移量——《Kafka权威指南》笔记

Kafka 消费者 —— 从 Kafka 读取消息

应用程序使用 KafkaConsumer 向 kafka 订阅消息,并从订阅主题上接收消息。

消费者和消费者群组

如果有一个场景,生产者向主题写入消息的速度超过单个消费者的速度,应用程序会更不上消费生成的速度。显然此刻需要对消费者进行横向伸缩。因此我们可以使用多个消费者从同个主题读取数据,对消息进行分流。

Kafka 消费者从属于一个消费者群组。一个群组里的消费者订阅的是同一个主题,每个消费者接收主题一部分分区的消息。

横向伸缩

假设主题有4个分区,我们只创建一个消费者群组,而这个群组只有唯一的消费者,因此消费者C1将收到主题T1的所有分区的消息。

在这里插入图片描述

如果G1里新增一个消费者C2,那么每个消费者将从两个分区接收消息。

在这里插入图片描述

如果群组G1有四个消费者,那么每个消费者可以分配到一个分区。

在这里插入图片描述

当然,如果我们往群组里添加了更多的消费者,超过主题的分区数量,那么有一部分消费者会 闲置 ,不会接收到任何消息。

在这里插入图片描述

因为,Kafka消费者有时经常会做一些 高延迟 的操作,比如将接收的数据写到HDFS或数据库,或者使用数据进行比较耗时的计算。

因此往群组里增加消费者是 横向伸缩 消费能力的主要方式。

除了增加消费者横向伸缩单个应用程序外,我们还可以增加消费者群组,虽然两个群组获取相同主题的消息,但是G1与G2之间是互不影响的,重要的一点是横向伸缩并不会对性能造成负面影响。

在这里插入图片描述

群组和分区再均衡

引入

一个新的消费者加入群组,那么它读取的是原本其他消费者读取的消息。

一个消费者因为被关闭或者发生崩溃时,退出群组,原本它读取的消息将被群组里其他的消费者读取。

概念

当一个消费者的分区所有权转移到另外一个消费者的时候,这种行为被称为 再均衡。它为群组带来的高可用和伸缩性。

弊端

但正常情况下,不希望这样的行为发生,因为在 再均衡 期间,消费者无法读取消息,造成整个群组一小段消息无法被消费(不可用)。另外,当分区被重新分配给另外一个消费者时,消费者当前的读取状态会丢失,有可能还需要去刷新缓存。

心跳机制

消费者会向被指派为 群组协调器的broker (不同群组有着不同的协调器) 发送心跳来维持它们和群组的从属关系及它们对分区的所有权的关系(消费者是否崩溃活跃)。

只要消费者正常的时间间隔发送心跳,就被认为是活跃的,即说明它还在读取分区中的消息。一般会在轮询消息和提交偏移量发送心跳(新版本可独立发送心跳)。

如果消费者停止发送心跳时间足够长,会话就会过期,群组协调器的broker就会认为它已经死亡,就会触发一次再均衡

创建Kafka消费者

相比较创建生产者,消费者仍然是三个参数:bootstrap.servers,key.deserializer,value.deserializer

		//给消费者配置信息
        Properties properties = new Properties();

        //链接集群
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop102:9092,hadoop103:9092");
        //开启自动提交
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,true);
        //自动提交Offset信息时间间隔
        properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,"1000");

        //Key,Value 反序列化
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer");
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer");

        //配置消费者组
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"bigdata");

        //重置消费者的offset
        //1.未初始化 2.消息丢失 3.earliest:从最早的一条消息开始offset
        properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");

        //创建消费者
        KafkaConsumer<String,String> consumer = new KafkaConsumer<>(properties);

订阅主题

创建好消费者之后,下一步就是订阅主题了。

subscribe()方法接受了一个主题列表作为参数。

consumer.subscribe(Collections.singletonList("BigData"));

此时我们创建了一个只包含单个元素的列表,主题叫做"BigData"

我们也可以在调用subscribe()方法时传入一个正则表达式,因此可以匹配到多个主题,如果有人创建了新的主题并且匹配,则消费者就会触发一次 再均衡,消费者就会读取新的主题。

通常是在Kafka和其他系统之间复制数据时,使用正则表达式来订阅多个主题。

如果我们要订阅所有与 user 相关的主题,就可以这样做:

consumer.subscribe("user.*");

轮询

消息轮询是消费者API的核心,通过一个简单的轮询向服务器请求数据。

一旦消费者订阅了主题,

轮询就会处理所有的细节,包括群组协调、分区再均衡、发送心跳和获取数据。

while (true){
            //获取数据
            ConsumerRecords<String,String> consumerRecords = consumer.poll(100);

            //订阅主题
            for (ConsumerRecord<String,String> consumerRecord : consumerRecords) {

                System.out.println(consumerRecord.key() + "---" + consumerRecord.value());
            }
}

poll()方法的参数是一个超时时间,用于控制poll()方法的阻塞时间(消费者缓冲区已经没有可用的数据就会发生阻塞)。如果该参数设为0,消费者如果没有消息可消费则立即返回。

poll()方法返回一个记录列表。每条记录都包含着记录所属主题的信息、记录所在分区的信息、记录在分区里的偏移量,以及记录里的键值对。

轮询不只是获取数据那么简单。

在第一次调用新消费者的poll()方法时,它会负责查找 GroupCoordinator,然后加入群组,接受分配的分区。如果发生了再均衡,整个过程也是在轮询期间进行的。

线程安全

在同一群组里,我们无法让一个线程运行着多个消费者,也无法让多个线程安全地共享一个消费者。

按照规则,一个消费者使用一个线程,最好是把消费者的逻辑封装在自己的对象里,使用Java的ExecutorSerivce启动多个线程。

消费者配置

  • fetch.min.bytes

    • 该属性指定了消费者获取记录的最小字节数。
  • fetch.max.wait.ms

    • 该属性指定了broker的最长等待时间
  • max.partition.fetch.bytes

    • 该属性制定了服务器从每个分区里返回给消费者的最大字节数。默认1MB
    • 该属性必须比broker能够接收的最大消息的字节数(max.message.size)要大,否则消费者可能无法读取这些消息。
  • session.timeout.ms

    • 指定了消费者被认为死亡之前可以与服务器断开连接的时间,默认3s。
    • 即指定了消费者可以多久不发送心跳。如果超过该时长,协调器会认定该消费者死亡,就会触发一次再均衡。
    • 该属性与 heart.interval.ms 紧密相关,即指定了发送心跳的频率,heart.ms必须要比session.ms要小,一般是三分之一,即1s。
    • 把该属性值设置更小,就可以更快的检测和恢复崩溃节点。如果设置更大就会减少没必要的再均衡,不过需要更长时间来检测检点崩溃。
  • auto.offset.reset

    • 当消费者长时间失效,包含偏移量记录过时并被删除,那么如何做处理呢。
    • 默认是 latest,意味着在偏移量无效的情况下,消费者将从最新的记录开始读取的数据。
    • 另外一个值是 earliest,意思是在偏移量无效的情况下,消费者将从起始位置读取分区的记录。
  • enable.auto.commit

    • 该属性指定了消费者是否自动提交偏移量,默认值是true。
    • 为了尽量避免出现重复数据和丢失,可以把它设为false,由自己控制何时提交偏移量。
    • 如果设置为true,还可以配置 auto.commit.interval.ms来控制提交的频率。
  • partition.assignment.strategy

    分区会分配给群组里的消费者。

    PartitionAssignor(群主)根据给定的消费者和主题,决定哪些分区应该被分配给哪个消费者。Kafka有两个默认的分配策略。

    • Range

      • 该策略会把主题的若干个连续的分区分配给消费者。

      假定C1和C2同时订阅了T1和T2,而且每个主题都有3个分区。那么消费者C1有可能分配到这两个主题的分区0和分区1,而C2分配到这两个主题的分区2。

      • C1比C2分配到更多的分区,因此只要使用了 Range 策略,而且当分区数量无法被消费者数量整除,就会出现这种情况。

      在这里插入图片描述

    • RoundRobin

      • 该策略把主题的所有分区逐个分配给消费者

      此时C1将分到T1的分区0和分区2以及T2的分区1,C2将分配到主题T1的分区1以及T2的分区0和分区2。

      • RoundRobin策略会给所有消费者分配相同数量的分区(或最多就差一个)

      在这里插入图片描述

  • max.poll.records

    • 该属性用于控制单次调用call()方法能够返回的记录数量,可以控制在轮询里需要的处理的数据量
  • receive.buffer.bytes 和 send.buffer.bytes

    • socket在读写数据时用到的TCP缓冲区也可以设置大小。

提交和偏移量

Kafka不像其他的JMS队列那样需要得到消费者的确认,这是Kafka的一个独特之处。相反消费者可以使用Kafka来跟踪数据在分区里的位置(偏移量)。

我们把更新分区当前位置的操作叫做 提交

通常消费者都会往一个叫作 _consumer_offset 的特殊主题发送消息,而这个消息包含着每个分区的偏移量。如果此时消费者发生崩溃或者新的消费者加入群组,就会触发再均衡。因此消费者为了能够继续原来的工作,消费者就需要读取每个分区最后一次提交的偏移量,然后从偏移量指定的地方继续处理。

如果提交的偏移量小于客户端处理的最后一个消息的偏移量,那么处于两个偏移量之间的消息就会被重复处理

在这里插入图片描述

如果提交的偏移量大于客户端处理的最后一个消息的偏移量,那么处于这两个偏移量之间的消息将会被丢失

在这里插入图片描述

自动提交的利弊

最简单的方式就是让消费者自动提交偏移量,把 enable.auto.commit = true ,那么没过5s,消费者就会自动把poll()所接收的最大偏移量提交上去。

当然也有缺陷,

如果我们默认5s提交时间间隔,在最近一次提交之后的3s发生了再平衡,再平衡后,消费者就会从最后一次提交的偏移量开始读取消息,但这时候的偏移量就已经落后了3s,因此这3s内到达的消息就会被重复处理。

而这种情况是无法避免的。

提交当前偏移量

为了能够控制偏移量提交的时间来消除丢失消息的可能性,并在发生再均衡时减少重复消息的数量

消费者API就提供了偏移量的方式,我们可以在必要的情况下提交当前的偏移量,而不是基于时间间隔提交。

因此 auto.commit.offset = false ,让应用程序决定何时提交偏移量。

使用 commitSync() 提交偏移量最简单也是最可靠的。

while (true){
            //获取数据
            ConsumerRecords<String,String> consumerRecords = consumer.poll(100);

            //订阅主题
            for (ConsumerRecord<String,String> consumerRecord : consumerRecords) {

                System.out.println(consumerRecord.key() + "---" + consumerRecord.value());
                //if (Integer.parseInt(consumerRecord.value()) > 20){

               // }
            }
    		try{
                //处理完一批记录立即提交
                consumer.commitSync();
            }catch (CommitFailedException e){
                //提交失败
                e.printStackTrace();
            }
        }

commitSync()将会提交有poll()返回的最新偏移量,所以在处理完所有记录后要确保调用了 commitSync() ,否则还是会有丢失消息或重复消费的风险。

异步提交

同步提交有一个不足之处,在broker对提交请求做出回应之前,应用程序会一直阻塞,而这样会限制应用程序的吞吐量。

这时候就可以使用 异步提交API 。

我们只管发送提交请求,无需等待broker的响应。

consumer.commitAsync();

异步提交的好处是,在成功提交或者碰到了无法恢复的错误,commitSync()会一直重试,而commitAsync()不会,当然这也是一个坏处,也容易出现重复消费。

commitAsync()也支持回调,在broker做出响应时会执行回调。回调经常被用于记录提交错误。

consumer.commitAsync(new OffsetCommitCallback() {
                    @Override
                    public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
                        e.printStackTrace();
                    }
                });

发送提交请求然后继续做其他事情,如果提交失败,错误信息和偏移量会被记录下来。

不过如果你要用它来进行重试,一定注意提交的顺序。

重试异步提交

我们可以使用一个单调递增的序列号来维护异步提交的顺序

在每次提交偏移量之后或者在回调里提交偏移量时递增序列号。

在重试前,

先检查回调的序列号和即将提交的偏移量是否相等,

如果相等,说明没有新的提交,那么可以安全地进行重试。

如果序列号比较大,说明已经有一个新的提交已经发送出去了,应该停止重试

同步和异步组合提交

为了这是发生在关闭消费者或再均衡之前的最后一次提交,就能确保能够提交成功。

因此,在消费者关闭前一般会使用 commitSync() 和 commitAsync()。

try {
            while (true){
                //获取数据
                ConsumerRecords<String,String> consumerRecords = consumer.poll(100);

                //订阅主题
                for (ConsumerRecord<String,String> consumerRecord : consumerRecords) {

                    System.out.println(consumerRecord.key() + "---" + consumerRecord.value());
                }
                //1
                consumer.commitAsync();
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                //2
                consumer.commitSync();
            }finally {
                consumer.close();
            }
        }

1:如果一切正常,我们使用commitAsync()方法来提交,这样的速度更快,而且即使这次提交失败,下次提交很可能成功。

2:如果直接关闭消费者,就没有所谓的"下一次提交"了。使用commitSync()方法会一直重试,直至提交成功或者发生无法恢复的错误。

提交特定的偏移量

AsWeAllKnown,提交偏移量的频率与处理消息批次的频率是一样的。

如果想更频繁的提交偏移量可以通过提交特定的偏移量来实现。

反序列化器

像生产者的序列化器一样,仍然推荐使用 Avro 来进行序列化和反序列化消息。

我会另外写一篇如何将 Avro 和 Kafka 集成的文章。

独立消费者

有时候,我们的场景很简单,

我们只需要一个消费者从一个主题的所有分区或者特定分区读取数据。

这时候就不再需要消费群组和再均衡了,

只需要把主题或者分区分配给消费者,然后开始读取消息并提交偏移量。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可以使用 KafkaJava 客户端 API 来实现统计 Kafka Topic 每小时的偏移量,具体的实现步骤如下: 1. 使用 KafkaJava 客户端 API 创建一个 KafkaConsumer 实例,配置相关的属性,如 Kafka 集群的地址、消费者组、序列化和反序列化器等。 2. 使用 KafkaConsumer 的 assign 方法手动分配 TopicPartition,获取每个 TopicPartition 的当前偏移量(offset)。 3. 使用 KafkaConsumer 的 seek 方法将每个 TopicPartition 的偏移量定位到上一个小时的开始位置,即当前时间的前一个小时。 4. 启动一个定时任务,每小时执行一次,将每个 TopicPartition 的偏移量统计出来,输出到日志或其他存储介质中。 下面是一个简单的示例代码: ```java import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.common.TopicPartition; import java.time.Duration; import java.util.Collections; import java.util.Properties; public class KafkaOffsetStatistics { public static void main(String[] args) { Properties props = new Properties(); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka1:9092,kafka2:9092,kafka3:9092"); props.put(ConsumerConfig.GROUP_ID_CONFIG, "offset-statistics"); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer"); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer"); KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props); consumer.subscribe(Collections.singletonList("test-topic")); for (TopicPartition partition : consumer.assignment()) { long currentOffset = consumer.position(partition); consumer.seek(partition, getStartOffset(currentOffset)); } while (true) { for (ConsumerRecord<String, String> record : consumer.poll(Duration.ofSeconds(1))) { // 处理消息 } } } private static long getStartOffset(long currentOffset) { long now = System.currentTimeMillis(); long lastHour = now - 3600 * 1000; long startOffset = currentOffset; while (startOffset >= 0) { consumer.seek(partition, startOffset); ConsumerRecord<String, String> record = consumer.poll(Duration.ofMillis(100)).iterator().next(); long timestamp = record.timestamp(); if (timestamp < lastHour) { return startOffset + 1; } startOffset--; } return 0; } private static void statisticsOffset(KafkaConsumer<String, String> consumer) { for (TopicPartition partition : consumer.assignment()) { long currentOffset = consumer.position(partition); long startOffset = getStartOffset(currentOffset); long hourOffset = currentOffset - startOffset; System.out.println(String.format("Topic %s, Partition %d, Hourly Offset %d", partition.topic(), partition.partition(), hourOffset)); } } } ``` 这段代码会启动一个 KafkaConsumer 实例,订阅一个主题(test-topic),然后手动分配每个 TopicPartition 的偏移量,并将其定位到上一个小时的开始位置。然后启动一个循环,每秒钟轮询一次 Kafka 集群获取消息,处理完消息后调用 statisticsOffset 方法统计每个 TopicPartition 的偏移量。getStartOffset 方法实现了根据当前偏移量计算上一个小时的开始位置的逻辑。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值