Kafka05 - Kafka消费者

Kafka消费者

消费方式

Pull模式,consumer主动从broker中拉取数据【Kafka采取的是这种方式】

  • Pull模式的不足之处就是如果Kafka没有数据,消费者可能会陷入循环之中,一直返回空的数据
  • 可以通过参数设置,consumer拉取数据为空或者没有达到一定数量的时候进行阻塞

Push模式,Kafka没有采取这种方式,因为这种方式broker决定了消息发送的速率,很难适应所有的消费者的消费的速率

一:Kafka消费者工作流程

1:消费者总体工作流程

  1. 每一个消费者的offset由消费者提交都系统主题尽心保存
  2. 每一个分区的数据只能由消费者组中的一个消费者进行消费
  3. 一个消费者可以消费多个分区的数据

2:消费者组原理

消费者组(Consumer Group),由多个consumer组成,形成一个消费者组的条件就是所有的消费者的groupId是相同的

  • 消费者组内的每一个消费者负责消费不同分区的数据一个分区只能由一个组内的消费者进行消费
  • 消费者组之间互不影响,所有的消费者都属于某一个消费者组,也就是消费者组是逻辑上的一个订阅者
  • 如果向消费者组中添加了更多的消费者,并且consumer.size > partition.size时,有一部分消费者就会空闲,不会消费任何的消息
  • 一个分区只能被一个消费者组中的消费者消费,而不是一个消费者组中的一个消费者只能消费一个分区的消息,一个消费者可能消费多个分区
2.1:消费者组初始化
  1. coor选一个Leader
  2. 告诉Leader要消费那个topic
  3. Leader定制消费方案
  4. 将消费方案发给coor
  5. 通知各个consumer

在这里插入图片描述

2.2:消息消费流程(重要)
  1. 创建消费者网络连接客户端ConsumerNetworkClient,主要用来跟kafka集群进行交互

    1. 前期做准备工作,首先调用sendFetches()方法,抓取数据初始化【Fetch.min.bytes, fetch.max.wait.ms, Fetch.max.bytes
    2. Fetch.min.bytes,要抓取数据的大小,默认是1字节
    3. fetch.max.wait.ms:一批数据中最大的等待时间,默认是500ms
    4. Fetch.max.bytes:每一个批次的最大抓取大小,默认是50M
  2. send()方法发送请求

  3. 发送完成之后,通过回调方法complatedFetches(queue)将发送结果传递给回调

  4. 消费者每次从回调队列中拉取指定条数的消息【默认的最大拉取条数是500条】

  5. 反序列化

  6. 经过拦截器:处理数据的核心位置 + 监控

  7. 处理数据

2.3:消费者重要参数
参数名称描述
bootstrap.servers向Kafka集群建立初始连接用到的host/port列表。
key.deserializer,value.deserializer指定接收消息的key和value的反序列化类型。一定要写全类名。
group.id(重要)标记消费者所属的消费者组。
enable.auto.commit(重要)默认值为true,消费者会自动周期性地向服务器提交偏移量。
auto.commit.interval.ms(重要)如果设置了 enable.auto.commit 的值为true,则该值定义了消费者偏移量向Kafka提交的频率,默认5s。
auto.offset.reset当Kafka中没有初始偏移量或当前偏移量在服务器中不存在,该如何处理?
earliest:自动重置偏移量到最早的偏移量。
latest:默认,自动重置偏移量为最新的偏移量。
none:如果消费组原来的(previous)偏移量不存在,则向消费者抛异常。
anything:向消费者抛异常。
offsets.topic.num.partitions__consumer_offsets的分区数,默认是50个分区。
heartbeat.interval.msKafka消费者和coordinator之间的心跳时间,默认3s。
该条目的值必须小于session.timeout.ms ,也不应该高于session.timeout.ms的1/3。
session.timeout.msKafka消费者和coordinator之间连接超时时间,默认45s。
超过该值,该消费者被移除,消费者组执行再平衡。
max.poll.interval.ms消费者处理消息的最大时长,默认是5分钟。
超过该值,该消费者被移除,消费者组执行再平衡。
fetch.min.bytes(重要)默认1个字节。消费者获取服务器端一批消息最小的字节数。
fetch.max.wait.ms(重要)默认500ms。
如果没有从服务器端获取到一批数据的最小字节数。该时间到,仍然会返回数据。
fetch.max.bytes(重要)默认Default: 52428800(50 m)。消费者获取服务器端一批消息最大的字节数。
如果服务器端一批次的数据大于该值(50m)仍然可以拉取回来这批数据,因此,这不是一个绝对最大值。
一批次的大小受message.max.bytes (broker config)or max.message.bytes (topic config)影响。
max.poll.records一次poll拉取数据返回消息的最大条数,默认是500条。

