1 Kafka入门
概念




架构




1.1 概述
1.1.1 初始Kafka
Kafka是一个由Scala和Java语言开发的,经典高吞吐量的分布式消息发布和订阅系统,也是大数据技术领域中用作数据交换的核心组件之一。以高吞吐,低延迟,高伸缩,高可靠性,高并发,且社区活跃度高等特性,从而备受广大技术组织的喜爱。
2010年,Linkedin公司为了解决消息传输过程中由各种缺陷导致的阻塞、服务无法访问等问题,主导开发了一款分布式消息日志传输系统。主导开发的首席架构师Jay Kreps因为喜欢写出《变形记》的西方表现主义文学先驱小说家Jay Kafka,所以给这个消息系统起了一个很酷,却和软件系统特性无关的名称Kafka。
因为备受技术组织的喜爱,2011年,Kafka软件被捐献给Apache基金会,并于7月被纳入Apache软件基金会孵化器项目进行孵化。2012年10月,Kafka从孵化器项目中毕业,转成Apache的顶级项目。由独立的消息日志传输系统转型为开源分布式事件流处理平台系统,被数千家公司用于高性能数据管道、流分析、数据集成和关键任务应用程序。
官网地址:https://kafka.apache.org/

1.1.2 消息队列
Kafka软件最初的设计就是专门用于数据传输的消息系统,类似功能的软件有RabbitMQ、ActiveMQ、RocketMQ等。这些软件名称中的MQ是英文单词Message Queue的简称,也就是所谓的消息队列的意思。这些软件的核心功能是传输数据,而Java中如果想要实现数据传输功能,那么这个软件一般需要遵循Java消息服务技术规范JMS(Java Message Service)。前面提到的ActiveMQ软件就完全遵循了JMS技术规范,而RabbitMQ是遵循了类似JMS规范并兼容JMS规范的跨平台的AMQP(Advanced Message Queuing Protocol)规范。除了上面描述的JMS,AMQP外,还有一种用于物联网小型设备之间传输消息的MQTT通讯协议。
Kafka拥有作为一个消息系统应该具备的功能,但是却有着独特的设计。可以这样说,Kafka借鉴了JMS规范的思想,但是却并没有完全遵循JMS规范。这也恰恰是软件名称为Kafka,而不是KafkaMQ的原因。
由上可知,无论学习哪一种消息传输系统,JMS规范都是大家应该首先了解的。所以咱们这里就对JMS规范做一个简单的介绍:
- JMS是Java平台的消息中间件通用规范,定义了主要用于消息中间件的标准接口。如果不是很理解这个概念,可以简单地将JMS类比为Java和数据库之间的JDBC规范。Java应用程序根据JDBC规范种的接口访问关系型数据库,而每个关系型数据库厂商可以根据JDBC接口来实现具体的访问规则。JMS定义的就是系统和系统之间传输消息的接口。
- 为了实现系统和系统之间的数据传输,JMS规范中定义很多用于通信的组件:

- JMS Provider:JMS消息提供者。其实就是实现JMS接口和规范的消息中间件,也就是我们提供消息服务的软件系统,比如RabbitMQ、ActiveMQ、Kafka。
- JMS Message:JMS消息。这里的消息指的就是数据。一般采用Java数据模型进行封装,其中包含消息头,消息属性和消息主体内容。
- JMS Producer **:**JMS消息生产者。所谓的生产者,就是生产数据的客户端应用程序,这些应用通过JMS接口发送JMS消息。
- JMS Consumer :JMS消息消费者。所谓的消费者,就是从消息提供者(JMS Provider)中获取数据的客户端应用程序,这些应用通过JMS接口接收JMS消息。
- JMS支持两种消息发送和接收模型:一种是P2P(Peer-to-Peer)点对点模型,另外一种是发布/订阅(Publish/Subscribe)模型。
- P2P 模型:P2P模型是基于队列的,消息生产者将数据发送到消息队列中,消息消费者从消息队列中接收消息。因为队列的存在,消息的异步传输成为可能。P2P模型的规定就是每一个消息数据,只有一个消费者,当发送者发送消息以后,不管接收者有没有运行都不影响消息发布到队列中。接收者在成功接收消息后会向发送者发送接收成功的消息
- 发布 / 订阅模型:所谓得发布订阅模型就是事先将传输的数据进行分类,我们管这个数据的分类称之为主题(Topic)。也就是说,生产者发送消息时,会根据主题进行发送。比如咱们的消息中有一个分类是NBA,那么生产者在生产消息时,就可以将NBA篮球消息数据发送到NBA主题中,这样,对NBA消息主题感兴趣的消费者就可以申请订阅NBA主题,然后从该主题中获取消息。这样,也就是说一个消息,是允许被多个消费者同时消费的。这里生产者发送消息,我们称之为发布消息,而消费者从主题中获取消息,我们就称之为订阅消息。Kafka采用就是这种模型。
1.1.3 生产者-消费者模式
生产者-消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通信,而通过阻塞队列来进行通信,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个消息缓冲区,平衡了生产者和消费者的处理能力。在数据传输过程中,起到了一个削弱峰值的作用,也就是我们经常说到的削峰。

图形中的缓冲区就是用来给生产者和消费者解耦的。在单点环境中,我们一般会采用阻塞式队列实现这个缓冲区。而在分布式环境中,一般会采用第三方软件实现缓冲区,这个第三方软件我们一般称之为中间件。纵观大多数应用场景,解耦合最常用的方式就是增加中间件。
遵循JMS规范的消息传输软件(RabbitMQ、ActiveMQ、Kafka、RocketMQ),我们一般就称之为消息中间件。使用软件的目的本质上也就是为了降低消息生产者和消费者之间的耦合性。提升消息的传输效率。
1.1.4 消息中间件对比
|----------------|--------------------------|--------------------------|-------------------|--------------------------------|
| 特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka |
| 单机吞吐量 | 万级,比RocketMQ,Kafka低一个数量级 | 万级,比RocketMQ,Kafka低一个数量级 | 10万级,支持高吞吐 | 10万级,支持高吞吐 |
| Topic数量对吞吐量的影响 | | | Topic可以达到几百/几千量级 | Topic可以达到几百量级,如果更多的话,吞吐量会大幅度下降 |
| 时效性 | ms级 | 微秒级别,延迟最低 | ms级 | ms级 |
| 可用性 | 高,基于主从架构实现高可用 | 高,基于主从架构实现高可用 | 非常高,分布式架构 | 非常高,分布式架构 |
| 消息可靠性 | 有较低的概率丢失数据 | 基本不丢 | 经过参数优化配置,可以做到0丢失 | 经过参数优化配置,可以做到0丢失 |
| 功能支持 | MQ领域的功能极其完备 | 并发能力强,性能极好,延时很低 | MQ功能较为完善,分布式,扩展性好 | 功能较为简单,支持简单的MQ功能,在大数据领域被广泛使用 |
| 其他 | 很早的软件,社区不是很活跃 | 开源,稳定,社区活跃度高 | 阿里开发,社区活跃度不高 | 开源,高吞吐量,社区活跃度极高 |
通过上面各种消息中间件的对比,大概可以了解,在大数据场景中我们主要采用kafka作为消息中间件,而在JaveEE开发中我们主要采用ActiveMQ、RabbitMQ、RocketMQ作为消息中间件。如果将JavaEE和大数据在项目中进行融合的话,那么Kafka其实是一个不错的选择。
1.1.5 Zookeeper
ZooKeeper是一个开放源码的分布式应用程序协调服务软件。在当前的Web软件开发中,多节点分布式的架构设计已经成为必然,那么如何保证架构中不同的节点所运行的环境,系统配置是相同的,就是一个非常重要的话题。一般情况下,我们会采用独立的第三方软件保存分布式系统中的全局环境信息以及系统配置信息,这样系统中的每一个节点在运行时就可以从第三方软件中获取一致的数据。也就是说通过这个第三方软件来协调分布式各个节点之间的环境以及配置信息。Kafka软件是一个分布式事件流处理平台系统,底层采用分布式的架构设计,就是说,也存在多个服务节点,多个节点之间Kafka就是采用ZooKeeper来实现协调调度的。
ZooKeeper 的核心作用:
- ZooKeeper的数据存储结构可以简单地理解为一个Tree结构,而Tree结构上的每一个节点可以用于存储数据,所以一般情况下,我们可以将分布式系统的元数据(环境信息以及系统配置信息)保存在ZooKeeper节点中。
- ZooKeeper创建数据节点时,会根据业务场景创建临时节点或永久(持久)节点。永久节点就是无论客户端是否连接上ZooKeeper都一直存在的节点,而临时节点指的是客户端连接时创建,断开连接后删除的节点。同时,ZooKeeper也提供了Watch(监控)机制用于监控节点的变化,然后通知对应的客户端进行相应的变化。Kafka软件中就内置了ZooKeeper的客户端,用于进行ZooKeeper的连接和通信。
其实,Kafka作为一个独立的分布式消息传输系统,还需要第三方软件进行节点间的协调调度,不能实现自我管理,无形中就导致Kafka和其他软件之间形成了耦合性,制约了Kafka软件的发展,所以从Kafka 2.8.X版本开始,Kafka就尝试增加了Raft算法实现节点间的协调管理,来代替ZooKeeper。不过Kafka官方不推荐此方式应用在生产环境中,计划在Kafka 4.X版本中完全移除ZooKeeper,让我们拭目以待。
1.2 快速上手
1.2.1 环境安装
作为开源分布式事件流处理平台,Kafka分布式软件环境的安装相对比较复杂,不利于Kafka软件的入门学习和练习。所以我们这里先搭建相对比较简单的windows单机环境,让初学者快速掌握软件的基本原理和用法,后面的课程中,我们再深入学习Kafka软件在生产环境中的安装和使用。
1.2.1.1安装Java8(略)
当前Java软件开发中,主流的版本就是Java 8,而Kafka 3.X官方建议Java版本更新至Java11,但是Java8依然可用。未来Kafka 4.X版本会完全弃用Java8,不过,咱们当前学习的Kafka版本为3.6.1版本,所以使用Java8即可,无需升级。
Kafka的绝大数代码都是Scala语言编写的,而Scala语言本身就是基于Java语言开发的,并且由于Kafka内置了Scala语言包,所以Kafka是可以直接运行在JVM上的,无需安装其他软件。你能看到这个课件,相信你肯定已经安装Java8了,基本的环境变量也应该配置好了,所以此处安装过程省略。
1.2.1.2安装Kafka
下载软件安装包 :kafka_2.12-3.6.1.tgz,下载地址:Apache Kafka
这里的3.6.1,是Kafka软件的版本。截至到2023年12月24日,Kafka最新版本为3.6.1。
2.12是对应的Scala开发语言版本。Scala2.12和Java8是兼容的,所以可以直接使用。
tgz是一种linux系统中常见的压缩文件格式,类似与windows系统的zip和rar格式。所以Windows环境中可以直接使用压缩工具进行解压缩。

解压文件:kafka_2.12-3.6.1.tgz,解压目录为非系统盘的根目录,比如e:/

为了访问方便,可以将解压后的文件目录改为kafka, 更改后的文件目录结构如下:
|-------------|-------------------|
| bin | linux系统下可执行脚本文件 |
| bin/windows | windows系统下可执行脚本文件 |
| config | 配置文件 |
| libs | 依赖类库 |
| licenses | 许可信息 |
| site-docs | 文档 |
| logs | 服务日志 |
1.2.1.3启动ZooKeeper
当前版本Kafka软件内部依然依赖ZooKeeper进行多节点协调调度,所以启动Kafka软件之前,需要先启动ZooKeeper软件。不过因为Kafka软件本身内置了ZooKeeper软件,所以无需额外安装ZooKeeper软件,直接调用脚本命令启动即可。具体操作步骤如下:
- 进入Kafka解压缩文件夹的config目录,修改zookeeper.properties配置文件
the directory where the snapshot is stored.
修改dataDir配置,用于设置ZooKeeper数据存储位置,该路径如果不存在会自动创建。
dataDir=E:/kafka_2.12-3.6.1/data/zk
- 打开DOS窗口,进入e:/kafka_2.12-3.6.1/bin/windows目录

- 因为本章节演示的是Windows环境下Kafka软件的安装和使用,所以启动ZooKeeper软件的指令为Windows环境下的bat批处理文件。调用启动指令时,需要传递配置文件的路径
因为当前目录为windows,所以需要通过相对路径找到zookeeper的配置文件。
zookeeper-server-start.bat ../../config/zookeeper.properties

- 出现如下界面,ZooKeeper启动成功。

- 为了操作方便,也可以在kafka解压缩后的目录中,创建脚本文件zk.cmd。
调用启动命令,且同时指定配置文件。
call bin/windows/zookeeper-server-start.bat config/zookeeper.properties
1.2.1.4启动Kafka
进入Kafka解压缩文件夹的config目录,修改server.properties配置文件
Listener name, hostname and port the broker will advertise to clients.
If not set, it uses the value for "listeners".
客户端访问Kafka服务器时,默认连接的服务为本机的端口9092,如果想要改变,可以修改如下配置
此处我们不做任何改变,默认即可
#advertised.listeners=PLAINTEXT://your.host.name:9092
A comma separated list of directories under which to store log files
配置Kafka数据的存放位置,如果文件目录不存在,会自动生成。
log.dirs=E:/kafka_2.12-3.6.1/data/kafka
- 打开DOS窗口,进入e:/kafka_2.12-3.6.1/bin/windows目录

- 调用启动指令,传递配置文件的路径
因为当前目录为windows,所以需要通过相对路径找到kafka的配置文件。
kafka-server-start.bat ../../config/server.properties

- 出现如下界面,Kafka启动成功。

- 为了操作方便,也可以在kafka解压缩后的目录中,创建脚本文件kfk.cmd。
调用启动命令,且同时指定配置文件。
call bin/windows/kafka-server-start.bat config/server.properties
- DOS窗口中,输入jps指令,查看当前启动的软件进程

这里名称为QuorumPeerMain的就是ZooKeeper软件进程,名称为Kafka的就是Kafka系统进程。此时,说明Kafka已经可以正常使用了。
1.2.2 消息主题
在消息发布/订阅(Publish/Subscribe)模型中,为了可以让消费者对感兴趣的消息进行消费,而不是对所有的数据进行消费,包括那些不感兴趣的消息,所以定义了主题(Topic)的概念,也就是说将不同的消息进行分类,分成不同的主题(Topic),然后消息生产者在生成消息时,就会向指定的主题(Topic)中发送。而消息消费者也可以订阅自己感兴趣的主题(Topic)并从中获取消息。
有很多种方式都可以操作Kafka消息中的主题(Topic):命令行、第三方工具、Java API、自动创建。而对于初学者来讲,掌握基本的命令行操作是必要的。所以接下来,我们采用命令行进行操作。
1.2.2.1创建主题
- 启动ZooKeeper,Kafka服务进程(略)
- 打开DOS窗口,进入e:/kafka_2.12-3.6.1/bin/windows目录

- DOS窗口输入指令,创建主题
Kafka是通过kafka-topics.bat 指令文件进行消息主题操作的。其中包含了对主题的查询,创建,删除等功能。
调用指令创建主题时,需要传递多个参数,而且参数的前缀为两个横线。因为参数比较多,为了演示方便,这里我们只说明必须传递的参数,其他参数后面课程中会进行讲解
--bootstrap-server : 把当前的DOS窗口当成Kafka的客户端,那么进行操作前,就需要连接服务器,这里的参数就表示服务器的连接方式,因为我们在本机启动Kafka服务进程,且Kafka默认端口为9092,所以此处,后面接的参数值为localhost:9092 ,用空格隔开
--create : 表示对主题的创建操作,是个操作参数,后面无需增加参数值
--topic : 主题的名称,后面接的参数值一般就是见名知意的字符串名称,类似于java中的字符串类型标识符名称,当然也可以使用数字,只不过最后还是当成数字字符串使用。
指令
kafka-topics.bat --bootstrap-server localhost:9092 --create --topic test

1.2.2.2查询主题
- DOS窗口输入指令,查看所有主题
Kafka是通过kafka-topics.bat 文件进行消息主题操作的。其中包含了对主题的查询,创建,删除等功能。
--bootstrap-server : 把当前的DOS窗口当成Kafka的客户端,那么进行操作前,就需要连接服务器,这里的参数就表示服务器的连接方式,因为我们在本机启动Kafka服务进程,且Kafka默认端口为9092,所以此处,后面接的参数值为localhost:9092,用空格隔开
--list : 表示对所有主题的查询操作,是个操作参数,后面无需增加参数值
指令
kafka-topics.bat --bootstrap-server localhost:9092 --list

- DOS窗口输入指令,查看指定主题信息
--bootstrap-server : 把当前的DOS窗口当成Kafka的客户端,那么进行操作前,就需要连接服务器,这里的参数就表示服务器的连接方式,因为我们在本机启动Kafka服务进程,且Kafka默认端口为9092,所以此处,后面接的参数值为localhost:9092,用空格隔开
--describe : 查看主题的详细信息
--topic : 查询的主题名称
指令
kafka-topics.bat --bootstrap-server localhost:9092 --describe --topic test

1.2.2.3修改主题
创建主题后,可能需要对某些参数进行修改,那么就需要使用指令进行操作。
- DOS窗口输入指令,修改指定主题的参数
Kafka是通过kafka-topics.bat 文件进行消息主题操作的。其中包含了对主题的查询,创建,删除等功能。
--bootstrap-server : 把当前的DOS窗口当成Kafka的客户端,那么进行操作前,就需要连接服务器,这里的参数就表示服务器的连接方式,因为我们在本机启动Kafka服务进程,且Kafka默认端口为9092,所以此处,后面接的参数值为localhost:9092,用空格隔开
--alter : 表示对所有主题的查询操作,是个操作参数,后面无需增加参数值
--topic : 修改的主题名称
--partitions : 修改的配置参数:分区数量
指令
kafka-topics.bat --bootstrap-server localhost:9092 --topic test --alter --partitions 2

1.2.2.4删除主题
如果主题创建后不需要了,或创建的主题有问题,那么我们可以通过相应的指令删除主题。
- DOS窗口输入指令,删除指定名称的主题
Kafka是通过kafka-topics.bat 文件进行消息主题操作的。其中包含了对主题的查询,创建,删除等功能。
--bootstrap-server : 把当前的DOS窗口当成Kafka的客户端,那么进行操作前,就需要连接服务器,这里的参数就表示服务器的连接方式,因为我们在本机启动Kafka服务进程,且Kafka默认端口为9092,所以此处,后面接的参数值为localhost:9092,用空格隔开
--delete: 表示对主题的删除操作,是个操作参数,后面无需增加参数值。默认情况下,删除操作是逻辑删除,也就是说数据存储的文件依然存在,但是通过指令查询不出来。如果想要直接删除,需要在server.properties文件中设置参数delete.topic.enable=true
--topic : 删除的主题名称
指令
kafka-topics.bat --bootstrap-server localhost:9092 --topic test --delete

