Kafka宝典

一.简介

1.什么是Kafka

  • Kafka 是一个分布式的基于发布/订阅模式的消息队列(Message Queue),主要应用于大数据实时处理领域。
  • 生产者往队列里写消息,消费者从队列里取消息进行业务逻辑处理。一般在架构设计中起到解耦、削峰、异步处理的作用。Kafka适合离线和在线消息消费。 Kafka消息保留在磁盘上,并在群集内复制以防止数据丢失。 Kafka构建在ZooKeeper同步服务之上。它与Apache Storm和Spark非常好地集成,用于实时流式数据分析。
  • kafka和JMS(Java Message Service)实现(activeMQ)不同的是:即使消息被消费,消息仍然不会被立即删除.日志文件将会根据broker中的配置要求,保留一定的时间之后删除;比如log文件保留2天,那么两天后,文件会被清除,无论其中的消息是否被消费.kafka通过这种简单的手段,来释放磁盘空间,以及减少消息消费之后对文件内容改动的磁盘IO开支.

2.Kafka的使用场景

  • 日志收集:一个公司可以用Kafka可以收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer,例如hadoop、Hbase、Solr等。
  • 消息系统:解耦和生产者和消费者、缓存消息等。
  • 用户活动跟踪:Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到hadoop、数据仓库中做离线分析和挖掘。
  • 运营指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告。
  • 流式处理:流行的框架(如Storm和Spark Streaming)从主题中读取数据,对其进行处理,并将处理后的数据写入新主题,供用户和应用程序使用。 Kafka的强耐久性在流处理的上下文中也非常有用。
  • 事件源

3.Kafka的好处

  • 解耦合:耦合的状态表示当你实现某个功能的时候,不是直接接入当前接口,而利用消息队列,可以将相应的消息发送到消息队列,这样的话,如果接口出了问题,将不会影响到当前的功能。

  • 异步处理:异步处理替代了之前的同步处理,异步处理不需要让流程走完就返回结果,可以将消息发送到消息队列中,然后返回结果,剩下让其他业务处理接口从消息队列中拉取消费处理即可。

  • 流量削峰:高流量的时候,使用消息队列作为中间件可以将流量的高峰保存在消息队列中,从而防止了系统的高请求,减轻服务器的请求处理压力。

4.消息队列的两种模式

  • 点对点模式(一对一,消费者主动拉取数据,消息收到后消息清除)
    消息生产者生产消息发送到Queue中,然后消息消费者从Queue中取出并且消费消息。消息被消费以后,queue 中不再有存储,所以消息消费者不可能消费到已经被消费的消息。Queue 支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费。

在这里插入图片描述

  • 发布/订阅模式(一对多,消费者消费数据之后不会清除消息)
    消息生产者(发布)将消息发布到 topic 中,同时有多个消息消费者(订阅)消费该消息。和点对点方式不同,发布到 topic 的消息会被所有订阅者消费。

在这里插入图片描述

二.Kafka的安装配置

1.jdk安装

  • 1.去Oracle官网下载需要安装的jdk版本,我这里用的是jdk-8u181-linux-x64.tar.gz
  • 2.将该压缩包放到/usr/local/jdk目录下,jdk目录需要自己手动创建,也可以叫java,名字自己随意取(见名知意),然后解压该压缩包,输入如下指令:tar zxvf jdk-8u181-linux-x64.tar.gz
  • 接下来就该配置环境变量了,输入以下指令进行配置:vim /etc/profile
  • 输入完毕并回车,在文件尾部添加如下信息:
export JAVA_HOME=/usr/local/jdk/jdk1.8.0_181
export CLASSPATH=$:CLASSPATH:$JAVA_HOME/lib/
export PATH=$PATH:$JAVA_HOME/bin

注意:第一行的JAVA_HOME=/usr/local/jdk/jdk1.8.0_181 此处等号右边的是自己的jdk实际解压目录。如果不是该目录则需要改成自己的实际目录,其他不变。

  • 编辑完之后,保存并退出,然后输入以下指令,刷新环境配置使其生效:source /etc/profile
  • 查看jdk是否安装成功,输入指令java -version即可。在这里插入图片描述

2.zookeeper的安装配置

  • 下载 zookeeper-3.4.12.tar.gz,并解压:
    tar -zxvf zookeeper-3.4.12.tar.gz

  • 进入到 /usr/local/zookeeper/zookeeper-3.4.12/conf 目录中:
    cd zookeeper-3.4.12/conf/

  • 复制 zoo_sample.cfg 文件的并命名为为 zoo.cfg:
    cp zoo_sample.cfg zoo.cfg

  • 用 vim 打开 zoo.cfg 文件并修改其内容为如下:

# 数据文件夹
dataDir=/usr/local/zookeeper/zookeeper-3.4.12/data
 
# 日志文件夹
dataLogDir=/usr/local/zookeeper/zookeeper-3.4.12/logs
  • 保存并关闭 zoo.cfg 文件
  • 默认情况下,Linux 系统中没有/tmp/zookeeper/data 和/tmp/zookeeper/log 这两个目录,所以接下来还要创建这两个目录:
mkdir -p /tmp/zookeeper/data
mkdir -p /tmp/zookeeper/log
  • 进入到 /usr/local/zookeeper/zookeeper-3.4.11/bin 目录中:cd …/bin/
  • 用 vim 打开 /etc/ 目录下的配置文件 profile:vim /etc/profile
    并在其尾部追加如下内容:
export ZOOKEEPER_HOME=/usr/local/zookeeper/zookeeper-3.4.12/
export PATH=$ZOOKEEPER_HOME/bin:$PATH
export PATH
  • 使 /etc/ 目录下的 profile 文件即可生效:source /etc/profile
  • 启动 zookeeper 服务:zkServer.sh start
  • 如打印如下信息则表明启动成功:
ZooKeeper JMX enabled by default
Using config: /usr/local/zookeeper/zookeeper-3.4.12/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
  • 关闭 zookeeper 服务:zkServer.sh stop

以上是关于 ZooKeeper 单机模式的安装与配置,一般在生产环境中使用的都是集群模式,集群模式的配置也比较简单,相比单机模式而言只需要修改一些配置即可。下面以3台机器为例来配置一个 ZooKeeper 集群。首先在这3台机器的 /etc/hosts 文件中添加3台集群的IP地址与机器域名的映射,示例如下(3个IP地址分别对应3台机器):

192.168.0.2 node1
192.168.0.3 node2
192.168.0.4 node3

然后在这3台机器的 zoo.cfg 文件中添加以下配置:

server.0=192.168.0.2:2888:3888
server.1=192.168.0.3:2888:3888
server.2=192.168.0.4:2888:3888

为了便于讲解上面的配置,这里抽象出一个公式,即 server.A=B:C:D。其中A是一个数字,代表服务器的编号,就是前面所说的 myid 文件里面的值。集群中每台服务器的编号都必须唯一,所以要保证每台服务器中的 myid 文件中的值不同。B代表服务器的IP地址。C表示服务器与集群中的 leader 服务器交换信息的端口。D表示选举时服务器相互通信的端口。如此,集群模式的配置就告一段落,可以在这3台机器上各自执行 zkServer.sh start 命令来启动服务。

3. Kafka的安装与配置