二:消费者API(用)

依赖引入部分和生产者部分参见Kafka生产者

1:独立消费者案例(订阅主题)

一个消费者组中只有一个消费者,也就是这一个消费者定义一个主题

在这里插入图片描述

在消费者API代码中必须配置消费者组id。

命令行启动消费者不填写消费者组id会被自动填写随机的消费者组id

公共配置工具类

package com.example.kafkademo.util;

import org.apache.kafka.clients.producer.ProducerConfig;

import java.util.Properties;

public class CommonUtils {
    /**
     * kafka生产者配置配置
     * @return 配置内容
     */
    public static Properties buildKafkaProperties() {
        // 1. 创建kafka生产者配置对象
        Properties properties = new Properties();
        // 2. 给kafka的配置对象添加信息
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
        // key, value初始化【必须有】
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");

        // =========> 添加自定义分区器 <============
        // properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, "com.example.kafkademo.producer.MyPartitioner")

        return properties;
    }
}

独立消费者订阅主题示例

package com.example.kafkademo.consumer;

import com.example.kafkademo.util.CommonUtils;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Properties;

/**
 * 独立消费者案例(订阅主题)
 */
public class SingleConsumerCustomTopic {
    public static void main(String[] args) {
        Properties properties = CommonUtils.buildKafkaProperties();
        // 配置消费者组(组名任意起名) 必须
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
        // 创建消费者对象
        try (KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties)) {
            // 注册要消费的主题[一个消费者可以消费多个主题]
            ArrayList<String> topicList = new ArrayList<>();
            topicList.add("first"); // 消费first主题
            kafkaConsumer.subscribe(topicList); // 订阅主题
            // 拉取数据并打印
            while (true) {
                // 设置1s消费一批数据
                ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1L));
                // 打印消费到的数据
                for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                    System.out.println(consumerRecord);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
       
    }
}

2:独立消费者案例(订阅分区)

在这里插入图片描述

package com.example.kafkademo.consumer;

import com.example.kafkademo.util.CommonUtils;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.TopicPartition;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

public class SingleConsumerCustomPartitions {
    public static void main(String[] args) {
        Properties properties = CommonUtils.buildKafkaProperties();
        // 配置消费者组
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
        try (KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties)) {
            // ==========> 消费某个主题的某个分区的数据 <============
            List<TopicPartition> topicPartitionList = new ArrayList<>();
            // 指定主题和分区 + assign() -> first主题 & 0号分区
            topicPartitionList.add(new TopicPartition("first", 0));
            // 注意这里是assign()
            kafkaConsumer.assign(topicPartitionList);
            // 准备进行消费
            while (true) {
                ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1L));
                for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                    System.out.println(consumerRecord);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3:消费者组实例(多消费者实例)

测试同一个主题的分区数据,只能由一个消费者组中的一个消费

在这里插入图片描述
复制一份基础消费者的代码,在IDEA中同时启动,即可启动同一个消费者组中的两个消费者

package com.example.kafkademo.consumer;

import com.example.kafkademo.util.CommonUtils;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Properties;

/**
 * 再启动一个
 */
public class SingleConsumerCustomTopicSecond {
    public static void main(String[] args) {
        Properties properties = CommonUtils.buildKafkaProperties();
        // 配置消费者组(组名任意起名) 必须
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
        // 创建消费者对象
        try (KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties)) {
            // 注册要消费的主题[一个消费者可以消费多个主题]
            ArrayList<String> topicList = new ArrayList<>();
            topicList.add("first"); // 消费first主题
            kafkaConsumer.subscribe(topicList); // 订阅主题
            // 拉取数据并打印
            while (true) {
                // 设置1s消费一批数据
                ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1L));
                // 打印消费到的数据
                for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                    System.out.println(consumerRecord);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        
    }
}

