Kafka知识点总结

Kafka

1.Kafka概述

1.1 定义

Kafka传统定义:Kafka是一个分布式的基于发布/订阅模式的消息队列(MessageQueue),主要应用于大数据实时处理领域。
发布/订阅:消息的发布者不会将消息直接发送给特定的订阅者,而是将发布的消息分为不同的类别,订阅者只接收感兴趣的消息
Kafka最新定义 : Kafka是 一个开源的分布式事件流平台(Event StreamingPlatform),被数千家公司用于高性能数据管道、流分析、数据集成和关键任务应用。

1.2 消息队列

目前企业中比较常见的消息队列产品主要有 Kafka、ActiveMQ 、RabbitMQ、 RocketMQ 等。
在大数据场景主要采用 Kafka 作为消息队列。在 JavaEE 开发中主要采用 ActiveMQ、RabbitMQ、RocketMQ。

1.2.1 传统消息队列的应用场景

传统的消息队列的主要应用场景包括:缓存/消峰解耦异步通信
缓冲/消峰:有助于控制和优化数据流经过系统的速度,解决生产消息和消费消息的处理速度不一致的情况。
在这里插入图片描述
解耦:允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。
在这里插入图片描述
异步通信允许用户把一个消息放入队列,但并不立即处理它,然后在需要的时候再去处理它们。

在这里插入图片描述

1.2.2 消息队列的两种模式

  1. 点对点模式
    • 消费者主动拉取数据,消息收到后清除消息

在这里插入图片描述

  1. 发布/订阅模式
  • 可以有多个消费主题(浏览、点赞、收藏、评论等)
  • 消费者消费数据之后,不删除数据
  • 每个消费者相互独立,都可以消费到数据
    在这里插入图片描述

1.3 kafka基础架构

在这里插入图片描述

  1. Producer:消息生产者,就是向 Kafka broker 发消息的客户端。
  2. Consumer:消息消费者,向 Kafka broker 取消息的客户端。
  3. Consumer Group(CG):消费者组,由多个 consumer 组成。消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费;消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
  4. Broker:一台 Kafka 服务器就是一个 broker。一个集群由多个 broker 组成。一个broker 可以容纳多个 topic。
  5. Topic:可以理解为一个队列,生产者和消费者面向的都是一个 topic。
  6. Partition:为了实现扩展性,一个非常大的 topic 可以分布到多个 broker(即服务器)上,一个 topic 可以分为多个 partition,每个 partition 是一个有序的队列。
  7. Replica:副本。一个 topic 的每个分区都有若干个副本,一个 Leader 和若干个Follower。
  8. Leader:每个分区多个副本的“主”,生产者发送数据的对象,以及消费者消费数据的对象都是 Leader。
  9. Follower:每个分区多个副本中的“从”,实时从 Leader 中同步数据,保持和Leader 数据的同步。Leader 发生故障时,某个 Follower 会成为新的 Leader。

1.4 为什么kafka性能非常高

  1. 使用到零拷贝技术属于linux操作系统内核自带;
  2. 顺序读写
  3. 分区模型架构
  4. 生产者投递消息批量投递模型
  5. 消息压缩

2.快速入门

2.1 kafka环境安装

  1. wget下载安装包
wget https://archive.apache.org/dist/kafka/3.0.0/kafka_2.12-3.0.0.tgz
  1. 解压
tar -zxvf kafka_2.12-3.0.0.tgz
# 改名
mv kafka_2.12-3.0.0 kafka
# 创建zk存储路径 和 kafka存储路径
mkdir -p /data/software/kafka/zk/datas
mkdir -p /data/software/kafka/datas

  1. 修改配置文件
    新版的kafka安装包中已经自带了 zookeeper的安装包,所以首先配置zookeeper.properties配置文件
vim /data/software/kafka/config/zookeeper.properties

#数据存放路径
dataDir=/data/software/kafka/zk/datas
# the port at which the clients will connect
clientPort=2181
# disable the per-ip limit on the number of connections since this is a non-production config
maxClientCnxns=0
# Disable the adminserver by default to avoid port conflicts.
# Set the port to something non-conflicting if choosing to enable this
admin.enableServer=false
# admin.serverPort=8080

修改kafka的server.properties配置文件

#配置kafka的server.properties配置文件
vim /data/software/kafka/config/server.properties

# broker 的编号,如果集群中有多个 broker,则每个 broker 的编号需要设置的不同
broker.id=0
#存放消息数据的文件地址
log.dirs=/data/software/kafka/datas
#如果允许外网访问,需要配置 advertised.listeners=PLAINTEXT://<公网IP>:9092
listeners=PLAINTEXT://172.31.186.177:9092
#注意:外网访问
advertised.listeners=PLAINTEXT://47.243.118.226:9092
  1. kafka配置文件详解
//broker.id是kafka broker的编号,集群里每个broker的id需不同,默认为0,如果使用集群,递增该编号即可。
broker.id=0

//如果允许外网访问,需要配置 advertised.listeners=PLAINTEXT://<公网IP>:9092
listeners=PLAINTEXT://<内网IP>:9092
//注意:外网访问
listeners=PLAINTEXT://<内网IP>:9092
advertised.listeners=PLAINTEXT://<公网IP>:9092

//设置Zookeeper地址,如果Zookeeper采用了集群模式,多个地址间使用英文逗号隔开
zookeeper.connect=192.168.123.102:2181,192.168.123.103:2181,192.168.123.104:2181

//num.partitions 为新建Topic的默认Partition(分区)数量,默认为1,partition数量提升,一定程度上可以提升并发性
num.partitions=1

如果配置多个目录,新创建的topic他把消息持久化的地方是,当前以逗号分割的目录中,那个分区数最少就放那一个
log.dirs=/data/software/kafka/datas

//这个是borker进行网络处理的线程数
num.network.threads=3

//这个是borker进行I/O处理的线程数
num.io.threads=8

//发送缓冲区buffer大小,数据不是一下子就发送的,先回存储到缓冲区了到达一定的大小后在发送,能提高性能
socket.send.buffer.bytes=102400

//kafka接收缓冲区大小,当数据到达一定大小后在序列化到磁盘
socket.receive.buffer.bytes=102400

//这个参数是向kafka请求消息或者向kafka发送消息的请请求的最大数,这个值不能超过java的堆栈大小
socket.request.max.bytes=104857600

//每个数据目录用来日志恢复的线程数目
num.recovery.threads.per.data.dir=1

//默认消息的最大持久化时间,168小时,7天
log.retention.hours=168

//轮转时间,当需要删除指定小时之前的数据时,该设置项很重要
log.roll.hours=12

//这个参数是:因为kafka的消息是以追加的形式落地到文件,当超过这个值的时候,kafka会新起一个文件
log.segment.bytes=1073741824

//每隔300000毫秒去检查上面配置的log失效时间
log.retention.check.interval.ms=300000

//是否启用log压缩,一般不用启用,启用的话可以提高性能
log.cleaner.enable=false

//设置zookeeper的连接超时时间
zookeeper.connection.timeout.ms=6000

  1. 启动
    启动zk
/data/software/kafka/bin/zookeeper-server-start.sh -daemon /data/software/kafka/config/zookeeper.properties

启动kafka

/data/software/kafka/bin/kafka-server-start.sh -daemon /data/software/kafka/config/server.properties

jps -l查看kafka是否启动成功
在这里插入图片描述

  1. 测试
  • 生产者发送消息
    主题是 fb,会自动创建 fb 的主题
    cd 到kafka的bin目录下执行 :
    ./kafka-console-producer.sh --broker-list ip:9092 --topic fb
    在这里插入图片描述
  • 消费者接受消息
    bin/kafka-console-consumer.sh --bootstrap-server ip:9092 --topic fb --from-beginning
    –from-beginning 从头开始将历史消息也读出来
    在这里插入图片描述
    之前操作了一下,生产者和消费者的消息可能不一样
  1. 停止
  • 先停止kafka
    /data/software/kafka/bin/kafka-server-stop.sh
  • 再停止zk
    /data/software/kafka/bin/zookeeper-server-stop.sh

2.2 kafka命令行操作参数

2.2.1 主题命令行操作

  1. 查看操作主题命令参数
./kafka-topics.sh
参数描述
–bootstrap-server <String: server toconnect to>连接的 Kafka Broker 主机名称和端口号。
–topic <String: topic>操作的 topic 名称。
–create创建主题。
–delete删除主题。
–alter修改主题。
–list查看所有主题。
–describe查看主题详细描述。
–partitions <Integer: # of partitions>设置分区数。
–replication-factor<Integer: replication factor>设置分区副本。
–config <String: name=value>更新系统默认的配置。
  1. 查看当前服务器中的所有 topic
./kafka-topics.sh --bootstrap-server 192.168.44.211:9092 --list
  1. 创建 first topic
./kafka-topics.sh --bootstrap-server 192.168.44.211:9092 --topic first --create --partitions 1 --replication-factor 1

在这里插入图片描述
选项说明:

  • topic 定义 topic 名
  • replication-factor 定义副本数
  • partitions 定义分区数
  1. 查看 first 主题的详情
./kafka-topics.sh --bootstrap-server 127.0.0.1:9092 --topic first --describe
  1. 修改分区数(注意:分区数只能增加,不能减少)
 ./kafka-topics.sh --bootstrap-server 127.0.0.1:9092 --topic first --alter --partitions 2	

6)再次查看 first 主题的详情

 ./kafka-topics.sh --bootstrap-server 127.0.0.1:9092 --topic first --describe

7)删除 topic

./kafka-topics.sh --bootstrap-server 127.0.0.1:9092 --topic first --delete

在这里插入图片描述

2.2.2 生产者命令行操作

  1. 查看操作生产者命令参数
./kafka-console-producer.sh
参数描述
–bootstrap-server <String: server toconnect to>连接的 Kafka Broker 主机名称和端口号。
–topic <String: topic>操作的 topic 名称。
  1. 发送消息
./kafka-console-producer.sh --bootstrap-server 127.0.0.1:9092 --topic first
>1
>2

2.2.3 消费者命令行操作

  1. 查看操作消费者命令参数