在安装完 JDK 和 ZooKeeper 之后,就可以执行 Kafka broker 的安装了,首先也是从官网中下载安装包,示例中选用的安装包是 kafka_2.11-2.0.0.tgz,将其复制至/opt 目录下并进行解压缩,示例如下:


[root@node1 opt]# ll kafka_2.11-2.0.0.tgz 
-rw-r--r-- 1 root root 55751827 Jul 31 10:45 kafka_2.11-2.0.0.tgz
[root@node1 opt]# tar zxvf kafka_2.11-2.0.0.tgz
# 解压之后当前/opt目录下生成一个名为kafka_2.11-2.0.0的文件夹
[root@node1 opt]# cd kafka_2.11-2.0.0
[root@node1 kafka_2.11-2.0.0]#
# Kafka的根目录$KAFKA_HOME即为/opt/kafka_2.11-2.0.0,可以将Kafka_HOME添加到/etc/profile文件中,具体做法可以参考前面JDK和ZooKeeper的安装示例

接下来需要修改 broker 的配置文件$KAFKA_HOME/conf/server.properties。主要关注以下几个配置参数即可:

#broker 的全局唯一编号,不能重复,只能是数字。
broker.id=0
#处理网络请求的线程数量
num.network.threads=3
#用来处理磁盘 IO 的线程数量
num.io.threads=8
#发送套接字的缓冲区大小
socket.send.buffer.bytes=102400
#接收套接字的缓冲区大小
socket.receive.buffer.bytes=102400
#请求套接字的缓冲区大小
socket.request.max.bytes=104857600
#kafka 运行日志(数据)存放的路径,路径不需要提前创建,kafka 自动帮你创建,可以配置多个磁盘路径,路径与路径之间可以用","分隔
log.dirs=/opt/module/kafka/datas
#topic 在当前 broker 上的分区个数
num.partitions=1
#用来恢复和清理 data 下数据的线程数量
num.recovery.threads.per.data.dir=1
# 每个 topic 创建时的副本数,默认时 1 个副本
offsets.topic.replication.factor=1
#segment 文件保留的最长时间,超时将被删除
log.retention.hours=168
#每个 segment 文件的大小,默认最大 1G
log.segment.bytes=1073741824

# 检查过期数据的时间,默认 5 分钟检查一次是否数据过期
log.retention.check.interval.ms=300000
#配置连接 Zookeeper 集群地址(在 zk 根目录下创建/kafka,方便管理)
zookeeper.connect=hadoop102:2181,hadoop103:2181,hadoop104:2181/kafka

如果是单机模式,那么修改完上述配置参数之后就可以启动服务。如果是集群模式,那么只需要对单机模式的配置文件做相应的修改即可:确保集群中每个 broker 的 broker.id 配置参数的值不一样,以及 listeners 配置参数也需要修改为与 broker 对应的IP地址或域名,之后就可以各自启动服务。注意,在启动 Kafka 服务之前同样需要确保 zookeeper.connect 参数所配置的 ZooKeeper 服务已经正确启动。

启动 Kafka 服务的方式比较简单,在$KAFKA_HOME 目录下执行下面的命令即可:

bin/kafka-server-start.sh config/server.properties

如果要在后台运行 Kafka 服务,那么可以在启动命令中加入 -daemon 参数或&字符,示例如下:

bin/kafka-server-start.sh –daemon config/server.properties
# 或者
bin/kafka-server-start.sh config/server.properties &

集群启停脚本

vim kf.sh
#! /bin/bash
case $1 in
"start"){
 for i in hadoop102 hadoop103 hadoop104
 do
 echo " --------启动 $i Kafka-------"
 ssh $i "/opt/module/kafka/bin/kafka-server-start.sh -
daemon /opt/module/kafka/config/server.properties"
 done
};;
"stop"){
 for i in hadoop102 hadoop103 hadoop104
 do
 echo " --------停止 $i Kafka-------"
 ssh $i "/opt/module/kafka/bin/kafka-server-stop.sh "
 done
};;
esac

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

4.docker-compose安装kafka

创建docker-compose.yml

version: '3'

services:
  zookeeper:
    image: wurstmeister/zookeeper
    environment:
      TZ: Asia/Shanghai
    restart: always
    ports:
      - "2181:2181"

  #消息队列
  kafka:
    image: wurstmeister/kafka
    ports:
      - 19092:19092
    environment:
      - KAFKA_LISTENERS=INTERNAL://:19093,CLIENT://:19092
      - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1
      - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,CLIENT:PLAINTEXT
      - KAFKA_INTER_BROKER_LISTENER_NAME=INTERNAL
      - ALLOW_PLAINTEXT_LISTENER=yes
      - KAFKA_ADVERTISED_LISTENERS=INTERNAL://127.0.0.1:19093,CLIENT://127.0.0.1:19092
      - KAFKA_ADVERTISED_HOST_NAME=kafka
      - KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181
      - KAFKA_AUTO_CREATE_TOPICS_ENABLE=true
      - TZ=Asia/Shanghai
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /data2/wuyongyu/kafka1/logs:/kafka
    depends_on:
      - zookeeper

执行

docker-compose up -d

三.Kafka的基础架构和相关命令

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

1.角色

Kafka像其他Mq一样,也有自己的基础架构,主要存在生产者Producer、Kafka集群Broker、消费者Consumer、注册消息Zookeeper.

  • Producer:消息生产者,向Kafka中发布消息的角色。
  • Consumer:消息消费者,即从Kafka中拉取消息消费的客户端。
  • Consumer Group:消费者组,消费者组则是一组中存在多个消费者,消费者消费Broker中当前Topic的不同分区中的消息,消费者组之间互不影响,所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。某一个分区中的消息只能够一个消费者组中的一个消费者所消费
  • Broker:经纪人,一台Kafka服务器就是一个Broker,一个集群由多个Broker组成,一个Broker可以容纳多个Topic。
  • Topic:主题,可以理解为一个队列,生产者和消费者都是面向一个Topic
  • Partition:分区,为了实现扩展性,一个非常大的Topic可以分布到多个Broker上,一个Topic可以分为多个Partition,每个Partition是一个有序的队列(分区有序,不能保证全局有序)
  • Replica:副本Replication,为保证集群中某个节点发生故障,节点上的Partition数据不丢失,Kafka可以正常的工作,Kafka提供了副本机制,一个Topic的每个分区有若干个副本,一个Leader和多个Follower
  • Leader:每个分区多个副本的主角色,生产者发送数据的对象,以及消费者消费数据的对象都是Leader。
  • Follower:每个分区多个副本的从角色,实时的从Leader中同步数据,保持和Leader数据的同步,Leader发生故障的时候,某个Follower会成为新的Leader。

2.数据可靠性保证

为保证producer发送的数据,能可靠的发送到指定的topic,topic的每个partition收到producer发送的数据后,都需要向producer发送ack(acknowledgement确认收到),如果producer收到ack,就会进行下一轮的发送,否则重新发送数据。
在这里插入图片描述

2.kafka启动命令

  • 先启动zookeeper
    在这里插入图片描述
  • 单机启动kafka
./bin/kafka-server-start.sh -daemon ./config/server.properties

在这里插入图片描述

3.主题命令行操作

在这里插入图片描述

  • 创建主题
 ./bin/kafka-topics.sh  --bootstrap-server localhost:9092 --topic test  --create  --replication-factor 1 --partitions 1

选项说明:

–topic 定义 topic 名

–replication-factor 定义副本数