启动代码中的生产者发送消息,在IDEA控制台即可看到两个消费者在消费不同分区的数据

如果只发生到一个分区,可以在发送时增加延迟代码Thread.sleep(2);

ConsumerRecord(topic = first, partition = 0, leaderEpoch = 2, offset = 3, CreateTime = 1629169606820, serialized key size = -1, serialized value size = 8, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = hello1)
ConsumerRecord(topic = first, partition = 1, leaderEpoch = 3, offset = 2, CreateTime = 1629169609524, serialized key size = -1, serialized value size = 6, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = hello2)
ConsumerRecord(topic = first, partition = 2, leaderEpoch = 3, offset = 21, CreateTime = 1629169611884, serialized key size = -1, serialized value size = 6, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = hello3)

⚠️ 重新发送到一个全新的主题中,由于默认创建的主题分区数为1,可以看到只能有一个消费者消费到数据

三:分区的分配和再平衡

1:分配分区的前提

首先kafka设定了默认的消费逻辑:一个分区只能被同一个消费组(ConsumerGroup)内的一个消费者消费。
在这个消费逻辑设定下,假设目前某消费组内只有一个消费者C0,订阅了一个topic,这个topic包含6个分区,也就是说这个消费者C0订阅了6个分区,这时候可能会发生下列三种情况:

  • 如果这时候消费者组内新增了一个消费者C1,这个时候就需要把之前分配给C0的6个分区拿出来3个分配给C1;
  • 如果这时候这个topic多了一些分区,就要按照某种策略,把多出来的分区分配给C0和C1;
  • 如果这时候C1消费者挂掉了或者退出了,不在消费者组里了,那所有的分区需要再次分配给C0

在这里插入图片描述

参数名称描述
heartbeat.interval.msKafka消费者和coordinator之间的心跳时间,默认3s。
该条目的值必须小于session.timeout.ms,也不应该高于session.timeout.ms 的1/3。
session.timeout.msKafka消费者和coordinator之间连接超时时间,默认45s。
超过该值,该消费者被移除,消费者组执行再平衡。
max.poll.interval.ms消费者处理消息的最大时长,默认是5分钟。
超过该值,该消费者被移除,消费者组执行再平衡。
partition.assignment.strategy消费者分区分配策略,默认策略是Range + CooperativeSticky。
Kafka可以同时使用多个分区分配策略。
可以选择的策略包括:Range、RoundRobin、Sticky、CooperativeSticky

2:Range策略

在这里插入图片描述

3:RoundRobin策略

在各个消费者中修改分区策略

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

在这里插入图片描述

4:Sticky策略

粘性分区定义:在执行一次新的分配之前,考虑上一次分配的结果,尽量少的调整分配的变动,可以节省大量的开销。

粘性分区是Kafka从0.11.x版本开始引入这种分配策略

首先会尽量均衡的放置分区到消费者上面,在出现同一消费者组内消费者出现问题的时候,会尽量保持原有分配的分区不变化。

Sticky分配策略的原理比较复杂,它的设计主要实现了两个目的:

  • 分区的分配要尽可能的均匀;
  • 分区的分配尽可能的与上次分配的保持相同。

如果这两个目的发生了冲突,优先实现第一个目的。

四:offset位移

1:offset的默认维护位置