./kafka-console-consumer.sh
参数描述
–bootstrap-server <String: server toconnect to>连接的 Kafka Broker 主机名称和端口号。
–topic <String: topic>操作的 topic 名称。
–from-beginning从头开始消费。
–group <String: consumer group id>指定消费者组名称。
  1. 消费消息
  • 消费 first 主题中的数据。
./kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic first
  • 把主题中所有的数据都读取出来(包括历史数据)。
./kafka-console-consumer.sh --bootstrap-server 127.0.0.1:9092 --from-beginning --topic first

3.Kafka 生产者

3.1 生产者消息发送流程

3.1.1 发送原理

在消息发送的过程中,涉及到了两个线程——main 线程和 Sender 线程。在 main 线程中创建了一个双端队列 RecordAccumulator。main 线程将消息发送给 RecordAccumulator,Sender 线程不断从 RecordAccumulator 中拉取消息发送到 Kafka Broker。
生产过程由两个线程协调运行,分别为主线程和sender线程(发送线程)。

主线程中,由KafkaProducer创建消息,然后通过可能的拦截器、序列化器和分区器的作用,缓存消息到消息加载器(RecordAccumulator,也称为消息收集器)中的缓存队列,这个队列默认32M。当满足以下两个条件的任意一个
条件一:消息累计达到batch.size,默认是16kb。
条件二:等待时间达到linger.ms,默认是0毫秒。
所以在默认情况下,由于等待时间是0毫秒,所以只要消息来一条就会发送一条。Sender线程负责从消息加载器(RecordAccumulator)中获取消息并将其发送到Kafka中。
在这里插入图片描述

3.2 异步发送 API

3.2.1 普通异步发送

需求:创建 Kafka 生产者,采用异步的方式发送到 Kafka Broker

  1. 导入依赖
    <dependencies>
        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka-clients</artifactId>
            <version>3.0.0</version>
        </dependency>
    </dependencies>
  1. 编写不带回调函数的 API 代码
    public static void main(String[] args) {
        // 0配置
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.44.211:9092");
        //序列化
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        //1.创建kafka的生产者
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);
        //2.发送数据
        for (int i = 0; i < 5; i++) {
            kafkaProducer.send(new ProducerRecord<>("first", "kafka" + i));
            System.out.println(i);
        }
        //3.关闭资源
        kafkaProducer.close();
    }

  1. 测试结果
    在这里插入图片描述

3.2.2 带回调函数的异步发送

回调函数会在 producer 收到 ack 时调用,为异步调用,该方法有两个参数,分别是元数据信息(RecordMetadata)和异常信息(Exception),如果 Exception 为 null,说明消息发送成功,如果 Exception 不为 null,说明消息发送失败。

    public static void main(String[] args) {
        // 0配置
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.44.211:9092");
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        //1.创建kafka的生产者
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);
        //2.发送数据
        for (int i = 0; i < 5; i++) {
            kafkaProducer.send(new ProducerRecord<>("first", "kafka" + i), new Callback() {
                @Override
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    if(e == null){
                        System.out.println("主题:"+recordMetadata.topic()+"分区:"+recordMetadata.partition());
                    }else {
                        e.printStackTrace();
                    }
                }
            });
            System.out.println(i);
        }
        //3.关闭资源
        kafkaProducer.close();
    }

3.4 生产者分区

3.4.1 分区好处

  1. 便于合理使用存储资源,每个Partition在一个Broker上存储,可以把海量的数据按照分区切割成一块一块数据存储在多台Broker上。合理控制分区的任务,可以实现负载均衡的效果。
  2. 提高并行度,生产者可以以分区为单位发送数据;消费者可以以分区为单位进行消费数据。

在这里插入图片描述

3.4.2 生产者发送消息的分区策略

  1. 指明partition的情况下,直接将指明的值作为partition值;例如partition=0,所有数据写入分区0
  2. 没有指明partition值但有key的情况下,将key的hash值与topic的partition数进行取余得到partition值;
    例如:key1的hash值=5, key2的hash值=6 ,topic的partition数=2,那么key1 对应的value1写入1号分区,key2对应的value2写入0号分区。
  3. 既没有partition值又没有key值的情况下,Kafka采用Sticky Partition(黏性分区器),会随机选择一个分区,并尽可能一直使用该分区,待该分区的batch已满或者已完成,Kafka再随机一个分区进行使用(和上一次的分区不同)。
    例如:第一次随机选择0号分区,等0号分区当前批次满了(默认16k)或者linger.ms设置的时间到, Kafka再随机一个分区进行使用(如果还是0会继续随机)。

同一个业务数据放在同一个分区,可以指定相同的分区或者不指定分区key相同

  • 案例一
    将数据发往指定 partition 的情况下,例如,将所有数据发往分区 1 中。
    public static void main(String[] args) {
        // 0配置
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.44.211:9092");
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        //1.创建kafka的生产者
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);
        //2.发送数据
        for (int i = 0; i < 5; i++) {
            kafkaProducer.send(new ProducerRecord<>("first",1, "1","kafka" + i), new Callback() {
                @Override
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    if(e == null){
                        System.out.println("主题:"+recordMetadata.topic()+"分区:"+recordMetadata.partition());
                    }else {
                        e.printStackTrace();
                    }
                }
            });
            System.out.println(i);
        }
        //3.关闭资源
        kafkaProducer.close();
    }

3.4.3 自定义分区器

如果研发人员可以根据企业需求,自己重新实现分区器。

  1. 需求
    例如我们实现一个分区器实现,发送过来的数据中如果包含 kafka,就发往 0 号分区,不包含 kafka,就发往 1 号分区。
  2. 实现步骤
    (1)定义类实现 Partitioner 接口。
    (2)重写 partition()方法。
public class MyPartitioner implements Partitioner {

    /**
     * 返回信息对应的分区
     * @param topic 主题
     * @param key 消息的 key
     * @param keyBytes 消息的 key 序列化后的字节数组
     * @param value 消息的 value
     * @param valueBytes 消息的 value 序列化后的字节数组
     * @param cluster 集群元数据可以查看分区信息
     * @return
     */
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        //获取数据

        String s = value.toString();
        int partition;
        if(s.contains("kafka")){
            partition = 1;
        }else {
            partition = 0;
        }
        return partition;
    }

    //关闭资源
    @Override
    public void close() {

    }
    //配置方法
    @Override
    public void configure(Map<String, ?> configs) {

    }
}

(3) 使用分区器的方法,在生产者的配置中添加分区器参数。

public class CustomProducerCallbackPartitons {

    public static void main(String[] args) {
        // 0配置
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.44.211:9092");
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        //关联自定义分区器
        properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,MyPartitioner.class.getName());
        //1.创建kafka的生产者
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);
        //2.发送数据
        for (int i = 0; i < 5; i++) {
            kafkaProducer.send(new ProducerRecord<>("first","1","kafka" + i), new Callback() {
                @Override
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    if(e == null){
                        System.out.println("主题:"+recordMetadata.topic()+"分区:"+recordMetadata.partition());
                    }else {
                        e.printStackTrace();
                    }
                }
            });
            System.out.println(i);
        }
        //3.关闭资源
        kafkaProducer.close();
    }

}

3.4.4Kafka如何保证消息顺序一致性问题

  • 消息顺序一致性产生的背景:
    在kafka中我们会对mq实现分区、多个消费者集群消费消息,生产者投递的消息会给多个不同的消费者分摊消费消息,无法保证每个消息执行的先后顺序问题。

  • 解决消息顺序一致性问题核心思想:就是生产者投递消息,一定要存放在同一个分区中,最终被同一个消费者消费消息。

  • 既能够保证消息顺序一致性的问题,有能够提高效率,在kafka中就存在分区的架构模型实现一个消费者对应一个分区消费消息。

3.4.5 解决消息顺序一致性问题如何提高效率

  1. 增加多个不同的Partition分区 实现一个消费者对应一个Partition分区
  2. 绝大多数的项目使用到mq时候,是不需要保证消息顺序一致性问题。

3.5 生产经验——生产者如何提高吞吐量

在这里插入图片描述

  • batch.size:批次大小,默认16k
  • linger.ms:等待时间,修改为5-100ms一次拉一个,来了就走
  • compression.type:压缩snappy,提高吞吐量
  • RecordAccumulator:缓冲区大小,默认为32m
    是把双刃剑:如果参数过大会导致延迟比较高,视情况而定
        //缓冲区大小
        properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,33554432);
        //批次大小
        properties.put(ProducerConfig.BATCH_SIZE_CONFIG,16384);
        //压缩方式 gzip、snappy、lz4 和 zstd
        properties.put(ProducerConfig.COMPRESSION_TYPE_CONFIG,"snappy");
        //等待时间 默认0
        properties.put(ProducerConfig.LINGER_MS_CONFIG,1);
 public static void main(String[] args) {
        //0.配置
        Properties properties = new Properties();
        //连接kafka
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.44.211:9092");
        //序列化配置
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
        //缓冲区大小
        properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,33554432);
        //批次大小
        properties.put(ProducerConfig.BATCH_SIZE_CONFIG,16384);
        //压缩方式 gzip、snappy、lz4 和 zstd
        properties.put(ProducerConfig.COMPRESSION_TYPE_CONFIG,"snappy");
        //等待时间 默认0
        properties.put(ProducerConfig.LINGER_MS_CONFIG,1);
        //1.创建生产者
        KafkaProducer<Object, Object> kafkaProducer = new KafkaProducer<>(properties);
        //2.发送数据
        for(int i=0;i<5;i++){
            kafkaProducer.send(new ProducerRecord<>("first","kafka"+i));
        }
        //3.关闭资源
        kafkaProducer.close();
    }

3.6 生产经验——数据可靠性

3.6.1 ack应答原理

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
思考:Leader收到数据,所有Follower都开始同步数据,但有一个Follower,因为某种故障,迟迟不能与Leader进行同步,那这个问题怎么解决呢?
Leader维护了一个动态的in-sync replica set(ISR),意为和Leader保持同步的Follower+Leader集合(leader:0,isr:0,1,2)。
如果Follower长时间未向Leader发送通信请求或同步数据, 则该Follower将被踢出ISR。该时间阈值由replica.lag.time.max.ms数设定,默认30s。例如2超时,(leader:0,isr:0,1)。
这样就不用等长期联系不上或者已经故障的节点。