注意:windows系统中由于权限或进程锁定的问题,删除topic会导致kafka服务节点异常关闭。

请在后续的linux系统下演示此操作。
1.2.3 生产数据
消息主题创建好了,就可以通过Kafka客户端向Kafka服务器的主题中发送消息了。Kafka生产者客户端并不是一个独立的软件系统,而是一套API接口,只要通过接口能连接Kafka并发送数据的组件我们都可以称之为Kafka生产者。下面我们就演示几种不同的方式:
1.2.3.1命令行操作
- 打开DOS窗口,进入e:/kafka_2.12-3.6.1/bin/windows目录

- DOS窗口输入指令,进入生产者控制台
Kafka是通过kafka-console-producer.bat 文件进行消息生产者操作的。
调用指令时,需要传递多个参数,而且参数的前缀为两个横线,因为参数比较多。为了演示方便,这里我们只说明必须传递的参数,其他参数后面课程中会进行讲解
--bootstrap-server : 把当前的DOS窗口当成Kafka的客户端,那么进行操作前,就需要连接服务器,这里的参数就表示服务器的连接方式,因为我们在本机启动Kafka服务进程,且Kafka默认端口为9092,所以此处,后面接的参数值为localhost:9092 ,用空格隔开。早期版本的Kafka也可以通过 --broker-list参数进行连接,当前版本已经不推荐使用了。
--topic : 主题的名称,后面接的参数值就是之前已经创建好的主题名称。
指令
kafka-console-producer.bat --bootstrap-server localhost:9092 --topic test

- 控制台生产数据

注意:这里的数据需要回车后,才能真正将数据发送到Kafka服务器。
1.2.3.2工具操作
有的时候,使用命令行进行操作还是有一些麻烦,并且操作起来也不是很直观,所以我们一般会采用一些小工具进行快速访问。这里我们介绍一个kafkatool_64bit.exe工具软件。软件的安装过程比较简单,根据提示默认安装即可,这里就不进行介绍了。
- 安装好以后,我们打开工具

- 点击左上角按钮File -> Add New Connection...建立连接

- 点击Test按钮测试

- 增加连接

- 按照下面的步骤,生产数据

- 增加成功后,点击绿色箭头按钮进行查询,工具会显示当前数据

1.2.3.3 Java API
一般情况下,我们也可以通过Java程序来生产数据,所以接下来,我们就演示一下IDEA中使用Kafka Java API来生产数据:
- 创建Kafka项目

- 修改pom.xml文件,增加Maven依赖

<dependencies>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.6.1</version>
</dependency>
</dependencies>
- 创建 com.atguigu.kafka.test.KafkaProducerTest类

- 添加main方法,并增加生产者代码