在这里插入图片描述

  • __consumer_offsets主题里面采用key和value的方式存储数据。

  • key是group.id+topic+分区号,value就是当前offset的值。

  • 每隔一段时间,kafka内部会对这个topic进行compact,也就是每个group.id+topic+分区号就保留最新数据。

消费offset案例

思想:__consumer_offsets为Kafka中的topic,那就可以通过消费者进行消费

在配置文件config/consumer.properties中添加配置exclude.internal.topics=false,默认是true,表示不能消费系统主题。

为了查看该系统主题数据,所以该参数修改为false

采用命令行方式,创建一个新的topic

bin/kafka-topics.sh --bootstrap-server hadoop102:9092 
    --create --topic kafkarep
    --partitions 2 
    --replication-factor 2

启动生产者往kafkarep生产数据

bin/kafka-console-producer.sh --topic kafkarep --bootstrap-server hadoop102:9092

启动消费者消费kafkarep中的数据

bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 
    --topic kafkarep
    --group test

🎉 指定消费者组名称,就可以更好观察数据存储位置(因为key是group.id+topic+分区号)

[hadoop102 kafka]$ bin/kafka-console-consumer.sh 
	--topic __consumer_offsets 
	--bootstrap-server  hadoop102:9092 
	--consumer.config config/consumer.properties  
	--formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter" 
	--from-beginning
[offset,kafkarep,1]::OffsetAndMetadata(offset=7, leaderEpoch=Optional[0], metadata=, commitTimestamp=1622442520203, expireTimestamp=None)
[offset,kafkarep,0]::OffsetAndMetadata(offset=8, leaderEpoch=Optional[0], metadata=, commitTimestamp=1622442520203, expireTimestamp=None)

2:自动提交offset

为了使我们可以能够专注于自己的业务逻辑,Kafka提供了自动自交的offset的功能,自动提交offset的相关参数:

  • enable.auto.commit:是否开启自动提交的offset功能,默认是true
  • auto.commit.interval.ms:自动提交offset的时间间隔,默认是5s
参数名称描述
enable.auto.commit默认值为true,消费者会自动周期性地向服务器提交偏移量。
auto.commit.interval.ms如果设置了enable.auto.commit的值为true,则该值定义了消费者偏移量向Kafka提交的频率,默认5s。
// 配置消费者组
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
// 是否自动提交offset
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
// 提交offset的时间周期是1000ms, 默认是5s
properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 1000);

3:手动提交offset

虽然自动提交offset十分简单便利,但是由于其是基于时间提交的,开发人员很难把握offset提交的时机,因此Kafka还提供了手动提交offset的API

手动提交有两种方式:分别是同步提交和异步提交

  • 同步提交(commitSync):必须等待offset提交完毕,再去消费下一批数据
  • 异步提交(commitAsync):发送完成提交offset请求之后,就开始消费下一批数据了

二者的共同点是都将会提交最高的偏移量,区别是同步提交会阻塞当前的线程并可以进行重试,异步提交不会阻塞线程同时没有失败重试机制,可能提交失败

同步手动提交(commitSync)

由于同步提交offset有失败重试机制,故更加可靠,但是由于一直等待提交结果,提交的效率比较低

try (KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties)) {
    // 订阅要消费的主题
    consumer.subscribe(Collections.singletonList("first"));
    // 消费消息
    while (true) {
        ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1L));
        for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
            System.out.println(consumerRecord.value());
        }
        // 同步手动提交
        consumer.commitSync();
    }
} catch (Exception e) {
    e.printStackTrace();
}

异步手动提交

虽然同步提交offset更可靠一些,但是由于其会阻塞当前线程,直到提交成功。因此吞吐量会受到很大的影响。

因此更多的情况下,会选用异步提交offset的方式

try (KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties)) {
    // 订阅要消费的主题
    consumer.subscribe(Collections.singletonList("first"));
    // 消费消息
    while (true) {
        ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1L));
        for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
            System.out.println(consumerRecord.value());
        }
        // 异步手动提交
        consumer.commitAsync();
    }
} catch (Exception e) {
    e.printStackTrace();
}