数据可靠性分析
如果分区副本设置为1个,或者ISR里应答的最小副本数量( min.insync.replicas 默认为1)设置为1,和ack=1的效果是一样的,仍然有丢数的风险(leader:0,isr:0)。

  • 数据完全可靠条件 = ACK级别设置为-1 + 分区副本大于等于2 + ISR里应答的最小副本数量大于等于2

可靠性总结:

  1. acks=0,生产者发送过来数据就不管了,可靠性差,效率高;
  2. acks=1,生产者发送过来数据Leader应答,可靠性中等,效率中等;
  3. acks=-1,生产者发送过来数据Leader和ISR队列里面所有Follwer应答,可靠性高,效率低;
    在生产环境中,acks=0很少使用;acks=1,一般用于传输普通日志,允许丢个别数据;acks=-1,一般用于传输和钱相关的数据,对可靠性要求比较高的场景。

数据重复性分析
在这里插入图片描述
配置

        //acks
        properties.put(ProducerConfig.ACKS_CONFIG,"1");
        //重试次数
        properties.put(ProducerConfig.RETRIES_CONFIG,3);
        public static void main(String[] args) {
        // 0配置
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.44.211:9092");
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        //acks
        properties.put(ProducerConfig.ACKS_CONFIG,"1");
        //重试次数 默认是只int的最大值 2147483647
        properties.put(ProducerConfig.RETRIES_CONFIG,3);
        //1.创建kafka的生产者
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);
        //2.发送数据
        for (int i = 0; i < 5; i++) {
            kafkaProducer.send(new ProducerRecord<>("first","kafka" + i));
            System.out.println(i);
        }
        //3.关闭资源
        kafkaProducer.close();
    }

3.7 生产经验——数据去重

数据重复的原因
Kafka生产者生产消息到partition,如果直接发送消息,kafka会将消息保存到分区中,但Kafka会返回一个ack给生产者,表示当前操作是否成功,是否已经保存了这条消息。如果ack响应的过程失败了,此时生产者会重试,继续发送没有发送成功的消息,Kafka又会保存一条一模一样的消息

3.7.1 数据传递语义

  • 至少一次(At Least Once)= ACK级别设置为-1 + 分区副本大于等于2 + ISR里应答的最小副本数量大于等于2
  • 最多一次(At Most Once)= ACK级别设置为0
  • 总结:
    • At Least Once可以保证数据不丢失,但是不能保证数据不重复;
    • At Most Once可以保证数据不重复,但是不能保证数据不丢失。
  • 精确一次(Exactly Once):对于一些非常重要的信息,比如和钱相关的数据,要求数据既不能重复也不丢失。

Kafka 0.11版本以后,引入了一项重大特性:幂等性和事务。

3.7.2 幂等性

  1. 幂等性原理
  • 幂等性就是指Producer不论向Broker发送多少次重复数据,Broker端都只会持久化一条,保证了不重复。
  • 精确一次(Exactly Once) = 幂等性 + 至少一次( ack=-1 + 分区副本数>=2 + ISR最小副本数量>=2) 。
  • 重复数据的判断标准:具有<PID, Partition, SeqNumber>相同主键的消息提交时,Broker只会持久化一条。其中PID是Kafka每次重启都会分配一个新的;Partition 表示分区号;Sequence Number是单调自增的。
  • 所以幂等性只能保证的是在单分区单会话内不重复。
    在这里插入图片描述
  1. 如何使用幂等性
    开启参数 enable.idempotence 默认为 true,false 关闭。

3.7.3 生产者事务

  1. Kafka 事务原理
    说明:开启事务,必须开启幂等性。
    在这里插入图片描述
  2. Kafka的事务API
// 1 初始化事务
void initTransactions();
// 2 开启事务
void beginTransaction() throws ProducerFencedException;
// 3 在事务内提交已经消费的偏移量(主要用于消费者)
void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets,String consumerGroupId) throws ProducerFencedException;
// 4 提交事务
void commitTransaction() throws ProducerFencedException;
// 5 放弃事务(类似于回滚事务的操作)
void abortTransaction() throws ProducerFencedException;
  1. 单个 Producer,使用事务保证消息的仅一次发送
    public static void main(String[] args) {
        // 0配置
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.44.211:9092");
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        //指定事务id
        properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG,"tranactional_id_01");
        //1.创建kafka的生产者
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);
        //初始化事务
        kafkaProducer.initTransactions();
        //开始事务
        kafkaProducer.beginTransaction();
        try {
            //2.发送数据
            for (int i = 0; i < 5; i++) {
                kafkaProducer.send(new ProducerRecord<>("first", "kafka" + i));
                System.out.println(i);
            }
            
            //提交事务
            kafkaProducer.commitTransaction();
        }catch (Exception e){
            //放弃十四五 类似与回滚
            kafkaProducer.abortTransaction();
        }finally {
            //3.关闭资源
            kafkaProducer.close();
        }
    }

3.8 生产经验——数据有序

在这里插入图片描述

3.9 生产经验——数据乱序

  1. kafka在1.x版本之前保证数据单分区有序,条件如下:
    max.in.flight.requests.per.connection=1(不需要考虑是否开启幂等性)。
  2. kafka在1.x及以后版本保证数据单分区有序,条件如下:
  • 未开启幂等性
    max.in.flight.requests.per.connection需要设置为1。
  • 开启幂等性
    max.in.flight.requests.per.connection需要设置小于等于5

原因说明:因为在kafka1.x以后,启用幂等后,kafka服务端会缓存producer发来的最近5个request的元数据,
故无论如何,都可以保证最近5个request的数据都是有序的。

在这里插入图片描述

4.Kafka Broker

4.1 Kafka Broker 工作流程

4.1.1 Zookeeper 存储的 Kafka 信息

在这里插入图片描述

4.1.2 Kafka Broker 总体工作流程

在这里插入图片描述

  1. 每台kafaka的Breaker启动之后都会向zk中进行注册
  2. 选举controller,每个breaker节点都有controller节点,谁先在zk中注册,谁就是选举出来的controller节点
  3. 选举出来的controller监听brokers里面的ids的变化
  4. Controller决定leader选举(选举规则:在isr中存活为前提,按照AR中排在前面的优先。例如ar[1,0,2], isr [1,0,2],那么leader就会按照1,0,2的顺序轮询)
  5. Controller将选举出来的节点信息上传到zk
  6. 其他Controller中zk中同步相关信息(已log的形式进行存储,底层是Segment,1个G1个G的存储,存储的.log文件,索引的.index文件)
  7. 如果Breaker中的leader挂了
  8. Controller监控到节点的变化
  9. 重新获取leader信息和ISR信息
  10. 选举新的leader
  11. 更新leader及LSR

相关概念

  • AR:分区中的所有 Replica 统称为 AR
  • ISR:所有与 Leader 副本保持一定程度同步的Replica(包括 Leader 副本在内)组成 ISR
  • OSR:与 Leader 副本同步滞后过多的 Replica 组成了 OSR

4.1.3 Controller选举机制

集群中第一个启动的Broker会通过在Zookeeper中创建临时节点/controller来让自己成为控制器,其他Broker启动时也会在zookeeper中创建临时节点,但是发现节点已经存在,所以它们会收到一个异常,意识到控制器已经存在,那么就会在Zookeeper中创建watch对象,便于它们收到控制器变更的通知。

那么如果控制器由于网络原因与Zookeeper断开连接或者异常退出,那么其他broker通过watch收到控制器变更的通知,就会去尝试创建临时节点/controller,如果有一个Broker创建成功,那么其他broker就会收到创建异常通知,也就意味着集群中已经有了控制器,其他Broker只需创建watch对象即可。

4.1.4 Kafka Controller控制器的职责

具备控制器身份的broker需要比其他普通的broker多一份职责,具体细节如下:

  1. 监听broker相关的变化。为Zookeeper中的/brokers/ids/节点添加BrokerChangeListener,用来处理broker增减的变化。
  2. 监听topic相关的变化。为Zookeeper中的/brokers/topics节点添加TopicChangeListener,用来处理topic增减的变化。例如如图举例/brokers/topics节点内容:
    某个topic 的分区信息等内容;为Zookeeper中的/admin/delete_topics节点添加TopicDeletionListener,用来处理删除topic的动作。
  3. 从Zookeeper中读取获取当前所有与topic、partition以及broker有关的信息并进行相应的管理。对于所有topic所对应的Zookeeper中的/brokers/topics/[topic]节点添加PartitionModificationsListener,用来监听topic中的分区分配变化。
  4. 更新集群的元数据信息,同步到其他普通的broker节点中。

4.1.5 Kafka Controller控制器的作用(同上职责)

Kafka控制器的作用是管理和协调Kafka集群,具体如下:

  1. 主题管理:创建、删除Topic,以及增加Topic分区等操作都是由控制器执行。
  2. 分区重分配:执行Kafka的reassign脚本对Topic分区重分配的操作,当使用kafka-topics.sh脚本为某个topic增加分区数量时,同样还是由控制器负责分区的重新分配,也是由控制器实现
  3. 当检测到某个分区的ISR集合发生变化时,由控制器负责通知所有broker更新其元数据信息
  4. 当某个分区的leader副本出现故障时,由控制器负责为该分区选举新的leader副本
  5. Preferred leader选举:因为在Kafka集群长时间运行中,broker的宕机或崩溃是不可避免的,leader就会发生转移,即使broker重新回来,也不会是leader了。在众多leader的转移过程中,就会产生leader不均衡现象,可能一小部分broker上有大量的leader,影响了整个集群的性能,所以就需要把leader调整回最初的broker上,这就需要Preferred leader选举。

如果集群中有一个Broker发生异常退出了,那么控制器就会检查这个broker是否有分区的副本leader,如果有那么这个分区就需要一个新的leader,此时控制器就会去遍历其他副本,决定哪一个成为新的leader,同时更新分区的ISR集合。
如果有一个Broker加入集群中,那么控制器就会通过Broker ID去判断新加入的Broker中是否含有现有分区的副本,如果有,就会从分区副本中去同步数据。