package com.atguigu.kafka.test;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import java.util.HashMap;
import java.util.Map;
public class KafkaProducerTest {
public static void main(String[] args) {
// TODO 配置属性集合
Map<String, Object> configMap = new HashMap<>();
// TODO 配置属性:Kafka服务器集群地址
configMap.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
// TODO 配置属性:Kafka生产的数据为KV对,所以在生产数据进行传输前需要分别对K,V进行对应的序列化操作
configMap.put(
ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.StringSerializer");
configMap.put(
ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.StringSerializer");
// TODO 创建Kafka生产者对象,建立Kafka连接
// 构造对象时,需要传递配置参数
KafkaProducer<String, String> producer = new KafkaProducer<>(configMap);
// TODO 准备数据,定义泛型
// 构造对象时需要传递 【Topic主题名称】,【Key】,【Value】三个参数
ProducerRecord<String, String> record = new ProducerRecord<String, String>(
"test", "key1", "value1"
);
// TODO 生产(发送)数据
producer.send(record);
// TODO 关闭生产者连接
producer.close();
}
}
1.2.4 消费数据
消息已经通过Kafka生产者客户端发送到Kafka服务器中了。那么此时,这个消息就会暂存在Kafka中,我们也就可以通过Kafka消费者客户端对服务器指定主题的消息进行消费了。
1.2.4.1命令行操作
- 打开DOS窗口,进入e:/kafka_2.12-3.6.1/bin/windows目录

- DOS窗口输入指令,进入消费者控制台
Kafka是通过kafka-console-consumer.bat 文件进行消息消费者操作的。
调用指令时,需要传递多个参数,而且参数的前缀为两个横线,因为参数比较多。为了演示方便,这里我们只说明必须传递的参数,其他参数后面课程中会进行讲解
--bootstrap-server : 把当前的DOS窗口当成Kafka的客户端,那么进行操作前,就需要连接服务器,这里的参数就表示服务器的连接方式,因为我们在本机启动Kafka服务进程,且Kafka默认端口为9092,所以此处,后面接的参数值为localhost:9092 ,用空格隔开。早期版本的Kafka也可以通过 --broker-list参数进行连接,当前版本已经不推荐使用了。
--topic : 主题的名称,后面接的参数值就是之前已经创建好的主题名称。其实这个参数并不是必须传递的参数,因为如果不传递这个参数的话,那么消费者会消费所有主题的消息。如果传递这个参数,那么消费者只能消费到指定主题的消息数据。
--from-beginning : 从第一条数据开始消费,无参数值,是一个标记参数。默认情况下,消费者客户端连接上服务器后,是不会消费到连接之前所生产的数据的。也就意味着如果生产者客户端在消费者客户端连接前已经生产了数据,那么这部分数据消费者是无法正常消费到的。所以在实际环境中,应该是先启动消费者客户端,再启动生产者客户端,保证消费数据的完整性。增加参数后,Kafka就会从第一条数据开始消费,保证消息数据的完整性。
指令
kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic test --from-beginning

1.2.4.2 Java API
一般情况下,我们可以通过Java程序来消费(获取)数据,所以接下来,我们就演示一下IDEA中Kafka Java API如何消费数据:
- 创建Maven项目并增加Kafka依赖
- 创建com.atguigu.kafka.test.KafkaConsumerTest类

- 添加main方法,并增加消费者代码


package com.atguigu.kafka.test;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class KafkaConsumerTest {
public static void main(String[] args) {
// TODO 配置属性集合
Map<String, Object> configMap = new HashMap<String, Object>();
// TODO 配置属性:Kafka集群地址
configMap.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
// TODO 配置属性: Kafka传输的数据为KV对,所以需要对获取的数据分别进行反序列化
configMap.put(
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.StringDeserializer");
configMap.put(
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.StringDeserializer");
// TODO 配置属性: 读取数据的位置 ,取值为earliest(最早),latest(最晚)
configMap.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");
// TODO 配置属性: 消费者组
configMap.put("group.id", "atguigu");
// TODO 配置属性: 自动提交偏移量
configMap.put("enable.auto.commit", "true");
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(configMap);
// TODO 消费者订阅指定主题的数据
consumer.subscribe(Collections.singletonList("test"));
while ( true ) {
// TODO 每隔100毫秒,抓取一次数据
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofMillis(100));
// TODO 打印抓取的数据
for (ConsumerRecord<String, String> record : records) {
System.out.println("K = " + record.key() + ", V = " + record.value());
}
}
}
}
1.2.5 源码关联(可选)
将源码压缩包kafka-3.6.1-src.tgz解压缩到指定位置

Kafka3.6.1的源码需要使用JDK17和Scala2.13进行编译才能查看,所以需要进行安装
1.2.5.1 安装Java17
再资料文件夹中双击安装包jdk-17_windows-x64_bin.exe

根据安装提示安装即可。
1.2.5.2 安装Scala
进入Scala官方网站https://www.scala-lang.org/下载Scala压缩包scala-2.13.12.zip。
在IDEA中安装Scala插件

项目配置中关联Scala就可以了

1.2.5.3 安装Gradle
进入Gradle官方网站https://gradle.org/releases/下载Gradle安装包,根据自己需要选择不同版本进行下载。下载后将Gradle文件解压到相应目录
新增系统环境GRADLE_HOME,指定gradle安装路径,并将%GRADLE_HOME%\bin添加到path中
Gradle安装及环境变量配置完成之后,打开Windows的cmd命令窗口,输入gradle --version

在解压缩目录中打开命令行,依次执行gradle idea命令

在命令行中执行gradle build --exclude-task test命令

使用IDE工具IDEA打开该项目目录

1.2.6 总结
本章作为Kafka软件的入门章节,介绍了一些消息传输系统中的基本概念以及单机版Windows系统中Kafka软件的基本操作。如果仅从操作上,感觉Kafka和数据库的功能还是有点像的。比如:
- 数据库可以创建表保存数据,kafka可以创建主题保存消息。
- Java客户端程序可以通过JDBC访问数据库:保存数据、修改数据、查询数据,kafka可以通过生产者客户端生产数据,通过消费者客户端消费数据。
从这几点来看,确实有相像的地方,但其实两者的本质并不一样:
- 数据库的本质是为了更好的组织和管理数据,所以关注点是如何设计更好的数据模型用于保存数据,保证核心的业务数据不丢失,这样才能准确地对数据进行操作。
- Kafka的本质是为了高效地传输数据。所以软件的侧重点是如何优化传输的过程,让数据更快,更安全地进行系统间的传输。
通过以上的介绍,你会发现,两者的区别还是很大的,不能混为一谈。接下来的章节我们会给大家详细讲解Kafka在分布式环境中是如何高效地传输数据的。
2 Kafka基础
2.1 集群部署
生产环境都是采用linux系统搭建服务器集群,但是我们的重点是在于学习kafka的基础概念和核心组件,所以这里我们搭建一个简单易用的windows集群方便大家的学习和练习。Linux集群的搭建会在第3章给大家进行讲解。
2.1.1 解压文件
在磁盘根目录创建文件夹cluster,文件夹名称不要太长

将kafka安装包kafka-3.6.1-src.tgz解压缩到kafka文件夹

2.1.2 安装ZooKeeper
(1)修改文件夹名为kafka-zookeeper
因为kafka内置了ZooKeeper软件,所以此处将解压缩的文件作为ZooKeeper软件使用。

(2)修改config/zookeeper.properties文件

2.1.3 安装Kafka
(1)将上面解压缩的文件复制一份,改名为kafka-node-1

(2)修改config/server.properties配置文件


(3)将kafka-node-1文件夹复制两份,改名为kafka-node-2,kafka-node-3

(4)分别修改kafka-node-2,kafka-node-3文件夹中的配置文件server.properties
- 将文件内容中的broker.id=1分别改为broker.id=2,broker.id=3
- 将文件内容中的9091分别改为9092,9093(如果端口冲突,请重新设置)
- 将文件内容中的kafka-node-1分别改为kafka-node-2,kafka-node-3
2.1.4 封装启动脚本
因为Kafka启动前,必须先启动ZooKeeper,并且Kafka集群中有多个节点需要启动,所以启动过程比较繁琐,这里我们将启动的指令进行封装。
(1)在kafka-zookeeper文件夹下创建zk.cmd批处理文件

(2)在zk.cmd文件中添加内容

(3)在kafka-node-1,kafka-node-2,kafka-node-3文件夹下分别创建kfk.cmd批处理文件

(4)在kfk.bat文件中添加内容

(5)在cluster文件夹下创建cluster.cmd批处理文件,用于启动kafka集群

(6)在cluster.cmd文件中添加内容

(7)在cluster文件夹下创建cluster-clear.cmd批处理文件,用于清理和重置kafka数据

(8)在cluster-clear.cmd文件中添加内容

(9)双击执行cluster.cmd文件,启动Kafka集群
集群启动命令后,会打开多个黑窗口,每一个窗口都是一个kafka服务,请不要关闭,一旦关闭,对应的kafka服务就停止了。如果启动过程报错,主要是因为zookeeper和kafka的同步问题,请先执行cluster-clear.cmd文件,再执行cluster.cmd文件即可。

2.2 集群启动
启动服务器







启动服务器底层实现




2.2.1 相关概念
2.2.1.1 代理:Broker
使用Kafka前,我们都会启动Kafka服务进程,这里的Kafka服务进程我们一般会称之为Kafka Broker或Kafka Server。因为Kafka是分布式消息系统,所以在实际的生产环境中,是需要多个服务进程形成集群提供消息服务的。所以每一个服务节点都是一个broker,而且在Kafka集群中,为了区分不同的服务节点,每一个broker都应该有一个不重复的全局ID,称之为broker.id,这个ID可以在kafka软件的配置文件server.properties中进行配置

咱们的Kafka集群中每一个节点都有自己的ID,整数且唯一。
|-----------|---------------|---------------|---------------|
| 主机 | kafka-broker1 | kafka-broker2 | kafka-broker3 |
| broker.id | 1 | 2 | 3 |
2.2.1.2 控制器:Controller
Kafka是分布式消息传输系统,所以存在多个Broker服务节点,但是它的软件架构采用的是分布式系统中比较常见的主从(Master - Slave)架构,也就是说需要从多个Broker中找到一个用于管理整个Kafka集群的Master节点,这个节点,我们就称之为Controller。它是Apache Kafka的核心组件非常重要。它的主要作用是在Apache Zookeeper的帮助下管理和协调控制整个Kafka集群。

如果在运行过程中,Controller节点出现了故障,那么Kafka会依托于ZooKeeper软件选举其他的节点作为新的Controller,让Kafka集群实现高可用。

Kafka集群中Controller的基本功能:
Broker管理
监听 /brokers/ids节点相关的变化:
Broker数量增加或减少的变化
Broker对应的数据变化
Topic管理
新增:监听 /brokers/topics节点相关的变化
修改:监听 /brokers/topics节点相关的变化
删除:监听 /admin/delete_topics节点相关的变化
Partation管理
监听 /admin/reassign_partitions节点相关的变化
监听 /isr_change_notification节点相关的变化
监听 /preferred_replica_election节点相关的变化
数据服务
启动分区状态机和副本状态机
2.2.2 启动ZooKeeper
Kafka集群中含有多个服务节点,而分布式系统中经典的主从(Master - Slave)架构就要求从多个服务节点中找一个节点作为集群管理Master,Kafka集群中的这个Master,我们称之为集群控制器Controller

如果此时Controller节点出现故障,它就不能再管理集群功能,那么其他的Slave节点该如何是好呢?

如果从剩余的2个Slave节点中选一个节点出来作为新的集群控制器是不是一个不错的方案,我们将这个选择的过程称之为:选举(elect)。方案是不错,但是问题就在于选哪一个Slave节点呢?不同的软件实现类似的选举功能都会有一些选举算法,而Kafka是依赖于ZooKeeper软件实现Broker节点选举功能。

ZooKeeper如何实现Kafka的节点选举呢?这就要说到我们用到ZooKeeper的3个功能:
- 一个是在ZooKeeper软件中创建节点Node,创建一个Node时,我们会设定这个节点是持久化创建,还是临时创建。所谓的持久化创建,就是Node一旦创建后会一直存在,而临时创建,是根据当前的客户端连接创建的临时节点Node,一旦客户端连接断开,那么这个临时节点Node也会被自动删除,所以这样的节点称之为临时节点。
- ZooKeeper节点是不允许有重复的,所以多个客户端创建同一个节点,只能有一个创建成功。
- 另外一个是客户端可以在ZooKeeper的节点上增加监听器,用于监听节点的状态变化,一旦监听的节点状态发生变化,那么监听器就会触发响应,实现特定监听功能。
有了上面的三个知识点,我们这里就介绍一下Kafka是如何利用ZooKeeper实现Controller节点的选举的:
- 第一次启动Kafka集群时,会同时启动多个Broker节点,每一个Broker节点就会连接ZooKeeper,并尝试创建一个临时节点 /controller
- 因为ZooKeeper中一个节点不允许重复创建,所以多个Broker节点,最终只能有一个Broker节点可以创建成功,那么这个创建成功的Broker节点就会自动作为Kafka集群控制器节点,用于管理整个Kafka集群。
- 没有选举成功的其他Slave节点会创建Node监听器,用于监听 /controller节点的状态变化。
- 一旦Controller节点出现故障或挂掉了,那么对应的ZooKeeper客户端连接就会中断。ZooKeeper中的 /controller 节点就会自动被删除,而其他的那些Slave节点因为增加了监听器,所以当监听到 /controller 节点被删除后,就会马上向ZooKeeper发出创建 /controller 节点的请求,一旦创建成功,那么该Broker就变成了新的Controller节点了。
现在我们能明白启动Kafka集群之前,为什么要先启动ZooKeeper集群了吧。就是因为ZooKeeper可以协助Kafka进行集群管理。
2.2.3 启动Kafka
ZooKeeper已经启动好了,那我们现在可以启动多个Kafka Broker节点构建Kafka集群了。构建的过程中,每一个Broker节点就是一个Java进程,而在这个进程中,有很多需要提前准备好,并进行初始化的内部组件对象。
2.2.3.1初始化ZooKeeper
Kafka Broker启动时,首先会先创建ZooKeeper客户端(KafkaZkClient),用于和ZooKeeper进行交互。客户端对象创建完成后,会通过该客户端对象向ZooKeeper发送创建Node的请求,注意,这里创建的Node都是持久化Node。

|-----------------------------|--------|-----------------------------------------------------------------------------------------------------------------------|
| 节点 | 类型 | 说明 |
| /admin/delete_topics | 持久化节点 | 配置需要删除的topic,因为删除过程中,可能broker下线,或执行失败,那么就需要在broker重新上线后,根据当前节点继续删除操作,一旦topic所有的分区数据全部删除,那么当前节点的数据才会进行清理 |
| /brokers/ids | 持久化节点 | 服务节点ID标识,只要broker启动,那么就会在当前节点中增加子节点,brokerID不能重复 |
| /brokers/topics | 持久化节点 | 服务节点中的主题详细信息,包括分区,副本 |
| /brokers/seqid | 持久化节点 | seqid主要用于自动生产brokerId |
| /config/changes | 持久化节点 | kafka的元数据发生变化时,会向该节点下创建子节点。并写入对应信息 |
| /config/clients | 持久化节点 | 客户端配置,默认为空 |
| /config/brokers | 持久化节点 | 服务节点相关配置,默认为空 |
| /config/ips | 持久化节点 | IP配置,默认为空 |
| /config/topics | 持久化节点 | 主题配置,默认为空 |
| /config/users | 持久化节点 | 用户配置,默认为空 |
| /consumers | 持久化节点 | 消费者节点,用于记录消费者相关信息 |
| /isr_change_notification | 持久化节点 | ISR列表发生变更时候的通知,在kafka当中由于存在ISR列表变更的情况发生,为了保证ISR列表更新的及时性,定义了isr_change_notification这个节点,主要用于通知Controller来及时将ISR列表进行变更。 |
| /latest_producer_id_block | 持久化节点 | 保存PID块,主要用于能够保证生产者的任意写入请求都能够得到响应。 |
| /log_dir_event_notification | 持久化节点 | 主要用于保存当broker当中某些数据路径出现异常时候,例如磁盘损坏,文件读写失败等异常时候,向ZooKeeper当中增加一个通知序号,Controller节点监听到这个节点的变化之后,就会做出对应的处理操作 |
| /cluster/id | 持久化节点 | 主要用于保存kafka集群的唯一id信息,每个kafka集群都会给分配要给唯一id,以及对应的版本号 |
2.2.3.2初始化服务
Kafka Broker中有很多的服务对象,用于实现内部管理和外部通信操作。

2.2.3.2.1 启动任务调度器
每一个Broker在启动时都会创建内部调度器(KafkaScheduler )并启动,用于完成节点内部的工作任务。底层就是Java中的定时任务线程池ScheduledThreadPoolExecutor
2.2.3.2.2 创建数据管理器
每一个Broker在启动时都会创建数据管理器(LogManager),用于接收到消息后,完成后续的数据创建,查询,清理等处理。
2.2.3.2.3 创建远程数据管理器
每一个Broker在启动时都会创建远程数据管理器(RemoteLogManager),用于和其他Broker节点进行数据状态同步。
2.2.3.2.4 创建副本管理器
每一个Broker在启动时都会创建副本管理器(ReplicaManager),用于对主题的副本进行处理。
2.2.3.2.5 创建ZK元数据缓存
每一个Broker在启动时会将ZK的关于Kafka的元数据进行缓存,创建元数据对象(ZkMetadataCache)
2.2.3.2.6 创建Broker通信对象
每一个Broker在启动时会创建Broker之间的通道管理器对象(BrokerToControllerChannelManager),用于管理Broker和Controller之间的通信。
2.2.3.2.7 创建网络通信对象
每一个Broker在启动时会创建自己的网络通信对象(SocketServer),用于和其他Broker之间的进行通信,其中包含了Java用于NIO通信的Channel、Selector对象。

2.2.3.2.8 注册Broker节点
Broker启动时,会通过ZK客户端对象向ZK注册当前的Broker 节点ID,注册后创捷的ZK节点为临时节点。如果当前Broker的ZK客户端断开和ZK的连接,注册的节点会被删除。
2.2.3.3启动控制器
控制器(KafkaController)是每一个Broker启动时都会创建的核心对象,用于和ZK之间建立连接并申请自己为整个Kafka集群的Master管理者。如果申请成功,那么会完成管理者的初始化操作,并建立和其他Broker之间的数据通道接收各种事件,进行封装后交给事件管理器,并定义了process方法,用于真正处理各类事件。

2.2.3.3.1 初始化通道管理器
创建通道管理器(ControllerChannelManager),该管理器维护了Controller和集群所有Broker节点之间的网络连接,并向Broker发送控制类请求及接收响应。
2.2.3.3.2 初始化事件管理器
创建事件管理器(ControllerEventManager)维护了Controller和集群所有Broker节点之间的网络连接,并向Broker发送控制类请求及接收响应。
2.2.3.3.3 初始化状态管理器
创建状态管理器(ControllerChangeHandler )可以监听 /controller 节点的操作,一旦节点创建(ControllerChange),删除(Reelect),数据发生变化(ControllerChange),那么监听后执行相应的处理。
2.2.3.3.4 启动控制器
控制器对象启动后,会向事件管理器发送Startup事件,事件处理线程接收到事件后会通过ZK客户端向ZK申请 /controller 节点,申请成功后,执行当前节点成为Controller的一些列操作。主要是注册各类 ZooKeeper 监听器、删除日志路径变更和 ISR 副本变更通知事件、启动 Controller 通道管理器,以及启动副本状态机和分区状态机。
2.3 创建主题
Topic主题是Kafka中消息的逻辑分类,但是这个分类不应该是固定的,而是应该由外部的业务场景进行定义(注意:Kafka中其实是有两个固定的,用于记录消费者偏移量和事务处理的主题),所以Kafka提供了相应的指令和客户端进行主题操作。
创建主题





创建主题-底层实现



2.3.1 相关概念
2.3.1.1 主题:Topic
Kafka是分布式消息传输系统,采用的数据传输方式为发布,订阅模式,也就是说由消息的生产者发布消息,消费者订阅消息后获取数据。为了对消费者订阅的消息进行区分,所以对消息在逻辑上进行了分类,这个分类我们称之为主题:Topic。消息的生产者必须将消息数据发送到某一个主题,而消费者必须从某一个主题中获取消息,并且消费者可以同时消费一个或多个主题的数据。Kafka集群中可以存放多个主题的消息数据。
为了防止主题的名称和监控指标的名称产生冲突,官方推荐主题的名称中不要同时包含下划线和点。

2.3.1.2 分区:Partition
Kafka消息传输采用发布、订阅模式,所以消息生产者必须将数据发送到一个主题,假如发送给这个主题的数据非常多,那么主题所在broker节点的负载和吞吐量就会受到极大的考验,甚至有可能因为热点问题引起broker节点故障,导致服务不可用。一个好的方案就是将一个主题从物理上分成几块,然后将不同的数据块均匀地分配到不同的broker节点上,这样就可以缓解单节点的负载问题。这个主题的分块我们称之为:分区partition。默认情况下,topic主题创建时分区数量为1,也就是一块分区,可以指定参数--partitions改变。Kafka的分区解决了单一主题topic线性扩展的问题,也解决了负载均衡的问题。
topic主题的每个分区都会用一个编号进行标记,一般是从0开始的连续整数数字。Partition分区是物理上的概念,也就意味着会以数据文件的方式真实存在。每个topic包含一个或多个partition,每个partition都是一个有序的队列。partition中每条消息都会分配一个有序的ID,称之为偏移量:Offset

2.3.1.3 副本:Replication
分布式系统出现错误是比较常见的,只要保证集群内部依然存在可用的服务节点即可,当然效率会有所降低,不过只要能保证系统可用就可以了。咱们Kafka的topic也存在类似的问题,也就是说,如果一个topic划分了多个分区partition,那么这些分区就会均匀地分布在不同的broker节点上,一旦某一个broker节点出现了问题,那么在这个节点上的分区就会出现问题,那么Topic的数据就不完整了。所以一般情况下,为了防止出现数据丢失的情况,我们会给分区数据设定多个备份,这里的备份,我们称之为:副本Replication。
Kafka支持多副本,使得主题topic可以做到更多容错性,牺牲性能与空间去换取更高的可靠性。

注意: 这里不能将多个备份放置在同一个broker中,因为一旦出现故障,多个副本就都不能用了,那么副本的意义就没有了。
2.3.1.4 副本类型:Leader & Follower
假设我们有一份文件,一般情况下,我们对副本的理解应该是有一个正式的完整文件,然后这个文件的备份,我们称之为副本。但是在Kafka中,不是这样的,所有的文件都称之为副本,只不过会选择其中的一个文件作为主文件,称之为:Leader(主导)副本,其他的文件作为备份文件,称之为:Follower(追随)副本。在Kafka中,这里的文件就是分区,每一个分区都可以存在1个或多个副本,只有Leader副本才能进行数据的读写,Follower副本只做备份使用。

2.3.1.5 日志:Log
Kafka最开始的应用场景就是日志场景或MQ场景,更多的扮演着一个日志传输和存储系统,这是Kafka立家之本。所以Kafka接收到的消息数据最终都是存储在log日志文件中的,底层存储数据的文件的扩展名就是log。
主题创建后,会创建对应的分区数据Log日志。并打开文件连接通道,随时准备写入数据。
2.3.2 创建第一个主题
创建主题Topic的方式有很多种:命令行,工具,客户端API,自动创建。在server.properties文件中配置参数auto.create.topics.enable=true时,如果访问的主题不存在,那么Kafka就会自动创建主题,这个操作不在我们的讨论范围内。由于我们学习的重点在于学习原理和基础概念,所以这里我们选择比较基础的命令行方式即可。
我们首先创建的主题,仅仅指明主题的名称即可,其他参数暂时无需设定。
2.3.2.1 执行指令


2.3.2.2 ZooKeeper节点变化
指令执行后,当前Kafka会增加一个主题,因为指令中没有配置分区和副本参数,所以当前主题分区数量为默认值1,编号为0,副本为1,编号为所在broker的ID值。为了方便集群的管理,创建topic时,会同时在ZK中增加子节点,记录主题相关配置信息:
- /config/topics节点中会增加first-topic节点。

- /brokers/topics节点中会增加first-topic节点以及相应的子节点。

|----------------------------------------|----------|-------------------|-------------|---------------------------------|
| 节点 | 节点类型 | 数据名称 | 数据值 | 说明 |
| /topics/first-topic | 持久类型 | removing_replicas | 无 | |
| /topics/first-topic | 持久类型 | partitions | {"0":[3]} | 分区配置 |
| /topics/first-topic | 持久类型 | topic_id | 随机字符串 | |
| /topics/first-topic | 持久类型 | adding_replicas | 无 | |
| /topics/first-topic | 持久类型 | version | 3 | |
| /topics/first-topic/partitions | 持久类型 | | | 主题分区节点,每个主题都应该设置分区,保存在该节点 |
| /topics/first-topic/partitions/0 | 持久类型 | | | 主题分区副本节点,因为当前主题只有一个分区,所以编号为0 |
| /topics/first-topic/partitions/0/state | 持久类型 | controller_epoch | 7 | 主题分区副本状态节点 |
| /topics/first-topic/partitions/0/state | 持久类型 | leader | 3 | Leader副本所在的broker Id |
| /topics/first-topic/partitions/0/state | 持久类型 | version | 1 | |
| /topics/first-topic/partitions/0/state | 持久类型 | leader_epoch | 0 | |
| /topics/first-topic/partitions/0/state | 持久类型 | isr | [3] | 副本同步列表,因为当前只有一个副本,所以副本中只有一个副本编号 |
2.3.2.3 数据存储位置
主题创建后,需要找到一个用于存储分区数据的位置,根据上面ZooKeeper存储的节点配置信息可以知道,当前主题的分区数量为1,副本数量为1,那么数据存储的位置就是副本所在的broker节点,从当前数据来看,数据存储在我们的第三台broker上。




路径中的00000000000000000000.log文件就是真正存储消息数据的文件,文件名称中的0表示当前文件的起始偏移量为0,index文件和timeindex文件都是数据索引文件,用于快速定位数据。只不过index文件采用偏移量的方式进行定位,而timeindex是采用时间戳的方式。
2.3.3 创建第二个主题
接下来我们创建第二个主题,不过创建时,我们需要设定分区参数 --partitions,参数值为3,表示创建3个分区
2.3.3.1 执行指令


2.3.3.2 ZooKeeper节点变化
指令执行后,当前Kafka会增加一个主题,因为指令中指定了分区数量(--partitions 3),所以当前主题分区数量为3,编号为[0、1、2],副本为1,编号为所在broker的ID值。为了方便集群的管理,创建Topic时,会同时在ZK中增加子节点,记录主题相关配置信息:
- /config/topics节点中会增加second-topic节点。

- /brokers/topics节点中会增加second-topic节点以及相应的子节点。

|-----------------------------------------|----------|-------------------|---------------------------------|--------------------------------------|
| 节点 | 节点类型 | 数据名称 | 数据值 | 说明 |
| /topics/second-topic | 持久类型 | removing_replicas | 无 | |
| /topics/second-topic | 持久类型 | partitions | {"2":[3],"1":[2],"0":[1]} | 分区配置 |
| /topics/second-topic | 持久类型 | topic_id | 随机字符串 | |
| /topics/second-topic | 持久类型 | adding_replicas | 无 | |
| /topics/second-topic | 持久类型 | version | 3 | |
| /topics/second-topic/partitions | 持久类型 | | | 主题分区节点,每个主题都应该设置分区,保存在该节点 |
| /topics/second-topic/partitions/0 | 持久类型 | | | 主题分区副本节点,因为当前主题有3个分区,第一个分区编号为0 |
| /topics/second-topic/partitions/0/state | 持久类型 | controller_epoch | 7 | 主题分区副本状态节点 |
| /topics/second-topic/partitions/0/state | 持久类型 | leader | 1 | Leader副本所在的broker Id |
| /topics/second-topic/partitions/0/state | 持久类型 | version | 1 | |
| /topics/second-topic/partitions/0/state | 持久类型 | leader_epoch | 0 | |
| /topics/second-topic/partitions/0/state | 持久类型 | isr | [1] | 副本同步列表,因为当前只有一个副本,所以副本中只有一个副本编号 |
| /topics/second-topic/partitions/1 | 持久类型 | | | 主题分区副本节点,因为当前主题有3个分区,当前为第2个分区,所以编号为1 |
| /topics/second-topic/partitions/1/state | 持久类型 | controller_epoch | 7 | 主题分区副本状态节点 |
| /topics/second-topic/partitions/1/state | 持久类型 | leader | 2 | Leader副本所在的broker Id |
| /topics/second-topic/partitions/1/state | 持久类型 | version | 1 | |
| /topics/second-topic/partitions/1/state | 持久类型 | leader_epoch | 0 | |
| /topics/second-topic/partitions/1/state | 持久类型 | isr | [2] | 副本同步列表,因为当前只有一个副本,所以副本中只有一个副本编号 |
| /topics/second-topic/partitions/2 | 持久类型 | | | 主题分区副本节点,因为当前主题有3个分区,当前为第3个分区,所以编号为2 |
| /topics/second-topic/partitions/2/state | 持久类型 | controller_epoch | 7 | 主题分区副本状态节点 |
| /topics/second-topic/partitions/2/state | 持久类型 | leader | 3 | Leader副本所在的broker Id |
| /topics/second-topic/partitions/2/state | 持久类型 | version | 1 | |
| /topics/second-topic/partitions/2/state | 持久类型 | leader_epoch | 0 | |
| /topics/second-topic/partitions/2/state | 持久类型 | isr | [3] | 副本同步列表,因为当前只有一个副本,所以副本中只有一个副本编号 |
2.3.3.3 数据存储位置
主题创建后,需要找到一个用于存储分区数据的位置,根据上面ZooKeeper存储的节点配置信息可以知道,当前主题的分区数量为3,副本数量为1,那么数据存储的位置就是每个分区Leader副本所在的broker节点。










atguigu@kafka-broker3 datas\]$ cd second-topic-2
\[atguigu@kafka-broker3 second-topic-2\]$ ll

#### 2.3.4 创建第三个主题
接下来我们创建第三个主题,不过创建时,我们需要设定副本参数 --replication-factor,参数值为3,表示每个分区创建3个副本。
##### 2.3.4.1 执行指令

\[atguigu@kafka-broker1 \~\]$ cd /opt/module/kafka
\[atguigu@kafka-broker1 kafka\]$ bin/kafka-topics.sh --bootstrap-server kafka-broker1:9092 --create --topic third-topic --partitions 3 **--replication-factor 3**
##### 2.3.4.2 ZooKeeper节点变化
指令执行后,当前Kafka会增加一个主题,因为指令中指定了分区数量和副本数量(--replication-factor 3),所以当前主题分区数量为3,编号为\[0、1、2\],副本为3,编号为\[1、2、3\]。为了方便集群的管理,创建Topic时,会同时在ZK中增加子节点,记录主题相关配置信息:
* /config/topics节点中会增加third-topic节点。

* /brokers/topics节点中会增加third-topic节点以及相应的子节点。

|----------------------------------------|----------|-------------------|---------------------------------------------|---------------------------------------------------------|
| **节点** | **节点类型** | **数据名称** | **数据值** | **说明** |
| /topics/third-topic | 持久类型 | removing_replicas | 无 | |
| /topics/third-topic | 持久类型 | partitions | {"2":\[1,2,3\],"1":\[3,1,2\],"0":\[2,3,1\]} | 分区配置 |
| /topics/third-topic | 持久类型 | topic_id | 随机字符串 | |
| /topics/third-topic | 持久类型 | adding_replicas | 无 | |
| /topics/third-topic | 持久类型 | version | 3 | |
| /topics/third-topic/partitions | 持久类型 | | | 主题分区节点,每个主题都应该设置分区,保存在该节点 |
| /topics/third-topic/partitions/0 | 持久类型 | | | 主题分区副本节点,因为当前主题有3个分区,第一个分区编号为0 |
| /topics/third-topic/partitions/0/state | 持久类型 | controller_epoch | 7 | 主题分区副本状态节点 |
| /topics/third-topic/partitions/0/state | 持久类型 | leader | 2 | Leader副本所在的broker Id |
| /topics/third-topic/partitions/0/state | 持久类型 | version | 1 | |
| /topics/third-topic/partitions/0/state | 持久类型 | leader_epoch | 0 | |
| /topics/third-topic/partitions/0/state | 持久类型 | isr | \[2,3,1\] | 副本同步列表,因为当前有3个副本,所以列表中的第一个副本就是Leader副本,其他副本均为follower副本 |
| /topics/third-topic/partitions/1 | 持久类型 | | | 主题分区副本节点,因为当前主题有3个分区,当前为第2个分区,所以编号为1 |
| /topics/third-topic/partitions/1/state | 持久类型 | controller_epoch | 7 | 主题分区副本状态节点 |
| /topics/third-topic/partitions/1/state | 持久类型 | leader | 3 | Leader副本所在的broker Id |
| /topics/third-topic/partitions/1/state | 持久类型 | version | 1 | |
| /topics/third-topic/partitions/1/state | 持久类型 | leader_epoch | 0 | |
| /topics/third-topic/partitions/1/state | 持久类型 | isr | \[3,1,2\] | 副本同步列表,因为当前有3个副本,所以列表中的第一个副本就是Leader副本,其他副本均为follower副本 |
| /topics/third-topic/partitions/2 | 持久类型 | | | 主题分区副本节点,因为当前主题有3个分区,当前为第3个分区,所以编号为2 |
| /topics/third-topic/partitions/2/state | 持久类型 | controller_epoch | 7 | 主题分区副本状态节点 |
| /topics/third-topic/partitions/2/state | 持久类型 | leader | 1 | Leader副本所在的broker Id |
| /topics/third-topic/partitions/2/state | 持久类型 | version | 1 | |
| /topics/third-topic/partitions/2/state | 持久类型 | leader_epoch | 0 | |
| /topics/third-topic/partitions/2/state | 持久类型 | isr | \[1,2,3\] | 副本同步列表,因为当前有3个副本,所以列表中的第一个副本就是Leader副本,其他副本均为follower副本 |
##### 2.3.4.3 数据存储位置
主题创建后,需要找到一个用于存储分区数据的位置,根据上面ZooKeeper存储的节点配置信息可以知道,当前主题的分区数量为3,副本数量为3,那么数据存储的位置就是每个分区副本所在的broker节点。
\[atguigu@kafka-broker1 \~\]$ cd /opt/module/kafka/datas
\[atguigu@kafka-broker1 datas\]$ ll

\[atguigu@kafka-broker1 datas\]$ cd third-topic-2
\[atguigu@kafka-broker1 third-topic-2\]$ ll

\[atguigu@kafka-broker2 \~\]$ cd /opt/module/kafka/datas
\[atguigu@kafka-broker2 datas\]$ ll

\[atguigu@kafka-broker2 datas\]$ cd third-topic-0
\[atguigu@kafka-broker2 third-topic-0\]$ ll

\[atguigu@kafka-broker3 \~\]$ cd /opt/module/kafka/datas
\[atguigu@kafka-broker3 datas\]$ ll

\[atguigu@kafka-broker3 datas\]$ cd third-topic-1
\[atguigu@kafka-broker3 third-topic-1\]$ ll

#### 2.3.5 创建主题流程
Kafka中主题、分区以及副本的概念都和数据存储相关,所以是非常重要的。前面咱们演示了一下创建主题的具体操作和现象,那么接下来,我们就通过图解来了解一下Kafka是如何创建主题,并进行分区副本分配的。
##### 2.3.5.1 命令行提交创建指令

1. 通过命令行提交指令,指令中会包含操作类型(**--create** )、topic的名称(**--topic** )、主题分区数量(**--partitions** )、主题分区副本数量(**--replication-facotr** )、副本分配策略(**--replica-assignment**)等参数。
2. 指令会提交到客户端进行处理,客户端获取指令后,会首先对指令参数进行校验。
1. 操作类型取值:create、list、alter、describe、delete,只能存在一个。
2. 分区数量为大于1的整数。
3. 主题是否已经存在
4. 分区副本数量大于1且小于Short.MaxValue,一般取值小于等于Broker数量。
3. 将参数封装主题对象(NewTopic)。
4. 创建通信对象,设定请求标记(CREATE_TOPICS),查找Controller,通过通信对象向Controller发起创建主题的网络请求。
##### 2.3.5.2 Controller接收创建主题请求

1. Controller节点接收到网络请求(Acceptor),并将请求数据封装成请求对象放置在队列(requestQueue)中。
2. 请求控制器(KafkaRequestHandler)周期性从队列中获取请求对象(BaseRequest)。
3. 将请求对象转发给请求处理器(KafkaApis),根据请求对象的类型调用创建主题的方法。
##### 2.3.5.3 创建主题

(1)请求处理器(KafkaApis)校验主题参数。
* 如果分区数量没有设置,那么会采用Kafka启动时加载的配置项:**num.partitions** **(默认值为** **1** **)**
* 如果副本数量没有设置,那么会采用Kafka启动时记载的配置项:
**default.replication.factor** **(默认值为** **1** **)**
(2)在创建主题时,如果使用了replica-assignment参数,那么就按照指定的方案来进行分区副本的创建;如果没有指定replica-assignment参数,那么就按照Kafka内部逻辑来分配,内部逻辑按照机架信息分为两种策略:【未指定机架信息】和【指定机架信息】。当前课程中采用的是【未指定机架信息】副本分配策略:
* 分区起始索引设置0
* 轮询所有分区,计算每一个分区的所有副本位置:
副本起始索引 = (分区编号 + 随机值) % BrokerID列表长度。
其他副本索引 = 。。。随机值(基本算法为使用随机值执行多次模运算)
**##################################################################**
**#** **假设**
**#** **当前分区编号** **: 0**
**# BrokerID** **列表** **:** **【** **1** **,** **2** **,** **3** **,** **4** **】**
**#** **副本数量** **: 4**
**#** **随机值(** **BrokerID** **列表长度)** **: 2**
**#** **副本分配间隔随机值(** **BrokerID** **列表长度)** **: 2**
**##################################################################**
# **第一个副本索引:(分区编号** **+** **随机值)** **% BrokerID** **列表长度** **=** **(** **0 + 2** **)** **% 4 = 2**
# 第一个副本所在BrokerID : 3
# **第二个副本索引(第一个副本索引** **+** **(** **1 +** **(副本分配间隔** **+ 0** **)** **%** **(** **BrokerID** **列表长度** **- 1** **)))** **% BrokerID** **列表长度** **=** **(** **2 +** **(** **1+** **(** **2+0** **)** **%3** **))** **% 4 = 1**
# 第二个副本所在BrokerID:2
# **第三个副本索引:(第一个副本索引** **+** **(** **1 +** **(副本分配间隔** **+ 1** **)** **%** **(** **BrokerID** **列表长度** **- 1** **)))** **% BrokerID** **列表长度** **=** **(** **2 +** **(** **1+** **(** **2+1** **)** **%3** **))** **% 4 = 3**
# 第三个副本所在BrokerID:4
# **第四个副本索引:(第一个副本索引** **+** **(** **1 +** **(副本分配间隔** **+ 2** **)** **%** **(** **BrokerID** **列表长度** **- 1** **)))** **% BrokerID** **列表长度** **=** **(** **2 +** **(** **1+** **(** **2+2** **)** **%3** **))** **% 4 = 0**
# 第四个副本所在BrokerID:1
# 最终分区0的副本所在的Broker节点列表为【3,2,4,1】
# 其他分区采用同样算法
* 通过索引位置获取副本节点ID
* 保存分区以及对应的副本ID列表。
(3)通过ZK客户端在ZK端创建节点:
* 在 /config/topics节点下,增加当前主题节点,节点类型为持久类型。
* 在 /brokers/topics节点下,增加当前主题及相关节点,节点类型为持久类型。
(4)Controller节点启动后,会在/brokers/topics节点增加监听器,一旦节点发生变化,会触发相应的功能:
* 获取需要新增的主题信息
* 更新当前Controller节点保存的主题状态数据
* 更新分区状态机的状态为:NewPartition
* 更新副本状态机的状态:NewReplica
* 更新分区状态机的状态为:OnlinePartition,从正常的副本列表中的获取第一个作为分区的**Leader** **副本** ,所有的副本作为分区的同步副本列表,我们称之为**ISR( In-Sync Replica)** **。** 在ZK路径**/brokers/topics/** **主题名** 上增加分区节点**/partitions** **,** 及状态**/state**节点。
* 更新副本状态机的状态:OnlineReplica
1. Controller节点向主题的各个分区副本所属Broker节点发送LeaderAndIsrRequest请求,向所有的Broker发送UPDATE_METADATA请求,更新自身的缓存。
* Controller向分区所属的Broker发送请求
* Broker节点接收到请求后,根据分区状态信息,设定当前的副本为Leader或Follower,并创建底层的数据存储文件目录和空的数据文件。
文件目录名:主题名 + 分区编号
|----------------------------|----------------|
| **文件名** | **说明** |
| 0000000000000000.log | 数据文件,用于存储传输的小心 |
| 0000000000000000.index | 索引文件,用于定位数据 |
| 0000000000000000.timeindex | 时间索引文件,用于定位数据 |
### 2.4 生产消息








Topic主题已经创建好了,接下来我们就可以向该主题生产消息了,这里我们采用Java代码通过Kafka Producer API的方式生产数据。
#### 2.4.1 生产消息的基本步骤
**创建** **Map** **类型的配置对象,根据场景增加相应的配置属性:**
|---------------------------------------|-------------------------------------------------------------------|--------|---------|----------------------------------|
| **参数名** | **参数作用** | **类型** | **默认值** | **推荐值** |
| bootstrap.servers | 集群地址,格式为: brokerIP1:端口号,brokerIP2:端口号 | 必须 | | |
| key.serializer | 对生产数据Key进行序列化的类完整名称 | 必须 | | Kafka提供的字符串序列化类:StringSerializer |
| value.serializer | 对生产数据Value进行序列化的类完整名称 | 必须 | | Kafka提供的字符串序列化类:StringSerializer |
| interceptor.classes | 拦截器类名,多个用逗号隔开 | 可选 | | |
| batch.size | 数据批次字节大小。此大小会和数据最大估计值进行比较,取大值。 估值=61+21+(keySize+1+valueSize+1+1) | 可选 | 16K | |
| retries | 重试次数 | 可选 | 整型最大值 | 0或整型最大值 |
| request.timeout.ms | 请求超时时间 | 可选 | 30s | |
| linger.ms | 数据批次在缓冲区中停留时间 | 可选 | | |
| acks | 请求应答类型:all(-1), 0, 1 | 可选 | all(-1) | 根据数据场景进行设置 |
| retry.backoff.ms | 两次重试之间的时间间隔 | 可选 | 100ms | |
| buffer.memory | 数据收集器缓冲区内存大小 | 可选 | 32M | 64M |
| max.in.flight.requests.per.connection | 每个节点连接的最大同时处理请求的数量 | 可选 | 5 | 小于等于5 |
| enable.idempotence | 幂等性, | 可选 | true | 根据数据场景进行设置 |
| partitioner.ignore.keys | 是否放弃使用数据key选择分区 | 可选 | false | |
| partitioner.class | 分区器类名 | 可选 | null | |
**创建待发送数据**
在kafka中传递的数据我们称之为消息(message)或记录(record),所以Kafka发送数据前,需要将待发送的数据封装为指定的数据模型:


相关属性必须在构建数据模型时指定,其中主题和value的值是必须要传递的。如果配置中开启了自动创建主题,那么Topic主题可以不存在。value就是我们需要真正传递的数据了,而Key可以用于数据的分区定位。
**创建生产者对象,发送生产的数据:**
根据前面提供的配置信息创建生产者对象,通过这个生产者对象向Kafka服务器节点发送数据,而具体的发送是由生产者对象创建时,内部构建的多个组件实现的,多个组件的关系有点类似于生产者消费者模式。

(1)数据生产者(**KafkaProducer**):生产者对象,用于对我们的数据进行必要的转换和处理,将处理后的数据放入到数据收集器中,类似于生产者消费者模式下的生产者。这里我们简单介绍一下内部的数据转换处理:
1. 如果配置拦截器栈(interceptor.classes),那么将数据进行拦截处理。某一个拦截器出现异常并不会影响后续的拦截器处理。
2. 因为发送的数据为KV数据,所以需要根据配置信息中的序列化对象对数据中Key和Value分别进行序列化处理。
3. 计算数据所发送的分区位置。
4. 将数据追加到数据收集器中。
(2)数据收集器(**RecordAccumulator**):用于收集,转换我们产生的数据,类似于生产者消费者模式下的缓冲区。为了优化数据的传输,Kafka并不是生产一条数据就向Broker发送一条数据,而是通过合并单条消息,进行批量(批次)发送,提高吞吐量,减少带宽消耗。
1. 默认情况下,一个发送批次的数据容量为16K,这个可以通过参数batch.size进行改善。
2. 批次是和分区进行绑定的。也就是说发往同一个分区的数据会进行合并,形成一个批次。
3. 如果当前批次能容纳数据,那么直接将数据追加到批次中即可,如果不能容纳数据,那么会产生新的批次放入到当前分区的批次队列中,这个队列使用的是Java的双端队列Deque。旧的批次关闭不再接收新的数据,等待发送
(3)数据发送器(**Sender**):线程对象,用于从收集器对象中获取数据,向服务节点发送。类似于生产者消费者模式下的消费者。因为是线程对象,所以启动后会不断轮询获取数据收集器中已经关闭的批次数据。对批次进行整合后再发送到Broker节点中
1. 因为数据真正发送的地方是Broker节点,不是分区。所以需要将从数据收集器中收集到的批次数据按照可用Broker节点重新组合成List集合。
2. 将组合后的\<节点,List\<批次\>\>的数据封装成客户端请求(请求键为:Produce)发送到网络客户端对象的缓冲区,由网络客户端对象通过网络发送给Broker节点。
3. Broker节点获取客户端请求,并根据请求键进行后续的数据处理:向分区中增加数据。

#### 2.4.2 生产消息的基本代码

```java
// TODO 配置属性集合
Map
假设【 1,2,3,4,5 】 5 个分区分到 3 个消费者 :
5 / 3 = 1, 5 % 3 = 2 => 剩余的两个补在第一个和第二个中[1+1][1+1][1] => 结果为[1,2][3,4][5]


缺点:Range分配策略针对单个Topic的情况下显得比较均衡,但是假如Topic多的话, member排序靠前的可能会比member排序靠后的负载多很多。是不是也不够理想。

还有就是如果新增或移除消费者成员,那么会导致每个消费者都需要去建立新的分区节点的连接,更新本地的分区缓存,效率比较低。

- StickyAssignor (粘性分区)
在第一次分配后,每个组成员都保留分配给自己的分区信息。如果有消费者加入或退出,那么在进行分区再分配时(一般情况下,消费者退出45s后,才会进行再分配,因为需要考虑可能又恢复的情况),尽可能保证消费者原有的分区不变,重新对加入或退出消费者的分区进行分配。


从图中可以看出,粘性分区分配策略分配的会更加均匀和高效一些。
- CooperativeStickyAssignor
前面的三种分配策略再进行重分配时使用的是EAGER协议,会让当前的所有消费者放弃当前分区,关闭连接,资源清理,重新加入组和等待分配策略。明显效率是比较低的,所以从Kafka2.4版本开始,在粘性分配策略的基础上,优化了重分配的过程,使用的是COOPERATIVE协议,特点就是在整个再分配的过程中从图中可以看出,粘性分区分配策略分配的会更加均匀和高效一些,COOPERATIVE协议将一次全局重平衡,改成每次小规模重平衡,直至最终收敛平衡的过程。
Kafka消费者默认的分区分配就是RangeAssignor , CooperativeStickyAssignor
2.6.3.4偏移量offset
偏移量offset是消费者消费数据的一个非常重要的属性。默认情况下,消费者如果不指定消费主题数据的偏移量,那么消费者启动消费时,无论当前主题之前存储了多少历史数据,消费者只能从连接成功后当前主题最新的数据偏移位置读取,而无法读取之前的任何数据,如果想要获取之前的数据,就需要设定配置参数或指定数据偏移量。
2.6.3.4.1 起始偏移量
在消费者的配置中,我们可以增加偏移量相关参数auto.offset.reset,用于从最开始获取主题数据,

java
package com.atguigu.test;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class KafkaConsumerTest {
public static void main(String[] args) {
// TODO 创建消费者配置参数集合
Map<String, Object> paramMap = new HashMap<>();
paramMap.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
paramMap.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
paramMap.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
paramMap.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
paramMap.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
// TODO 通过配置,创建消费者对象
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(paramMap);
// TODO 订阅主题
consumer.subscribe(Arrays.asList("test"));
while ( true ) {
// TODO 消费数据
final ConsumerRecords<String, String> poll = consumer.poll(Duration.ofMillis(100));
// TODO 遍历数据
for (ConsumerRecord<String, String> record : poll) {
System.out.println( record );
}
}
}
}
参数取值有3个:

- earliest:对于同一个消费者组,从头开始消费。就是说如果这个topic有历史消息存在,现在新启动了一个消费者组,且auto.offset.reset=earliest,那将会从头开始消费(未提交偏移量的场合)。

- latest:对于同一个消费者组,消费者只能消费到连接topic后,新产生的数据(未提交偏移量的场合)。

- none:生产环境不使用
2.6.3.4.3 指定偏移量消费
除了从最开始的偏移量或最后的偏移量读取数据以外,Kafka还支持从指定的偏移量的位置开始消费数据。

java
package com.atguigu.test;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class KafkaConsumerOffsetTest {
public static void main(String[] args) {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
paramMap.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
paramMap.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
paramMap.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
KafkaConsumer<String, String> c = new KafkaConsumer<String, String>(paramMap);
// TODO 订阅主题
c.subscribe(Collections.singletonList("test"));
// TODO 拉取数据,获取基本集群信息
c.poll(Duration.ofMillis(100));
// TODO 根据集群的基本信息配置需要消费的主题及偏移量
final Set<TopicPartition> assignment = c.assignment();
for (TopicPartition topicPartition : assignment) {
if ( topicPartition.topic().equals("test") ) {
c.seek(topicPartition, 0);
}
}
// TODO 拉取数据
while (true) {
final ConsumerRecords<String, String> poll = c.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : poll) {
System.out.println( record.value() );
}
}
}
}
2.6.3.4.4 偏移量提交
生产环境中,消费者可能因为某些原因或故障重新启动消费,那么如果不知道之前消费数据的位置,重启后再消费,就可能重复消费(earliest)或漏消费(latest)。所以Kafka提供了保存消费者偏移量的功能,而这个功能需要由消费者进行提交操作。这样消费者重启后就可以根据之前提交的偏移量进行消费了。注意,一旦消费者提交了偏移量,那么kafka会优先使用提交的偏移量进行消费。此时,auto.offset.reset参数是不起作用的。
- 自动提交
所谓的自动提交就是消费者消费完数据后,无需告知kafka当前消费数据的偏移量,而是由消费者客户端API周期性地将消费的偏移量提交到Kafka中。这个周期默认为5000ms,可以通过配置进行修改。

java
package com.atguigu.test;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class KafkaConsumerCommitAutoTest {
public static void main(String[] args) {
// TODO 创建消费者配置参数集合
Map<String, Object> paramMap = new HashMap<>();
paramMap.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
// TODO 启用自动提交消费偏移量,默认取值为true
paramMap.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
// TODO 设置自动提交offset的时间周期为1000ms,默认5000ms
paramMap.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 1000);
paramMap.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
paramMap.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
paramMap.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
// TODO 通过配置,创建消费者对象
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(paramMap);
// TODO 订阅主题
consumer.subscribe(Arrays.asList("test"));
while ( true ) {
// TODO 消费数据
final ConsumerRecords<String, String> poll = consumer.poll(Duration.ofMillis(100));
// TODO 遍历数据
for (ConsumerRecord<String, String> record : poll) {
System.out.println( record );
}
}
}
}
- 手动提交
基于时间周期的偏移量提交,是我们无法控制的,一旦参数设置的不合理,或单位时间内数据量消费的很多,却没有来及的自动提交,那么数据就会重复消费。所以Kafka也支持消费偏移量的手动提交,也就是说当消费者消费完数据后,自行通过API进行提交。不过为了考虑效率和安全,kafka同时提供了异步提交和同步提交两种方式供我们选择。注意:需要禁用自动提交auto.offset.reset=false,才能开启手动提交
异步提交:向Kafka发送偏移量offset提交请求后,就可以直接消费下一批数据,因为无需等待kafka的提交确认,所以无法知道当前的偏移量一定提交成功,所以安全性比较低,但相对,消费性能会提高

java
package com.atguigu.test;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class KafkaConsumerCommitASyncTest {
public static void main(String[] args) {
// TODO 创建消费者配置参数集合
Map<String, Object> paramMap = new HashMap<>();
paramMap.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
// TODO 禁用自动提交消费偏移量,默认取值为true
paramMap.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
paramMap.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
paramMap.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
paramMap.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
// TODO 通过配置,创建消费者对象
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(paramMap);
// TODO 订阅主题
consumer.subscribe(Arrays.asList("test"));
while ( true ) {
// TODO 消费数据
final ConsumerRecords<String, String> poll = consumer.poll(Duration.ofMillis(100));
// TODO 遍历处理数据
for (ConsumerRecord<String, String> record : poll) {
System.out.println( record );
}
// TODO 异步提交偏移量
// 此处需要注意,需要在拉取数据完成处理后再提交
// 否则提前提交了,但数据处理失败,下一次消费数据就拉取不到了
consumer.commitAsync();
}
}
}
同步提交:必须等待Kafka完成offset提交请求的响应后,才可以消费下一批数据,一旦提交失败,会进行重试处理,尽可能保证偏移量提交成功,但是依然可能因为以外情况导致提交请求失败。此种方式消费效率比较低,但是安全性高。

java
package com.atguigu.test;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class KafkaConsumerCommitSyncTest {
public static void main(String[] args) {
// TODO 创建消费者配置参数集合
Map<String, Object> paramMap = new HashMap<>();
paramMap.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
// TODO 禁用自动提交消费偏移量,默认取值为true
paramMap.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
paramMap.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
paramMap.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
paramMap.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
// TODO 通过配置,创建消费者对象
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(paramMap);
// TODO 订阅主题
consumer.subscribe(Arrays.asList("test"));
while ( true ) {
// TODO 消费数据
final ConsumerRecords<String, String> poll = consumer.poll(Duration.ofMillis(100));
// TODO 遍历处理数据
for (ConsumerRecord<String, String> record : poll) {
System.out.println( record );
}
// TODO 同步提交偏移量
// 此处需要注意,需要在拉取数据完成处理后再提交
// 否则提前提交了,但数据处理失败,下一次消费数据就拉取不到了
consumer.commitSync();
}
}
}
2.6.3.5消费者事务
无论偏移量使用自动提交还是,手动提交,特殊场景中数据都有可能会出现重复消费。

如果提前提交偏移量,再处理业务,又可能出现数据丢失的情况。

对于单独的Consumer来讲,事务保证会比较弱,尤其是无法保证提交的信息被精确消费,主要原因就是消费者可以通过偏移量访问信息,而不同的数据文件生命周期不同,同一事务的信息可能会因为重启导致被删除的情况。所以一般情况下,想要完成kafka消费者端的事务处理,需要将数据消费过程和偏移量提交过程进行原子性绑定,也就是说数据处理完了,必须要保证偏移量正确提交,才可以做下一步的操作,如果偏移量提交失败,那么数据就恢复成处理之前的效果。
对于生产者事务而言,消费者消费的数据也会受到限制。默认情况下,消费者只能消费到生产者提交的数据,也就是未提交完成的数据,消费者是看不到的。如果想要消费到未提交的数据,需要更高消费事务隔离级别

java
package com.atguigu.test;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.*;
public class KafkaConsumerTransactionTest {
public static void main(String[] args) {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
paramMap.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
paramMap.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
// TODO 隔离级别:已提交读,读取已经提交事务成功的数据(默认)
//paramMap.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed");
// TODO 隔离级别:未提交读,读取已经提交事务成功和未提交事务成功的数据
paramMap.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_uncommitted");
paramMap.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
KafkaConsumer<String, String> c = new KafkaConsumer<String, String>(paramMap);
c.subscribe(Collections.singletonList("test"));
while (true) {
final ConsumerRecords<String, String> poll = c.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : poll) {
System.out.println( record.value() );
}
}
}
}
2.6.3.6偏移量的保存
由于消费者在消费消息的时候可能会由于各种原因而断开消费,当重新启动消费者时我们需要让它接着上次消费的位置offset继续消费,因此消费者需要实时的记录自己以及消费的位置。
0.90版本之前,这个信息是记录在zookeeper内的,在0.90之后的版本,offset保存在__consumer_offsets这个topic内。
每个consumer会定期将自己消费分区的offset提交给kafka内部topic:__consumer_offsets,提交过去的时候,key是consumerGroupId+topic+分区号

value就是当前offset的值,kafka会定期清理topic里的消息,最后就保留最新的那条数据。

因为__consumer_offsets可能会接收高并发的请求,kafka默认给其分配50个分区(可以通过offsets.topic.num.partitions设置),均匀分配到Kafka集群的多个Broker中。Kafka采用hash(consumerGroupId) % __consumer_offsets主题的分区数来计算我们的偏移量提交到哪一个分区。因为偏移量也是保存到主题中的,所以保存的过程和生产者生产数据的过程基本相同。
2.6.3.7消费数据
消费者消费数据时,一般情况下,只是设定了订阅的主题名称,那是如何消费到数据的呢。我们这里说一下服务端拉取数据的基本流程。

(1)服务端获取到用户拉取数据的请求
Kafka消费客户端会向Broker发送拉取数据的请求FetchRequest,服务端Broker获取到请求后根据请求标记FETCH交给应用处理接口KafkaApis进行处理。
(2)通过副本管理器拉取数据
副本管理器需要确定当前拉取数据的分区,然后进行数据的读取操作
(3)判定首选副本
2.4版本前,数据读写的分区都是Leader分区,从2.4版本后,kafka支持Follower副本进行读取。主要原因就是跨机房或者说跨数据中心的场景,为了节约流量资源,可以从当前机房或数据中心的副本中获取数据。这个副本称之未首选副本。
(4)拉取分区数据
Kafka的底层读取数据是采用日志段LogSegment对象进行操作的。
(5)零拷贝
为了提高数据读取效率,Kafka的底层采用nio提供的FileChannel零拷贝技术,直接从操作系统内核中进行数据传输,提高数据拉取的效率。
3 Kafka进阶




3.1 Controller选举
Controller,是Apache Kafka的核心组件。它的主要作用是在Apache Zookeeper的帮助下管理和协调控制整个Kafka集群。
集群中的任意一台Broker都能充当Controller的角色,但是,在整个集群运行过程中,只能有一个Broker成为Controller。也就是说,每个正常运行的Kafka集群,在任何时刻都有且只有一个Controller。

最先在Zookeeper上创建临时节点/controller成功的Broker就是Controller。Controller重度依赖Zookeeper,依赖zookeepr保存元数据,依赖zookeeper进行服务发现。Controller大量使用Watch功能实现对集群的协调管理。如果此时,作为Controller的Broker节点宕掉了。那么zookeeper的临时节点/controller就会因为会话超时而自动删除。而监控这个节点的Broker就会收到通知而向ZooKeeper发出创建/controller节点的申请,一旦创建成功,那么创建成功的Broker节点就成为了新的Controller。

有一种特殊的情况,就是Controller节点并没有宕掉,而是因为网络的抖动,不稳定,导致和ZooKeeper之间的会话超时,那么此时,整个Kafka集群就会认为之前的Controller已经下线(退出)从而选举出新的Controller,而之前的Controller的网络又恢复了,以为自己还是Controller了,继续管理整个集群,那么此时,整个Kafka集群就有两个controller进行管理,那么其他的broker就懵了,不知道听谁的了,这种情况,我们称之为脑裂现象,为了解决这个问题,Kafka通过一个任期(epoch:纪元)的概念来解决,也就是说,每一个Broker当选Controller时,会告诉当前Broker是第几任Controller,一旦重新选举时,这个任期会自动增1,那么不同任期的Controller的epoch值是不同的,那么旧的controller一旦发现集群中有新任controller的时候,那么它就会完成退出操作(清空缓存,中断和broker的连接,并重新加载最新的缓存),让自己重新变成一个普通的Broker。
3.2 Broker上线下线
Controller 在初始化时,会利用 ZK 的 watch 机制注册很多不同类型的监听器,当监听的事件被触发时,Controller 就会触发相应的操作。Controller 在初始化时,会注册多种类型的监听器,主要有以下几种:
- 监听 /admin/reassign_partitions 节点,用于分区副本迁移的监听
- 监听 /isr_change_notification 节点,用于 Partition ISR 变动的监听
- 监听 /admin/preferred_replica_election 节点,用于需要进行 Partition 最优 leader 选举的监听
- 监听 /brokers/topics 节点,用于 Topic 新建的监听
- 监听 /brokers/topics/TOPIC_NAME 节点,用于 Topic Partition 扩容的监听
- 监听 /admin/delete_topics 节点,用于 Topic 删除的监听
- 监听 /brokers/ids 节点,用于 Broker 上下线的监听。
每台 Broker 在上线时,都会与ZK建立一个建立一个session,并在 /brokers/ids下注册一个节点,节点名字就是broker id,这个节点是临时节点,该节点内部会有这个 Broker 的详细节点信息。Controller会监听/brokers/ids这个路径下的所有子节点,如果有新的节点出现,那么就代表有新的Broker上线,如果有节点消失,就代表有broker下线,Controller会进行相应的处理,Kafka就是利用ZK的这种watch机制及临时节点的特性来完成集群 Broker的上下线。无论Controller监听到的哪一种节点的变化,都会进行相应的处理,同步整个集群元数据
3.3 数据偏移量定位
分区是一个逻辑工作单元,其中记录被顺序附加分区上 (kafka只能保证分区消息的有序性,而不能保证消息的全局有序性)。但是分区不是存储单元,分区进一步划分为Segment (段),这些段是文件系统上的实际文件。为了获得更好的性能和可维护性,可以创建多个段,而不是从一个巨大的分区中读取,消费者现在可以更快地从较小的段文件中读取。创建具有分区名称的目录,并将该分区的所有段作为各种文件进行维护。在理想情况下,数据流量分摊到各个Parition中,实现了负载均衡的效果。在分区日志文件中,你会发现很多类型的文件,比如: .index、.timeindex、.log等
每个数据日志文件会对用一个LogSegment对象,并且都有一个基准偏移量,表示当前 LogSegment 中第一条消息的偏移量offset。

偏移量是一个 64 位的长整形数,固定是20位数字,长度未达到,用 0 进行填补,索引文件和日志文件都由该作为文件名命名规则:
00000000000000000000.index:索引文件,记录偏移量映射到 .log 文件的字节偏移量,此映射用于从任何特定偏移量读取记录
0000000000000000000.timeindex:时间戳索引文件,此文件包含时间戳到记录偏移量的映射,该映射使用.index文件在内部映射到记录的字节偏移量。这有助于从特定时间戳访问记录
00000000000000000000.log:此文件包含实际记录,并将记录保持到特定偏移量,文件名描述了添加到此文件的起始偏移量,如果日志文件名为 00000000000000000004.log ,则当前日志文件的第一条数据偏移量就是4(偏移量从 0 开始)

多个数据日志文件在操作时,只有最新的日志文件处于活动状态,拥有文件写入和读取权限,其他的日志文件只有只读的权限。
偏移量索引文件用于记录消息偏移量与物理地址之间的映射关系。时间戳索引文件则根据时间戳查找对应的偏移量。Kafka 中的索引文件是以稀疏索引的方式构造消息的索引,并不保证每一个消息在索引文件中都有对应的索引项。每当写入一定量的消息时,偏移量索引文件和时间戳索引文件分别增加一个偏移量索引项和时间戳索引项。通过修改 log.index.interval.bytes的值,改变索引项的密度。
数据位置索引保存在index文件中,log日志默认每写入4K(log.index.interval.bytes设定的),会写入一条索引信息到index文件中,因此索引文件是稀疏索引,它不会为每条日志都建立索引信息,索引文件的数据结构则是由相对offset(4byte)+position(4byte)组成,由于保存的是相对第一个消息的相对offset,只需要4byte就可以,节省空间,实际查找后还需要计算回实际的offset,这对用户是不可见的。
如果消费者想要消费某一个偏移量的数据,那么Kafka会通过Kafka 中存在一个 ConcurrentSkipListMap(跳跃表)定位到00000000000000000000.index索引文件 ,通过二分法在偏移量索引文件中找到不大于指定偏移量的最大索引项,然后从日志分段文件中的物理位置开始顺序查找偏移量为指定值的消息。
3.4 Topic删除
kafka删除topic消息的三种方式:
- 方法一:快速配置删除法(确保topic数据不要了)
- kafka启动之前,在server.properties配置delete.topic.enable=true
- 执行命令bin/kafka-topics.sh --delete --topic test --zookeeper zk:2181或者使用kafka-manager集群管理工具删除
注意:如果kafka启动之前没有配置delete.topic.enable=true,topic只会标记为marked for deletion,加上配置,重启kafka,之前的topick就真正删除了。
- 方法二:设置删除策略(确保topic数据不要了)
请参考日志清理和压缩
- 方法三:手动删除法(不推荐)(确保topic数据不要了)
前提:不允许更改server.properties配置
- 删除zk下面topic(test)
启动bin/zkCli.sh
ls /brokers/topics
rmr /brokers/topics/test
ls /brokers/topics
查topic是否删除:bin/kafka-topics.sh --list --zookeeper zk:2181
删除各broker下topic数据
3.5 日志清理和压缩
Kafka软件的目的本质是用于传输数据,而不是存储数据,但是为了均衡生产数据速率和消费者的消费速率,所以可以将数据保存到日志文件中进行存储。默认的数据日志保存时间为7天,可以通过调整如下参数修改保存时间:
- log.retention.hours:小时(默认:7天,最低优先级)
- log.retention.minutes,分钟
- log.retention.ms,毫秒(最高优先级)
- log.retention.check.interval.ms,负责设置检查周期,默认5分钟。
日志一旦超过了设置的时间,Kafka中提供了两种日志清理策略:delete和compact。
- delete:将过期数据删除
- log.cleanup.policy = delete(所有数据启用删除策略)
(1)基于时间:默认打开。以segment中所有记录中的最大时间戳作为该文件时间戳。
(2)基于大小:默认关闭。超过设置的所有日志总大小,删除最早的segment。
log.retention.bytes,默认等于-1,表示无穷大。
**思考:**如果一个segment中有一部分数据过期,一部分没有过期,怎么处理?

- compact :日志压缩
基本思路就是将相同key的数据,只保留最后一个
- log.cleanup.policy = compact(所有数据启用压缩策略)

注意:因为数据会丢失,所以这种策略只适用保存数据最新状态的特殊场景。
3.7 页缓存
页缓存是操作系统实现的一种主要的磁盘缓存,以此用来减少对磁盘I/O的操作。具体来说,就是把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问。为了弥补性能上的差异 ,现代操作系统越来越多地将内存作为磁盘缓存,甚至会将所有可用的内存用于磁盘缓存,这样当内存回收时也几乎没有性能损失,所有对于磁盘的读写也将经由统一的缓存。
当一个进程准备读取磁盘上的文件内容时,操作系统会先查看待读取的数据所在的页(page)是否在页缓存(page cache)中,如果存在(命中)则直接返回数据,从而避免了对物理磁盘I/O操作;如果没有命中,则操作系统会向磁盘发起读取请示并将读取的数据页写入页缓存,之后再将数据返回进程。同样,如果一个进程需要将数据写入磁盘,那么操作系统也会检测数据对应的页是否在页缓存中,如果不存在,则会先在页缓存中添加相应的页,最后将数据写入对应的页。被修改过后的页也就变成了脏页,操作系统会在合适的时间把脏页中的数据写入磁盘,以操作数据的一致性。
对一个进程页言,它会在进程内部缓存处理所需的数据,然而这些数据有可能还缓存在操作系统的页缓存中,因此同一份数据有可能被缓存了2次。并且,除非使用Direct I/O的方式,否则页缓存很难被禁止。
Kafka中大量使用了页缓存,这是Kafka实现高吞吐的重要因此之一。虽然消息都是先被写入页缓存,然后由操作系统负责具体的刷盘任务,但在Kafka中同样提供了同步刷盘及间断性强制刷盘(fsync)的功能,这些功能可以通过log.flush.interval.message、log.flush.interval.ms等参数来控制。同步刷盘可以提高消息的可靠性,防止由于机器掉电等异常造成处于页缓存而没有及时写入磁盘的消息丢失。不过一般不建议这么做,刷盘任务就应交由操作系统去调配,消息的可靠性应该由多副本机制来保障,而不是由同步刷盘这种严重影响性能的行为来保障。
3.8 零拷贝★
kafka的高性能是多方面协同的结果,包括宏观架构、分布式partition存储、ISR数据同步、以及"无所不用其极"的高效利用磁盘/操作系统特性。其中零拷贝并不是不需要拷贝,通常是说在IO读写过程中减少不必要的拷贝次数。
这里我们要说明是,内核在执行操作时同一时间点只会做一件事,比如Java写文件这个操作,为了提高效率,这个操作是分为3步:第一步java将数据写入自己的缓冲区,第二步java需要写入数据的磁盘页可能就在当前的页缓存(Page Cache)中,所以java需要将自己的缓冲区的数据写入操作系统的页缓存(Page Cache)中。第三步操作系统会在页缓存数据满了后,将数据实际刷写到磁盘文件中。

在这个过程,Java Application数据的写入和页缓存的数据刷写对于操作系统来讲是不一样的,可以简单理解为,页缓存的数据刷写属于内核的内部操作,而是用于启动的应用程序的数据操作属于内核的外部操作,权限会受到一定的限制。所以内核在执行不同操作时,就需要将不同的操作环境加载到执行空间中,也就是说,当java想要将数据写入页缓存时,就需要调用用户应用程序的操作,这就是用户态操作。当需要将页缓存数据写入文件时,就需要中断用户用用程序操作,而重新加载内部操作的运行环境,这就是内核态操作。可以想象,如果存在大量的用户态和内核态的切换操作,IO性能就会急剧下降。所以就存在零拷贝操作,减少用户态和内核态的切换,提高效率。Kafka消费者消费数据以及Follower副本同步数据就采用的是零拷贝技术。

3.9 顺写日志
Kafka 中消息是以topic进行分类的,生产者生产消息,消费者消费消息,都是面向 topic的。在Kafka中,一个topic可以分为多个partition,一个partition分为多个segment,每个 segment对应三个文件:.index文件、.log文件、.timeindex文件。
topic 是逻辑上的概念,而patition是物理上的概念,每个patition对应一个log文件,而log文件中存储的就是producer生产的数据,patition生产的数据会被不断的添加到log文件的末端,且每条数据都有自己的offset。

Kafka底层采用的是FileChannel.wrtieTo进行数据的写入,写的时候并不是直接写入文件,而是写入ByteBuffer,然后当缓冲区满了,再将数据顺序写入文件,无需定位文件中的某一个位置进行写入,那么就减少了磁盘查询,数据定位的过程。所以性能要比随机写入,效率高得多。
官网有数据表明,同样的磁盘,顺序写能到600M/s,而随机写只有100K/s。这与磁盘的机械结构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间。

3.10 Linux集群部署
Kafka从早期的消息传输系统转型为开源分布式事件流处理平台系统,所以很多核心组件,核心操作都是基于分布式多节点的,所以我们这里把分布式软件环境安装一下。
3.10.1 集群规划
|----------|-------------------|-------------------|-------------------|
| 服务节点 | kafka-broker1 | kafka-broker2 | kafka-broker3 |
| 服务进程 | QuorumPeerMain | QuorumPeerMain | QuorumPeerMain |
| 服务进程 | Kafka | Kafka | Kafka |
3.10.2 安装虚拟机
生产环境中,我们会使用多台服务器搭建Kafka集群系统,但是对于学习来讲,准备多台独立的服务器还是比较困难的,所以我们这里采用虚拟机的方式进行学习。
3.10.2.1 VMware安装(略)
VMware可以使用户在一台计算机上同时运行多个操作系统,还可以像Windows应用程序一样来回切换。用户可以如同操作真实安装的系统一样操作虚拟机系统,甚至可以在一台计算机上将几个虚拟机系统连接为一个局域网或者连接到互联网。课程中使用的虚拟机都是基于VMware软件环境运行的,所以需要在本机安装VMware软件,具体的安装过程请参考课程资料中的《02-尚硅谷技术之模板虚拟机环境准备.docx》
3.10.2.2模板机安装(略)
为了让大家学习的重心放在技术本身,而不是环境上,我们将基础的虚拟机环境进行了封装,形成了模板机,这样大家直接下载使用即可。
|------------------------------|----------------|
| 模板机属性 | 模板机属性值 |
| IP 地址 | 192.168.10.100 |
| 主机名称 | hadoop100 |
| 内存(虚拟) | 4G |
| 硬盘(虚拟) | 50G |
| 登录账号 ( 全小写 ) | root |
| 密码 | 000000 |
3.10.2.3克隆虚拟机
(1)在VMware软件中打开虚拟机

在打开的窗口中选择你下载的模板机

(2)利用模板机hadoop100,克隆三台虚拟机:kafka-broker1、kafka-broker2、kafka-broker2。克隆时,要先关闭hadoop100。
a)在模板机上点击右键选择管理 -> 克隆

b)选择创建完整克隆

c)填写虚拟机名称以及存储的位置,点击完成即可。


(3)启动三台克隆机,分别修改克隆机IP,以下以kafka-broker2举例说明
a)使用root用户登录,密码为000000
b)启动终端窗口,修改克隆虚拟机的静态IP
修改IP文件
vim /etc/sysconfig/network-scripts/ifcfg-ens33
改成
DEVICE=ens33
TYPE=Ethernet
ONBOOT=yes
BOOTPROTO=static
NAME="ens33"
IPADDR=192.168.10.102
PREFIX=24
GATEWAY=192.168.10.2
DNS1=192.168.10.2
c)查看Linux虚拟机的虚拟网络编辑器,编辑->虚拟网络编辑器->VMnet8


d)查看Windows系统适配器VMware Network Adapter VMnet8的IP地址

e)保证Linux系统ifcfg-ens33文件中IP地址、虚拟网络编辑器地址和Windows系统VM8网络IP地址相同。
(4)修改克隆机主机名,以下以kafka-broker1举例说明
a)使用root用户登录
b)修改主机名
修改主机名
vim /etc/hostname
kafka-broker1
c)配置Linux克隆机主机名称映射hosts文件,打开/etc/hosts
修改主机名称映射
vim /etc/hosts
添加如下内容:
192.168.10.101 kafka-broker1
192.168.10.102 kafka-broker2
192.168.10.103 kafka-broker3
192.168.10.104 kafka-broker4
(5)重启克隆机kafka-broker1
重启
reboot
(6)修改windows的主机映射文件(hosts文件)
a)如果操作系统是window7,进入C:\windows\system32\drivers\etc路径, 直接修改host文件,添加如下内容:
192.168.10.101 kafka-broker1
192.168.10.102 kafka-broker2
192.168.10.103 kafka-broker3
192.168.10.104 kafka-broker4
b)如果操作系统是window10,先将C:\windows\system32\drivers\etc路径下的host文件拷贝到桌面后,添加如下内容,添加完成后,覆盖回原目录即可。
192.168.10.101 kafka-broker1
192.168.10.102 kafka-broker2
192.168.10.103 kafka-broker3
192.168.10.104 kafka-broker4
3.10.2.4分发脚本
在分布式环境中,一般都需要在多个服务器节点安装软件形成服务集群。但是在每个服务器中单独安装软件的过程是非常麻烦的,所以我们可以采用在单一的服务器节点中安装软件,一般安装成功后,将安装好的软件分发(复制)到其他服务器节点的方式,这种方式非常方便且实用的,但是需要注意的是,软件分发完成后,需要根据软件要求修改每个服务器节点自己的配置内容。
我们这里启动第一台克隆的虚拟机。使用虚拟机远程工具访问。这里的工具没有任何的要求,只要可以正常访问就可以。如果大家没有安装过,咱们的课程资料中有一个MobaXterm_Installer_v20.2.zip工具,大家可以安装使用。安装过程比较简单,默认安装即可。
(1)通过远程工具连接虚拟机

点击左上角的session按钮,弹出配置窗口,点击SSH按钮

分别创建3个虚拟机的连接。第一次登录要求输入密码,密码为000000.
(2)在kafka-broker1虚拟机中创建xsync分发脚本文件,用于向多个虚拟机同步文件。
bash
# 进入/root目录
cd /root
# 创建bin目录
mkdir bin
# 进入/root/bin目录
cd bin
# 创建xsync文件
vim xsync
(3)然后增加文件内容
bash
#!/bin/bash
#1. 判断参数个数
if [ $# -lt 1 ]
then
echo Not Enough Arguement!
exit;
fi
#2. 遍历集群所有机器
for host in kafka-broker1 kafka-broker2 kafka-broker3
do
echo ==================== $host ====================
#3. 遍历所有目录,挨个发送
for file in $@
do
#4 判断文件是否存在
if [ -e $file ]
then
#5. 获取父目录
pdir=$(cd -P $(dirname $file); pwd)
#6. 获取当前文件的名称
fname=$(basename $file)
ssh $host "mkdir -p $pdir"
rsync -av $pdir/$fname $host:$pdir
else
echo $file does not exists!
fi
done
done
(4)修改xsync文件权限
bash
# 进入/root/bin目录
cd /root/bin
# 修改权限
chmod 777 xsync
3.10.2.5 SSH无密登录配置
分发文件时,需要通过脚本切换主机进行指令操作,切换主机时,是需要输入密码的,每一次都输入就显得有点麻烦,所以这里以虚拟机kafka-broker1为例配置SSH免密登录(其他节点执行同样步骤即可),配置完成后,脚本执行时就不需要输入密码了。
a)生成公钥和私钥
生产公钥和私钥
ssh-keygen -t rsa
然后敲(三个回车),就会生成两个文件id_rsa(私钥)、id_rsa.pub(公钥)
b)将公钥拷贝到要免密登录的目标机器上,拷贝过程需要输入目标机器密码
ssh-copy-id 目标机器
ssh-copy-id kafka-broker1
ssh-copy-id kafka-broker2
ssh-copy-id kafka-broker3
3.10.3 安装JDK1.8
3.10.3.1卸载现有JDK
# 不同节点都要执行操作
rpm -qa | grep -i java | xargs -n1 sudo rpm -e --nodeps
3.10.3.2上传Java压缩包
将jdk-8u212-linux-x64.tar.gz文件上传到虚拟机的/opt/software目录中

3.10.3.3解压Java压缩包
bash
# 进入/opt/software目录
cd /opt/software/
# 解压缩文件到指定目录
tar -zxvf jdk-8u212-linux-x64.tar.gz -C /opt/module/
# 进入/opt/module目录
cd /opt/module
# 改名
mv jdk1.8.0_212/ java
3.10.3.4配置Java环境变量
(1)新建 /etc/profile.d/my_env.sh文件
vim /etc/profile.d/my_env.sh
(2)添加内容
#JAVA_HOME
export JAVA_HOME=/opt/module/java
export PATH=PATH:JAVA_HOME/bin
(3)让环境变量生效
source /etc/profile.d/my_env.sh
3.10.3.5安装测试
java -version

3.10.3.6分发软件
bash
# 分发环境变量文件
xsync /etc/profile.d/my_env.sh
# 进入/opt/module路径
cd /opt/module
# 调用分发脚本将本机得Java安装包分发到其他两台机器
xsync java
# 在每个节点让环境变量生效
3.10.4 安装ZooKeeper
3.10.4.1上传ZooKeeper压缩包
将apache-zookeeper-3.7.1-bin.tar.gz文件上传到三台虚拟机的/opt/software目录中

3.10.4.2解压ZooKeeper压缩包
bash
# 进入到/opt/software目录中
cd /opt/software/
# 解压缩文件到指定目录
tar -zxvf apache-zookeeper-3.7.1-bin.tar.gz -C /opt/module/
# 进入/opt/module目录
cd /opt/module
# 文件目录改名
mv apache-zookeeper-3.7.1-bin/ zookeeper
3.10.4.3配置服务器编号
(1)在/opt/module/zookeeper/目录下创建zkData
进入/opt/module/zookeeper目录
cd /opt/module/zookeeper
创建zkData文件目录
mkdir zkData
(2)创建myid文件
进入/opt/module/zookeeper/zkData目录
cd /opt/module/zookeeper/zkData
创建myid文件
vim myid
(3)在文件中增加内容
1
3.10.4.4修改配置文件
(1)重命名/opt/module/zookeeper/conf目录下的zoo_sample.cfg文件为zoo.cfg文件
bash
# 进入cd /opt/module/zookeeper/conf文件目录
cd /opt/module/zookeeper/conf
# 修改文件名称
mv zoo_sample.cfg zoo.cfg
# 修改文件内容
vim zoo.cfg
(2)修改zoo.cfg文件
bash
# 以下内容为修改内容
dataDir=/opt/module/zookeeper/zkData
# 以下内容为新增内容
####################### cluster ##########################
# server.A=B:C:D
#
# A是一个数字,表示这个是第几号服务器
# B是A服务器的主机名
# C是A服务器与集群中的主服务器(Leader)交换信息的端口
# D是A服务器用于主服务器(Leader)选举的端口
#########################################################
server.1=kafka-broker1:2888:3888
server.2=kafka-broker2:2888:3888
server.3=kafka-broker3:2888:3888
3.10.4.5启动ZooKeeper
# 在每个节点下执行如下操作
进入zookeeper目录
cd /opt/module/zookeeper
启动ZK服务
bin/zkServer.sh start
3.10.4.6关闭ZooKeeper
# 在每个节点下执行如下操作
进入zookeeper目录
cd /opt/module/zookeeper
关闭ZK服务
bin/zkServer.sh stop
3.10.4.7查看ZooKeeper状态
# 在每个节点下执行如下操作
进入zookeeper目录
cd /opt/module/zookeeper
查看ZK服务状态
bin/zkServer.sh status
3.10.4.8分发软件
进入/opt/module路径
cd /opt/module
调用分发脚本将本机得ZooKeeper安装包分发到其他两台机器
xsync zookeeper
# 分别将不同虚拟机 /opt/module/zookeeper/zkData 目录下 myid 文件进行修改
vim /opt/module/zookeeper/zkData/myid
# kafka-broker1:1
# kafka-broker2:2
# kafka-broker3:3
3.10.4.9启停脚本
ZooKeeper软件的启动和停止比较简单,但是每一次如果都在不同服务器节点执行相应指令,也会有点麻烦,所以我们这里将指令封装成脚本文件,方便我们的调用。
(5)在虚拟机kafka-broker1的/root/bin目录下创建zk.sh脚本文件
在/root/bin这个目录下存放的脚本,root用户可以在系统任何地方直接执行
bash
# 进入/root/bin目录
cd /root/bin
# 创建zk.sh脚本文件
vim zk.sh
在脚本中增加内容:
#!/bin/bash
case $1 in
"start"){
for i in kafka-broker1 kafka-broker2 kafka-broker3
do
echo ---------- zookeeper $i 启动 ------------
ssh $i "/opt/module/zookeeper/bin/zkServer.sh start"
done
};;
"stop"){
for i in kafka-broker1 kafka-broker2 kafka-broker3
do
echo ---------- zookeeper $i 停止 ------------
ssh $i "/opt/module/zookeeper/bin/zkServer.sh stop"
done
};;
"status"){
for i in kafka-broker1 kafka-broker2 kafka-broker3
do
echo ---------- zookeeper $i 状态 ------------
ssh $i "/opt/module/zookeeper/bin/zkServer.sh status"
done
};;
esac
(6)增加脚本文件权限
给zk.sh文件授权
chmod 777 zk.sh
(7)脚本调用方式
启动ZK服务
zk.sh start
查看ZK服务状态
zk.sh status
停止ZK服务
zk.sh stop
3.10.4.10客户端工具安装
我们可以通过ZooKeeper软件自带命令行客户端对保存的信息进行访问,也可以采用一些工具软件进行访问,这里我们给大家介绍一个ZooKeeper的客户端工具prettyZoo。
(1)软件安装
安装的过程非常简单,直接点击课程资料中的prettyZoo-win.msi安装包默认安装即可。

(2)连接ZooKeeper

(3)查看ZooKeeper节点信息

3.10.5 安装Kafka
3.10.5.1上传Kafka压缩包
将kafka_2.12-3.6.1.tgz文件上传到三台虚拟机的/opt/software目录中

3.10.5.2解压Kafka压缩包
bash
# 进入/opt/software目录
cd /opt/software
# 解压缩文件到指定目录
tar -zxvf kafka_2.12-3.6.1.tgz -C /opt/module/
# 进入/opt/module目录
cd /opt/module
# 修改文件目录名称
mv kafka_2.12-3.6.1/ kafka
3.10.5.3修改配置文件
bash
# 进入cd /opt/module/kafka/config文件目录
cd /opt/module/kafka/config
# 修改配置文件
vim server.properties
输入以下内容:
bash
#broker的全局唯一编号,每个服务节点不能重复,只能是数字。
broker.id=1
#broker对外暴露的IP和端口 (每个节点单独配置)
advertised.listeners=PLAINTEXT://kafka-broker1:9092
#处理网络请求的线程数量
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=kafka-broker1:2181,kafka-broker2:2181,kafka-broker3:2181/kafka
3.10.5.4分发kafka软件
bash
# 进入 /opt/module目录
cd /opt/module
# 执行分发指令
xsync kafka
# 按照上面的配置文件内容,在每一个Kafka节点进行配置,请注意配置文件中红色字体内容
vim /opt/module/kafka/config/server.properties
3.10.5.5配置环境变量
(1)修改 /etc/profile.d/my_env.sh文件
vim /etc/profile.d/my_env.sh
(2)添加内容
#KAFKA_HOME
export KAFKA_HOME=/opt/module/kafka
export PATH=PATH:KAFKA_HOME/bin
(3)让环境变量生效
source /etc/profile.d/my_env.sh
(4)分发环境变量,并让环境变量生效
xsync /etc/profile.d/my_env.sh
# 每个节点执行刷新操作
source /etc/profile.d/my_env.sh
3.10.5.6启动Kafka
启动前请先启动ZooKeeper服务
进入/opt/module/kafka目录
cd /opt/module/kafka
执行启动指令
bin/kafka-server-start.sh -daemon config/server.properties
3.10.5.7关闭Kafka
进入/opt/module/kafka目录
cd /opt/module/kafka
执行关闭指令
bin/kafka-server-stop.sh
3.10.5.8启停脚本
(1)在虚拟机kafka-broker1的/root/bin目录下创建kfk.sh脚本文件,对kafka服务的启动停止等指令进行封装
进入/root/bin目录
cd /root/bin
创建kfk.sh脚本文件
vim kfk.sh
在脚本中增加内容:
bash
#! /bin/bash
case $1 in
"start"){
for i in kafka-broker1 kafka-broker2 kafka-broker3
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 kafka-broker1 kafka-broker2 kafka-broker3
do
echo " --------停止 $i Kafka-------"
ssh $i "/opt/module/kafka/bin/kafka-server-stop.sh "
done
};;
esac
(2)增加脚本文件权限
给文件授权
chmod 777 kfk.sh
(3)脚本调用方式
启动kafka
kfk.sh start
停止Kafka
kfk.sh stop
注意 :停止Kafka集群时,一定要等Kafka所有节点进程全部停止后再停止ZooKeeper集群。因为Zookeeper集群当中记录着Kafka集群相关信息,Zookeeper集群一旦先停止,Kafka集群就没有办法再获取停止进程的信息,只能手动杀死Kafka进程了。
(4)联合脚本
因为Kafka启动前,需要先启动ZooKeeper,关闭时,又需要将所有Kafka全部关闭后,才能关闭ZooKeeper,这样,操作起来感觉比较麻烦,所以可以将之前的2个脚本再做一次封装。
a)在虚拟机kafka-broker1的/root/bin目录下创建xcall脚本文件
进入/root/bin目录
cd /root/bin
创建xcall文件
vim xcall
在脚本中增加内容:
bash
#! /bin/bash
for i in kafka-broker1 kafka-broker2 kafka-broker3
do
echo --------- $i ----------
ssh $i "$*"
done
b)增加脚本文件权限
进入/root/bin目录
cd /root/bin
增加权限
chmod 777 xcall
c)在虚拟机kafka-broker1的/root/bin目录下创建cluster.sh脚本文件
进入/root/bin目录
cd /root/bin
创建cluster.sh脚本文件
vim cluster.sh
在脚本中增加内容:
bash
#!/bin/bash
case $1 in
"start"){
echo ================== 启动 Kafka集群 ==================
#启动 Zookeeper集群
zk.sh start
#启动 Kafka采集集群
kfk.sh start
};;
"stop"){
echo ================== 停止 Kafka集群 ==================
#停止 Kafka采集集群
kfk.sh stop
#循环直至 Kafka 集群进程全部停止
kafka_count=$(xcall jps | grep Kafka | wc -l)
while [ $kafka_count -gt 0 ]
do
sleep 1
kafka_count=$(xcall | grep Kafka | wc -l)
echo "当前未停止的 Kafka 进程数为 $kafka_count"
done
#停止 Zookeeper集群
zk.sh stop
};;
esac
d)增加脚本文件权限
进入/root/bin目录
cd /root/bin
增加权限
chmod 777 cluster.sh
e)脚本调用方式
集群启动
cluster.sh start
集群关闭
cluster.sh stop
3.10.6 测试集群
3.10.6.1启动Kafka集群
因为已经将ZooKeeper和Kafka的启动封装为脚本,所以可以分别调用脚本启动或调用集群脚本启动
启动集群
cluster.sh start

输入指令查看进程
xcall 后面跟着linux指令操作,可以同时对多个服务器节点同时执行相同指令
xcall jps

3.10.6.2查看Kafka状态
使用客户端工具访问kafka


3.10.6.3关闭Kafka集群
关闭集群
cluster.sh stop
查看进程
xcall jps


3.11 Kafka-Eagle监控
Kafka-Eagle框架可以监控Kafka集群的整体运行情况,在生产环境中经常使用。
3.11.1 MySQL环境准备
Kafka-Eagle的安装依赖于MySQL,MySQL主要用来存储可视化展示的数据。如果集群中之前安装过MySQL可以跳过该步骤。
3.11.1.1安装包准备
将资料里mysql文件夹及里面所有内容上传到虚拟机/opt/software/mysql目录下
bash
# 文件清单
install_mysql.sh
mysql-community-client-8.0.31-1.el7.x86_64.rpm
mysql-community-client-plugins-8.0.31-1.el7.x86_64.rpm
mysql-community-common-8.0.31-1.el7.x86_64.rpm
mysql-community-icu-data-files-8.0.31-1.el7.x86_64.rpm
mysql-community-libs-8.0.31-1.el7.x86_64.rpm
mysql-community-libs-compat-8.0.31-1.el7.x86_64.rpm
mysql-community-server-8.0.31-1.el7.x86_64.rpm
mysql-connector-j-8.0.31.jar
3.11.1.2执行mysql安装
进入/opt/software/mysql目录
cd /opt/software/mysql
执行install_mysql.sh
sh install_mysql.sh
# 安装得过程略慢,请耐心等候
3.11.1.3 mysql的基本配置
bash
#!/bin/bash
set -x
[ "$(whoami)" = "root" ] || exit 1
[ "$(ls *.rpm | wc -l)" = "7" ] || exit 1
test -f mysql-community-client-8.0.31-1.el7.x86_64.rpm && \
test -f mysql-community-client-plugins-8.0.31-1.el7.x86_64.rpm && \
test -f mysql-community-common-8.0.31-1.el7.x86_64.rpm && \
test -f mysql-community-icu-data-files-8.0.31-1.el7.x86_64.rpm && \
test -f mysql-community-libs-8.0.31-1.el7.x86_64.rpm && \
test -f mysql-community-libs-compat-8.0.31-1.el7.x86_64.rpm && \
test -f mysql-community-server-8.0.31-1.el7.x86_64.rpm || exit 1
# 卸载MySQL
systemctl stop mysql mysqld 2>/dev/null
rpm -qa | grep -i 'mysql\|mariadb' | xargs -n1 rpm -e --nodeps 2>/dev/null
rm -rf /var/lib/mysql /var/log/mysqld.log /usr/lib64/mysql /etc/my.cnf /usr/my.cnf
set -e
# 安装并启动MySQL
yum install -y *.rpm >/dev/null 2>&1
systemctl start mysqld
#更改密码级别并重启MySQL
sed -i '/\[mysqld\]/avalidate_password.length=4\nvalidate_password.policy=0' /etc/my.cnf
systemctl restart mysqld
# 更改MySQL配置
tpass=$(cat /var/log/mysqld.log | grep "temporary password" | awk '{print $NF}')
cat << EOF | mysql -uroot -p"${tpass}" --connect-expired-password >/dev/null 2>&1
set password='000000';
update mysql.user set host='%' where user='root';
alter user 'root'@'%' identified with mysql_native_password by '000000';
flush privileges;
EOF
安装成功后,Mysql的root用户的密码被修改为000000,请连接mysql客户端后,确认root用户的密码插件为下图所示内容。
select user,host,plugin from mysql.user;

如果插件与截图不同,请登录MySQL客户端后重试下列SQL,否则无法远程登录
update mysql.user set host='%' where user='root';
alter user 'root'@'%' identified with mysql_native_password by '000000';
flush privileges;
exit;
# 退出后,请重新登录后进行确认
3.11.2 修改Kafka集群配置
修改/opt/module/kafka/bin/kafka-server-start.sh脚本文件中的内容
bash
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
注意:每个节点都要进行修改。
分发修改后得文件
xsync /opt/module/kafka/bin/kafka-server-start.sh
3.11.3 Kafka-Eagle安装
3.11.3.1安装包准备
将kafka-eagle-bin-3.0.1.tar.gz上传到虚拟机/opt/software目录下,并解压缩
bash
# 进入到software文件目录
cd /opt/software
# 解压缩文件
tar -zxvf kafka-eagle-bin-3.0.1.tar.gz
# 进入解压缩目录,目录中包含efak-web-3.0.1-bin.tar.gz
cd /opt/software/kafka-eagle-bin-3.0.1
# 解压缩efal压缩文件
tar -zxvf efak-web-3.0.1-bin.tar.gz -C /opt/module/
# 修改名称
cd /opt/module
mv efak-web-3.0.1 efak
3.11.3.2修改配置文件
修改/opt/module/efak/conf/system-config.properties文件
bash
######################################
# multi zookeeper & kafka cluster list
# Settings prefixed with 'kafka.eagle.' will be deprecated, use 'efak.' instead
######################################
efak.zk.cluster.alias=cluster1
cluster1.zk.list=kafka-broker1:2181,kafka-broker2:2181,kafka-broker3:2181/kafka
######################################
# zookeeper enable acl
######################################
cluster1.zk.acl.enable=false
cluster1.zk.acl.schema=digest
cluster1.zk.acl.username=test
cluster1.zk.acl.password=test
######################################
# broker size online list
######################################
cluster1.efak.broker.size=20
######################################
# zk client thread limit
######################################
kafka.zk.limit.size=32
######################################
# EFAK webui port
######################################
efak.webui.port=8048
######################################
# kafka jmx acl and ssl authenticate
######################################
cluster1.efak.jmx.acl=false
cluster1.efak.jmx.user=keadmin
cluster1.efak.jmx.password=keadmin123
cluster1.efak.jmx.ssl=false
cluster1.efak.jmx.truststore.location=/data/ssl/certificates/kafka.truststore
cluster1.efak.jmx.truststore.password=ke123456
######################################
# kafka offset storage
######################################
cluster1.efak.offset.storage=kafka
######################################
# kafka jmx uri
######################################
cluster1.efak.jmx.uri=service:jmx:rmi:///jndi/rmi://%s/jmxrmi
######################################
# kafka metrics, 15 days by default
######################################
efak.metrics.charts=true
efak.metrics.retain=15
######################################
# kafka sql topic records max
######################################
efak.sql.topic.records.max=5000
efak.sql.topic.preview.records.max=10
######################################
# delete kafka topic token
######################################
efak.topic.token=keadmin
######################################
# kafka sasl authenticate
######################################
cluster1.efak.sasl.enable=false
cluster1.efak.sasl.protocol=SASL_PLAINTEXT
cluster1.efak.sasl.mechanism=SCRAM-SHA-256
cluster1.efak.sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username="kafka" password="kafka-eagle";
cluster1.efak.sasl.client.id=
cluster1.efak.blacklist.topics=
cluster1.efak.sasl.cgroup.enable=false
cluster1.efak.sasl.cgroup.topics=
cluster2.efak.sasl.enable=false
cluster2.efak.sasl.protocol=SASL_PLAINTEXT
cluster2.efak.sasl.mechanism=PLAIN
cluster2.efak.sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required username="kafka" password="kafka-eagle";
cluster2.efak.sasl.client.id=
cluster2.efak.blacklist.topics=
cluster2.efak.sasl.cgroup.enable=false
cluster2.efak.sasl.cgroup.topics=
######################################
# kafka ssl authenticate
######################################
cluster3.efak.ssl.enable=false
cluster3.efak.ssl.protocol=SSL
cluster3.efak.ssl.truststore.location=
cluster3.efak.ssl.truststore.password=
cluster3.efak.ssl.keystore.location=
cluster3.efak.ssl.keystore.password=
cluster3.efak.ssl.key.password=
cluster3.efak.ssl.endpoint.identification.algorithm=https
cluster3.efak.blacklist.topics=
cluster3.efak.ssl.cgroup.enable=false
cluster3.efak.ssl.cgroup.topics=
######################################
# kafka sqlite jdbc driver address
######################################
# 配置mysql连接
efak.driver=com.mysql.jdbc.Driver
efak.url=jdbc:mysql://kafka-broker1:3306/ke?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
efak.username=root
efak.password=000000
######################################
# kafka mysql jdbc driver address
######################################
#efak.driver=com.mysql.cj.jdbc.Driver
#efak.url=jdbc:mysql://kafka-broker1:3306/ke?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
#efak.username=root
#efak.password=123456
3.11.3.3添加环境变量
创建/etc/profile.d/my_env.sh脚本文件
bash
#创建脚本文件
vim /etc/profile.d/my_env.sh
# 增加如下内容
# kafkaEFAK
export KE_HOME=/opt/module/efak
export PATH=$PATH:$KE_HOME/bin
# 刷新环境变量
source /etc/profile.d/my_env.sh
3.11.3.4启动集群
(1)启动ZooKeeper,Kafka集群
启动集群
cluster.sh start
(2)启动efak
进入efak文件目录
cd /opt/module/efak
启动efak
bin/ke.sh start
停止efak
bin/ke.sh stop

3.11.3.5登录页面查看监控数据

账号为admin,密码为123456



3.12 KRaft模式
Kafka作为一种高吞吐量的分布式发布订阅消息系统,在消息应用中广泛使用,尤其在需要实时数据处理和应用程序活动跟踪的场景,kafka已成为首选服务。在Kafka2.8之前,Kafka强依赖zookeeper来负责集群元数据的管理,这也导致当Zookeeper集群性能发生抖动时,Kafka的性能也会收到很大的影响。2.8版本之后,kafka开始提供KRaft(Kafka Raft,依赖Java 8+ )模式,开始去除对zookeeper的依赖。最新的3.6.1版本中,Kafka依然兼容zookeeper Controller,但Kafka Raft元数据模式,已经可以在不依赖zookeeper的情况下独立启动Kafka了。官方预计会在Kafka 4.0中移除ZooKeeper,让我们拭目以待。

3.12.1 kraft模式的优势
- 更简单的部署和管理------通过只安装和管理一个应用程序,无需安装更多的软件,简化软件的安装部署。这也使得在边缘的小型设备中更容易利用 Kafka。
- 提高可扩展性------KRaft 的恢复时间比 ZooKeeper 快一个数量级。这使我们能够有效地扩展到单个集群中的数百万个分区。ZooKeeper 的有效限制是数万
- 更有效的元数据传播------基于日志、事件驱动的元数据传播可以提高 Kafka 的许多核心功能的性能。另外它还支持元数据主题的快照。
- 由于不依赖zookeeper,集群扩展时不再受到zookeeper读写能力限制;
- controller不再动态选举,而是由配置文件规定。这样我们可以有针对性的加强controller节点的配置,而不是像以前一样对随机controller节点的高负载束手无策。
3.12.2 Kafka-KRaft集群部署
3.12.2.1在三个节点解压kafka压缩包
bash
# 进入software目录
cd /opt/software
# 解压缩文件
tar -zxvf kafka_2.12-3.6.1.tgz -C /opt/module/
# 修改名称
mv /opt/module/kafka_2.12-3.6.1/ /opt/module/kafka-kraft
3.12.2.2修改config/kraft/server.properties配置文件
bash
#kafka的角色(controller相当于主机、broker节点相当于从机,主机类似zk功能)
process.roles=broker, controller
#节点ID
node.id=1
#controller服务协议别名
controller.listener.names=CONTROLLER
#全Controller列表
controller.quorum.voters=1@kafka-broker1:9093,2@kafka-broker2:9093,3@kafka-broker3:9093
#不同服务器绑定的端口
listeners=PLAINTEXT://:9092,CONTROLLER://:9093
#broker服务协议别名
inter.broker.listener.name=PLAINTEXT
#broker对外暴露的地址
advertised.listeners=PLAINTEXT://kafka-broker1:9092
#协议别名到安全协议的映射
listener.security.protocol.map=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,SSL:SSL,SASL_PLAINTEXT:SASL_PLAINTEXT,SASL_SSL:SASL_SSL
#kafka数据存储目录
log.dirs=/opt/module/kafka-kraft/datas
3.12.2.3修改不同节点的配置
分发软件
xsync /opt/module/kafka-kraft
不同节点需要对node.id相应改变,值需要和controller.quorum.voters对应
不同节点需要根据各自的主机名称,修改相应的advertised.listeners地址。
3.12.2.4初始化集群数据目录
- 首先在每个部署节点生成存储目录唯一ID
进入kafka目录
cd /opt/module/kafka-kraft
生产存储ID
bin/kafka-storage.sh random-uuid
J7s9e8PPTKOO47PxzI39VA
- 用生成的ID格式化每一个kafka数据存储目录
bin/kafka-storage.sh format -t J7s9e8PPTKOO47PxzI39VA -c /opt/module/kafka-kraft/config/kraft/server.properties
3.12.2.5启停Kafka集群
进入到/opt/module/kafka-kraft目录
cd /opt/module/kafka-kraft
执行启动脚本
bin/kafka-server-start.sh -daemon config/kraft/server.properties
执行停止命令
bin/kafka-server-stop.sh
3.12.2.6启停脚本封装
- 在/root/bin目录下创建脚本文件kfk2.sh,并增加内容
bash
#! /bin/bash
case $1 in
"start"){
for i in kafka-broker1 kafka-broker2 kafka-broker3
do
echo " --------启动 $i kafka-kraft -------"
ssh $i "/opt/module/kafka-kraft/bin/kafka-server-start.sh -daemon /opt/module/kafka-kraft/config/kraft/server.properties"
done
};;
"stop"){
for i in kafka-broker1 kafka-broker2 kafka-broker3
do
echo " --------停止 $i kafka-kraft -------"
ssh $i "/opt/module/kafka-kraft/bin/kafka-server-stop.sh "
done
};;
esac
- 添加执行权限
添加权限
chmod 777 kfk2.sh
- 启动和停止集群
启动集群
kfk2.sh start
停止集群
kfk2.sh stop
4 Kafka集成
4.1 大数据应用场景
4.1.1 Flume集成
Flume也是日志采集器,类似于ELK中的LogStash软件功能。早期设计的功能就是通过Flume采集过来数据,然后将数据写入HDFS分布式文件存储系统,不过,随着功能的扩展,现在也可以把采集的数据写入到kafka当中,作为实时数据使用。

4.1.1.1安装Flume
4.1.1.1.1安装地址
Flume官网地址:http://flume.apache.org/
文档查看地址:http://flume.apache.org/FlumeUserGuide.html
下载地址:Index of /dist/flume
4.1.1.1.2安装部署
- 将压缩包apache-flume-1.10.1-bin.tar.gz上传到linux系统的/opt/software目录下
- 将软件压缩包解压缩到/opt/module目录中,并修改名称
解压缩文件
tar -zxf /opt/software/apache-flume-1.10.1-bin.tar.gz -C /opt/module/
修改名称
mv /opt/module/apache-flume-1.10.1-bin /opt/module/flume
- 生产环境中,可以设置flume的堆内存为4G或以上。
修改/opt/module/flume/conf/flume-env.sh文件,配置如下参数(虚拟机环境暂不配置)
修改JVM配置
export JAVA_OPTS="-Xms4096m -Xmx4096m -Dcom.sun.management.jmxremote"
4.1.1.2 增加集成配置
4.1.1.2.1 flume采集数据到Kafka的配置
(1)在linux系统解压缩后的flume软件目录中,创建job目录
进入flume软件目录
cd /opt/module/flume
创建job目录
mkdir job
(2)创建配置文件:file_to_kafka.conf
进入job目录
cd /opt/module/flume/job
创建文件
vim file_to_kafka.conf
(3)增加文件内容
bash
# 定义组件
a1.sources = r1
a1.channels = c1
# 配置source
a1.sources.r1.type = TAILDIR
a1.sources.r1.filegroups = f1
# 日志(数据)文件
a1.sources.r1.filegroups.f1 = /opt/module/data/test.log
a1.sources.r1.positionFile = /opt/module/flume/taildir_position.json
# 配置channel
# 采用Kafka Channel,省去了Sink,提高了效率
a1.channels.c1.type = org.apache.flume.channel.kafka.KafkaChannel
a1.channels.c1.kafka.bootstrap.servers = kafka-broker1:9092,kafka-broker2:9092,kafka-broker3:9092
a1.channels.c1.kafka.topic = test
a1.channels.c1.parseAsFlumeEvent = false
# 组装
a1.sources.r1.channels = c1
4.1.1.3 集成测试
4.1.1.3.1 启动Zookeeper、Kafka集群

4.1.1.3.2 执行flume操作采集数据到Kafka
进入flume
cd /opt/module/flume
执行
bin/flume-ng agent -n a1 -c conf/ -f job/file_to_kafka.conf
4.1.2 SparkStreaming集成
Spark是分布式计算引擎,是一款非常强大的离线分布式计算框架,其中的SparkStreaming模块用于准实时数据处理,其中就可以将Kafka作为数据源进行处理。

4.1.2.1 编写功能代码
4.1.2.1.1 修改pom.xml文件,增加依赖
XML
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.12</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.12</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming-kafka-0-10_2.12</artifactId>
<version>3.3.1</version>
</dependency>
4.1.2.1.2 编写功能代码
java
package com.atguigu.kafka.test;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.function.Function;
import org.apache.spark.storage.StorageLevel;
import org.apache.spark.streaming.Duration;
import org.apache.spark.streaming.api.java.JavaInputDStream;
import org.apache.spark.streaming.api.java.JavaPairDStream;
import org.apache.spark.streaming.api.java.JavaReceiverInputDStream;
import org.apache.spark.streaming.api.java.JavaStreamingContext;
import org.apache.spark.streaming.kafka010.ConsumerStrategies;
import org.apache.spark.streaming.kafka010.KafkaUtils;
import org.apache.spark.streaming.kafka010.LocationStrategies;
import scala.Tuple2;
import java.util.*;
public class Kafka4SparkStreamingTest {
public static void main(String[] args) throws Exception {
// TODO 创建配置对象
SparkConf conf = new SparkConf();
conf.setMaster("local[*]");
conf.setAppName("SparkStreaming");
// TODO 创建环境对象
JavaStreamingContext ssc = new JavaStreamingContext(conf, new Duration(3 * 1000));
// TODO 使用kafka作为数据源
// 创建配置参数
HashMap<String, Object> map = new HashMap<>();
map.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"localhost:9092");
map.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
map.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
map.put(ConsumerConfig.GROUP_ID_CONFIG,"atguigu");
map.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"latest");
// 需要消费的主题
ArrayList<String> strings = new ArrayList<>();
strings.add("test");
JavaInputDStream<ConsumerRecord<String, String>> directStream =
KafkaUtils.createDirectStream(
ssc,
LocationStrategies.PreferBrokers(),
ConsumerStrategies.<String, String>Subscribe(strings,map));
directStream.map(new Function<ConsumerRecord<String, String>, String>() {
@Override
public String call(ConsumerRecord<String, String> v1) throws Exception {
return v1.value();
}
}).print(100);
ssc.start();
ssc.awaitTermination();
}
}
4.1.2.2 集成测试
4.1.2.2.1 启动Zookeeper、Kafka集群

4.1.2.2.2 执行Spark程序

4.1.3 Flink集成
Flink是分布式计算引擎,是一款非常强大的实时分布式计算框架,可以将Kafka作为数据源进行处理。

4.1.3.1 编写功能代码
4.1.3.1.1 修改pom.xml文件,增加相关依赖
XML
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-java</artifactId>
<version>1.17.0</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java</artifactId>
<version>1.17.0</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-clients</artifactId>
<version>1.17.0</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka</artifactId>
<version>1.17.0</version>
</dependency>
4.1.3.1.2 编写功能代码
java
package com.atguigu.kafka;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.connector.kafka.source.KafkaSource;
import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
public class Kafka4FlinkTest {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
KafkaSource<String> kafkaSource = KafkaSource.<String>builder()
.setBootstrapServers("localhost:9092")
.setTopics("test")
.setGroupId("atguigu")
.setStartingOffsets(OffsetsInitializer.latest())
.setValueOnlyDeserializer(new SimpleStringSchema())
.build();
DataStreamSource<String> stream = env.fromSource(kafkaSource, WatermarkStrategy.noWatermarks(), "kafka-source");
stream.print("Kafka");
env.execute();
}
}
4.1.3.2 集成测试
4.1.3.2.1 启动Zookeeper、Kafka集群

4.1.3.2.2 执行Flink程序

4.2 Java应用场景
4.2.1 SpringBoot集成
Spring Boot帮助您创建可以运行的、独立的、生产级的基于Spring的应用程序。您可以使用Spring Boot创建Java应用程序,这些应用程序可以通过使用java-jar或更传统的war部署启动。
我们的目标是:
- 为所有Spring开发提供从根本上更快、广泛访问的入门体验。
- 开箱即用,但随着要求开始偏离默认值,请迅速离开。
- 提供大型项目(如嵌入式服务器、安全性、指标、健康检查和外部化配置)常见的一系列非功能性功能。
- 绝对没有代码生成(当不针对原生图像时),也不需要XML配置。

4.2.1.1 创建SpringBoot项目
4.2.1.1.1 创建SpringBoot项目

4.2.1.1.2 修改pom.xml文件
XML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.atguigu</groupId>
<artifactId>springboot-kafka</artifactId>
<version>0.0.1</version>
<name>springboot-kafka</name>
<description>Kafka project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</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</artifactId>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.6.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-json</artifactId>
<version>5.8.11</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-db</artifactId>
<version>5.8.11</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4.2.1.1.3 在resources中增加application.yml文件
XML
spring:
kafka:
bootstrap-servers: localhost:9092
producer:
acks: all
batch-size: 16384
buffer-memory: 33554432
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
retries: 0
consumer:
group-id: test#消费者组
#消费方式: 在有提交记录的时候,earliest与latest是一样的,从提交记录的下一条开始消费
# earliest:无提交记录,从头开始消费
#latest:无提交记录,从最新的消息的下一条开始消费
auto-offset-reset: earliest
enable-auto-commit: true #是否自动提交偏移量offset
auto-commit-interval: 1s #前提是 enable-auto-commit=true。自动提交的频率
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
max-poll-records: 2
properties:
#如果在这个时间内没有收到心跳,该消费者会被踢出组并触发{组再平衡 rebalance}
session.timeout.ms: 120000
#最大消费时间。此决定了获取消息后提交偏移量的最大时间,超过设定的时间(默认5分钟),服务端也会认为该消费者失效。踢出并再平衡
max.poll.interval.ms: 300000
#配置控制客户端等待请求响应的最长时间。
#如果在超时之前没有收到响应,客户端将在必要时重新发送请求,
#或者如果重试次数用尽,则请求失败。
request.timeout.ms: 60000
#订阅或分配主题时,允许自动创建主题。0.11之前,必须设置false
allow.auto.create.topics: true
#poll方法向协调器发送心跳的频率,为session.timeout.ms的三分之一
heartbeat.interval.ms: 40000
#每个分区里返回的记录最多不超max.partitions.fetch.bytes 指定的字节
#0.10.1版本后 如果 fetch 的第一个非空分区中的第一条消息大于这个限制
#仍然会返回该消息,以确保消费者可以进行
#max.partition.fetch.bytes=1048576 #1M
listener:
#当enable.auto.commit的值设置为false时,该值会生效;为true时不会生效
#manual_immediate:需要手动调用Acknowledgment.acknowledge()后立即提交
#ack-mode: manual_immediate
missing-topics-fatal: true #如果至少有一个topic不存在,true启动失败。false忽略
#type: single #单条消费?批量消费? #批量消费需要配合 consumer.max-poll-records
type: batch
concurrency: 2 #配置多少,就为为每个消费者实例创建多少个线程。多出分区的线程空闲
template:
default-topic: "test"
server:
port: 9999
4.2.1.2 编写功能代码
4.2.1.2.1 创建配置类:SpringBootKafkaConfig
java
package com.atguigu.springkafka.config;
public class SpringBootKafkaConfig {
public static final String TOPIC_TEST = "test";
public static final String GROUP_ID = "test";
}
4.2.1.2.2 创建生产者控制器:KafkaProducerController
java
package com.atguigu.springkafka.controller;
import com.atguigu.springkafka.config.SpringBootKafkaConfig;
import lombok.extern.slf4j.Slf4j;
import cn.hutool.json.JSONUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;
@RestController
@RequestMapping("/kafka")
@Slf4j
public class KafkaProducerController {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@ResponseBody
@PostMapping(value = "/produce", produces = "application/json")
public String produce(@RequestBody Object obj) {
try {
String obj2String = JSONUtil.toJsonStr(obj);
kafkaTemplate.send(SpringBootKafkaConfig.TOPIC_TEST, obj2String);
return "success";
} catch (Exception e) {
e.getMessage();
}
return "success";
}
}
4.2.1.2.3 创建消费者:KafkaDataConsumer
java
package com.atguigu.springkafka.component;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import com.atguigu.springkafka.config.SpringBootKafkaConfig;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.KafkaHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
@Component
@Slf4j
public class KafkaDataConsumer {
@KafkaListener(topics = SpringBootKafkaConfig.TOPIC_TEST, groupId = SpringBootKafkaConfig.GROUP_ID)
public void topic_test(List<String> messages, @Header(KafkaHeaders.RECEIVED_TOPIC) String topic) {
for (String message : messages) {
final JSONObject entries = JSONUtil.parseObj(message);
System.out.println(SpringBootKafkaConfig.GROUP_ID + " 消费了: Topic:" + topic + ",Message:" + entries.getStr("data"));
//ack.acknowledge();
}
}
}
4.2.1.3 集成测试
4.2.1.3.1 启动ZooKeeper

4.2.1.3.2 启动Kafka
4.2.1.3.3 启动应用程序

4.2.1.3.4 生产数据测试
可以采用任何的工具进行测试,我们这里采用postman发送POST数据

消费者消费数据

5 Kafka优化
5.1 资源配置
5.1.1 操作系统
Kafka的网络客户端底层使用Java NIO的Selector方式,而Selector在Linux的实现是epoll,在Windows上实现机制为select。因此Kafka部署在Linux会有更高效的I/O性能。
数据在磁盘和网络之间进行传输时候,在Linux上可以享受到零拷贝机制带来的快捷和便利高效,而Windows在一定程度上会使用零拷贝操作。
所以建议Kafka部署在Linux操作系统上。
5.1.2 磁盘选择
Kafka 存储方式为顺序读写,机械硬盘的最大劣势在于随机读写慢。所以使用机械硬盘并不会造成性能低下。所以磁盘选用普通机械硬盘即可,Kafka自身已经有冗余机制,而且通过分区的设计,实现了负载均衡的功能。不做磁盘组raid阵列也是可以的。
磁盘空间需要多少,需要根据具体场景进行简单估算
设计场景:日志数据每天向kafka发送1亿条数据,每条数据有两个副本防止数据丢失,数据保存两周,每条消息平均大小为1KB。
1)每天1亿条1KB消息,保存两份,则每天总大小为:
(100000000*1KB*2)/1024/1024≈200GB
2)kafka除了消息数据还有其他类型的数据,故增加10%的冗余空间,则需要220GB
3)两周时间则为 220GB*14≈3TB
4)如果启用压缩,压缩比约在 0.75 左右,则总存储空间规划为3TB*0.75=2.25TB
5.1.3 网络带宽
如果网络为万兆带宽,基本不会出现网络瓶颈,如果数据量特别大,按照下文中的设计场景进行计算。如果网络为百兆或者千兆带宽,在处理较大数据量场景下会出现网络瓶颈,可按照下面的传统经验公式进行计算处理,也可按照下述场景按照自己生产实际情况进行设计。
经验公式:服务器台数 = 2 × ( 生产者峰值生产速率 × 副本数 ÷ 100) + 1
带宽情况最容易成为 kafka 的瓶颈。
设计场景:如果机房为千兆带宽,我们需要在一小时内处理1TB的数据,需要多少台kafka 服务器?
1)由于带宽为千兆网,1000Mbps=1Gbps,则每秒钟每个服务器能收到的数据量为 1Gb=1000Mb
2)假设 Kafka 占用整个服务器网络的70%(其他 30%为别的服务预留),则Kafka可以使用到700Mb 的带宽,但是如果从常规角度考虑,我们不能总让Kafka顶满带宽峰值,所以需要预留出2/3甚至3/4的资源,也就是说,Kafka单台服务器使用带宽实际应为 700Mb/3=240Mb
3)1 小时需要处理1TB数据,1TB=1024*1024*8Mb=8000000Mb,则一秒钟处理数据量为:8000000Mb/3600s=2330Mb 数据。
4)需要的服务器台数为:2330Mb/240Mb≈10 台。
5)考虑到消息的副本数如果为 2,则需要20台服务器,副本如果为3,则需要30台服务器。
5.1.4 内存配置
Kafka运行过程中设计到的内存主要为JVM的堆内存和操作系统的页缓存,每个Broker节点的堆内存建议10-15G内存,而数据文件(默认为1G)的25%在内存就可以了。综合上述,Kafka在大数据场景下能够流畅稳定运行至少需要11G,建议安装Kafka的服务器节点的内存至少大于等于16G。
5.1.5 CPU选择
观察所有的Kafka与线程相关的配置,一共有以下几个

在生产环境中,建议CPU核数最少为16核,建议32核以上,方可保证大数据环境中的Kafka集群正常处理与运行。
5.2 集群容错
5.2.1 副本分配策略
Kafka采用分区机制对数据进行管理和存储,每个Topic可以有多个分区,每个分区可以有多个副本。应根据业务需求合理配置副本,一般建议设置至少2个副本以保证高可用性。
5.2.2 故障转移方案
当Kafka集群中的某个Broker节点发生故障时,其负责的分区副本将会被重新分配到其他存活的Broker节点上,并且会自动选择一个备份分区作为新的主分区来处理消息的读写请求。
5.2.3 数据备份与恢复
Kafka采用基于日志文件的存储方式,每个Broker节点上都有副本数据的本地备份。在数据备份方面,可以通过配置Kafka的数据保留策略和数据分区调整策略来保证数据的持久性和安全性;在数据恢复方面,可以通过查找备份数据并进行相应的分区副本替换来恢复数据。
5.3 参数配置优化
|---------------------------------------|----------------|--------|----------|--------------|
| 参数名 | 默认参数值 | 位置 | 优化场景 | 备注 |
| num.network.threads | 3 | 服务端 | 低延迟 | |
| num.io.threads | 8 | 服务端 | 低延迟 | |
| socket.send.buffer.bytes | 102400(100K) | 服务端 | 高吞吐 | |
| socket.receive.buffer.bytes | 65536(64K) | 服务端 | 高吞吐场景 | |
| max.in.flight.requests.per.connection | 5 | 生产端 | 幂等 | |
| buffer.memory | 33554432(32M) | 生产端 | 高吞吐 | |
| batch.size | 16384(16K) | 生产端 | 提高性能 | |
| linger.ms | 0 | 生产端 | 提高性能 | |
| fetch.min.bytes | 1 | 消费端 | 提高性能 | 网络交互次数 |
| max.poll.records | 500 | 消费端 | 批量处理 | 控制批量获取消息数量 |
| fetch.max.bytes | 57671680 (55M) | 消费端 | 批量处理 | 控制批量获取消息字节大小 |
5.4 数据压缩和批量发送
通过压缩和批量发送可以优化Kafka的性能表现。Kafka支持多种数据压缩算法,包括Gzip、Snappy、LZ4和zstd。在不同场景下,需要选择合适的压缩算法,以确保性能最优。
下面的表格为网络上不同压缩算法的测试数据,仅作参考
|----------|----------|----------|-----------|
| 压缩算法 | 压缩比率 | 压缩效率 | 解压缩效率 |
| snappy | 2.073 | 580m/s | 2020m/s |
| lz4 | 2.101 | 800m/s | 4220m/s |
| zstd | 2.884 | 520m/s | 1600m/s |
从表格数据可以直观看出,zstd有着最高得压缩比,而LZ4算法,在吞吐量上表现得非常高效。对于Kafka而言,在吞吐量上比较:lz4 > snappy>zstd>gzip。而在压缩比上:zstd>lz4>gzip>snappy
Kafka支持两种批处理方式:异步批处理和同步批处理。在不同场景下,需要选择合适的批处理方式,进行性能优化。同时需要合理设置批处理参数,如batch.size、linger.ms等。
6 Kafka常见问题
6.1 Kafka都有哪些组件?

6.2 Kafka的LSO、LEO、 HW 的含义?
LSO,LEO,HW其实都是kafka中的偏移量。只不过它们代表的含义是不相同的。
这里的LSO有两层含义:
一个是Log Start Offset , 一个是Log Stable Offset,第一个表示数据文件的起始偏移量,同学们还记的,咱们的log文件的文件名吗,文件名中的那个数字就是当前文件的LSO, 第二个表示的位移值是用来判断事务型消费者的可见性,就是所谓的事务隔离级别,一个叫read_commited, 一个叫read_uncommited。当然了,如果你的生产者或消费者没有使用事务,那么这个偏移量没有任何的意义。
LEO 表示 Log End Offset,就是下一个要写入的数据偏移量,所以这个偏移量的数据是不存在的
HW表示高水位线偏移量的意思。是kafka为了数据的一致性所增加的一种数据隔离方式。简单的理解,就是消费者只能消费到,小于高水位线偏移量的数据。
6.3 Controller选举是怎么实现的?
这里的controller选举主要指的还是Kafka依赖于ZK实现的controller选举机制,也就是说,kafka的所有broker节点会监听ZK中的一个controller临时节点,如果这个节点没有创建,那么broker就会申请创建,一旦创建成功,那么创建成功的broker就会当选为集群的管理者controller,一旦失去了和ZK的通信,那么临时节点就会消失,此时就会再次进行controller的选举,选举的规则是完全一样的,一旦新的controller选举,那么controller纪元会被更新。
6.4 分区副本AR, ISR, OSR的含义?
这里的AR可以理解为分区的所有副本集合。而ISR表示的就是正在同步数据的副本列表,列表的第一个就是分区的Leader副本,其他的副本就是Follower副本。OSR就是没有处于同步数据的副本列表。一旦副本拉取数据满足了特点的条件,那么会从OSR中移除并增加到ISR中。同样,如果副本没有拉取数据满足了特定的条件,就会从ISR中移除,放入到OSR中。这就是所谓的ISR列表的收缩和扩张。kafka使用这种ISR的方式有效的权衡了数据可靠性和性能之间的关系
6.5 Producer生产消息是如何实现的?
这里所谓的生产消息。指的就是生产者客户端的生产数据的基本流程。咱们之前的图形中,就把这个流程已经画出来了。我相信图形比文字应该更容易记忆,所以请大家参考前面的生产者组件。
6.6 Producer ACK应答策略?
ACK应答机制其实就是生产者发送数据后kafka接收确认方式。Kafka确认的方式有3种:
第一种是当生产者数据发送到网络客户端的缓冲区后,Kafka就认为数据收到了,那么就会进行响应,也就是应答。但是这种方式,数据可靠性是非常低的,因为不能保证数据一定会写入日志文件。但是发送效率影响不大。
第二种是当主题分区的Leader副本将数据写入日志后,Kafka才认为数据收到了,然后再对生产者进行响应。这种方式,发送数据的效率会降低,但是可靠性会高一些。而可靠性最高的就是第三种方式,
第三种方式就是主题分区的ISR副本列表种所有的副本都已经将数据写入日志后。Kafka才认为数据收到了,然后再对生产者进行响应。这种方式,发送数据的效率会非常低。生产者对象可以根据生产环境和业务要求对应答机制进行配置。
三种方式分别对应0,1和-1(all)。另外,生产者数据幂等性操作要求ACK应答处理机制必须为-1,而ACK的参数默认值也是-1
6.7 Producer 消息重复或消息丢失的原因?
Producer消息重复和消息丢失的原因,主要就是kafka为了提高数据可靠性所提供的重试机制,如果禁用重试机制,那么一旦数据发送失败,数据就丢失了。而数据重复,恰恰是因为开启重试机制后,如果因为网络阻塞或不稳定,导致数据重新发送。那么数据就有可能是重复的。所以kafka提供了幂等性操作解决数据重复,并且幂等性操作要求必须开启重试功能和ACK取值为-1,这样,数据就不会丢失了。
kafka提供的幂等性操作只能保证同一个生产者会话中同一个分区中的数据不会重复,一旦数据发送过程中,生产者对象重启,那么幂等性操作就会失效。那么此时就需要使用Kafka的事务功能来解决跨会话的幂等性操作。但是跨分区的幂等性操作是无法实现的。
6.8 Follower拉取Leader消息是如何实现的?
这个问题说的是数据拉取流程,请大家参考数据拉取流程
6.9 Consumer拉取消息是如何实现的?
这个问题说的是数据拉取流程,请大家参考数据拉取流程
6.10 Consumer消息重复或消息丢失的原因?
这里主要说的是消费者提交偏移量的问题。消费者为了防止意外情况下,重启后不知道从哪里消费,所以会每5s时间自动保存偏移量。但是这种自动保存偏移量的操作是基于时间的,一旦未达到时间,消费者重启了,那么消费者就可能重复消费数据。
Kafka提供自动保存偏移量的功能的同时,也提供了手动保存偏移量的2种方式,一个是同步提交,一个是异步提交。本质上都是提交一批数据的最后一个偏移量的值,但是可能会出现,偏移量提交完毕,但是拉取的数据未处理完毕,消费者重启了。那么此时有的数据就消费不到了,也就是所谓的数据丢失。
6.11 Kafka数据如何保证有序?
这里的有序我们要考虑的点比较多,但是总结起来就是生产有序,存储有序,消费有序。
所谓的生产有序就是生产者对象需要给数据增加序列号用于标记数据的顺序,然后再服务端进行缓存数据的比对,一旦发现数据是乱序的,那么就需要让生产者客户端进行数据的排序,然后重新发送数据,保证数据的有序。不过这里的缓存数据的比对,最多只能有5条数据比对,所以生产者客户端需要配置参数,将在途请求缓冲区的请求队列数据设置为5,否则数据依然可能乱序。因为服务端的缓存数据是以分区为单位的,所以这就要求生产者客户端需要将数据发送到一个分区中,如果数据发送到多个分区,是无法保证顺序的。这就是生产有序的意思。那存储有序指的是kafka的服务端获取数据后会将数据顺序写入日志文件,这样就保证了存储有序,当然也只能是保证一个分区的数据有序。接下来就是消费有序。所谓的消费有序其实就是kafka在存储数据时会给数据增加一个访问的偏移量值,那消费者只能按照偏移量的方式顺序访问,并且一个分区的数据只能被消费者组中的一个消费者消费,那么按照偏移量方式的读取就不会出现乱序的情况。所以综合以上的描述。Kafka就能够实现数据的有序。