–partitions 定义分区数
在这里插入图片描述

  • 查看操作主题命令参数
./bin/kafka-topics.sh --bootstrap-server 192.168.111.5:9092 --list

在这里插入图片描述

  • 查看主题详情
./bin/kafka-topics.sh --bootstrap-server 192.168.111.5:9092 --describe --topic test

分区数为1,副本为1

副本的leader在0上面,对leader进行处理
在这里插入图片描述

  • 修改主题分区数
 ./bin/kafka-topics.sh --bootstrap-server 192.168.111.5:9092 --alter --topic test --partitions 3

分区数只能增加,不能减少
在这里插入图片描述

  • 删除主题
    在这里插入图片描述

4.生产者和消费者命令行操作

生产者

./bin/kafka-console-producer.sh

在这里插入图片描述

  • 发送消息
    在这里插入图片描述

消费者

./bin/kafka-console-consumer.sh

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

  • 消费消息
    消费 sise主题中的数据
 ./bin/kafka-console-consumer.sh --bootstrap-server 192.168.111.5:9092 --topic sise

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

  • 把主题中所有的数据都读取出来(包括历史数据)
./bin/kafka-console-consumer.sh --bootstrap-server 192.168.111.5:9092 --from-beginning  --topic sise

无序是因为多分区,kafka只保证单分区的顺序性,不保证多分区聚合的顺序性
在这里插入图片描述

四.Kafka 生产者

1.原理

在消息发送的过程中,涉及到了两个线程——main 线程和 Sender 线程。在 main 线程 中创建了一个双端队列 RecordAccumulator。main 线程将消息发送给 RecordAccumulator, Sender 线程不断从 RecordAccumulator 中拉取消息发送到 Kafka Broker,Kafka Broker收到之后会进行副本备份,然后回应acks。
在这里插入图片描述

  • 生产者重要参数列表
    在这里插入图片描述
    在这里插入图片描述

2.异步发送

异步发送是指不需要手动重试,只要把消息递交到dquene即可;消息发送失败会自动重试,不需要我们在回调函数中手动重试。

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1. 创建 kafka 生产者的配置对象
        Properties properties = new Properties();
        //给kafka配置对象添加配置信息:bootstrap.servers
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.111.5: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.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());

        //创建kafka生产者对象
        KafkaProducer<String,String> producer = new KafkaProducer<String, String>(properties);
        
        //调用send方法 发送消息
        for (int i = 0; i < 5; i++) {
            producer.send(new ProducerRecord<>("sise","hello world  "+i),new Callback(){
                //该方法在 Producer 收到 ack 时调用,为异步调用
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    if (e ==null){
                        System.out.println("主题:"+recordMetadata.topic()+",分区:"+recordMetadata.partition());
                    } else {
                        e.printStackTrace();
                    }
                }
            });
        }
        producer.close();
    }

在这里插入图片描述

在这里插入图片描述

问题

(localhost/127.0.0.1:9092) could not be established. Broker may not be availab

修改kafka配置文件config/server.properties

advertised.listeners才是真正的对外代理地址!那么listeners的作用就不是对外提供服务代理,而是监听

# 允许外部端口连接
listeners = PLAINTEXT://0.0.0.0:9092
# 外部代理地址
advertised.listeners = PLAINTEXT://192.168.66.151:9092

注意:先关掉kafka,再关掉zookeeper 并重启

3.同步发送

只需在异步发送的基础上,再调用一下 get()方法即可。
在这里插入图片描述

在这里插入图片描述

4.生产者选择分区发送数据

  • 分区好处
    (1)便于合理使用存储资源,每个Partition在一个Broker上存储,可以把海量的数据按照分区切割成一
    块一块数据存储在多台Broker上。合理控制分区的任务,可以实现负载均衡的效果。
    (2)提高并行度,生产者可以以分区为单位发送数据;消费者可以以分区为单位进行消费数据。
    在这里插入图片描述
  • 生产者发送消息的分区策略
    默认的分区器 DefaultPartitioner
/**
* The default partitioning strategy:
* <ul>
* <li>If a partition is specified in the record, use it
* <li>If no partition is specified but a key is present choose a 
partition based on a hash of the key
* <li>If no partition or key is present choose the sticky 
partition that changes when the batch is full.
* 
* See KIP-480 for details about sticky partitioning.
*/
public class DefaultPartitioner implements Partitioner {
 … …
}

在这里插入图片描述
eg1:
指定数据发送到 2号分区,key 为空
在这里插入图片描述
在这里插入图片描述

eg2:
没有指明 partition 值但有 key 的情况下,将 key 的 hash 值与 topic 的 partition 数进行取余得到 partition 值。
在这里插入图片描述
在这里插入图片描述

  • 自定义分区器

例如我们实现一个分区器实现,发送过来的数据中如果包含 sise,就发往 0 号分区,不包含 sise,就发往 1 号分区。