4.2Kafka 副本

4.2.1 副本基本信息

  1. Kafka 副本作用:提高数据可靠性。
  2. Kafka 默认副本 1 个,生产环境一般配置为 2 个,保证数据可靠性;太多副本会增加磁盘存储空间,增加网络上数据传输,降低效率。
  3. Kafka 中副本分为:Leader 和 Follower。Kafka 生产者只会把数据发往 Leader,然后 Follower 找 Leader 进行同步数据。
  4. Kafka 分区中的所有副本统称为 AR(Assigned Repllicas)。
    • AR = ISR + OSR
    • ISR表示和 Leader 保持同步的 Follower 集合。如果 Follower 长时间未向 Leader 发送通信请求或同步数据,则该 Follower 将被踢出 ISR。该时间阈值由 replica.lag.time.max.ms参数设定,默认 30s。Leader 发生故障之后,就会从 ISR 中选举新的 Leader。
    • OSR,表示 Follower 与 Leader 副本同步时,延迟过多的副本

4.2.2 Leader 选举流程

Kafka 集群中有一个 broker 的 Controller 会被选举为 Controller Leader,负责管理集群broker 的上下线,所有 topic 的分区副本分配和 Leader 选举等工作。Controller 的信息同步工作是依赖于 Zookeeper 的。
在这里插入图片描述

4.2. Leader 和 Follower 故障处理细节

LEO(Log End Offset):每个副本的最后一个offset,LEO其实就是最新的offset + 1。
HW(High Watermark):所有副本中最小的LEO 。

4.2.1 Follower故障

在这里插入图片描述
步骤

  1. Follower发生故障后会被临时踢出ISR
  2. 这个期间Leader和Follower继续接收数据
  3. 待该Follower恢复后,Follower会读取本地磁盘记录的上次的HW,并将log文件高于HW的部分截取掉,从HW开始向Leader进行同步。
  4. 等该Follower的LEO大于等于该Partition的HW,即Follower追上Leader之后,就可以重新加入ISR了。

4.2.2Leader故障

在这里插入图片描述

  1. Leader发生故障之后,会从ISR中选出一个新的Leader
  2. 为保证多个副本之间的数据一致性,其余的Follower会先将各自的log文件高于HW的部分截掉,然后从新的Leader同步数据。

4.3 分区副本分配

如果 kafka 服务器只有 4 个节点,那么设置 kafka 的分区数大于服务器台数,在 kafka
底层如何分配存储副本呢?

  1. 创建 16 分区,3 个副本
    创建一个新的 topic,名称为 second。
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --create --partitions 16 --replication-factor 3 --topic second
  1. 查看分区和副本情况。
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic second
Topic: second4 Partition: 0 Leader: 0 Replicas: 0,1,2 Isr: 0,1,2
Topic: second4 Partition: 1 Leader: 1 Replicas: 1,2,3 Isr: 1,2,3
Topic: second4 Partition: 2 Leader: 2 Replicas: 2,3,0 Isr: 2,3,0
Topic: second4 Partition: 3 Leader: 3 Replicas: 3,0,1 Isr: 3,0,1
Topic: second4 Partition: 4 Leader: 0 Replicas: 0,2,3 Isr: 0,2,3
Topic: second4 Partition: 5 Leader: 1 Replicas: 1,3,0 Isr: 1,3,0
Topic: second4 Partition: 6 Leader: 2 Replicas: 2,0,1 Isr: 2,0,1
Topic: second4 Partition: 7 Leader: 3 Replicas: 3,1,2 Isr: 3,1,2
Topic: second4 Partition: 8 Leader: 0 Replicas: 0,3,1 Isr: 0,3,1
Topic: second4 Partition: 9 Leader: 1 Replicas: 1,0,2 Isr: 1,0,2
Topic: second4 Partition: 10 Leader: 2 Replicas: 2,1,3 Isr: 2,1,3
Topic: second4 Partition: 11 Leader: 3 Replicas: 3,2,0 Isr: 3,2,0
Topic: second4 Partition: 12 Leader: 0 Replicas: 0,1,2 Isr: 0,1,2
Topic: second4 Partition: 13 Leader: 1 Replicas: 1,2,3 Isr: 1,2,3
Topic: second4 Partition: 14 Leader: 2 Replicas: 2,3,0 Isr: 2,3,0
Topic: second4 Partition: 15 Leader: 3 Replicas: 3,0,1 Isr: 3,0,1

在这里插入图片描述

4.4生产经验——手动调整分区副本存储

在生产环境中,每台服务器的配置和性能不一致,但是Kafka只会根据自己的代码规则创建对应的分区副本,就会导致个别服务器存储压力较大。所有需要手动调整分区副本的存储。

需求:创建一个新的topic,4个分区,两个副本,名称为three。将 该topic的所有副本都存储到broker0和broker1两台服务器上。

在这里插入图片描述
手动调整分区副本存储的步骤如下:

  1. 创建一个新的 topic,名称为 three。
hadoop102:9092 --create --partitions 4 --replication-factor 2 --topic three
  1. 查看分区副本存储情况。
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic three
  1. 创建副本存储计划(所有副本都指定存储在 broker0、broker1 中)。
vim increase-replication-factor.json

输入如下内容:

{
"version":1,
"partitions":[{"topic":"three","partition":0,"replicas":[0,1]},
{"topic":"three","partition":1,"replicas":[0,1]},
{"topic":"three","partition":2,"replicas":[1,0]},
{"topic":"three","partition":3,"replicas":[1,0]}]
}
  1. 执行副本存储计划。
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file increase-replication-factor.json --execute
  1. 验证副本存储计划。
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file increase-replication-factor.json --verify
  1. 查看分区副本存储情况。
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic three

4.5 生产经验——Leader Partition 负载平衡

正常情况下,Kafka本身会自动把Leader Partition均匀分散在各个机器上,来保证每台机器的读写吞吐量都是均匀的。但是如果某些broker宕机,会导致Leader Partition过于集中在其他少部分几台broker上,这会导致少数几台broker的读写请求压力过高,其他宕机的broker重启之后都是follower partition,读写请求很低,造成集群负载不均衡。

在这里插入图片描述

参数名称描述
auto.leader.rebalance.enable默认是 true。 自动 Leader Partition 平衡。生产环境中,leader 重选举的代价比较大,可能会带来性能影响,建议设置为 false 关闭。
leader.imbalance.per.broker.percentage默认是 10%。每个 broker 允许的不平衡的 leader的比率。如果每个 broker 超过了这个值,控制器会触发 leader 的平衡。
leader.imbalance.check.interval.seconds默认值 300 秒。检查 leader 负载是否平衡的间隔时间。

4.6 生产经验——增加副本因子

在生产环境当中,由于某个主题的重要等级需要提升,我们考虑增加副本。副本数的增加需要先制定计划,然后根据计划执行。

  1. 创建 topic
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --create --partitions 3 --replication-factor 1 --topic four
  1. 手动增加副本存储
  • 创建副本存储计划(所有副本都指定存储在 broker0、broker1、broker2 中)。
vim increase-replication-factor.json

输入如下内容:

{"version":1,"partitions":[{"topic":"four","partition":0,"replicas":[0,1,2]},
{"topic":"four","partition":1,"replicas":[0,1,2]},
{"topic":"four","partition":2,"replicas":[0,1,2]}]}

执行副本存储计划。

bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file increase-replication-factor.json --execute

4.7 文件存储

4.7.1 文件存储机制

Topic是逻辑上的概念,而partition是物理上的概念,每个partition对应于一个log文件,该log文件中存储的就是Producer生产的数据。Producer生产的数据会被不断追加到该log文件末端,为防止log文件过大导致数据定位效率低下,Kafka采取了分片和索引机制,将每个partition分为多个segment(默认1个g)。每个segment包括:“.index”文件、“.log”文件和.timeindex等文件。这些文件位于一个文件夹下,该文件夹的命名规则为:topic名称+分区序号,例如:first-0。

在这里插入图片描述

4.7.2 index和log文件详解

在这里插入图片描述
查找方法

  1. 根据目标offset定位Segment文件
  2. 找到小于等于目标offset的最大offset对应的索引项
  3. 定位到log文件
  4. 向下遍历找到目标Record

注意:

  • index为稀疏索引,大约每往log文件写入4kb数据,会往index文件写入一条索引。参数log.index.interval.bytes默认4kb
  • Index文件中保存的offset为相对offset,这样能确保offset的值所占空间不会过大,因此能将offset的值控制在固定大小

说明:日志存储参数配置

参数描述
log.segment.bytesKafka 中 log 日志是分成一块块存储的,此配置是指 log 日志划分成块的大小,默认值 1G。
log.index.interval.byte默认 4kb,kafka 里面每当写入了 4kb 大小的日志(.log),然后就往 index 文件里面记录一个索引。 稀疏索引。

4.7.3 Kafka如何查找指定offset的Message的

在这里插入图片描述

  1. 原理分析:
  • 一个分区中拆分n多个不同的小的segment 文件:Segment0
  • 容量满的情况下 500条消息 offset 500:Segment500
  • 容量满的情况下 500条消息 offset 1000:Segment1000
  1. 步骤
    1. 查到offset=7的情况下 对应的Segment文件,
    • 二分查找算法:查找到该分区中所有的Segment文件 list排序 每个Segment文件都是有一个命名规范offset=7在我们的Segment0文件中
    1. 先访问该index文件,根据offset值查询到物理存放位置。
    • 二分算法:Offset=7>6<9 所以定位到offset=6 获取到物理存放位置9807
    1. 根据该物理存放位置9807 去对应的log文件查找消息,依次向下查找+1次 获取到offset=7的消息。

为什么kafka中的 索引文件没有对每个消息建立索引呢?
目的是为了节约我们空间的资源
稀疏索引算法+二分查找算法+Segment分段设计算法,定位到邻居 在根据顺序遍历查找。

如果该offset消息 没有对应的索引的情况下,时间复杂度是为多少:(ON)
如果该offset消息 有对应的索引的情况下,时间复杂度是为多少:(O1)