4:指定Offset消费

auto.offset.reset = earliest | latest | none 默认是latest

当Kafka中没有初始偏移量(消费者组第一次消费)或服务器上不再存在当前偏移量时(例如该数据已被删除),该怎么办?

(1)earliest:自动将偏移量重置为最早的偏移量,–from-beginning。

(2)latest(默认值):自动将偏移量重置为最新偏移量。

(3)none:如果未找到消费者组的先前偏移量,则向消费者抛出异常

(4)任意指定offset位开始消费

核心就是先拿到有那些分区assignment = kafkaConsumer.assignment();,然后便利这些分区,手动的kafkaConsumer.seek(指定分区,指定offset)

// 订阅要消费的主题
kafkaConsumer.subscribe(Collections.singletonList("first"));

Set<TopicPartition> assignment= new HashSet<>();
while (assignment.size() == 0) {
    kafkaConsumer.poll(Duration.ofSeconds(1));
    // 获取消费者分区分配信息(有了分区分配信息才能开始消费)
    assignment = kafkaConsumer.assignment();
}
// 遍历所有分区,并指定offset从1700的位置开始消费
for (TopicPartition tp: assignment) {
    kafkaConsumer.seek(tp, 1700);
}

指定时间消费

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

// 订阅要消费的主题
kafkaConsumer.subscribe(Collections.singletonList("first"));

// ==============> begin <==================
Set<TopicPartition> assignment = new HashSet<>();
while (assignment.size() == 0) {
    kafkaConsumer.poll(Duration.ofSeconds(1));
    // 获取消费者分区分配信息(有了分区分配信息才能开始消费)
    assignment = kafkaConsumer.assignment();
}
HashMap<TopicPartition, Long> timestampToSearch = new HashMap<>();
// 封装集合存储,每个分区对应一天前的数据
for (TopicPartition topicPartition : assignment) {
    timestampToSearch.put(topicPartition, System.currentTimeMillis() - 24 * 3600 * 1000);
}
// 获取从1天前开始消费的每个分区的offset
Map<TopicPartition, OffsetAndTimestamp> offsets = kafkaConsumer.offsetsForTimes(timestampToSearch);

// 遍历每个分区,对每个分区设置消费时间。
for (TopicPartition topicPartition : assignment) {
    OffsetAndTimestamp offsetAndTimestamp = offsets.get(topicPartition);
    // 根据时间指定开始消费的位置
    if (offsetAndTimestamp != null){
        kafkaConsumer.seek(topicPartition, offsetAndTimestamp.offset());
    }
}
// ==============> end <==================

5:漏消费和重复消费

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

漏消费:先提交offset后消费,有可能会造成数据的漏消费。

在这里插入图片描述

五:生产经验

1:消费者事务

如果想完成Consumer端的精准一次性消费,那么需要Kafka消费端将消费过程和提交offset过程做原子绑定。

此时我们需要将Kafka的offset保存到支持事务的自定义介质(比如MySQL)

2:数据积压(消费者如何提高吞吐量)

  1. 如果是Kafka的消费能力不足,则可以考虑增加Topic的分区数,并且同时提升消费组的消费者的数量,消费者数 = 分区数
  2. 如果是下游的数据处理不及时,提高每一个批次拉取的数量。如果批次拉取的数据过少(拉取数据/处理时间 < 生产速度),使得处理的数据小于生产的数据,也会造成数据积压【比如一次最多拉取500条 -> 一次最多拉取1000条】

在这里插入图片描述

参数名称描述
fetch.max.bytes默认Default: 52428800(50M)。消费者获取服务器端一批消息最大的字节数。
如果服务器端一批次的数据大于该值(50m)仍然可以拉取回来这批数据,因此,这不是一个绝对最大值。
一批次的大小受message.max.bytes[broker config]或 max.message.bytes[topic config]影响。
max.poll.records一次poll拉取数据返回消息的最大条数,默认是500条
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值