(1)定义类实现 Partitioner 接口。
(2)实现 3 个方法:partition,close,configure; 重写 partition()方法; 返回分区号

    /*
     * 返回信息对应的分区
     * @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 msgValue = value.toString();
        //创建partition
        int partition;
        //判断消息是否包含sise
        if (msgValue.contains("sise")){
            partition=0;
        } else {
            partition = 1;
        }
        //返回分区号
        return partition;
    }

    @Override
    public void close() {
    }

    @Override
    public void configure(Map<String, ?> map) {
    }
}

使用分区器的方法,在生产者的配置中添加自定义分区器

properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.sise.kafka.MyPartitioner");

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

5.生产者提高吞吐量

在这里插入图片描述
batch.size:批次大小,默认 16K properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);

linger.ms:等待时间,默认linger.ms是0毫秒,也就是说没发布一条消息都会直接往broker发送,这样会影响效率,如果把linger.ms和batch.size调太大就会导致消息的延迟

compression.type: 把消息压缩后发送过去提高吞吐量,默认 none,可配置值 gzip、snappy、lz4 和 zstd

recordAccumulator调大,当分区很多时,如果缓存区太小会导致消息加到缓冲区太慢(?)

        //batch.size:批次大小,默认 16K
        properties.put(ProducerConfig.BATCH_SIZE_CONFIG,16384);
        //linger.ms:等待时间,默认 0
        properties.put(ProducerConfig.LINGER_MS_CONFIG,1);
        //RecordAccumulator:缓冲区大小,默认 32M:buffer.memory
        properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,33554432);
        //compression.type:压缩,默认 none,可配置值 gzip、snappy、lz4 和 zstd
        properties.put(ProducerConfig.COMPRESSION_TYPE_CONFIG,"snappy");

6.数据可靠和数据重复

acks=0:表示producer发送消息后,不管broker是否接收到,都会继续发下一条消息,这样会导致消息丢失

acks=1: 表示producer发送消息后,broker的leader进行数据落盘后响应acks,代表这条消息发送完成,但是当leader对消息落盘后,还没有同步到follower就挂了,而且ack已经回复了,重新选举一个leader之后,这条消息也不会再发送了,这样就导致了消息也会丢失

acks=-1: 表示producer发送消息后,要等broker在leader进行数据落盘后,并且同步到follower成功后才返回ack,但是这样当同步到follower时,出现一个follower宕机,导致一直无响应ack,最后导致服务崩盘,事情更加严重

        // 设置 acks
        properties.put(ProducerConfig.ACKS_CONFIG, "all");
        // 重试次数 retries,默认是 int 最大值,2147483647
        properties.put(ProducerConfig.RETRIES_CONFIG, 3);

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
当acks=-1会存在数据重复问题

producer发送消息后,leader刚和其他follower同步完成后返回ack时挂了,导致producer没有收到ack,这是重新选举了leader,producer由于没有收到消息所以重新发送了一个消息,这是因为之前宕掉的leader已经同步过了,这是就导致了消息重复了
在这里插入图片描述

解决方法:使用幂等性

        // 设置 acks
        properties.put(ProducerConfig.ACKS_CONFIG, "all");
        // 重试次数 retries,默认是 int 最大值,2147483647
        properties.put(ProducerConfig.RETRIES_CONFIG, 3);

7.幂等性和事务

幂等性就是指Producer不论向Broker发送多少次重复数据,Broker端都只会持久化一条,保证了不重复。
精确一次(Exactly Once) = 幂等性 + 至少一次( ack=-1 + 分区副本数>=2 + ISR最小副本数量>=2) 。

重复数据的判断标准:具有<PID, Partition, SeqNumber>相同主键的消息提交时,Broker只会持久化一条。其中PID是Kafka每次重启都会分配一个新的;Partition 表示分区号;Sequence Number是单调自增的。

所以幂等性只能保证的是在单分区单会话内不重复。
在这里插入图片描述

  • 如何使用幂等性
    开启参数 enable.idempotence 默认为 true,false 关闭。

  • 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;
  • 单个 Producer,使用事务保证消息的仅一次发送
public class ProducerTransaction {
    public static void main(String[] args) {
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
                "192.168.9.50:9092");

        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
                StringSerializer.class.getName());

        // 设置事务 id(必须),事务 id 任意起名
        properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG,
                "transaction_id_0");
        KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);

        // 初始化事务
        producer.initTransactions();
        // 开启事务
        producer.beginTransaction();


        // 4. 调用 send 方法,发送消息
        try {
            for (int i = 0; i < 5; i++) {
                producer.send(new ProducerRecord("first","atguigu " + i));
            }
            int i = 10/0;
            // 提交事务
            producer.commitTransaction();
        } catch (Exception e){
            // 终止事务
            producer.abortTransaction();
        } finally {
            // 5. 关闭资源
            producer.close();
        }
    }
}

8.数据乱序

在这里插入图片描述

五.Kafka Broker

1.Kafka Broker 工作流程

  • Zookeeper中存储的Kafka 信息
    在这里插入图片描述
  • 工作流程

1、broker启动后注册id到zk

2、3、哪个broker先注册成为controller(只要一个),controller启动时就起一个监视器监视ZK/brokers/ids/子节点。当存在broker启动加入集群后都会在ZK/brokers/ids/增加一个子节点brokerId,控制器的监视器发现这种变化后,控制器开始执行broker加入的相关流程并更新元数据信息到集群。

4、controller决定了leader选举

5、controller将所有节点和leader上传至zk,也就是leader和isr(存活的节点包括leader本身)

6、Controller Follower节点从zk同步相关信息

producer发送消息后,broker会对数据精选副本备份,然后进行数据落盘

7、当 leader挂了,那么controller leader就会监听到节点变化,然后去获取isr进行选举新的leader(在isr存活为前提,按照ar排在前面的优先)

8、如果controller leader挂了那么参考下面 控制器Leader如何Failover

在这里插入图片描述

2.controller的作用

https://www.bilibili.com/read/cv15660873/

1、Kafka Controller是Kakfa服务端Broker的概念,Broker集群有多台,但只有一台Broker可以扮演控制器的角色;

2、某台Broker一旦成为Controller,它用于以下权力:完成对集群成员管理、主题维护和分区的管理,如集群broker信息、Topic维护、Partition维护、分区选举ISR、同步元信息给其他Broker等。

  • 为何要引进controller ?
    多个watch对zk负载太大

在这里插入图片描述
历史版本的实现架构如上图所示,对于分区和副本的状态的管理依赖于zookeeper的Watcher和队列:每一个broker都会在zookeeper注册Watcher,所以zookeeper就会出现大量的Watcher, 如果宕机的broker上的partition比较多,会造成多个Watcher触发,造成集群内大规模调整;每一个replica都要去再次zookeeper上注册监视器,当集群规模很大的时候,zookeeper负担很重。所以严重依赖zk实现元数据的协调同步,这样做的弊端有以下几点:

当Kafka集群规模很大的时候,zookeeper负担很重。因为所有Broker的所有主题分区都要与zk建立连接,分区上下线和Broker的宕机或重新启动,会触发多个watch,给zk带来大量的写压力;

zk压力大,容易引起网络风暴,不利于Kafka元数据的稳定性;

这种设计容易出现脑裂(zk集群的brain-split原理,请自行补充知识)和羊群效应。

  • 优化后

Kafka官方团队也意识到严重依赖zk带来的弊端,于是对架构做了优化和调整。核心思想:

降低对zk的依赖程度,减少zk压力

元数据的控制和管理由中心化节点维护;

异步化同步元数据给集群所有节点,并做好同步的容错处理。

在这里插入图片描述

  • 控制器的启动初始化

1、当broker启动的时候,都会创建KafkaController对象;

2、每个节点上的KafkaController会在指定的zookeeper路径下创建临时节点,只有第一个成功创建的节点的KafkaController才可以成为Leader,其余的都是follower;

3、集群中只能有一个Leader对外提供服务;

  • 控制器运行时对元信息的处理流程

只有Controller Leader向zk注册watch并监听元数据的变化,其他broker不用监听zookeeper的状态变化。大概流程:

  1. 用户通过命令或控制台执行集群的管理操作,比如:扩锁容Broker集群、创建/删除Topic、扩/缩容Partition;
  2. 第一步会触发集群在zk对应路径上变更节点信息,比如/brokers/topics;
  3. Controller Leader收到watch的监听推送消息,通过监听接口收到对应的变更信息,比如主题信息、分区信息、集群Broker信息等;
  4. Controller Leader根据推送的事件类型,做不同的逻辑处理。比如:Broker加入或删除的相关流程、Topic新增或删除的相关流程、 分区重分配引起的重分配相关流程 、Partition扩展的相关流程(即分区Leader选举和ISR同步);
  5. Controller Leader同步元信息至集群的Controller Follower节点。
  • 控制器Leader如何Failover

如果控制器Leader所在broker退出、崩溃或与ZK会话失效则ZK会删除/controller内该子节点,各个broker的监视器收到这种变化后,每个broker开始竞争直到有一个竞争成为新的控制器Leader,并向/controller注册子节点,以及向/controller_EPOCH注册子节点。此时,新一轮的controller周期开始运行,新的Leader节点继续肩负起监听元数据变化、处理元信息并同步给集群的任务。

3.Broker 重要参数

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

3.分区与副本

在这里插入图片描述

首先,从数据组织形式来说,kafka有三层形式,kafka有多个主题,每个主题有多个分区,每个分区又有多条消息。

而每个分区可以分布到不同的机器上,这样一来,从服务端来说,分区可以实现高伸缩性,以及负载均衡,动态调节的能力。

当然多分区就意味着每条消息都难以按照顺序存储,那么是不是意味着这样的业务场景kafka就无能为力呢?不是的,最简单的做法可以使用单个分区,单个分区,所有消息自然都顺序写入到一个分区中,就跟顺序队列一样了。而复杂些的,还有其他办法,那就是使用按消息键,将需要顺序保存的消息存储的单独的分区,其他消息存储其他分区,这个在下面会介绍

我们可以通过replication-factor指定创建topic时候所创建的分区数。

bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic test

比如这里就是创建了1个分区,的主题。值得注意的是,还有一种创建主题的方法,是使用zookeeper参数的,那种是比较旧的创建方法,这里是使用bootstrap参数的。

  • 分区写入策略

所谓分区写入策略,即是生产者将数据写入到kafka主题后,kafka如何将数据分配到不同分区中的策略。

常见的有三种策略,轮询策略,随机策略,和按键保存策略。其中轮询策略是默认的分区策略,而随机策略则是较老版本的分区策略,不过由于其分配的均衡性不如轮询策略,故而后来改成了轮询策略为默认策略。

轮询策略

所谓轮询策略,即按顺序轮流将每条数据分配到每个分区中。

举个例子,假设主题test有三个分区,分别是分区A,分区B和分区C。那么主题对接收到的第一条消息写入A分区,第二条消息写入B分区,第三条消息写入C分区,第四条消息则又写入A分区,依此类推。

轮询策略是默认的策略,故而也是使用最频繁的策略,它能最大限度保证所有消息都平均分配到每一个分区。除非有特殊的业务需求,否则使用这种方式即可。

随机策略

随机策略,也就是每次都随机地将消息分配到每个分区。其实大概就是先得出分区的数量,然后每次获取一个随机数,用该随机数确定消息发送到哪个分区。

在比较早的版本,默认的分区策略就是随机策略,但其实使用随机策略也是为了更好得将消息均衡写入每个分区。但后来发现对这一需求而言,轮询策略的表现更优,所以社区后来的默认策略就是轮询策略了。

按键保存策略

按键保存策略,就是当生产者发送数据的时候,可以指定一个key,计算这个key的hashCode值,按照hashCode的值对不同消息进行存储。

至于要如何实现,那也简单,只要让生产者发送的时候指定key就行。欸刚刚不是说默认的是轮询策略吗?其实啊,kafka默认是实现了两个策略,没指定key的时候就是轮询策略,有的话那激素按键保存策略了。

上面有说到一个场景,那就是要顺序发送消息到kafka。前面提到的方案是让所有数据存储到一个分区中,但其实更好的做法,就是使用这种按键保存策略。

让需要顺序存储的数据都指定相同的键,而不需要顺序存储的数据指定不同的键,这样一来,即实现了顺序存储的需求,又能够享受到kafka多分区的优势,岂不美哉。

实现自定义分区

kafka提供了两种让我们自己选择分区的方法,第一种是在发送producer的时候,在ProducerRecord中直接指定,但需要知道具体发送的分区index,所以并不推荐。

第二种则是需要实现Partitioner.class类,并重写类中的partition(String topic, Object key, byte[] keyBytes,Object value, byte[] valueBytes, Cluster cluster) 方法。后面在生成kafka producer客户端的时候直接指定新的分区类就可以了。

  • 副本

说完了分区,再来说说副本。先说说副本的基本内容,在kafka中,每个主题可以有多个分区,每个分区又可以有多个副本。这多个副本中,只有一个是leader,而其他的都是follower副本。仅有leader副本可以对外提供服务。

多个follower副本通常存放在和leader副本不同的broker中。通过这样的机制实现了高可用,当某台机器挂掉后,其他follower副本也能迅速”转正“,开始对外提供服务。

  • 副本的作用

在kafka中,实现副本的目的就是冗余备份,且仅仅是冗余备份,所有的读写请求都是由leader副本进行处理的。follower副本仅有一个功能,那就是从leader副本拉取消息,尽量让自己跟leader副本的内容一致。

正常情况下,kafka里的数据都不能只有一份。假设我们保存了N个副本,即topic每个partition都有N个副本(Replica)。并且副本的个数一定小于broker个数。(因为每份数据的副本必须保存在不同的broker,否则没有意义,因为如果一份数据的副本保存在同一个broker,那么这个broker挂了,则数据依然丢失。)所以对于每个partition而言,每个broker上最多只有一个副本,因此我们常常使用broker-id表示副本。kafka还有个机制,就是会默认将副本均匀分布到所有的broker上。

  • 说说follower副本为什么不对外提供服务?

这个问题本质上是对性能和一致性的取舍。试想一下,如果follower副本也对外提供服务那会怎么样呢?首先,性能是肯定会有所提升的。但同时,会出现一系列问题。类似数据库事务中的幻读,脏读。

比如你现在写入一条数据到kafka主题a,消费者b从主题a消费数据,却发现消费不到,因为消费者b去读取的那个分区副本中,最新消息还没写入。而这个时候,另一个消费者c却可以消费到最新那条数据,因为它消费了leader副本。

看吧,为了提高那么些性能而导致出现数据不一致问题,那显然是不值得的。

  • leader副本挂掉后,如何选举新副本?

如果你对zookeeper选举机制有所了解,就知道zookeeper每次leader节点挂掉时,都会通过内置id,来选举处理了最新事务的那个follower节点。

从结果上来说,kafka分区副本的选举也是类似的,都是选择最新的那个follower副本,但它是通过一个In-sync(ISR)副本集合实现。

kafka会将与leader副本保持同步的副本放到ISR副本集合中。当然,leader副本是一直存在于ISR副本集合中的,在某些特殊情况下,ISR副本中甚至只有leader一个副本。

当leader挂掉时,kakfa通过zookeeper感知到这一情况,在ISR副本中选取新的副本成为leader,对外提供服务。

但这样还有一个问题,前面提到过,有可能ISR副本集合中,只有leader,当leader副本挂掉后,ISR集合就为空,这时候怎么办呢?这时候如果设置unclean.leader.election.enable参数为true,那么kafka会在非同步,也就是不在ISR副本集合中的副本中,选取出副本成为leader,但这样意味这消息会丢失,这又是可用性和一致性的一个取舍了。

  • ISR副本集合保存的副本的条件是什么?

上面一直说ISR副本集合中的副本就是和leader副本是同步的,那这个同步的标准又是什么呢?

答案其实跟一个参数有关:replica.lag.time.max.ms。

前面说到follower副本的任务,就是从leader副本拉取消息,如果持续拉取速度慢于leader副本写入速度,慢于时间超过replica.lag.time.max.ms后,它就变成“非同步”副本,就会被踢出ISR副本集合中。但后面如何follower副本的速度慢慢提上来,那就又可能会重新加入ISR副本集合中了。

4. 节点服役和退役

服役新节点

执行负载均衡操作

  • 创建一个要均衡的主题
vim topics-to-move.json


{
 "topics": [
 {"topic": "first"}
 ],
 "version": 1
}
  • 生成一个负载均衡的计划
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --topics-to-move-json-file 
topics-to-move.json --broker-list "0,1,2,3" --generate
Current partition replica assignment
{"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[0,2,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[2,1,0],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[1,0,2],"log_dirs":["any","any","any"]}]}
Proposed partition reassignment configuration
{"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,3,0],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[3,0,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[0,1,2],"log_dirs":["any","any","any"]}]}
  • 创建副本存储计划(所有副本存储在 broker0、broker1、broker2、broker3 中)。
vim increase-replication-factor.json

{"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,3,0],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[3,0,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[0,1,2],"log_dirs":["any","any","any"]}]}
  • 执行副本存储计划。
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 
--reassignment-json-file increase-replication-factor.json --execute
  • 验证副本存储计划。
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file 
increase-replication-factor.json --verify
Status of partition reassignment:
Reassignment of partition first-0 is complete.
Reassignment of partition first-1 is complete.
Reassignment of partition first-2 is complete.
Clearing broker-level throttles on brokers 0,1,2,3
Clearing topic-level throttles on topic first

退役旧节点

1.执行负载均衡操作

先按照退役一台节点,生成执行计划,然后按照服役时操作流程执行负载均衡。

  • 创建一个要均衡的主题
vim topics-to-move.json


{
 "topics": [
 {"topic": "first"}
 ],
 "version": 1
}

  • 创建执行计划。
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --topics-to-move-json-file 
topics-to-move.json --broker-list "0,1,2" --generate
Current partition replica assignment
{"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,0,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[3,1,2],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[0,2,3],"log_dirs":["any","any","any"]}]}


Proposed partition reassignment configuration
{"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,0,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[0,1,2],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[1,2,0],"log_dirs":["any","any","any"]}]}
  • 创建副本存储计划(所有副本存储在 broker0、broker1、broker2 中)。
vim increase-replication-factor.json
{"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,0,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[0,1,2],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[1,2,0],"log_dirs":["any","any","any"]}]}
  • 执行副本存储计划。
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file 
increase-replication-factor.json --execute
  • 验证副本存储计划。
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file 
increase-replication-factor.json --verify

```java
Status of partition reassignment:
Reassignment of partition first-0 is complete.
Reassignment of partition first-1 is complete.
Reassignment of partition first-2 is complete.
Clearing broker-level throttles on brokers 0,1,2,3
Clearing topic-level throttles on topic first
2.执行停止命令

在 hadoop105 上执行停止命令即可。

```java
 bin/kafka-server-stop.sh 

5.leader选举流程

在这里插入图片描述

Kafka 集群中有一个 broker 的 Controller 会被选举为 Controller Leader,负责管理集群 broker 的上下线,所有 topic 的分区副本分配和 Leader 选举等工作。

每个主题有多个分区,每个分区有多个副本,但是具体操作的还是leader副本

查看leader分配情况(leader的选举是按ar进行轮询选举的)

Replicas就是ar

 bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe 
--topic atguigu1

Topic: atguigu1 TopicId: awpgX_7WR-OX3Vl6HE8sVg PartitionCount: 4 ReplicationFactor: 4
Configs: segment.bytes=1073741824
Topic: atguigu1 Partition: 0 Leader: 3 Replicas: 3,0,2,1 Isr: 3,0,2,1
Topic: atguigu1 Partition: 1 Leader: 1 Replicas: 1,2,3,0 Isr: 1,2,3,0
Topic: atguigu1 Partition: 2 Leader: 0 Replicas: 0,3,1,2 Isr: 0,3,1,2
Topic: atguigu1 Partition: 3 Leader: 2 Replicas: 2,1,0,3 Isr: 2,1,0,3

把 broker3关闭治好后,分区0的leader由3往后退,变成了0

 bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe 
--topic atguigu1
Topic: atguigu1 TopicId: awpgX_7WR-OX3Vl6HE8sVg PartitionCount: 4 ReplicationFactor: 4
Configs: segment.bytes=1073741824
Topic: atguigu1 Partition: 0 Leader: 0 Replicas: 3,0,2,1 Isr: 0,2,

6.Follower 故障处理

在这里插入图片描述

在这里插入图片描述

7.leader故障处理

在这里插入图片描述

8.分区副本分配

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

1)创建 16 分区,3 个副本

每个的间隔都是有顺序的

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

9.手动调整分区副本存储

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

  • 创建一个新的 topic,名称为 three。
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --create 
--partitions 4 --replication-factor 2 --topic three
  • 查看分区副本存储情况、
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic three
  • 创建副本存储计划(所有副本都指定存储在 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]}]
}
  • 执行副本存储计划。
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 
--reassignment-json-file increase-replication-factor.json --execute
  • 验证副本存储计划。
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 
--reassignment-json-file increase-replication-factor.json --verify
  • 查看分区副本存储情况
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic three

10.Leader Partition 负载平衡

如果集群中出现z机器宕机导致leader集中到了部分几台服务器就会出现有些机器压力过高,kafka提供了自平衡
在这里插入图片描述

12.新增保存副本因子

在这里插入图片描述

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

创建 topic

bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --create 
--partitions 3 --replication-factor 1 --topic four

手动增加副本存储

  • 创建副本存储计划(所有副本都指定存储在 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

13.文件存储机制

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

14. 文件存储位置

kafka数据的存储位置,在config/server.properties中的log.dirs中配置;

本次演示kafka的日志存储配置为:log.dirs=/tmp/kafka-logs

15.分区与存储方式的关系

partition是以文件的形式存储在文件系统中,比如,创建了一个名为kafkaData的topic,有4个partition,那么在Kafka的数据目录中(由配置文件中的log.dirs指定的)中就有这样4个目录: kafkaData-0, kafkaData-1,kafkaData-2,kafkaData-3,其命名规则为<topic_name>-<partition_id>,里面存储的分别就是这4个partition的数据。

3、每个数据目录的子目录都有xx.index ,xx.log ,xx.timeindex三个文件组成

在这里插入图片描述

16.演示

1、创建一个主题

创建一个带有4个分区,2个副本的topic(kafkaData)

[root@master bin]# ./kafka-topics.sh --create --zookeeper master:2181,slaves1:2181,slaves2:2181 --replication-factor 2 --partitions 4 --topic kafkaData 
Created topic "kafkaData".

2、查看数据目录中的效果

[root@master kafka-logs]# ls /tmp/kafka-logs/
 
kafkaData-0
kafkaData-1
 
[root@slaves1 bin]# ls /tmp/kafka-logs/
 
kafkaData-1
kafkaData-2
kafkaData-3
 
[root@slaves2 bin]# ls /tmp/kafka-logs/
 
kafkaData-0
kafkaData-2
kafkaData-3

由上可以看出kafka的第一个分区kafka-0的两个副本分别在master、slaves2两个节点上;其他同理;

命令查看

[root@master bin]# ./kafka-topics.sh --describe --zookeeper master:2181,slaves1:2181,slaves2:2181 --topic kafkaData
Topic:kafkaData	PartitionCount:4	ReplicationFactor:2	Configs:
	Topic: kafkaData	Partition: 0	Leader: 2	Replicas: 2,0	Isr: 2,0
	Topic: kafkaData	Partition: 1	Leader: 0	Replicas: 0,1	Isr: 0,1
	Topic: kafkaData	Partition: 2	Leader: 1	Replicas: 1,2	Isr: 1,2
	Topic: kafkaData	Partition: 3	Leader: 2	Replicas: 2,1	Isr: 2,1

Leader: 指定主分区的broker id;
Replicas: 副本在那些机器上;
Isr : 可以做为主分区的broker id;

3、向此主题写入大批量数据

4、查看segment file

以kafkaData-0为例:在这里插入图片描述
使用kafka安装bin目录下的kafka-run-class.sh分别查看这些文件的内容:

(1)查看log文件:

[root@master bin]# ./kafka-run-class.sh  kafka.tools.DumpLogSegments --files /tmp/kafka-logs/kafkaData-0/00000000000000000000.log  --print-data-log
...
offset: 7211 position: 448934 CreateTime: 1587632825139 isvalid: true payloadsize: 29 magic: 1 compresscodec: NONE crc: 995429819 payload: 阳光小区,11,1587632825139
offset: 7212 position: 448997 CreateTime: 1587632825139 isvalid: true payloadsize: 28 magic: 1 compresscodec: NONE crc: 2299568067 payload: 单身小区,5,1587632825139
offset: 7213 position: 449059 CreateTime: 1587632825139 isvalid: true payloadsize: 29 magic: 1 compresscodec: NONE crc: 2772987037 payload: 花花小区,12,1587632825139
offset: 7214 position: 449122 CreateTime: 1587632825139 isvalid: true payloadsize: 28 magic: 1 compresscodec: NONE crc: 2369864650 payload: 阳光小区,6,1587632825139
offset: 7215 position: 449184 CreateTime: 1587632825139 isvalid: true payloadsize: 28 magic: 1 compresscodec: NONE crc: 820724779 payload: 单身小区,4,1587632825139
...

**payload:**为消息体

(2)查看index文件:

[root@master bin]# ./kafka-run-class.sh  kafka.tools.DumpLogSegments --files /tmp/kafka-logs/kafkaData-0/00000000000000000000.index  --print-data-log
...
offset: 1269114 position: 79002134
offset: 1269231 position: 79009410
offset: 1269316 position: 79014708
offset: 1269456 position: 79023419
offset: 1269715 position: 79039540
offset: 1269838 position: 79047192
offset: 1269933 position: 79053095
offset: 1270083 position: 79062430
...

(3)查看timeindex文件

[root@master bin]# ./kafka-run-class.sh  kafka.tools.DumpLogSegments --files /tmp/kafka-logs/kafkaData-0/00000000000000000000.timeindex  --print-data-log
...
timestamp: 1587632824453 offset: 1867
timestamp: 1587632824473 offset: 1975
timestamp: 1587632824507 offset: 1987
timestamp: 1587632824658 offset: 2657
timestamp: 1587632824759 offset: 3057
timestamp: 1587632824810 offset: 3468
...

注意:

segment file 组成:由2部分组成,分别为index file和data file,这两个文件是一一对应的,后缀”.index”和”.log”分别表示索引文件和数据文件;

segment file 命名规则:partition的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset,ofsset的数值最大为64位(long类型),20位数字字符长度,没有数字用0填充。

17.数据存储原理分析

1、说明

(1)在生产环境中,kafkaData-0下不会只存在一个index、log、timeindex文件;而是像这样:
在这里插入图片描述
(2)、我们将index文件称为索引文件,里面存储着大量元数据;log文件称为数据文件,里面存储着大量消息;

2、数据文件建立索引原理

数据文件分段使得可以在一个较小的数据文件中查找对应offset的Message了,但是这依然需要顺序扫描才能找到对应offset的Message。为了进一步提高查找的效率,Kafka为每个分段后的数据文件建立了索引文件,文件名与数据文件的名字是一样的,只是文件扩展名为.index。

索引文件中包含若干个索引条目,每个条目表示数据文件中一条Message的索引。索引包含两个部分(均为4个字节的数字),分别为相对offset和position。

相对offset:因为数据文件分段以后,每个数据文件的起始offset不为0,相对offset表示这条Message相对于其所属数据文件中最小的offset的大小。举例,分段后的一个数据文件的offset是从20开始,那么offset为25的Message在index文件中的相对offset就是25-20 = 5。存储相对offset可以减小索引文件占用的空间。

position,表示该条Message在数据文件中的绝对位置。只要打开文件并移动文件指针到这个position就可以读取对应的Message了。

Kafka高效文件存储设计特点

(1)Kafka把topic中一个parition大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用。

(2)通过索引信息可以快速定位message和确定response的最大大小。

(3)通过index元数据全部映射到memory,可以避免segment file的IO磁盘操作。

(4)通过索引文件稀疏存储,可以大幅降低index文件元数据占用空间大小。

稀疏索引

在这里插入图片描述
为什么使用稀疏索引?

因为占用内存少,内存可是宝贵的资源,而且Kafka面对海量消息存储时,使用稠密索引带来的额外存储空间占用会给存储带来很大的挑战,与其这样不如牺牲一点检索效率换取更大的索引存储空间。这样可以避免索引文件占用过多的内存,从而可以在内存中保存更多的索引,而牺牲的检索效率对消息延迟影响不太大,换句话说业务能够容忍。Kafka对应的就是Broker 端参数log.index.interval.bytes 值,默认4KB,即4KB的消息建一条索引。

18.文件清理策略

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 所有数据启用删除策略

(1)基于时间:默认打开。以 segment 中所有记录中的最大时间戳作为该文件时间戳。 (2)基于大小:默认关闭。超过设置的所有日志总大小,删除最早的 segment。 log.retention.bytes,默认等于-1,表示无穷大。

思考:如果一个 segment 中有一部分数据过期,一部分没有过期,怎么处理?

答案1:看所有数据是否超过7天,超过7天就删掉。不超过7天就保留,通俗就是不求同年同月同日生,但求同年同月同日四
答案2:看日志大小,如果超过设置的所有日志总大小,则删除,没超过,则保留

2)compact 日志压缩:
在这里插入图片描述

19.高效读写数据(面试题)

1)Kafka 本身是分布式集群,可以采用分区技术,并行度高

2)读数据采用稀疏索引,可以快速定位要消费的数据

3)顺序写磁盘 Kafka 的 producer 生产数据,要写入到 log 文件中,写的过程是一直追加到文件末端, 为顺序写。官网有数据表明,同样的磁盘,顺序写能到 600M/s,而随机写只有 100K/s。这 与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间。
在这里插入图片描述
4)页缓存 + 零拷贝技术
在这里插入图片描述
在这里插入图片描述

六.Kafka 消费者

1.消费方式

kafka消费方式是pull模式
在这里插入图片描述

2.消费者工作流程

一个消费者可以消费多个分区的数据,但是每个分区的数据只能由一个消费组中一个消费者消费;kafka将消费偏移量offset保存到系统主题(之前是保存到zookeeper,但是这样会导致与zookeeper交互过于频繁,影响效率)在这里插入图片描述

3.消费者组原理

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

4.消费者组初始化流程

1、consumer发送joinGroup请求

2、coordinator选出一个consumer作为leader

3、把要消费的topic元数据发送给leader消费者

4、consumer leader制定消费方案

5、把消费方案发给coordinator

6、coordinator把方案发给其他各个consumer

在这里插入图片描述

group发送sendFetches发送消费请求,CousumerNetworkClient去抓取数据放到队列,group去队列拉取消费数据反序列化然后处理
在这里插入图片描述
在这里插入图片描述
+消费者参数
在这里插入图片描述
在这里插入图片描述

5. 消费者 API

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

  • 需求
    在这里插入图片描述
    注意:在消费者 API 代码中必须配置消费者组 id。命令行启动消费者不填写消费者组
    id 会被自动填写随机的消费者组 id。

  • 消费者

public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1. 创建 kafka 生产者的配置对象
        Properties properties = new Properties();
        //给kafka配置对象添加配置信息:bootstrap.servers
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.111.5:9092");
        // key,value 反序列化(必须)
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());

        //配置消费者组
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"dessw");
        //创建消费者对象
        KafkaConsumer<String,String> kafkaConsumer =new KafkaConsumer<String, String>(properties);

        //注册要消费的主题(可以多个)
        ArrayList<String> topics = new ArrayList<>();
        topics.add("first");
        topics.add("second");
        kafkaConsumer.subscribe(topics);


        //拉取数据打印
        while (true) {
            //设置1s消费一批数据
            ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));

            //打印消费到的数据
            for (ConsumerRecord<String,String> record : consumerRecords){
                System.out.println(record);
            }
        }
    }
  • 生产者
    在这里插入图片描述
  • 消费
    在这里插入图片描述
    独立消费者案例(订阅分区)
  • 需求
    在这里插入图片描述
  • 消费者
public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1. 创建 kafka 生产者的配置对象
        Properties properties = new Properties();
        //给kafka配置对象添加配置信息:bootstrap.servers
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.111.5:9092");
        // key,value 反序列化(必须)
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());

        //配置消费者组
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"dessw");
        //创建消费者对象
        KafkaConsumer<String,String> kafkaConsumer =new KafkaConsumer<String, String>(properties);

        //消费某个主题的某个分区
        ArrayList<TopicPartition> topicPartitions = new ArrayList<>();
        topicPartitions.add(new TopicPartition("first",0));
        kafkaConsumer.assign(topicPartitions);

        //拉取数据打印
        while (true) {
            //设置1s消费一批数据
            ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));

            //打印消费到的数据
            for (ConsumerRecord<String,String> record : consumerRecords){
                System.out.println(record);
            }
        }
    }

消费者组案例

  • 需求
    在这里插入图片描述

  • 生产者
    在这里插入图片描述

  • 消费者组
    复制一份基础消费者的代码,在 IDEA 中同时启动,即可启动同一个消费者组中
    的两个消费者。

public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1. 创建 kafka 生产者的配置对象
        Properties properties = new Properties();
        //给kafka配置对象添加配置信息:bootstrap.servers
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.111.5:9092");
        // key,value 反序列化(必须)
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());

        //配置消费者组
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"dessw");
        //创建消费者对象
        KafkaConsumer<String,String> kafkaConsumer =new KafkaConsumer<String, String>(properties);

        //注册要消费的主题(可以多个)
        ArrayList<String> topics = new ArrayList<>();
        topics.add("first");
        topics.add("second");
        kafkaConsumer.subscribe(topics);


        //消费某个主题的某个分区
//        ArrayList<TopicPartition> topicPartitions = new ArrayList<>();
//        topicPartitions.add(new TopicPartition("first",0));
//        kafkaConsumer.assign(topicPartitions);

        //拉取数据打印
        while (true) {
            //设置1s消费一批数据
            ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));

            //打印消费到的数据
            for (ConsumerRecord<String,String> record : consumerRecords){
                System.out.println(record);
            }
        }
    }

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

  • 结论
    启动代码中的生产者发送消息,在 IDEA 控制台即可看到两个消费者在消费不同
    分区的数据;
    重新发送到一个全新的主题中,由于默认创建的主题分区数为 1,可以看到只能
    有一个消费者消费到数据。

6.分区的分配以及再平衡

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Range 分区策略原理
在这里插入图片描述

RoundRobin 以及再平衡
在这里插入图片描述
在这里插入图片描述

Sticky 以及再平衡

在这里插入图片描述

7.offset 位移

offset 的默认维护位置
__consumer_offsets 主题里面采用 key 和 value 的方式存储数据。key 是 group.id+topic+ 分区号,value 就是当前 offset 的值。每隔一段时间,kafka 内部会对这个 topic 进行 compact,也就是每个 group.id+topic+分区号就保留最新数据。
在这里插入图片描述
在这里插入图片描述
自动提交 offset
在这里插入图片描述

在这里插入图片描述

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

手动提交 offset
在这里插入图片描述

  • 同步提交offset
    由于同步提交 offset 有失败重试机制,故更加可靠,但是由于一直等待提交结果,提交的效率比较低。以下为同步提交 offset 的示例。
public class CustomConsumerByHandSync {
 public static void main(String[] args) {
 // 1. 创建 kafka 消费者配置类
 Properties properties = new Properties();
 // 2. 添加配置参数
 // 添加连接
 properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
 // 配置序列化 必须
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, "test");
 // 是否自动提交 offset
 properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
 //3. 创建 kafka 消费者
 KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
 //4. 设置消费主题 形参是列表
 consumer.subscribe(Arrays.asList("first"));
 //5. 消费数据
 while (true){
 // 读取消息
 ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
 // 输出消息
 for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
   System.out.println(consumerRecord.value());
 }
   // 同步提交 offset
   consumer.commitSync();
 }
 }
}
  • 异步提交offset
    虽然同步提交 offset 更可靠一些,但是由于其会阻塞当前线程,直到提交成功。因此吞吐量会受到很大的影响。因此更多的情况下,会选用异步提交 offset 的方式。
public class CustomConsumerByHandAsync {
 public static void main(String[] args) {
 // 1. 创建 kafka 消费者配置类
 Properties properties = new Properties();
 // 2. 添加配置参数
 // 添加连接
 properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
 // 配置序列化 必须
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, "test");
 // 是否自动提交 offset
 properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
 //3. 创建 Kafka 消费者
 KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
 //4. 设置消费主题 形参是列表
 consumer.subscribe(Arrays.asList("first"));
 //5. 消费数据
 while (true){
 // 读取消息
 ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
 // 输出消息
 for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
	 System.out.println(consumerRecord.value());
 }
 // 异步提交 offset
 consumer.commitAsync();
	 }
	 }
}

指定 Offset 消费

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

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

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

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

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

在这里插入图片描述

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

public class CustomConsumerSeek {
 public static void main(String[] args) {
 // 0 配置信息
 Properties properties = new Properties();
 // 连接
 properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
 // key value 反序列化
 
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test2");
 // 1 创建一个消费者
 KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties);

 // 2 订阅一个主题
 ArrayList<String> topics = new ArrayList<>();
 topics.add("first");
 kafkaConsumer.subscribe(topics);
 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);
 }
 // 3 消费该主题数据
 while (true) {
  ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
 for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
   System.out.println(consumerRecord);
	 }
  }
 }
}

注意:每次执行完,需要修改消费者组名;

指定时间消费

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

public class CustomConsumerForTime {
 public static void main(String[] args) {
 // 0 配置信息
Properties properties = new Properties();
 // 连接
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
 // key value 反序列化 
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test2");
 // 1 创建一个消费者
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties);
 // 2 订阅一个主题
 ArrayList<String> topics = new ArrayList<>();
 topics.add("first");
 kafkaConsumer.subscribe(topics);
 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() - 1 * 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());
 }
 }

 // 3 消费该主题数据
 while (true) {
 ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));

 for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
 System.out.println(consumerRecord);
 }
 }
 }
}

漏消费和重复消费
重复消费:已经消费了数据,但是 offset 没提交。
漏消费:先提交 offset 后消费,有可能会造成数据的漏消费。
在这里插入图片描述
消费者事务
在这里插入图片描述
数据积压(消费者如何提高吞吐量)
在这里插入图片描述
在这里插入图片描述

七.Kafka-Kraft 模式

在这里插入图片描述

八.Kafka整合springboot

生产者

配置连接地址

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

消费者

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值