4.7.3 文件清理策略

Kafka 中默认的日志保存时间为 7 天,可以通过调整如下参数修改保存时间。

  • log.retention.hours,最低优先级小时,默认 7 天。
  • log.retention.minutes,分钟。
  • log.retention.ms,最高优先级毫秒。
  • log.retention.check.interval.ms,负责设置检查周期,默认 5 分钟。
    那么日志一旦超过了设置的时间,怎么处理呢?
    Kafka 中提供的日志清理策略有 delete 和 compact 两种。
  1. delete 日志删除:将过期数据删除
    • log.cleanup.policy = delete 所有数据启用删除策略
      • 基于时间:默认打开。以 segment 中所有记录中的最大时间戳作为该文件时间戳。
      • 基于大小:默认关闭。超过设置的所有日志总大小,删除最早的 segment。
    • log.retention.bytes,默认等于-1,表示无穷大。
      在这里插入图片描述
  2. compact 日志压缩
    compact日志压缩:对于相同key的不同value值,只保留最后一个版本。
  • log.cleanup.policy = compact 所有数据启用压缩策略
    在这里插入图片描述

压缩后的offset可能是不连续的,比如上图中没有6,当从这些offset消费消息时,将会拿到比这个offset大的offset对应的消息,实际上会拿到offset为7的消息,并从这个位置开始消费。这种策略只适合特殊场景,比如消息的key是用户ID,value是用户的资料,通过这种压缩策略,整个消息集里就保存了所有用户最新的资料。

4.8 高效读写数据

  1. Kafka 本身是分布式集群,可以采用分区技术,并行度高
  2. 读数据采用稀疏索引,可以快速定位要消费的数据
  3. 顺序写磁盘
    Kafka 的 producer 生产数据,要写入到 log 文件中,写的过程是一直追加到文件末端,为顺序写。官网有数据表明,同样的磁盘,顺序写能到 600M/s,而随机写只有 100K/s。这与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间。
    在这里插入图片描述
  4. 页缓存 + 零拷贝技术
  • 零拷贝:Kafka的数据加工处理操作交由Kafka生产者和Kafka消费者处理。Kafka Broker应用层不关心存储的数据,所以就不用走应用层,传输效率高。
  • PageCache页缓存:Kafka重度依赖底层操作系统提供的PageCache功 能。当上层有写操作时,操作系统只是将数据写入PageCache。当读操作发生时,先从PageCache中查找,如果找不到,再去磁盘中读取。实际上PageCache是把尽可能多的空闲内存都当做了磁盘缓存来使用。
    在这里插入图片描述
    零拷贝过程
  1. 使用到mmap内存映射 用户与内核空间实现共享虚拟内存,不需要在使用cpu将内核空间的数据拷贝到用户空间。
  2. 使用sendfile 2.4 版本linux 内核 使用dma技术将硬盘的数据拷贝到内核态,在使用dma技术将内核态数据拷贝到网卡。
  3. 最终只需要做两次上下文切换 两次dma数据拷贝(直接内存) 不需要cpu拷贝数据从而减少上下文切换提高cpu的利用率。
参数描述
log.flush.interval.messages强制页缓存刷写到磁盘的条数,默认是 long 的最大值,9223372036854775807。一般不建议修改,交给系统自己管理。
log.flush.interval.ms每隔多久,刷数据到磁盘,默认是 null。一般不建议修改,交给系统自己管理。

5.Kafka消费者

5.1 Kafka 消费方式

在这里插入图片描述

  1. pull(拉)方式
    consumer采用从broker中主动拉取数据。Kafka采用这种方式。
  2. push(推)模式:
    Kafka没有采用这种方式,因为由broker决定消息发送速率,很难适应所有消费者的消费速率。例如推送的速度是50m/s,Consumer1、Consumer2就来不及处理消息。

pull模式不足之处是,如 果Kafka没有数据,消费者可能会陷入循环中,一直返回空数据。

5.2 Kafka 消费者工作流程

5.2.1 消费者总体工作流程

在这里插入图片描述
生产者向每一个分区的Leader发送数据,follower主动跟Leader同步数据,消费者消费topic中的数据,offset用于记录消费的位置,每个消费者的offset由消费者提交到系统主题进行保存。

5.2.2 消费者组原理

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

  • 消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费,同一个分区的数据不能由消费者组中两个及两个以上的消费者进行消费。
  • 消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
    在这里插入图片描述
    在这里插入图片描述
    消费者组初始化流程
  • coordinator:辅助实现消费者组的初始化和分区的分配。
    coordinator节点选择 = groupid的hashcode值 % 50( __consumer_offsets的分区数量)
    例如: groupid的hashcode值 = 1,1% 50 = 1,那么__consumer_offsets 主题的1号分区,在哪个broker上,就选择这个节点的coordinator

在这里插入图片描述

  1. 所有的消费者下向coordinate发送joinGroup请求,由于同一个消费者组中的组id相同所以会发给同一个coordinate
  2. coordinate从消费者组中随机选择一个consumer作为Leader
  3. coordinate吧要消费的topic情况发送给leader消费者
  4. leader负责指定消费方案
  5. leader将消费方案发送给coordinate
  6. coordinate将消费方案发送给各个consumer
  7. 每个消费者都会和coordinator保持心跳(默认3s),一旦超时(session.timeout.ms=45s),该消费者会被移除,并触发再平衡;或者消费者处理消息的时间过长(max.poll.interval.ms5分钟),也会触发再平衡

消费者组详细消费流程
在这里插入图片描述

  1. 创建一个消费者网络连接客户端(ConsumerNetworkClient),主要用于与kafka集群进行交互
  2. consumer向ConsumerNetworkClient发送sendFetches方法,用于抓取数据初始化
  3. ConsumerNetworkClient发送send方法发送请求,通过onSuccess方法回调,获取对应的结果
  4. 达到拉取数据的最大条数后通过反序列化器、拦截器、处理器将数据发生给消费者组
    .

5.3消费者 API

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

  1. 需求:
    创建一个独立消费者,消费 first 主题中数据。
    在这里插入图片描述
 public static void main(String[] args) {
        //0.配置
        Properties properties = new Properties();
        //连接 bootstrap server
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.44.211:9092");
        //反序列化
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());

        //配置消费者groupId
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"test");
        //1.创建一个消费者
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);

        //2.定义一个主题 first
        ArrayList<String> topics = new ArrayList<>();
        topics.add("first");
        consumer.subscribe(topics);
        //3.消费数据
        while (true){
        //隔一秒拉取一次数据
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.println(consumerRecord);
            }
        }
    }

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

  1. 需求:创建一个独立消费者,消费 first 主题 0 号分区的数据。
    在这里插入图片描述
    public static void main(String[] args) {
        //0.配置
        Properties properties = new Properties();
        //连接
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.44.211:9092");
        //反序列化
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());

        //组id
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"test");
        //1.创建一个消费者
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
        //2.订阅主题对应的分区
        ArrayList<TopicPartition> topicIdPartitions = new ArrayList<>();
        topicIdPartitions.add( new TopicPartition("first",0));
        consumer.assign(topicIdPartitions);

        //消费数据
        while (true){
            //隔一秒拉取一次数据
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.println(consumerRecord);
            }
        }
    }

5.3.3 消费者组案例

  1. 需求:测试同一个主题的分区数据,只能由一个消费者组中的一个消费。
  2. 案例实操
  • 复制一份基础消费者的代码,在 IDEA 中同时启动,即可启动同一个消费者组中的两个消费者。
  • 启动代码中的生产者发送消息,在 IDEA 控制台即可看到两个消费者在消费不同分区的数据(如果只发生到一个分区,可以在发送时增加延迟代码 Thread.sleep(2);

效果

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.4 生产经验——分区的分配以及再平衡

  1. 一个consumer group中有多个consumer组成,一个 topic有多个partition组成,现在的问题是,到底由哪个consumer来消费哪个partition的数据。
  2. Kafka有四种主流的分区分配策略: Range、RoundRobin、Sticky、CooperativeSticky。可以通过配置参数partition.assignment.strategy,修改分区的分配策略。默认策略是Range + CooperativeSticky。Kafka可以同时使用多个分区分配策略。
参数名称描述
heartbeat.interval.msKafka 消费者和 coordinator 之间的心跳时间,默认 3s。该条目的值必须小于 session.timeout.ms,也不应该高于session.timeout.ms 的 1/3。
session.timeout.ms Kafka消费者和 coordinator 之间连接超时时间,默认 45s。超过该值,该消费者被移除,消费者组执行再平衡。
max.poll.interval.ms消费者处理消息的最大时长,默认是 5 分钟。超过该值,该消费者被移除,消费者组执行再平衡。
partition.assignment.strategy消 费 者 分 区 分 配 策 略 , 默 认 策 略 是 Range +CooperativeSticky。Kafka 可以同时使用多个分区分配策略。可 以 选 择 的 策 略 包 括 : Range 、 RoundRobin 、 Sticky 、CooperativeSticky

5.4.1 Range 以及再平衡

  1. 分配原理及策略
    在这里插入图片描述
    Range 是对每个 topic 而言的。
    首先对同一个 topic 里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序。
    假如现在有 7 个分区,3 个消费者,排序后的分区将会是0,1,2,3,4,5,6;消费者排序完之后将会是C0,C1,C2。
    例如,7/3 = 2 余 1 ,除不尽,那么 消费者 C0 便会多消费 1 个分区。 8/3=2余2,除不尽,那么C0和C1分别多
    消费一个。
    通过 partitions数/consumer数 来决定每个消费者应该消费几个分区。如果除不尽,那么前面几个消费者将会多
    消费 1 个分区。

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

  1. Range 分区分配策略案例
  • 修改主题 first 为 7 个分区。
./kafka-topics.sh --bootstrap-server 192.168.44.211:9092 --topic first --alter --partitions 7
  • 复制 CustomConsumer 类,创建 CustomConsumer2。这样可以由三个消费者CustomConsumer、CustomConsumer1、CustomConsumer2 组成消费者组,组名都为“test”,同时启动 3 个消费者
  • 启动 CustomProducer 生产者,发送 500 条消息,随机发送到不同的分区
  public static void main(String[] args) throws InterruptedException {
        // 0配置
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.44.211:9092");
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        //1.创建kafka的生产者
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);
        //2.发送数据
        for (int i = 0; i < 500; i++) {
            kafkaProducer.send(new ProducerRecord<>("first", "kafka" + i), new Callback() {
                @Override
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    if(e == null){
                        System.out.println("主题:"+recordMetadata.topic()+"分区:"+recordMetadata.partition());
                    }else {
                        e.printStackTrace();
                    }
                }
            });
            System.out.println(i);
            Thread.sleep(1);
        }
        //3.关闭资源
        kafkaProducer.close();
    }

说明:Kafka 默认的分区分配策略就是 Range + CooperativeSticky,所以不需要修改策略。

  • 观看 3 个消费者分别消费哪些分区的数据
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  1. Range 分区分配再平衡案例
  • 停止掉 0 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)。
    • 1 号消费者:消费到 3、4 号分区数据。
    • 2 号消费者:消费到 5、6 号分区数据。
    • 0 号消费者的任务会整体被分配到 1 号消费者或者 2 号消费者。

说明:0 号消费者挂掉后,消费者组需要按照超时时间 45s 来判断它是否退出,所以需要等待,时间到了 45s 后,判断它真的退出就会把任务分配给其他 broker 执行。

  • 再次重新发送消息观看结果(45s 以后)。
    • 1 号消费者:消费到 0、1、2、3 号分区数据。
    • 2 号消费者:消费到 4、5、6 号分区数据。

说明:消费者 0 已经被踢出消费者组,所以重新按照 range 方式分配。

5.4.2 RoundRobin 以及再平衡

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

  2. RoundRobin 分区分配策略案例

  • 依次在 CustomConsumer、CustomConsumer1、CustomConsumer2 三个消费者代码中修改分区分配策略为 RoundRobin。
 //设置分区分配策略
        properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,"org.apache.kafka.clients.consumer.RoundRobinAssignor");
  • 重启 3 个消费者,重复发送消息的步骤,观看分区结果。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  1. RoundRobin 分区分配再平衡案例
  • 停止掉 0 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)。
    • 1 号消费者:消费到 2、5 号分区数据
    • 2 号消费者:消费到 4、1 号分区数据
    • 0 号消费者的任务会按照 RoundRobin 的方式,把数据轮询分成 0 、6 和 3 号分区数据,分别由 1 号消费者或者 2 号消费者消费。

说明:0 号消费者挂掉后,消费者组需要按照超时时间 45s 来判断它是否退出,所以需要等待,时间到了 45s 后,判断它真的退出就会把任务分配给其他 broker 执行。

  • 再次重新发送消息观看结果(45s 以后)。
    1 号消费者:消费到 0、2、4、6 号分区数据
    2 号消费者:消费到 1、3、5 号分区数据

说明:消费者 0 已经被踢出消费者组,所以重新按照 RoundRobin 方式分配。

5.4.3 Sticky 以及再平衡

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

  1. 需求
    设置主题为 first,7 个分区;准备 3 个消费者,采用粘性分区策略,并进行消费,观察消费分配情况。然后再停止其中一个消费者,再次观察消费分配情况。
  2. 步骤
  • 修改分区分配策略为粘性。
    注意:3 个消费者都应该注释掉,之后重启 3 个消费者,如果出现报错,全部停止等会再重启,或者修改为全新的消费者组
        //设置分区分配策略
//        properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,"org.apache.kafka.clients.consumer.RoundRobinAssignor");
        properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,"org.apache.kafka.clients.consumer.StickyAssignor");
  • 使用同样的生产者发送 500 条消息。
    可以看到会尽量保持分区的个数近似划分分区。
  1. Sticky 分区分配再平衡案例
  2. 停止掉 0 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)。
    • 1 号消费者:消费到 2、5、3 号分区数据。
    • 2 号消费者:消费到 4、6 号分区数据。
    • 0 号消费者的任务会按照粘性规则,尽可能均衡的随机分成 0 和 1 号分区数据,分别由 1 号消费者或者 2 号消费者消费。

说明:0 号消费者挂掉后,消费者组需要按照超时时间 45s 来判断它是否退出,所以需要等待,时间到了 45s 后,判断它真的退出就会把任务分配给其他 broker 执行。

  • 再次重新发送消息观看结果(45s 以后)。
    • 1 号消费者:消费到 2、3、5 号分区数据。
    • 2 号消费者:消费到 0、1、4、6 号分区数据。

说明:消费者 0 已经被踢出消费者组,所以重新按照粘性方式分配

5.5 offset 位移

5.5.1 offset 的默认维护位置

Kafka0.9版本之前,consumer默认将offset保存在Zookeeper中,从0.9版本开始,consumer默认将offset保存在Kafka一个内置的topic日志文件中(默认为50个文件记录),该topic为__consumer_offsets
在这里插入图片描述
__consumer_offsets 主题里面采用 key 和 value 的方式存储数据。key 是 group.id+topic+分区号,value 就是当前 offset 的值。每隔一段时间,kafka 内部会对这个 topic 进行compact,也就是每个 group.id+topic+分区号就保留最新数据。

  1. 为什么消费者需要使用50个文件记录消费者消费记录呢?
    如果消费者(分组)比较多的,都记录在同一个日志文件中,读写操作就非常麻烦。

  2. 消费者怎么知道我应该读取那个日志文件 知道从那个offset开始消费呢?
    消费者 消费消息的时候:key=group-id.topic.partition
    group-id.topic.partition=mayikt.mttopic.0
    (key=group-id.topic.partition)%consumer_offsets.size(50)=12
    Offset消费记录 记录在consumer_offsets-12文件夹
    记录Offset消费记录 的是consumer分组对应消费记录 不是记录单个消费者消费记录。

5.5.2 自动提交 offset

为了使我们能够专注于自己的业务逻辑,Kafka提供了自动提交offset的功能。5s
自动提交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。

添加配置

        // 是否自动提交 offset
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,true);
        // 提交 offset 的时间周期 1000ms,默认 5s
        properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 1000);
    public static void main(String[] args) {
        //0.配置
        Properties properties = new Properties();
        //连接 bootstrap server
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.44.211:9092");
        //反序列化
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());

        //配置消费者groupId
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"test1");
        //设置分区分配策略
//        properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,"org.apache.kafka.clients.consumer.RoundRobinAssignor");
        properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,"org.apache.kafka.clients.consumer.StickyAssignor");

        // 是否自动提交 offset
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,true);
        // 提交 offset 的时间周期 1000ms,默认 5s
        properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 1000);

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

        //2.定义一个主题 first
        ArrayList<String> topics = new ArrayList<>();
        topics.add("first");
        consumer.subscribe(topics);
        //3.消费数据
        while (true){
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.println(consumerRecord);
            }
        }
    }

5.5.3 手动提交 offset

虽然自动提交offset十分简单便利,但由于其是基于时间提交的,开发人员难以把握offset提交的时机。因此Kafka还提供了手动提交offset的API。
手动提交offset的方法有两种:分别是commitSync(同步提交)和commitAsync(异步提交)。两者的相同点是,都会将本次提交的一批数据最高的偏移量提交;不同点是,同步提交阻塞当前线程,一直到提交成功,并且会自动失败重试(由不可控因素导致,也会出现提交失败);而异步提交则没有失败重试机制,故有可能提交失败。

  • commitSync(同步提交):必须等待offset提交完毕,再去消费下一批数据。
  • commitAsync(异步提交) :发送完提交offset请求后,就开始消费下一批数据了。
    关键配置
        //手动提交
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,true);
    public static void main(String[] args) {
        //0.配置
        Properties properties = new Properties();
        //连接 bootstrap server
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.44.211:9092");
        //反序列化
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());

        //配置消费者groupId
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"test1");
        //设置分区分配策略
        properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,"org.apache.kafka.clients.consumer.RoundRobinAssignor");

        // 是否自动提交 offset
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,true);
        // 提交 offset 的时间周期 1000ms,默认 5s
        properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 1000);

        //手动提交
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,true);

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

        //2.定义一个主题 first
        ArrayList<String> topics = new ArrayList<>();
        topics.add("first");
        consumer.subscribe(topics);
        //3.消费数据
        while (true){
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.println(consumerRecord);
            }
            consumer.commitSync();
        }
    }

5.5.4 指定 Offset 消费

auto.offset.reset = earliest | latest | none 默认是 latest。
当 Kafka 中没有初始偏移量(消费者组第一次消费)或服务器上不再存在当前偏移量时(例如该数据已被删除),该怎么办?

  1. earliest:自动将偏移量重置为最早的偏移量,–from-beginning。
  2. latest(默认值):自动将偏移量重置为最新偏移量。
  3. none:如果未找到消费者组的先前偏移量,则向消费者抛出异常。
    在这里插入图片描述
    public static void main(String[] args) {
        //0 配置信息
        Properties properties = new Properties();
        //连接信息
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.44.211:9092");
        //反序列化
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
        //消费者id
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"test1");

        //1.创建消费者
        KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties);
        //2.订阅主题
        ArrayList<String> topicList = new ArrayList<>();
        topicList.add("first");
        kafkaConsumer.subscribe(topicList);

        //指定配置进行消费
        Set<TopicPartition> assignment = kafkaConsumer.assignment();
        //保证分区分配的方案已经指定完毕(有了分区分配信息才能开始消费)
        while (assignment.size()==0){
            kafkaConsumer.poll(Duration.ofSeconds(1));
            assignment = kafkaConsumer.assignment();
        }

        //遍历所有分区,并指定 offset 从 1700 的位置开始消费
        for (TopicPartition topicPartition : assignment) {
            kafkaConsumer.seek(topicPartition,1700);
        }

        while (true){
            ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.println(consumerRecord);
            }

        }
    }

5.5.5 指定时间消费

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

    public static void main(String[] args) {
        //0 配置信息
        Properties properties = new Properties();
        //连接信息
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.44.211:9092");
        //反序列化
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
        //消费者id
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"test1");

        //1.创建消费者
        KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties);
        //2.订阅主题
        ArrayList<String> topicList = new ArrayList<>();
        topicList.add("first");
        kafkaConsumer.subscribe(topicList);

        //指定配置进行消费
        Set<TopicPartition> assignment = kafkaConsumer.assignment();
        //保证分区分配的方案已经指定完毕(有了分区分配信息才能开始消费)
        while (assignment.size()==0){
            kafkaConsumer.poll(Duration.ofSeconds(1));
            assignment = kafkaConsumer.assignment();
        }
        //封装集合存储,每个分区对应一天前的数据
        HashMap<TopicPartition, Long> topicPartitionLongHashMap = new HashMap<>();
        for (TopicPartition topicPartition : assignment) {
            topicPartitionLongHashMap.put(topicPartition,System.currentTimeMillis()-1000*60*60*24);
        }
        //获取从 1 天前开始消费的每个分区的 offset
        Map<TopicPartition, OffsetAndTimestamp> topicPartitionOffsetAndTimestampMap = kafkaConsumer.offsetsForTimes(topicPartitionLongHashMap);
        for (TopicPartition topicPartition : assignment) {
            OffsetAndTimestamp offsetAndTimestamp = topicPartitionOffsetAndTimestampMap.get(topicPartition);
            kafkaConsumer.seek(topicPartition,offsetAndTimestamp.offset());
        }

        while (true){
            ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.println(consumerRecord);
                System.out.println(new Date(consumerRecord.timestamp()));
            }

        }
    }

5.5.6 漏消费和重复消费

重复消费:已经消费了数据,但是 offset 没提交。
漏消费:先提交 offset 后消费,有可能会造成数据的漏消费。

在这里插入图片描述

5.6 生产经验——消费者事务

如果想完成Consumer端的精准一次性消费,那么需要Kafka消费端将消费过程和提交offset过程做原子绑定。此时我们需要将Kafka的offset保存到支持事务的自定义介质(比 如MySQL)。这部分知识会在后续项目部分涉及。

5.7 生产经验——数据积压(消费者如何提高吞吐量)

  1. 如果是Kafka消费能力不足,则可以考虑增加Topic的分区数,并且同时提升消费组的消费者数量,消费者数 = 分区数。(两者缺一不可)
    2.
  2. 如果是下游的数据处理不及时:提高每批次拉取的数量。批次拉取数据过少(拉取数据/处理时间 < 生产速度),使处理的数据小于生产的数据,也会造成数据积压。
    在这里插入图片描述
参数名称描述
fetch.max.bytes默认 Default: 52428800(50 m)。消费者获取服务器端一批消息最大的字节数。如果服务器端一批次的数据大于该值(50m)仍然可以拉取回来这批数据,因此,这不是一个绝对最大值。一批次的大小受 message.max.bytes (broker config)or max.message.bytes (topic config)影响。
max.poll.records一次 poll 拉取数据返回消息的最大条数,默认是 500 条

第 6 章 Kafka-Eagle 监控

Kafka-Eagle 框架可以监控 Kafka 集群的整体运行情况,在生产环境中经常使用
前提:安装mysql及kafka,关闭kafka集群

6.1 kafka环境准备

修改参数值
修改/data/software/kafka/bin/kafka-server-start.sh 命令中

 vim bin/kafka-server-start.sh

修改如下参数值:

if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then
 export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G"
fi

if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then
 export KAFKA_HEAP_OPTS="-server -Xms2G -Xmx2G -XX:PermSize=128m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=8 -XX:ConcGCThreads=5 -XX:InitiatingHeapOccupancyPercent=70"
 export JMX_PORT="9999"
 #export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G"
fi

6.3 Kafka-Eagle 安装

  1. 官网:https://www.kafka-eagle.org/
  2. 上传压缩包 kafka-eagle-bin-2.0.8.tar.gz
  3. 对文件进行解压
  4. 修改conf目录下的conf/system-config.properties
efak.zk.cluster.alias=cluster1
cluster1.zk.list=ip:2181/kafka
# offset 保存在 kafka
cluster1.efak.offset.storage=kafka
# 配置 mysql 连接
efak.driver=com.mysql.jdbc.Driver
efak.url=jdbc:mysql://127.0.0.1:3306/ke?useUnicode=true&characterEncoding=UT
F-8&zeroDateTimeBehavior=convertToNull
efak.username=root
efak.password=root
  1. 添加环境变量
sudo vim /etc/profile.d/my_env.sh
# kafkaEFAK
export KE_HOME=/opt/module/efak
export PATH=$PATH:$KE_HOME/bin

刷新

 source /etc/p
  1. 启动
    启动之前应该先启动kafka
 bin/ke.sh start

6.4 Kafka-Eagle 页面操作

在这里插入图片描述
在这里插入图片描述

7kafka集成SpringBoot

  1. xml
    <dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.9.0</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.10</version>
        </dependency>
    </dependencies>
  1. yml
spring:
  kafka:
    bootstrap-servers: 192.168.44.211:9092
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
    consumer:
      key-deserializer:  org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      group-id: first
  1. 实体类
package com.atguigu.kafka.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.io.Serializable;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
    private String username;
    private String password;
}

  1. 生产者
package com.atguigu.kafka.controller;

import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.atguigu.kafka.entity.User;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProductController {

    @Autowired
    private KafkaTemplate<String,String> kafkaTemplate;

    @RequestMapping("/add")
    public String add(String username,String password){
        //通过kafka发送出去
        User user = new User();
        user.setUsername(username);
        user.setPassword(password);
        String s = JSONUtil.toJsonStr(user);
        kafkaTemplate.send("first",s);
        return "ok";
    }
}

  1. 消费者
package com.atguigu.kafka.controller;


import cn.hutool.json.JSONUtil;
import com.atguigu.kafka.entity.User;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.KafkaListener;

@Configuration
public class KafkaConsumer {

    @KafkaListener(topics = "first")
    public void consumerTopic(String message){
        System.out.println(message);
        User user = JSONUtil.toBean(message, User.class);
        System.out.println(user);

    }
}

8. 生产调优手册

8.1 Kafka 硬件配置选择

8.1.1. 场景说明

100 万日活,每人每天 100 条日志,每天总共的日志条数是 100 万 * 100 条 = 1 亿条。
1 亿/24 小时/60 分/60 秒 = 1150 条/每秒钟。
每条日志大小:0.5k - 2k(取 1k)。
1150 条/每秒钟 * 1k ≈ 1m/s 。
高峰期每秒钟:1150 条 * 20 倍 = 23000 条。
每秒多少数据量:20MB/s。

8.1.2. 服务器台数选择

服务器台数= 2 * (生产者峰值生产速率 * 副本 / 100) + 1 = 2 * (20m/s * 2 / 100) + 1= 3 台
建议 3 台服务器。

8.1.3. 磁盘选择

kafka 底层主要是顺序写,固态硬盘和机械硬盘的顺序写速度差不多。
建议选择普通的机械硬盘。
每天总数据量:1 亿条 * 1k ≈ 100g
100g * 副本 2 * 保存时间 3 天 / 0.7 ≈ 1T
建议三台服务器硬盘总大小,大于等于 1T。

8.1.4. 内存选择

  • 堆内存
  1. Kafka 堆内存建议每个节点:10g ~ 15g
    在 kafka-server-start.sh 中修改
if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then
 export KAFKA_HEAP_OPTS="-Xmx10G -Xms10G"
fi
  1. 查看 Kafka 进程号
 jps

在这里插入图片描述

  1. 根据 Kafka 进程号,查看 Kafka 的 GC 情况
    在这里插入图片描述
    参数说明:
  • S0C:第一个幸存区的大小

  • S0U:第一个幸存区的使用大小

  • S1C:第二个幸存区的大小

  • S1U:第二个幸存区的使用大小

  • EC:伊甸园区的大小

  • EU:伊甸园区的使用大小

  • OC:老年代大小

  • OU:老年代使用大小

  • MC:方法区大小

  • MU:方法区使用大小

  • CCSC:压缩类空间大小

  • CCSU:压缩类空间使用大小

  • YGC:年轻代垃圾回收次数

  • YGCT:年轻代垃圾回收消耗时间

  • FGC:老年代垃圾回收次数

  • FGCT:老年代垃圾回收消耗时间
    GCT:垃圾回收消耗总时间;

  • 页缓存:页缓存是 Linux 系统服务器的内存。我们只需要保证 1 个 segment(1g)中25%的数据在内存中就好。
    每个节点页缓存大小 =(分区数 * 1g * 25%)/ 节点数。例如 10 个分区,页缓存大小=(10 * 1g * 25%)/ 3 ≈ 1g
    建议服务器内存大于等于 11G。

8.1.5. CPU 选择

num.io.threads = 8 负责写磁盘的线程数,整个参数值要占总核数的 50%。
num.replica.fetchers = 1 副本拉取线程数,这个参数占总核数的 50%的 1/3。
num.network.threads = 3 数据传输线程数,这个参数占总核数的 50%的 2/3。
建议 32 个 cpu core。

8.1.6 网络选择

网络带宽 = 峰值吞吐量 ≈ 20MB/s 选择千兆网卡即可。
100Mbps 单位是 bit;10M/s 单位是 byte ; 1byte = 8bit,100Mbps/8 = 12.5M/s。
一般百兆的网卡(100Mbps )、千兆的网卡(1000Mbps)、万兆的网卡(10000Mbps)。

8.2 Kafka 生产者

8.2.1 Kafka 生产者核心参数配置

在这里插入图片描述

参数名称描述
bootstrap.servers生 产 者 连 接 集 群 所 需 的 broker 地 址 清 单 。 例 如hadoop102:9092,hadoop103:9092,hadoop104:9092,可以设置 1 个或者多个,中间用逗号隔开。注意这里并非需要所有的 broker 地址,因为生产者从给定的 broker 里查找到其他 broker 信息。
key.serializer 和 value.serializer指定发送消息的 key 和 value 的序列化类型。一定要写全类名。
buffer.memoryRecordAccumulator 缓冲区总大小,默认 32m。
batch.size缓冲区一批数据最大值,默认 16k。适当增加该值,可以提高吞吐量,但是如果该值设置太大,会导致数据传输延迟增加。
linger.ms如果数据迟迟未达到 batch.size,sender 等待 linger.time之后就会发送数据。单位 ms,默认值是 0ms,表示没有延迟。生产环境建议该值大小为 5-100ms 之间。
acks0:生产者发送过来的数据,不需要等数据落盘应答。
1:生产者发送过来的数据,Leader 收到数据后应答。
-(all):生产者发送过来的数据,Leader+和 isr 队列里面的所有节点收齐数据后应答。默认值是-1,-1 和 all是等价的。
max.in.flight.requests.per.connection允许最多没有返回 ack 的次数,默认为 5,开启幂等性要保证该值是 1-5 的数字。
retries当消息发送出现错误的时候,系统会重发消息。retries 表示重试次数。默认是 int 最大值,2147483647。如果设置了重试,还想保证消息的有序性,需要设置
MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION=1
retry.backoff.ms两次重试之间的时间间隔,默认是 100ms。
enable.idempotence是否开启幂等性,默认 true,开启幂等性。
compression.type生产者发送的所有数据的压缩方式。默认是 none,也就是不压缩支持压缩类型:none、gzip、snappy、lz4 和 zstd。

8.2.2 生产者如何提高吞吐量

参数名称描述
buffer.memoryRecordAccumulator 缓冲区总大小,默认 32m。
batch.size缓冲区一批数据最大值,默认 16k。适当增加该值,可以提高吞吐量,但是如果该值设置太大,会导致数据传输延迟增加。
linger.ms如果数据迟迟未达到 batch.size,sender 等待 linger.time之后就会发送数据。单位 ms,默认值是 0ms,表示没有延迟。生产环境建议该值大小为 5-100ms 之间。
compression.type生产者发送的所有数据的压缩方式。默认是 none,也就是不压缩。支持压缩类型:none、gzip、snappy、lz4 和 zstd。

8.2.3 数据可靠性

参数名称描述
acks0:生产者发送过来的数据,不需要等数据落盘应答。
1:生产者发送过来的数据,Leader 收到数据后应答。
-1(all):生产者发送过来的数据,Leader+和 isr 队列里面的所有节点收齐数据后应答。默认值是-1,-1 和 all是等价的。
  • 至少一次(At Least Once)= ACK 级别设置为-1 + 分区副本大于等于 2 + ISR 里应答的最小副本数量大于等于 2

8.2.4 数据去重

  1. 配置参数
参数名称描述
enable.idempotence是否开启幂等性,默认 true,表示开启幂等性。
  1. Kafka 的事务一共有如下 5 个 API
// 1 初始化事务
void initTransactions();
// 2 开启事务
void beginTransaction() throws ProducerFencedException;
// 3 在事务内提交已经消费的偏移量(主要用于消费者)
void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets,String consumerGroupId) throws ProducerFencedException;
// 4 提交事务
void commitTransaction() throws ProducerFencedException;
// 5 放弃事务(类似于回滚事务的操作)
void abortTransaction() throws ProducerFencedException;

8.2.5 数据有序

单分区内,有序(有条件的,不能乱序);多分区,分区与分区间无序;

8.2.6 数据乱序

参数名称描述
enable.idempotence是否开启幂等性,默认 true,表示开启幂等性。
max.in.flight.requests.per.connection允许最多没有返回 ack 的次数,默认为 5,开启幂等性要保证该值是 1-5 的数字

8.3 Kafka Broker

8.3.1 Broker 核心参数配置

在这里插入图片描述

参数名称描述
replica.lag.time.max.msISR 中,如果 Follower 长时间未向 Leader 发送通信请求或同步数据,则该 Follower 将被踢出 ISR。该时间阈值,默认 30s。
auto.leader.rebalance.enable默认是 true。 自动 Leader Partition 平衡。建议关闭。
leader.imbalance.per.broker.percentage默认是 10%。每个 broker 允许的不平衡的 leader的比率。如果每个 broker 超过了这个值,控制器会触发 leader 的平衡。
leader.imbalance.check.interval.seconds默认值 300 秒。检查 leader 负载是否平衡的间隔时间。
log.segment.bytesKafka 中 log 日志是分成一块块存储的,此配置是指 log 日志划分 成块的大小,默认值 1G。
log.index.interval.bytes默认 4kb,kafka 里面每当写入了 4kb 大小的日志(.log),然后就往 index 文件里面记录一个索引。
log.retention.hoursKafka 中数据保存的时间,默认 7 天。
log.retention.minutesKafka 中数据保存的时间,分钟级别,默认关闭。
log.retention.msKafka 中数据保存的时间,毫秒级别,默认关闭。
log.retention.check.interval.ms检查数据是否保存超时的间隔,默认是 5 分钟。
log.retention.bytes默认等于-1,表示无穷大。超过设置的所有日志总大小,删除最早的 segment。
log.cleanup.policy默认是 delete,表示所有数据启用删除策略;如果设置值为 compact,表示所有数据启用压缩策略。
num.io.threads默认是 8。负责写磁盘的线程数。整个参数值要占总核数的 50%。
num.replica.fetchers默认是 1。副本拉取线程数,这个参数占总核数的 50%的 1/3
num.network.threads默认是 3。数据传输线程数,这个参数占总核数的 50%的 2/3 。
log.flush.interval.messages强制页缓存刷写到磁盘的条数,默认是 long 的最大值,9223372036854775807。一般不建议修改,交给系统自己管理。
log.flush.interval.ms每隔多久,刷数据到磁盘,默认是 null。一般不建议修改,交给系统自己管理。

8.4Kafka 消费者

8.4.1 Kafka 消费者核心参数配置

在这里插入图片描述
在这里插入图片描述

参数名称描述
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 条。

8.4.2 消费者再平衡

参数名称描述
heartbeat.interval.ms Kafka消费者和 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

8.4.3 指定 Offset 消费

kafkaConsumer.seek(topic, 1000);

8.4.4 指定时间消费

HashMap<TopicPartition, Long> timestampToSearch = new HashMap<>();
timestampToSearch.put(topicPartition, System.currentTimeMillis() -1 * 24 * 3600 * 1000);
kafkaConsumer.offsetsForTimes(timestampToSearch);

8.4.5 消费者如何提高吞吐量

增加分区数;

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --alter --topic first --partitions 3
参数名称描述
fetch.max.bytes默认 Default: 52428800(50 m)。消费者获取服务器端一批消息最大的字节数。如果服务器端一批次的数据大于该值(50m)仍然可以拉取回来这批数据,因此,这不是一个绝对最大值。一批次的大小受 message.max.bytes (broker config)or max.message.bytes (topic config)影响。
max.poll.records一次 poll 拉取数据返回消息的最大条数,默认是 500 条

8.5Kafka 总体

8.5.1 如何提升吞吐量

  1. 提升生产吞吐量
  • buffer.memory:发送消息的缓冲区大小,默认值是 32m,可以增加到 64m。
  • batch.size:默认是 16k。如果 batch 设置太小,会导致频繁网络请求,吞吐量下降;如果 batch 太大,会导致一条消息需要等待很久才能被发送出去,增加网络延时。
  • linger.ms,这个值默认是 0,意思就是消息必须立即被发送。一般设置一个 5-100毫秒。如果 linger.ms 设置的太小,会导致频繁网络请求,吞吐量下降;如果 linger.ms 太长,会导致一条消息需要等待很久才能被发送出去,增加网络延时。
  • compression.type:默认是 none,不压缩,但是也可以使用 lz4 压缩,效率还是不错的,压缩之后可以减小数据量,提升吞吐量,但是会加大 producer 端的 CPU 开销。
  1. 增加分区
  2. 消费者提高吞吐量
  • 调整 fetch.max.bytes 大小,默认是 50m。
  • 调整 max.poll.records 大小,默认是 500 条。
  1. 增加下游消费者处理能力

8.5.2 数据精准一次

  1. 生产者角度
  • acks 设置为-1 (acks=-1)。
  • 幂等性(enable.idempotence = true) + 事务 。
  1. broker 服务端角度
  • 分区副本大于等于 2 (–replication-factor 2)。
  • ISR 里应答的最小副本数量大于等于 2 (min.insync.replicas = 2)。
  1. 消费者
  • 事务 + 手动提交 offset (enable.auto.commit = false)。
  • 消费者输出的目的地必须支持事务(MySQL、Kafka)。

8.5.3 合理设置分区数

  1. 创建一个只有 1 个分区的 topic。
  2. 测试这个 topic 的 producer 吞吐量和 consumer 吞吐量。
  3. 假设他们的值分别是 Tp 和 Tc,单位可以是 MB/s。
  4. 然后假设总的目标吞吐量是 Tt,那么分区数 = Tt / min(Tp,Tc)。

例如:producer 吞吐量 = 20m/s;consumer 吞吐量 = 50m/s,期望吞吐量 100m/s;
分区数 = 100 / 20 = 5 分区
分区数一般设置为:3-10 个
分区数不是越多越好,也不是越少越好,需要搭建完集群,进行压测,再灵活调整分区个数。

8.5.4 单条日志大于 1m

参数名称描述
message.max.bytes默认 1m,broker 端接收每个批次消息最大值。
max.request.size默认 1m,生产者发往 broker 每个请求消息最大值。针对 topic级别设置消息体的大小。
replica.fetch.max.bytes默认 1m,副本同步数据,每个批次消息最大值。
fetch.max.bytes默认 Default: 52428800(50 m)。消费者获取服务器端一批消息最大的字节数。如果服务器端一批次的数据大于该值(50m)仍然可以拉取回来这批数据,因此,这不是一个绝对最大值。一批次的大小受 message.max.bytes (broker config)or max.message.bytes (topic config)影响。

8.5.5 服务器挂了

在生产环境中,如果某个 Kafka 节点挂掉。
正常处理办法:

  1. 先尝试重新启动一下,如果能启动正常,那直接解决。
  2. 如果重启不行,考虑增加内存、增加 CPU、网络带宽。
  3. 如果将 kafka 整个节点误删除,如果副本数大于等于 2,可以按照服役新节点的方式重新服役一个新节点,并执行负载均衡。
  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值