Kafka介绍

目录

1. Kafka概述

1.1 前言

1.2 定义

1.3 消息队列

1.4 Kafka 基础架构

1.5 名词解释

2. Kafka快速入门

2.1 安装部署

2.1.1 集群规划

2.1.2 集群部署

2.1.3 集群启停脚本

2.2 目录介绍

2.3 Kafka命令行操作

2.3.1 主题命令行操作

2.3.2 生产者命令行操作

2.3.3 消费者命令行操作

3. Kafka生产者

3.1 生产者消息发送流程

3.1.1 发送原理

3.1.2 生产者重要参数列表

3.2 异步发送 API

3.2.1 普通异步发送

3.2.2 带回调函数的异步发送

3.3 同步API

3.4 生产者分区

3.4.1 分区好处

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

3.4.3 自定义分区器

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

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

3.7 生产经验——数据去重

3.7.1 数据传递语义

3.7.2 幂等性

3.7.3 生产者事务

3.8 生产经验——数据有序​编辑

3.9 生产经验——数据乱序​编辑

4. Kafka Broker

4.1 Kafka Broker 工作流程

4.1.1 Zookeeper 存储的 Kafka 信息

4.1.2 Kafka Broker 总体工作流程

4.1.3 Broker 重要参数

4.2 生产经验——节点服役和退役

4.2.1 服役新节点

4.3 Kafka 副本

4.3.1 副本基本信息

4.3.3 Leader 和 Follower 故障处理细节

4.3.4 分区副本分配

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

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

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

4.4 文件存储

4.4.1 文件存储机制

4.4.2 文件清理策略

4.5 高效读写数据

5. Kafka 消费者

5.1 Kafka 消费方式

5.2 Kafka 消费者工作流程

5.2.1 消费者总体工作流程

5.2.2 消费者组原理

5.2.3 消费者重要参数

5.3 消费者 API

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

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

5.3.3 消费者组案例

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

5.4.1 Range 以及再平衡

5.4.2 RoundRobin 以及再平衡

5.4.3 Sticky 以及再平衡

5.5 offset 位移

5.5.1 offset 的默认维护位置

5.5.2 自动提交 offset

5.5.3 手动提交 offset

5.5.4 指定 Offset 消费

5.5.5 指定时间消费

5.5.6 漏消费和重复消费

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

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

6. Kafka-Eagle 监控

6.1 MySQL 环境准备

6.2 Kafka 环境准备

6.3 Kafka-Eagle 安装

6.4 Kafka-Eagle 页面操作

7. Kafka-Kraft 模式

7.1 Kafka-Kraft 架构

7.2 Kafka-Kraft 集群部署

10. 番外补充

1. 消费策略:

2. 确定coordinator


1. Kafka概述

1.1 前言

前端埋点记录用户购买海购人参丸的行为数据(浏览、点赞、收藏、评论等)。对用户行为,界面变化进行监听,收集数据,保存到日志中,通过flume感知数据变化,上传到Hadoop上,kafka主要在中间起到缓冲的作用

1.2 定义

Kafka传统定义:Kafka是一个分布式的基于发布/订阅模式的消息队列(MessageQueue),主要应用于大数据实时处理领域

分布式是指多台服务器完成一个目标

发布/订阅的理解:消息的发布者不会将消息直接发送给特定的订阅者,而是将发布的消息分为不同的类别,订阅者只接收感兴趣的消息

如上图,不同类别比如浏览,点赞,收藏,hadoop作为消费者

Kafka最新定义 : Kafka是 一个开源的分布式事件流平台 (Event StreamingPlatform),被数千家公司用于高性能数据管道、流分析、数据集成和关键任务应用。

现在功能有海量数据的存储,海量数据的计算,不单单是发布订阅消息,但是现在主要还是消息队列,缓冲的作用

1.3 消息队列

目 前企 业中比 较常 见的 消息 队列产 品主 要有 Kafka、ActiveMQ 、RabbitMQ 、RocketMQ等

在大数据场景主要采用 Kafka 作为消息队列。在 JavaEE 开发中主要采用 ActiveMQ、RabbitMQ、RocketMQ

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

传统的消息队列的主要应用场景包括:缓存/消峰解耦异步通信

缓冲/消峰:有助于控制和优化数据流经过系统的速度,解决生产消息和消费消息的处理速度不一致的情况

解耦:允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。

之前数据源可以写到任何目的地,每一种都要写一遍代码,也不方便管理,但是加上消息队列后,数据源直接将数据写到消息队列里面,目的地直接从消息队列中拿消息就可以了,方便开发,起到解耦的作用

异步通信:允许用户把一个消息放入队列,但并不立即处理它,然后在需要的时候再去处理它们

主要是将核心功能完成之后,后面的一些逻辑可以异步执行

1.3.2 消息队列的两种模式

点对点模式

  • 消费者主动拉取数据,消息收到后清除消息

  • 就是一个生产者对应一个消费者,对消息进行消费之后,进行应答,并删除消息

发布/订阅模式

  • 可以有多个topic主题(浏览、点赞、收藏、评论等)

  • 消费者消费数据之后,不删除数据

  • 每个消费者相互独立,都可以消费到数据

  • 发布订阅应用更广泛,处理复杂的业务场景

1.4 Kafka 基础架构

1.为方便扩展,并提高吞吐量,一个topic分为多个partition

  • 发送大数据给消费者,一台服务器存不下,多建几个broker,就是多几个服务器进行存储

2.配合分区的设计,提出消费者组的概念,组内每个消费者并行消费

  • 每个服务器如果只有一个消费者进行消费,很慢,可以多弄几个消费者进行消费,一个消费者负责部分分区的数据,分区越多,那就多准备消费者

3.为提高可用性,为每个partition增加若干副本,类似NameNode HA,生产数据只针对leader副本

4.ZK中记录谁是leader,Kafka2.8.0以后也可以配置不采用ZK,zookeeper主要记录服务器的运行状态,记录哪个分区是leader

1.5 名词解释

(1)Producer:消息生产者,就是向 Kafka broker 发消息的客户端。

(2)Consumer:消息消费者,向 Kafka broker 取消息的客户端。

(3)Consumer Group(CG):消费者组,由多个 consumer 组成。消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费;消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。

(4)Broker:一台 Kafka 服务器就是一个 broker。一个集群由多个 broker 组成。一个broker 可以容纳多个 topic。

(5)Topic:可以理解为一个队列,生产者和消费者面向的都是一个 topic。

(6)Partition:为了实现扩展性,一个非常大的 topic 可以分布到多个 broker(即服务器)上,一个 topic 可以分为多个 partition,每个 partition 是一个有序的队列。

(7)Replica:副本。一个 topic 的每个分区都有若干个副本,一个 Leader 和若干个Follower。

(8)Leader:每个分区多个副本的“主”,生产者发送数据的对象,以及消费者消费数据的对象都是 Leader。

(9)Follower:每个分区多个副本中的“从”,实时从 Leader 中同步数据,保持和Leader 数据的同步。Leader 发生故障时,某个Follower 会成为新的 Leader。

2. Kafka快速入门

2.1 安装部署

2.1.1 集群规划

hadoop102hadoop103hadoop104
zkzkzk
kafkakafkakafka

2.1.2 集群部署

0)官方下载地址Apache Kafka 1)解压安装包

[atguigu@hadoop102 software]$ tar -zxvf kafka_2.12-3.0.0.tgz -C 
/opt/module/

2)修改解压后的文件名称

[atguigu@hadoop102 module]$ mv kafka_2.12-3.0.0/ kafka

3)进入到/opt/module/kafka 目录,修改配置文件

[atguigu@hadoop102 kafka]$ cd config/
[atguigu@hadoop102 config]$ vim server.properties

输入以下内容:

#broker 的全局唯一编号,不能重复,只能是数字。表示kafka在集群中的唯一标识
broker.id=0
#kafka 运行日志(数据)存放的路径,路径不需要提前创建,kafka 自动帮你创建,可以配置多个磁盘路径,路径与路径之间可以用","分隔
log.dirs=/opt/module/kafka/datas
#配置连接 Zookeeper 集群地址(在 zk 根目录下创建/kafka,方便管理),zookeeper默认连接的本地的zookeeper,这里新建了一个kafka的文件目录,后续删除kafka文件就不需要一个一个去找,直接删除这个目录就可以了
zookeeper.connect=hadoop102:2181,hadoop103:2181,hadoop104:2181/kafka
-------------------------------------------------------------------------------------------------------------------
老师后面的暂时没设置
#处理网络请求的线程数量
num.network.threads=3
#用来处理磁盘 IO 的线程数量
num.io.threads=8
#发送套接字的缓冲区大小
socket.send.buffer.bytes=102400
#接收套接字的缓冲区大小
socket.receive.buffer.bytes=102400
#请求套接字的缓冲区大小
socket.request.max.bytes=104857600
#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

4)分发安装包

[atguigu@hadoop102 module]$ xsync kafka/

5)分别在 hadoop103 和 hadoop104 上修改配置文件/opt/module/kafka/config/server.properties中的 broker.id=1、broker.id=2 注:broker.id 不得重复,整个集群中唯一

[atguigu@hadoop103 module]$ vim kafka/config/server.properties
修改:
# The id of the broker. This must be set to a unique integer for 
each broker.
broker.id=1
[atguigu@hadoop104 module]$ vim kafka/config/server.properties
修改:
# The id of the broker. This must be set to a unique integer for 
each broker.
broker.id=2

6)配置环境变量 (1)在/etc/profile.d/my_env.sh 文件中增加 kafka 环境变量配置

[atguigu@hadoop102 module]$ sudo vim /etc/profile.d/my_env.sh

增加如下内容:

#KAFKA_HOME
export KAFKA_HOME=/opt/module/kafka
export PATH=$PATH:$KAFKA_HOME/bin

(2)刷新一下环境变量。

[atguigu@hadoop102 module]$ source /etc/profile

(3)分发环境变量文件到其他节点,并 source。这个分发好像是将其他服务器数据也是按照本台服务器的数据来

[atguigu@hadoop102 module]$ sudo /home/atguigu/bin/xsync /etc/profile.d/my_env.sh
#不同服务器刷新环境配置
[atguigu@hadoop103 module]$ source /etc/profile
[atguigu@hadoop104 module]$ source /etc/profile

xsync是个hadoop脚本,在资料包下,

7)启动集群 (1)先启动 Zookeeper 集群,然后启动 Kafka

[atguigu@hadoop102 kafka]$ zk.sh start 

他这个之前写过脚本,在这台机子上运行,其他两台服务器上也运行了,就是同步启动

xcall jps查看对应zookeeper的进程

(2)依次在 hadoop102、hadoop103、hadoop104 节点上启动 Kafka。

上图这么启动还不行,有提示需要加东西,相当于对默认配置进行覆盖

[atguigu@hadoop102 kafka]$ bin/kafka-server-start.sh -daemon config/server.properties
[atguigu@hadoop103 kafka]$ bin/kafka-server-start.sh -daemon config/server.properties
[atguigu@hadoop104 kafka]$ bin/kafka-server-start.sh -daemon config/server.properties

8)关闭集群

[atguigu@hadoop102 kafka]$ bin/kafka-server-stop.sh 
[atguigu@hadoop103 kafka]$ bin/kafka-server-stop.sh 
[atguigu@hadoop104 kafka]$ bin/kafka-server-stop.sh

2.1.3 集群启停脚本

一般在实际运用中,不可能想上面那样一台一台的kafka启动,会运行相应的脚本全都运行起来

1)在/home/atguigu/bin 目录下创建文件 kf.sh 脚本文件

[atguigu@hadoop102 bin]$ vim kf.sh

脚本如下:

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

2)添加执行权限

[atguigu@hadoop102 bin]$ chmod +x kf.sh
[atguigu@hadoop102 bin]$ chmod 777 kf.sh

3)启动集群命令

[atguigu@hadoop102 ~]$ kf.sh start

4)停止集群命令

[atguigu@hadoop102 ~]$ kf.sh stop

注意:停止 Kafka 集群时,一定要等 Kafka 所有节点进程全部停止后再停止 Zookeeper集群。因为 Zookeeper 集群当中记录着 Kafka 集群相关信息,Zookeeper 集群一旦先停止,Kafka 集群就没有办法再获取停止进程的信息,无法停止kafka了,kafka把一些信息存储在了zookeeper里面,如果zookeeper下线,kafka会跟zookeeper确定当前状态,发现连不上zookeeper,无法停止,只能手动杀死 Kafka 进程了。

2.2 目录介绍

bin目录

系统命令存放位置,启动、停止等相关操作脚本,

包含生产者、消费者、topic脚本,以及停止和启动脚本等等

config目录

配置信息,其他的像etc也是存储配置文件的文件夹

里面可以看到生产者、消费者以及服务端配置信息

libs目录

引入的是第三方依赖的jar包

2.3 Kafka命令行操作

2.3.1 主题命令行操作

1)查看操作主题命令参数

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh

里面主要介绍了对topic操作的命令,首先是要连接kafka

参数描述
--bootstrap-server <String: server toconnect to>连接的 Kafka Broker 主机名称和端口号
--topic <String: topic>操作的 topic 名称
--create创建主题
--delete删除主题
--alter修改主题
--list查看所有主题
--describe查看主题详细描述
--partitions <Integer: # of partitions>设置分区数
--replication-factor<Integer: replication factor>设置分区副本
--config <String: name=value>更新系统默认的配置

连接一般写两个,连接上一个其他服务器信息都能看见,一般连两台服务器,防止有一台服务器挂掉

看一下这个详细信息,topic名字是first,一个分区,三个副本,默认存储一个g

继续下面,partition:0,分区就一个,编号为0,leader是2,102,103,104分别对应0,1,2,所以104是leader,分区只能增加,不能减少,因为分区记录这消费信息,你把这个分区删掉之后呢,无法继续消费下一条信息了,不能通过命令行的方式修改副本数量,需要通过其他手段

2)查看当前服务器中的所有 topic

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server 
hadoop102:9092 --list

3)创建 first topic

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server 
hadoop102:9092 --create --partitions 1 --replication-factor 3 --topic first
选项说明:
--topic 定义 topic 名
--replication-factor 定义副本数
--partitions 定义分区数

4)查看 first 主题的详情

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server 
hadoop102:9092 --describe --topic first

5)修改分区数(注意:分区数只能增加,不能减少)

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server 
hadoop102:9092 --alter --topic first --partitions 3

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

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server 
hadoop102:9092 --describe --topic first

7)删除 topic(学生自己演示)

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server 
hadoop102:9092 --delete --topic first

2.3.2 生产者命令行操作

1)查看操作生产者命令参数

[atguigu@hadoop102 kafka]$ bin/kafka-console-producer.sh
参数描述
--bootstrap-server <String: server toconnect to>连接的 Kafka Broker 主机名称和端口号
--topic <String: topic>操作的 topic 名称

2)发送消息

[atguigu@hadoop102 kafka]$ bin/kafka-console-producer.sh --
bootstrap-server hadoop102:9092 --topic first
>hello world
>atguigu atguigu

2.3.3 消费者命令行操作

1)查看操作消费者命令参数

[atguigu@hadoop102 kafka]$ bin/kafka-console-consumer.sh
参数描述
--bootstrap-server <String: server toconnect to>连接的 Kafka Broker 主机名称和端口号
--topic <String: topic>操作的 topic 名称
--from-beginning从头开始消费
--group <String: consumer group id>指定消费者组名称

2)消费消息

消费 first 主题中的数据

[atguigu@hadoop102 kafka]$ bin/kafka-console-consumer.sh --
bootstrap-server hadoop102:9092 --topic first

把主题中所有的数据都读取出来(包括历史数据)

[atguigu@hadoop102 kafka]$ bin/kafka-console-consumer.sh --
bootstrap-server hadoop102:9092 --from-beginning --topic first

3. Kafka生产者

3.1 生产者消息发送流程

3.1.1 发送原理

  • 在消息发送的过程中,涉及到了两个线程——main 线程和 Sender 线程。在 main 线程中创建了一个双端队列 RecordAccumulator。main 线程将消息发送给 RecordAccumulator,把这个理解为缓存区,底层就是缓存队列,Sender 线程不断从 RecordAccumulator 中拉取消息发送到 Kafka Broker

  • 整个流程的目的就是将外部数据放到kafka集群中:首先创建main线程,通过main线程调用send方法,对数据进行发送,然后经过拦截器,可以对数据进行加工或者其他操作,用的话就增加功能,不用的话就不需要,然后就是经过序列化器,kafka有自己的序列化器,为什么不用Java的序列化器serverlizational呢,因为这个序列化太重了,有效数据占的很小,无效数据包括一些保证安全传输的内容,本来就是传输大数据,还包括这些无效数据,严重浪费时间,序列化方式最好的就是有效信息占大部分,校验信息只占一小部分,然后到分区器,数据过来具体往哪个分区里进行存储,里面有一定的规则,将数据发送对应的分区,底层是缓存队列,一个分区会创建一个队列,方便数据的管理,这部分都是在内存中完成的,内存默认值是32M,每个批次(我理解就是队列中一次存储的内容)默认16k,然后通过sender线程将缓冲中的数据发往急群中,只有数据累计到batch.size之后,sender才会发送数据,假设这个批次没有达到16k的话,是不会发送的,那存在这个问题,那这个批次里面数据一直不满,你也不能一直不发吧,这时候就会设定一个等待时间,到了这个时间之后就会发送数据,默认是0ms的话,就是过来一条发一条,最求的时效性,这时候就要看上述这两个参数调整了,你如果设置0ms,每批次默认参数设置就没有意义了,一次发一条效率比较低,就是你每秒钟的传送的数据量比较小

  • 里面通过请求来拉数据到broker,如果第一个数据发送过去没有应答的话,还是允许拉去第二条数据,最多可以拉去5个数据,就是5个请求都没有收到对方的应答,到第六个请求的时候,就不会再允许发送了,直到第一个请求有应答,才可以发送后面的请求

  • 这个selector主要是传输数据的通道,将底层链路进行打通,类似于输入流和输出流,将数据发送到broker里的分区,kafka有一个机制就是如果收到数据,会进行应答,有三种方式,如果应答的结果是成功,会把这个请求清掉,同时会清理掉每一个分区的数据,如果失败了的话,会进行重试,重试次数默认是int最大值,说白了就是死磕,直到成功为止

3.1.2 生产者重要参数列表

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

3.2 异步发送 API

3.2.1 普通异步发送

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

异步发送的目的是将外部的数据发送到队列里面,异步的话就是往队列里面发送数据,不管kafka集群中有没有收到消息,同步的话,就是一波数据都发送完了,才会发送下一波数据

2)代码编写

(1)创建工程 kafka

(2)导入依赖

<dependencies>
     <dependency>
     <groupId>org.apache.kafka</groupId>
     <artifactId>kafka-clients</artifactId>
     <version>3.0.0</version>
     </dependency>
</dependencies>

(3)创建包名:com.atguigu.kafka.producer

(4)编写不带回调函数的 API 代码

public class CustomProducer {
    public static void main(String[] args) throws InterruptedException {
        // 1. 创建 kafka 生产者的配置对象
        Properties properties = new Properties();
        // 2. 给 kafka 配置对象添加配置信息:bootstrap.servers,他这里面好像通过zookeeper设置能找对应的节点
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
                "hadoop102:9092");

        // key,value 序列化(必须):key.serializer,value.serializer
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.StringSerializer");

//        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());跟上面一个效果

        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.StringSerializer");
        // 3. 创建 kafka 生产者对象,key-message,key是string类型""-
        KafkaProducer<String, String> kafkaProducer = new
                KafkaProducer<String, String>(properties);
        // 4. 调用 send 方法,发送消息
        for (int i = 0; i < 5; i++) {
            /**
             * 1.发送到哪个主题
             * 2.发送到哪个分区
             * 3.key
             * 4.value
             * 这里面就是发送到哪个topic以及信息 
             */
            kafkaProducer.send(new
                    ProducerRecord<>("first", "atguigu " + i));
        }
        // 5. 关闭资源
        kafkaProducer.close();
    }
}

测试:

①在 hadoop102 上开启 Kafka 消费者

[atguigu@hadoop103 kafka]$ bin/kafka-console-consumer.sh --
bootstrap-server hadoop102:9092 --topic first

②在 IDEA 中执行代码,观察 hadoop102 控制台中是否接收到消息

[atguigu@hadoop102 kafka]$ bin/kafka-console-consumer.sh --
bootstrap-server hadoop102:9092 --topic first
atguigu 0
atguigu 1
atguigu 2
atguigu 3
atguigu 4

3.2.2 带回调函数的异步发送

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

这个回调返回的信息是到队列之后,在告诉生产者你在哪个队列,在哪个分区

public class CustomProducerCallBack {
    public static void main(String[] args) throws InterruptedException {
        // 1. 创建 kafka 生产者的配置对象
        Properties properties = new Properties();
        // 2. 给 kafka 配置对象添加配置信息:bootstrap.servers,他这里面好像通过zookeeper设置能找对应的节点
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");

        // key,value 序列化(必须):key.serializer,value.serializer
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.StringSerializer");
        // 3. 创建 kafka 生产者对象,key-message,key是string类型""-
        KafkaProducer<String, String> kafkaProducer = new
                KafkaProducer<String, String>(properties);
        // 4. 调用 send 方法,发送消息
        for (int i = 0; i < 5; i++) {
            kafkaProducer.send(new ProducerRecord<>("first", "atguigu " + i, new Callback() {
                @Override
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    if (e == null){
                        System.out.println("主题:" + recordMetadata.topic() + " 分区:" + recordMetadata.partition());
                    }
                }
            }));
        }
        // 5. 关闭资源
        kafkaProducer.close();
    }
}

测试:

①在 hadoop102 上开启 Kafka 消费者

[atguigu@hadoop103 kafka]$ bin/kafka-console-consumer.sh --
bootstrap-server hadoop102:9092 --topic first

②在 IDEA 中执行代码,观察 hadoop102 控制台中是否接收到消息

[atguigu@hadoop102 kafka]$ bin/kafka-console-consumer.sh --
bootstrap-server hadoop102:9092 --topic first
atguigu 0
atguigu 1
atguigu 2
atguigu 3
atguigu 4

③在 IDEA 控制台观察回调信息

主题:first->分区:0
主题:first->分区:0
主题:first->分区:1
主题:first->分区:1
主题:first->分区:1

3.3 同步API

上面那个是异步的,下面这个是同步的

public class CustomProducerCallBackSync {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // 1. 创建 kafka 生产者的配置对象
        Properties properties = new Properties();
        // 2. 给 kafka 配置对象添加配置信息:bootstrap.servers,他这里面好像通过zookeeper设置能找对应的节点
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");

        // key,value 序列化(必须):key.serializer,value.serializer
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.StringSerializer");
        // 3. 创建 kafka 生产者对象,key-message,key是string类型""-
        KafkaProducer<String, String> kafkaProducer = new
                KafkaProducer<String, String>(properties);
        // 4. 调用 send 方法,发送消息
        for (int i = 0; i < 5; i++) {
            // 加上get,同步发送
            kafkaProducer.send(new ProducerRecord<>("first", ("atguigu " + i))).get();
        }
        // 5. 关闭资源
        kafkaProducer.close();
    }
}

3.4 生产者分区

3.4.1 分区好处

这个分区就是前面说的那个partitioner分区器的分区

现在有100T数据,放到一台服务器储存不大可能,就考虑到分成一块一块的,引入分区,这里将100t数据分成三块,放到3台broker上,3台服务器,每台33t左右,这里面负载均衡的意思是,可以自定义存储大小,根据每台服务器的存储资源来进行分配,分区能够提高生产者往broker发送数据的并行度,提高传输效率,而且消费者也是以分区为单位进行消费数据,也会提高处理速度

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

1)默认的分区器DefaultPartitioner

在 IDEA 中 ctrl +n,全局查找 DefaultPartitioner

如果你指定了分区,就按照你指定的分区使用,如果你没有设置分区,那就按照你指定的key的hash选择分区,如果你没有设置分区也没有指定key,选择黏性分区,当数据满了会进行下一个分区

上面方法中指定了分区,就是指定了数据发往哪个分区,有的时候没有设置分区,设置的是key,进行相应的操作,那个黏性分区的特点就是尽可能使用这个分区,一个是批次满了或者时间到了就会换分区

2)应用场景

就是某一张表的数据,假设订单表的数据要求发往一个分区当中,这个时候你把这个key值设成表名

3.4.3 自定义分区器

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

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

2)实现步骤 (1)定义类实现 Partitioner 接口

(2)重写 partition()方法。

package com.atguigu.kafka.producer;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import java.util.Map;
/**
* 1. 实现接口 Partitioner
* 2. 实现 3 个方法:partition,close,configure
* 3. 编写 partition 方法,返回分区号
*/
public class MyPartitioner implements Partitioner {
 /**
 * 返回信息对应的分区
 * @param topic 主题
 * @param key 消息的 key
 * @param keyBytes 消息的 key 序列化后的字节数组
 * @param value 消息的 value
 * @param valueBytes 消息的 value 序列化后的字节数组
 * @param cluster 集群元数据可以查看分区信息
 * @return
 */
 @Override
 public int partition(String topic, Object key, byte[] 
	keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
     // 获取消息
     String msgValue = value.toString();
     // 创建 partition
     int partition;
     // 判断消息是否包含 atguigu
     if (msgValue.contains("atguigu")){
     	partition = 0;
     }else {
     	partition = 1;
     }
     // 返回分区号
     return partition;
     }
 // 关闭资源
@Override
 public void close() {
}
 // 配置方法
  @Override
    public void configure(Map<String, ?> configs) {
  } 
}

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

package com.atguigu.kafka.producer;
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
public class CustomProducerCallbackPartitions {
    public static void main(String[] args) throws 
        InterruptedException {
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop102
        :9092");
         properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, 
        StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, 
        StringSerializer.class.getName());
         // 添加自定义分区器********************************* 这样就产生了关联
        properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.atgui
        gu.kafka.producer.MyPartitioner");
         KafkaProducer<String, String> kafkaProducer = new 
        KafkaProducer<>(properties);
         for (int i = 0; i < 5; i++) {
         kafkaProducer.send(new ProducerRecord<>("first", 
        "atguigu " + i), new Callback() {
             @Override
             public void onCompletion(RecordMetadata metadata, 
            Exception e) {
                 if (e == null){
                     System.out.println(" 主题: " + 
                    metadata.topic() + "->" + "分区:" + metadata.partition()
                 );
                 }else {
                    e.printStackTrace();
                 }
             }
         });
         }
         kafkaProducer.close();
     } 
 }

(4)测试

①在 hadoop102 上开启 Kafka 消费者

[atguigu@hadoop103 kafka]$ bin/kafka-console-consumer.sh --
bootstrap-server hadoop102:9092 --topic first

②在 IDEA 控制台观察回调信息

主题:first->分区:0
主题:first->分区:0
主题:first->分区:0
主题:first->分区:0
主题:first->分区:0

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

图上仓库相当于RecordAccumulator,上面这三个参数配合使用,提高效率

public class CustomProducerParameters {
    public static void main(String[] args) {
        // 配置
        Properties properties = new Properties();
        /**
         * 1.连接kafka集群
         * 2.key和value序列化
         * 3.缓冲区大小,默认32M:buffer.memory
         * 4.批次大小,默认16k
         * 5.linger.ms,等待时间默认0ms
         * 6.压缩,默认none,可配置值gzip、snappy、lz4和zstd
         */
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
        properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
        properties.put(ProducerConfig.LINGER_MS_CONFIG, 1);
        // 这里面选压缩类型,
        properties.put(ProducerConfig.COMPRESSION_TYPE_CONFIG,"snappy")
        // 创建生产者
        KafkaProducer<Object, Object> kafkaProducer = new KafkaProducer<>(properties);
        // 发送数据
        for (int i = 0; i < 5; i++) {
            kafkaProducer.send(new ProducerRecord<>("first", "atguigu" + i));
        }
        // 关闭资源
        kafkaProducer.close();
    }
}

测试:

①在 hadoop102 上开启 Kafka 消费者

[atguigu@hadoop103 kafka]$ bin/kafka-console-consumer.sh --
bootstrap-server hadoop102:9092 --topic first

②在 IDEA 中执行代码,观察 hadoop102 控制台中是否接收到消息

[atguigu@hadoop102 kafka]$ bin/kafka-console-consumer.sh --
bootstrap-server hadoop102:9092 --topic first
atguigu 0
atguigu 1
atguigu 2
atguigu 3
atguigu 4

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

0)回顾发送流程

  • 应答级别分别为0,1,-1,0,只要发了就会进行应答,发下一波数据,不可靠,生产环境下一般没有人使用

  • ack=1,只要leader收到,并进行落盘,就进行应答,就可以发下一波数据

  • ack=-1/all,leader收到,并且所有的follower也都同步完毕,进行应答,保证了数据的可靠性,牺牲了一部分效率

1)ack 应答原理

  • 看图,0的时候你数据发送出来之后,到leader,数据还在内存当中,没有存到磁盘中,也没有同步到follower,此时leader挂掉之后,leader数据全部丢失,因为你已经应答了,人家以为你落盘了

  • 1的时候,leader收到数据后,落盘,就是落磁盘之后进行应答,即使follower没有同步完成,当应答完成之后,leader挂掉,会从其他follower中选一个当leader,但是呢,你其他的follower对数据没有同步,导致这个数据可能就丢失了,相比于0的时候丢的要少一些

  • -1的时候,leader和follower数据同步完成之后,进行应答,假设其中一个follower挂掉之后,无法进行同步该怎么操作,两种情况一个是发送请求,一个是同步数据

如果分区副本设置为1个,说明只有leader,或者ISR里应答的最小副本数量设置为1,也说明只剩一个leader,这样的话就跟ack=1是相同的情况了,分区副本大于等于2的意思是除了有一个leader之外,至少还有一个分区副本,isr里面最小副本大于等于2和前面这句话同一个意思,不能光只有leader副本

ack=-1的时候,会发生数据的重复,当生产者发送数据,leader收到,follower也完成同步,马上要应答leader挂了,这个时候就会挑一个follower成为新的leader,因为没有应答成功,生产者会再发一次数据,磁盘中就会有同样的两份数据,这种概率非常低,但是不能排除没有重复数据,以后解决

public class CustomProducerAck {
    public static void main(String[] args) throws InterruptedException {
        // 1. 创建 kafka 生产者的配置对象
        Properties properties = new Properties();
        // 2. 给 kafka 配置对象添加配置信息:bootstrap.servers
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
        // key,value 序列化(必须):key.serializer,value.serializer
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        // 设置 acks
        properties.put(ProducerConfig.ACKS_CONFIG, "all");
        // 重试次数 retries,默认是 int 最大值,2147483647
        properties.put(ProducerConfig.RETRIES_CONFIG, 3);
        // 3. 创建 kafka 生产者对象
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties);
        // 4. 调用 send 方法,发送消息
        for (int i = 0; i < 5; i++) {
            kafkaProducer.send(new ProducerRecord<>("first", "atguigu " + i));
        }
        // 5. 关闭资源
        kafkaProducer.close();
    }
}

3.7 生产经验——数据去重

3.7.1 数据传递语义

3.7.2 幂等性

1)幂等性原理

他这里面单会话的意思是kafka启动的每一次,每启动一次这个PID就会变一次

producer1,pid为1000,kafka集群有两个broker,一个topic,两个分区,生产者发送数据,数据位atguigu,pid为1000,sequence为0,两个分区都到了数据,因为分区不同都是可以收到数据的,相同的数据就不会在磁盘中落盘

2)如何使用幂等性

开启参数 enable.idempotence 默认为 true,false 关闭

3.7.3 生产者事务

为了解决服务器挂掉之后,重启有可能出现重复数据

1)Kafka 事务原理

事务底层依赖幂等性

  • 事务协调器是用来处理事务的,存储事务信息的特殊主题,事务提交的过程中需要持久化到硬盘上一些信息的,每一个broker节点都有事务协调器,根据transactional.id来判断由哪个分区的事务协调器处理事务,这个transactional.id是持久化主题里头,就是硬盘当中,服务器挂掉之后,重启producerId会变更,但是transational.id不变

  • 生产者向事务协调器请求producer id,就是给幂等性用的,直接返回pid,给完pid之后就开始给leader发送数据,发送完数据要提交请求,这个请求的消息会持久化到特殊主题当中,就是记录一下,方便后续的回滚,持久化完毕之后会告诉生产者,成功之后事务协调器会给节点发送commit请求,为了判断数据有没有真正的持久化完毕,如果所有数据都处理完了,正常返回,这样事务就结束了,最后持久化事务成功的信息,前面那个commit感觉是事务的记录,保存在特殊主题中

2)Kafka 的事务一共有如下 5 个 API

// 1 初始化事务
void initTransactions();
// 2 开启事务
void beginTransaction() throws ProducerFencedException;
// 3 在事务内提交已经消费的偏移量(主要用于消费者)
void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets,
 String consumerGroupId) throws 
ProducerFencedException;
// 4 提交事务
void commitTransaction() throws ProducerFencedException;
// 5 放弃事务(类似于回滚事务的操作)
void abortTransaction() throws ProducerFencedException;

3)单个 Producer,使用事务保证消息的仅一次发送

public class CustomProducerTransactions {
    public static void main(String[] args) throws InterruptedException {
        // 1. 创建 kafka 生产者的配置对象
        Properties properties = new Properties();
        // 2. 给 kafka 配置对象添加配置信息
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
        // key,value 序列化
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        // 设置事务 id(必须),事务 id 任意起名
        properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG,
                "transaction_id_0");
        // 3. 创建 kafka 生产者对象
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties);
        // 初始化事务
        kafkaProducer.initTransactions();
        // 开启事务
        kafkaProducer.beginTransaction();
        try {
            // 4. 调用 send 方法,发送消息
            for (int i = 0; i < 5; i++) {
                // 发送消息
                kafkaProducer.send(new ProducerRecord<>("first", "atguigu " + i));
            }
        // int i = 1 / 0;
            // 提交事务
            kafkaProducer.commitTransaction();
        } catch (Exception e) {
            // 终止事务
            kafkaProducer.abortTransaction();
        } finally {
            // 5. 关闭资源
            kafkaProducer.close();
        }
    }
}

3.8 生产经验——数据有序

消费者消费三个分区的数据保证不了有序,如果需要保证顺序的话,单分区内有序,多分区的排序的话,就需要把所有的数据拉到一起,进行重排序,这样就需要等到所有的数据都收齐,然后进行排序,这样效率比较低,还不如创建一个分区保证顺序

3.9 生产经验——数据乱序

就是在1.x版本之前,需要通过设置只能保存一个请求,来保证数据消费的顺序性,相当于来一个我应答一个消费一个,没有应答我不发下一个数据,在1.x版本之后没有开启幂等性的话也需要设置为1,如果开启幂等性的话,这个值就不能设置为1了,需要设置为5,因为只能保证5个请求是有序的,当其中有一个请求有问题,后面的请求在进来的话,会发现消息id不连续,后在后面等着,等到前面的消息应答之后在进行操作

4. Kafka Broker

4.1 Kafka Broker 工作流程

4.1.1 Zookeeper 存储的 Kafka 信息

(1)启动 Zookeeper 客户端

[atguigu@hadoop102 zookeeper-3.5.7]$ bin/zkCli.sh

(2)通过 ls 命令可以查看 kafka 相关信息

[zk: localhost:2181(CONNECTED) 2] ls /kafka

4.1.2 Kafka Broker 总体工作流程

每台kafka节点启动之后,都会向zookeeper进行注册,3台broker增加之后会相应增加节点,下面开始选择controller节点,broker节点上每一个都一个controller,那么谁会成为leader选举的老大,争先抢占节点,谁先抢到谁就负责后面leader选举的,假设第一个抢到,那他就是controllerleader,他开始监听brokers/ids这个节点,这个节点下有任何broker变化,这边都能监听的到,监听之后就开始真正的选举,按照选举规则,假设选举broker1为leader,把这个事情告诉zookeeper就是第五步,进行备份,其他controller节点会从这里面拉去对应的数据,主要就是防止controllerleader挂了,随时准备上位,上位的时候获取一定的信息,然后生产者往集群里面发送数据,follower主动跟leader进行同步数据,底层是segment,1g1g进行存储,怎么去查询数据呢,主要通过index索引的方式进行检索,log文件就是一个seg,然后进行一个应答,如果leader挂了,controller就监控到节点的变化,然后去拉state里面的信息,进行重新的选举,选举原则照旧,新的leader诞生为0,这就是大体流程

1)模拟 Kafka上下线,Zookeeper中数据变化

(1)查看/kafka/brokers/ids 路径上的节点

[zk: localhost:2181(CONNECTED) 2] ls /kafka/brokers/ids
[0, 1, 2]

(2)查看/kafka/controller 路径上的数据

[zk: localhost:2181(CONNECTED) 15] get /kafka/controller
{"version":1,"brokerid":0,"timestamp":"1637292471777"}

(3)查看/kafka/brokers/topics/first/partitions/0/state 路径上的数据

[zk: localhost:2181(CONNECTED) 16] get 
/kafka/brokers/topics/first/partitions/0/state
{"controller_epoch":24,"leader":0,"version":1,"leader_epoch":18,"
isr":[0,1,2]}

(4)停止 hadoop104 上的 kafka

[atguigu@hadoop104 kafka]$ bin/kafka-server-stop.sh

(5)再次查看/kafka/brokers/ids 路径上的节点

[zk: localhost:2181(CONNECTED) 3] ls /kafka/brokers/ids
[0, 1]

(6)再次查看/kafka/controller 路径上的数据

[zk: localhost:2181(CONNECTED) 15] get /kafka/controller
{"version":1,"brokerid":0,"timestamp":"1637292471777"}

(7)再次查看/kafka/brokers/topics/first/partitions/0/state 路径上的数据

[zk: localhost:2181(CONNECTED) 16] get 
/kafka/brokers/topics/first/partitions/0/state
{"controller_epoch":24,"leader":0,"version":1,"leader_epoch":18,"
isr":[0,1]}

(8)启动 hadoop104 上的 kafka

[atguigu@hadoop104 kafka]$ bin/kafka-server-start.sh -
daemon ./config/server.properties

(9)再次观察(1)、(2)、(3)步骤中的内容

4.1.3 Broker 重要参数

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

4.2 生产经验——节点服役和退役

4.2.1 服役新节点

1)新节点准备

(1)关闭 hadoop104,并右键执行克隆操作。

(2)开启 hadoop105,并修改 IP 地址。

[root@hadoop104 ~]# vim /etc/sysconfig/network-scripts/ifcfgens33
DEVICE=ens33
TYPE=Ethernet
ONBOOT=yes
BOOTPROTO=static
NAME="ens33"
IPADDR=192.168.10.105
PREFIX=24
GATEWAY=192.168.10.2
DNS1=192.168.10.2

4.3 Kafka 副本

4.3.1 副本基本信息

(1)Kafka 副本作用:提高数据可靠性

(2)Kafka 默认副本 1 个,生产环境一般配置为 2 个,保证数据可靠性;太多副本会增加磁盘存储空间,增加网络上数据传输,降低效率,每次增加一个副本,都需要将数据传送过去,这样就是涉及到大量的网络通讯,降低数据传输效率

(3)Kafka 中副本分为:Leader 和 Follower。Kafka 生产者只会把数据发往 Leader,然后 Follower 找 Leader 进行同步数据

(4)Kafka 分区中的所有副本统称为 AR(Assigned Repllicas)

  • AR = ISR + OSR

  • ISR,表示和 Leader 保持同步的 Follower 集合。如果 Follower 长时间未向 Leader 发送通信请求或同步数据,则该 Follower 将被踢出 ISR。该时间阈值由 replica.lag.time.max.ms参数设定,默认 30s。Leader 发生故障之后,就会从 ISR 中选举新的 Leader

  • OSR,表示 Follower 与 Leader 副本同步时,延迟过多的副本

4.3.2 Leader 选举流程

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

Controller 的信息同步工作是依赖于 Zookeeper 的

(1)创建一个新的 topic,4 个分区,4 个副本

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server 
hadoop102:9092 --create --topic atguigu1 --partitions 4 --replication-factor 4 Created topic atguigu1.

(2)查看 Leader 分布情况

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

(3)停止掉 hadoop105 的 kafka 进程,并查看 Leader 分区情况

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

(4)停止掉 hadoop104 的 kafka 进程,并查看 Leader 分区情况

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

(5)启动 hadoop105 的 kafka 进程,并查看 Leader 分区情况

[atguigu@hadoop105 kafka]$ bin/kafka-server-start.sh -daemon config/server.properties
[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe 
--topic atguigu1
Topic: atguigu1 TopicId: awpgX_7WR-OX3Vl6HE8sVg PartitionCount: 4 ReplicationFactor: 4
Configs: segment.bytes=1073741824
Topic: atguigu1 Partition: 0 Leader: 0 Replicas: 3,0,2,1 Isr: 0,1,3
Topic: atguigu1 Partition: 1 Leader: 1 Replicas: 1,2,3,0 Isr: 1,0,3
Topic: atguigu1 Partition: 2 Leader: 0 Replicas: 0,3,1,2 Isr: 0,1,3
Topic: atguigu1 Partition: 3 Leader: 1 Replicas: 2,1,0,3 Isr: 1,0,3

(6)启动 hadoop104 的 kafka 进程,并查看 Leader 分区情况

[atguigu@hadoop104 kafka]$ bin/kafka-server-start.sh -daemon config/server.properties
[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe 
--topic atguigu1
Topic: atguigu1 TopicId: awpgX_7WR-OX3Vl6HE8sVg PartitionCount: 4 ReplicationFactor: 4
Configs: segment.bytes=1073741824
Topic: atguigu1 Partition: 0 Leader: 0 Replicas: 3,0,2,1 Isr: 0,1,3,2
Topic: atguigu1 Partition: 1 Leader: 1 Replicas: 1,2,3,0 Isr: 1,0,3,2
Topic: atguigu1 Partition: 2 Leader: 0 Replicas: 0,3,1,2 Isr: 0,1,3,2
Topic: atguigu1 Partition: 3 Leader: 1 Replicas: 2,1,0,3 Isr: 1,0,3,2

(7)停止掉 hadoop103 的 kafka 进程,并查看 Leader 分区情况

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

4.3.3 Leader 和 Follower 故障处理细节

leader是跟生产者和消费者直接交互的,所有follower都是从leader拉取数据进行同步,就有时间先后的顺序,leader是先收到数据,就会多一些,follower比较少一些,isr表示0 1 2三台服务器正常启动 ,leo就是每个副本最后一个offset,还有高水位线,这里面能看到LEO是8,高水位线是5,消费者能够看到的是4

假设broker2挂了,第1件事就是从isr队列中踢出去,broker0和broker1还能正常工作,只要正常接收数据,对应的LEO和HW就往后走,假设LEO走到10,HW走到8,这个时候如果broker2重启,那会如何工作呢,将log文件高于HW的部分截取掉,是broker2这个副本的HW,高于部分截取掉,他认为这部分没有用,然后开始向后进行同步,假设broker2达到了broker1的位置

假设leader挂了,不管谁挂了都会从isr队列里面剔除,选举新的leader,假设1上位,所有人向1看齐,broker2明显消息比1多,为保证数据的一致性,高于broker1的部分会截取掉,就是包括5后面的全部截取掉,这个只能保证数据的一致性,保证不了数据丢失,不重复,比如说,原来leader已经处理到567,但是同步的时候567就不存在了,就丢失了

4.3.4 分区副本分配

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

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

(1)创建一个新的 topic,名称为 second

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server 
hadoop102:9092 --create --partitions 16 --replication-factor 3 --
topic second

(2)查看分区和副本情况

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server 
hadoop102:9092 --describe --topic second
Topic: second4 Partition: 0 Leader: 0 Replicas: 0,1,2 Isr: 0,1,2
Topic: second4 Partition: 1 Leader: 1 Replicas: 1,2,3 Isr: 1,2,3
Topic: second4 Partition: 2 Leader: 2 Replicas: 2,3,0 Isr: 2,3,0
Topic: second4 Partition: 3 Leader: 3 Replicas: 3,0,1 Isr: 3,0,1
Topic: second4 Partition: 4 Leader: 0 Replicas: 0,2,3 Isr: 0,2,3
Topic: second4 Partition: 5 Leader: 1 Replicas: 1,3,0 Isr: 1,3,0
Topic: second4 Partition: 6 Leader: 2 Replicas: 2,0,1 Isr: 2,0,1
Topic: second4 Partition: 7 Leader: 3 Replicas: 3,1,2 Isr: 3,1,2
Topic: second4 Partition: 8 Leader: 0 Replicas: 0,3,1 Isr: 0,3,1
Topic: second4 Partition: 9 Leader: 1 Replicas: 1,0,2 Isr: 1,0,2
Topic: second4 Partition: 10 Leader: 2 Replicas: 2,1,3 Isr: 2,1,3
Topic: second4 Partition: 11 Leader: 3 Replicas: 3,2,0 Isr: 3,2,0
Topic: second4 Partition: 12 Leader: 0 Replicas: 0,1,2 Isr: 0,1,2
Topic: second4 Partition: 13 Leader: 1 Replicas: 1,2,3 Isr: 1,2,3
Topic: second4 Partition: 14 Leader: 2 Replicas: 2,3,0 Isr: 2,3,0
Topic: second4 Partition: 15 Leader: 3 Replicas: 3,0,1 Isr: 3,0,1

尽可能保证数据分布在每个服务器上,尽可能保证每台服务器都做leader,同一台服务器做leader,其他服务器做follower也会轮流安排,同时保证数据的可靠性

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

下面这个例子,因为broker0和broker1的性能较好,会让这两台服务器多存储数据,另外两台尽量少存,场景就是有可能性能较差的是很早之前买的服务器,新买的服务器性能比较好

手动调整分区副本存储的步骤如下

(1)创建一个新的 topic,名称为 three

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server 
hadoop102:9092 --create --partitions 4 --replication-factor 2 --
topic three

(2)查看分区副本存储情况

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server 
hadoop102:9092 --describe --topic three

现在都想存储在102和103上

(3)创建副本存储计划(所有副本都指定存储在 broker0、broker1 中)

[atguigu@hadoop102 kafka]$ vim increase-replication-factor.json

输入如下内容:

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

(4)执行副本存储计划

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

(5)验证副本存储计划

[atguigu@hadoop102 kafka]$ bin/kafka-reassign-partitions.sh --
bootstrap-server hadoop102:9092 --reassignment-json-file 
increase-replication-factor.json --verify

(6)查看分区副本存储情况

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server 
hadoop102:9092 --describe --topic three

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

上面这个例子,因为sr队列第一个不是leader,说明这个集群服务器有宕机过,一般情况下,不开启,像上面这个不需要再平衡就已经均匀分布了,或者把这个10%往高调一点,因为在平衡操作比较浪费性能

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

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

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

1)创建 topic

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

命令行的方式增加不了

2)手动增加副本存储

(1)创建副本存储计划(所有副本都指定存储在 broker0、broker1、broker2 中)

[atguigu@hadoop102 kafka]$ vim increase-replication-factor.json

输入如下内容:

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

(2)执行副本存储计划

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

4.4 文件存储

4.4.1 文件存储机制

1)Topic 数据的存储机制

  • 底层存储形式.log文件保存的数据,.index是索引方便查找数据,.timeindex是时间超过7天删除数据,这个就是判断日志保存了多久

  • 看一下索引的名字,第一个文件索引是0-4095,依次类推

  • 梳理一下几个问题,生产的数据是不断追加的,不会对历史数据进行修改,相对速度会高一些,高效读写重要原因之一

2)思考:Topic 数据到底存储在什么位置

(1)启动生产者,并发送消息

[atguigu@hadoop102 kafka]$ bin/kafka-console-producer.sh --
bootstrap-server hadoop102:9092 --topic first
>hello world

(2)查看 hadoop102(或者 hadoop103、hadoop104)的/opt/module/kafka/datas/first-1 (first-0、first-2)路径上的文件

[atguigu@hadoop104 first-1]$ ls
00000000000000000092.index
00000000000000000092.log
00000000000000000092.snapshot
00000000000000000092.timeindex
leader-epoch-checkpoint
partition.metadata

(3)直接查看 log 日志,发现是乱码,序列化之后的文件

[atguigu@hadoop104 first-1]$ cat 00000000000000000092.log 
\CYnF|©|©ÿÿÿÿÿÿÿÿÿÿÿÿÿÿ"hello world

(4)通过工具查看 index 和 log 信息

[atguigu@hadoop104 first-1]$ kafka-run-class.sh kafka.tools.DumpLogSegments 
--files ./00000000000000000000.index 
Dumping ./00000000000000000000.index
offset: 3 position: 152
[atguigu@hadoop104 first-1]$ kafka-run-class.sh kafka.tools.DumpLogSegments 
--files ./00000000000000000000.log
Dumping datas/first-0/00000000000000000000.log
Starting offset: 0
baseOffset: 0 lastOffset: 1 count: 2 baseSequence: -1 lastSequence: -1 producerId: -1 
producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position: 
0 CreateTime: 1636338440962 size: 75 magic: 2 compresscodec: none crc: 2745337109 isvalid: 
true
baseOffset: 2 lastOffset: 2 count: 1 baseSequence: -1 lastSequence: -1 producerId: -1 
producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position: 
75 CreateTime: 1636351749089 size: 77 magic: 2 compresscodec: none crc: 273943004 isvalid: 
true
baseOffset: 3 lastOffset: 3 count: 1 baseSequence: -1 lastSequence: -1 producerId: -1 
producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position: 
152 CreateTime: 1636351749119 size: 77 magic: 2 compresscodec: none crc: 106207379 isvalid: 
true
baseOffset: 4 lastOffset: 8 count: 5 baseSequence: -1 lastSequence: -1 producerId: -1 
producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position: 
229 CreateTime: 1636353061435 size: 141 magic: 2 compresscodec: none crc: 157376877 isvalid: 
true
baseOffset: 9 lastOffset: 13 count: 5 baseSequence: -1 lastSequence: -1 producerId: -1 
producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position: 
370 CreateTime: 1636353204051 size: 146 magic: 2 compresscodec: none crc: 4058582827 isvalid: 
true

每个segment大小都是1个g,刚刚我们看了index文件里面就一个数据好像,举一个例子,找600的文件,因为大于522,小于1005,所以定位到segment-1,相对offset都是相对522比较,如果保存绝对的话值会增加很大,占用空间也会比较大

说明:日志存储参数配置

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

4.4.2 文件清理策略

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

  • log.retention.hours,最低优先级小时,默认 168小时,就是7天。

  • log.retention.minutes,分钟。 如果都设置了,越往下设置优先级越高

  • log.retention.ms,最高优先级毫秒。

  • log.retention.check.interval.ms,负责设置检查周期,默认 5 分钟

那么日志一旦超过了设置的时间,怎么处理呢?

Kafka 中提供的日志清理策略有 delete 和 compact 两种

1)delete 日志删除:将过期数据删除,默认策略

  • log.cleanup.policy = delete 所有数据启用删除策略

(1)基于时间:默认打开。以 segment 中所有记录中的最大时间戳作为该文件时间戳。

(2)基于大小:默认关闭。超过设置的所有日志总大小,删除最早的 segment,log.retention.bytes,默认等于-1,表示无穷大,表示关闭掉了,因为有的磁盘比较小,如果动不动就满了,有可能就把比较重要的数据就删了

思考:如果一个 segment 中有一部分数据过期,一部分没有过期,怎么处理?这种应该不用考虑,定义的就是每个segment最后一条数据时间过期就删

2)compact 日志压缩

压缩这种用的比较少,用在比较特殊的场景,好比存储的数据时用户信息,key是用户id,然后就是每个用户信息随着时间都在变化,只保留最新的数据就可以了

4.5 高效读写数据

注意是高效读和写,都要说清楚

1)Kafka本身是分布式集群,多个服务器干一件事肯定效率要高,还采用了分区技术,可以将海量数据分割为一块一块,存储在各个partition里面,解决了存储问题,同时生产和消费的并行度得到了提高

2)读数据采用稀疏索引,可以快速定位要消费的数据,底层以segment的形式存储数据,默认是一个g,创建了index索引方便定位,这块稀疏索引也要了解一下

3)顺序写磁盘,数据的增加是通过追加的方式进行的

  • Kafka 的 producer 生产数据,要写入到 log 文件中,写的过程是一直追加到文件末端,为顺序写。官网有数据表明,同样的磁盘,顺序写能到 600M/s,而随机写只有 100K/s。这与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间

  • 随机写磁盘需要多次寻找磁盘地址,这个过程需要频繁的切换磁道,所以过程比较慢。

  • 顺序写磁盘就是在一个磁道连续的写入,数据都排在一起,分布在连续的磁盘扇区,主需要一次寻址就能找到对应的数据,而kafka本身的数据是不需要删除数据的,是已追加的方式写到磁盘,所以这样就能保证磁盘数据连续紧凑,同时kafka是以segment log flie进行分段存储的,每次访问磁盘文件的时候只需要寻址最后一个segment file的磁盘空间,所以也能够保证写入和读取的效率

4)页缓存 + 零拷贝技术

消费数据的时候,通过代码需要将数据通过网络通信,夸节点通讯,需要走这个网卡

kafka这边直接从页缓存中加载了,直接走网卡没有走应用层,就是kafka没有通过代码进行二次修改操作数据,所有对数据的操作都放在了生产者和消费者上了,生产者方法,首先创建生产者,然后拦截器,序列化,分区器,发送,如果你想做修改的话,可以在拦截器里面进行修改,消费者那边就是一个相反的过程

参数描述
log.flush.interval.messages强制页缓存刷写到磁盘的条数,默认是 long 的最大值,9223372036854775807。一般不建议修改,交给系统自己管理。
log.flush.interval.ms每隔多久,刷数据到磁盘,默认是 null。一般不建议修改,交给系统自己管理。

5. Kafka 消费者

5.1 Kafka 消费方式

5.2 Kafka 消费者工作流程

5.2.1 消费者总体工作流程

新版本offset是维护在系统组件里面,用kafka系统主题帮我们存储offset,是基于硬盘进行存储的,老版本是存储在zookeeper里面,如果还放在zookeeper里面,那么所有消费者需要主动跟zookeeper大量交互,导致网络上数据传输非常频繁,压力比较大

5.2.2 消费者组原理

只要你有一个消费者,就必须有一个参数就是groupId,这么理解,就一个消费者的话也是个消费者组

消费者组选择哪个coordinator来辅助工作呢,消费者组的groupId的hashcode值对50求模,这个groupId就是我们未来写代码手动给的

过程:每个consumer都发起加入组的请求,然后这几个consumer组成一组,然后在这些consumer里面选出一个leader,每个消费者的消费信息都会发给对应的coordinator,然后coordinator的所有信息都会发送给consumer 的leader,这会就明白为啥是服务作用了吧,真正的分配交给了这个leadr消费者,这个消费者制定消费方案,就是哪个消费者消费哪个分区,制定完之后会把这个计划发给coordinator,然后把这个计划在分配给各个消费者,按照这个规则进行消费,每个消费者每过3s都要跟coordinator汇报,我还在,如果在45s内,没有任何消息发送,coordinator就会认为这个消费者挂了,那么其他的消费者就得把他这个任务完成了,还有一种情况,就是消费者处理消息的时间过长,也会认为自动下线,跟上面的一样操作

消费者组如果要开始工作,先要创建网络连接客户端consumerNetworkClient,通过这个跟kafka集群进行交互,消费者

5.2.3 消费者重要参数

5.3 消费者 API

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

1)需求:

创建一个独立消费者,消费 first 主题中数据

注意:在消费者 API 代码中必须配置消费者组 id。命令行启动消费者不填写消费者组id 会被自动填写随机的消费者组 id

2)实现步骤

(1)创建包名:com.atguigu.kafka.consumer

(2)编写代码

public class CustomConsumer {
    public static void main(String[] args) {
        // 1.创建消费者的配置对象
        Properties properties = new Properties();
        // 2.给消费者配置对象添加参数
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,
                "hadoop102:9092");
        // 配置序列化 必须
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
                StringDeserializer.class.getName());

        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
                StringDeserializer.class.getName());
        // 配置消费者组(组名任意起名) 必须要配
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
        // 创建消费者对象
        KafkaConsumer<String, String> kafkaConsumer = new
                KafkaConsumer<String, String>(properties);
        // 注册要消费的主题(可以消费多个主题)
        ArrayList<String> topics = new ArrayList<>();
        topics.add("first");
        kafkaConsumer.subscribe(topics);
        // 拉取数据打印
        while (true) {
            // 设置 1s 中消费一批数据,每隔1s拉取一下数据
            ConsumerRecords<String, String> consumerRecords =
                    kafkaConsumer.poll(Duration.ofSeconds(1));
            // 打印消费到的数据
            for (ConsumerRecord<String, String> consumerRecord :
                    consumerRecords) {
                System.out.println(consumerRecord);
            }
        }
    }
}

消费到的数据

3)测试

(1)在 IDEA 中执行消费者程序。

(2)在 Kafka 集群控制台,创建 Kafka 生产者,并输入数据。

[atguigu@hadoop102 kafka]$ bin/kafka-console-producer.sh --
bootstrap-server hadoop102:9092 --topic first
>hello

(3)在 IDEA 控制台观察接收到的数据

ConsumerRecord(topic = first, partition = 1, leaderEpoch = 3, 
offset = 0, CreateTime = 1629160841112, serialized key size = -1, 
serialized value size = 5, headers = RecordHeaders(headers = [], 
isReadOnly = false), key = null, value = hello)

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

1)需求:创建一个独立消费者,消费 first 主题 0 号分区的数据,他这里面应该把数据只发往某一个分区

2)实现步骤

(1)代码编写

public class CustomConsumerPartition {
    public static void main(String[] args) {
        Properties properties = new Properties();

        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092 ");
        // 配置序列化 必须
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
                StringDeserializer.class.getName());

        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
                StringDeserializer.class.getName());
        // 配置消费者组(必须),名字可以任意起
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
        KafkaConsumer<String, String> kafkaConsumer = new
                KafkaConsumer<>(properties);
        // 消费某个主题的某个分区数据
        ArrayList<TopicPartition> topicPartitions = new
                ArrayList<>();
        topicPartitions.add(new TopicPartition("first", 0));
        kafkaConsumer.assign(topicPartitions);
        while (true) {
            ConsumerRecords<String, String> consumerRecords =
                    kafkaConsumer.poll(Duration.ofSeconds(1));
            for (ConsumerRecord<String, String> consumerRecord :
                    consumerRecords) {
                System.out.println(consumerRecord);
            }
        }
    }
}

生产者代码

3)测试

(1)在 IDEA 中执行消费者程序。

(2)在 IDEA 中执行生产者程序 CustomProducerCallback()在控制台观察生成几个 0 号分区的数据

first 0 381
first 0 382
first 2 168
first 1 165
first 1 166

(3)在 IDEA 控制台,观察接收到的数据,只能消费到 0 号分区数据表示正确

ConsumerRecord(topic = first, partition = 0, leaderEpoch = 14, 
offset = 381, CreateTime = 1636791331386, serialized key size = -
1, serialized value size = 9, headers = RecordHeaders(headers = 
[], isReadOnly = false), key = null, value = atguigu 0)
ConsumerRecord(topic = first, partition = 0, leaderEpoch = 14, 
offset = 382, CreateTime = 1636791331397, serialized key size = -
1, serialized value size = 9, headers = RecordHeaders(headers = 
[], isReadOnly = false), key = null, value = atguigu 1)

5.3.3 消费者组案例

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

2)案例实操

(1)复制一份基础消费者的代码,在 IDEA 中同时启动,即可启动同一个消费者组中的两个消费者,因为你当时放topic的时候,起的groupId都是一样的,就分到一个consumer group里面,启动这三个一样的,就是启动了一个消费组里面的三个消费者

public class CustomConsumer1 {
    public static void main(String[] args) {
        // 1.创建消费者的配置对象
        Properties properties = new Properties();
        // 2.给消费者配置对象添加参数
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,
                "hadoop102:9092");
        // 配置序列化 必须

        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
                StringDeserializer.class.getName());

        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
                StringDeserializer.class.getName());
        // 配置消费者组 必须
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
        // 创建消费者对象
        KafkaConsumer<String, String> kafkaConsumer = new
                KafkaConsumer<String, String>(properties);
        // 注册主题
        ArrayList<String> topics = new ArrayList<>();
        topics.add("first");
        kafkaConsumer.subscribe(topics);
        // 拉取数据打印
        while (true) {
            // 设置 1s 中消费一批数据
            ConsumerRecords<String, String> consumerRecords =
                    kafkaConsumer.poll(Duration.ofSeconds(1));
            // 打印消费到的数据
            for (ConsumerRecord<String, String> consumerRecord :
                    consumerRecords) {
                System.out.println(consumerRecord);
            }
        }
    }
}

生产者代码

后面可以看到日志,每个消费者只消费一个分区的消息

(2)启动代码中的生产者发送消息,在 IDEA 控制台即可看到两个消费者在消费不同分区的数据(如果只发生到一个分区,可以在发送时增加延迟代码 Thread.sleep(2);

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

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

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

5.4.1 Range 以及再平衡

1)Range 分区策略原理

2)Range 分区分配策略案例

(1)修改主题 first 为 7 个分区

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server 
hadoop102:9092 --alter --topic first --partitions 7

注意:分区数可以增加,但是不能减少

(2)复制 CustomConsumer 类,创建 CustomConsumer2。这样可以由三个消费者CustomConsumer、CustomConsumer1、CustomConsumer2 组成消费者组,组名都为“test”,同时启动 3 个消费者

(3)启动 CustomProducer 生产者,发送 500 条消息,随机发送到不同的分区

public class CustomProducer {
    public static void main(String[] args) throws 
    InterruptedException {
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, 
        StringDeserializer.class.getName());
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);
        for (int i = 0; i < 7; i++) {
            kafkaProducer.send(new ProducerRecord<>("first", i, 
            "test", "atguigu"));
        }
        kafkaProducer.close();
    } 
}

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

(4)观看 3 个消费者分别消费哪些分区的数据

3)Range 分区分配再平衡案例

(1)停止掉 0 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)。

  • 1 号消费者:消费到 3、4 号分区数据。

  • 2 号消费者:消费到 5、6 号分区数据。

  • 0 号消费者的任务会整体被分配到 1 号消费者或者 2 号消费者。

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

(2)再次重新发送消息观看结果(45s 以后)。

  • 1 号消费者:消费到 0、1、2、3 号分区数据。

  • 2 号消费者:消费到 4、5、6 号分区数据。

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

5.4.2 RoundRobin 以及再平衡

1)RoundRobin 分区策略原理

针对所有topic而言

2)RoundRobin 分区分配策略案例

(1)依次在 CustomConsumer、CustomConsumer1、CustomConsumer2 三个消费者代码中修改分区分配策略为 RoundRobin

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

(2)重启 3 个消费者,重复发送消息的步骤,观看分区结果

3)RoundRobin 分区分配再平衡案例

(1)停止掉 0 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)。

  • 1 号消费者:消费到 2、5 号分区数据

  • 2 号消费者:消费到 4、1 号分区数据

  • 0 号消费者的任务会按照 RoundRobin 的方式,把数据轮询分成 0 、6 和 3 号分区数据,分别由 1 号消费者或者 2 号消费者消费。

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

(2)再次重新发送消息观看结果(45s 以后)。

  • 1 号消费者:消费到 0、2、4、6 号分区数据

  • 2 号消费者:消费到 1、3、5 号分区数据

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

5.4.3 Sticky 以及再平衡

感觉就是随机均匀分配

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

粘性分区是 Kafka 从 0.11.x 版本开始引入这种分配策略,首先会尽量均衡的放置分区到消费者上面,在出现同一消费者组内消费者出现问题的时候,会尽量保持原有分配的分区不变化

1)需求

设置主题为 first,7 个分区;准备 3 个消费者,采用粘性分区策略,并进行消费,观察消费分配情况。然后再停止其中一个消费者,再次观察消费分配情况。

2)步骤

(1)修改分区分配策略为粘性。

注意:3 个消费者都应该注释掉,之后重启 3 个消费者,如果出现报错,全部停止等会再重启,或者修改为全新的消费者组

// 修改分区分配策略
ArrayList<String> startegys = new ArrayList<>();
startegys.add("org.apache.kafka.clients.consumer.StickyAssignor");
properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, 
startegys);

(2)使用同样的生产者发送 500 条消息。

可以看到会尽量保持分区的个数近似划分分区

3)Sticky 分区分配再平衡案例

(1)停止掉 0 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)。

  • 1 号消费者:消费到 2、5、3 号分区数据。

  • 2 号消费者:消费到 4、6 号分区数据。

  • 0 号消费者的任务会按照粘性规则,尽可能均衡的随机分成 0 和 1 号分区数据,分别由 1 号消费者或者 2 号消费者消费。

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

(2)再次重新发送消息观看结果(45s 以后)。

  • 1 号消费者:消费到 2、3、5 号分区数据。

  • 2 号消费者:消费到 0、1、4、6 号分区数据。

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

5.5 offset 位移

5.5.1 offset 的默认维护位置

__consumer_offsets 主题里面采用 key 和 value 的方式存储数据。key 是 group.id+topic+分区号,value 就是当前 offset 的值。每隔一段时间,kafka 内部会对这个 topic 进行compact,也就是每个 group.id+topic+分区号就保留最新数据

1)消费 offset 案例

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

(1)在配置文件 config/consumer.properties 中添加配置 exclude.internal.topics=false,默认是 true,表示不能看/消费系统主题。为了查看该系统主题数据,所以该参数修改为 false

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

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server 
hadoop102:9092 --create --topic atguigu --partitions 2 --
replication-factor 2

(3)启动生产者往 atguigu 生产数据

[atguigu@hadoop102 kafka]$ bin/kafka-console-producer.sh --topic 
atguigu --bootstrap-server hadoop102:9092

(4)启动消费者消费 atguigu 数据

[atguigu@hadoop104 kafka]$ bin/kafka-console-consumer.sh --
bootstrap-server hadoop102:9092 --topic atguigu --group test

注意:指定消费者组名称,更好观察数据存储位置(key 是 group.id+topic+分区号)

(5)查看消费者消费主题__consumer_offsets

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

5.5.2 自动提交 offset

首先是生产者向对应分区里面发送,消费者主动拉取数据,不间断的拉,offset自动到达5s提交

参数名称描述
enable.auto.commit默认值为 true,消费者会自动周期性地向服务器提交偏移量。
auto.commit.interval.ms如果设置了 enable.auto.commit 的值为 true, 则该值定义了消费者偏移量向 Kafka 提交的频率,默认 5s

1)消费者自动提交 offset

public class CustomConsumerAutoOffset {
    public static void main(String[] args) {
        // 1. 创建 kafka 消费者配置类
        Properties properties = new Properties();
        // 2. 添加配置参数
        // 添加连接
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,
                "hadoop102:9092");

        // 配置序列化 必须
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.StringDeserializer");
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.StringDeserializer");
        // 配置消费者组
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
        // 是否自动提交 offset
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,
                true);
        // 提交 offset 的时间周期 1000ms,默认 5s
        properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,
                1000);
        //3. 创建 kafka 消费者
        KafkaConsumer<String, String> consumer = new
                KafkaConsumer<>(properties);
        //4. 设置消费主题 形参是列表
        consumer.subscribe(Arrays.asList("first"));
        //5. 消费数据
        while (true) {
            // 读取消息
            ConsumerRecords<String, String> consumerRecords =
                    consumer.poll(Duration.ofSeconds(1));
            // 输出消息
            for (ConsumerRecord<String, String> consumerRecord :
                    consumerRecords) {
                System.out.println(consumerRecord.value());
            }
        }
    }
}

5.5.3 手动提交 offset

可以自定义提交,比如想消费一条提交一条

1)同步提交 offset

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

public class CustomConsumerByHandSync {
    public static void main(String[] args) {
        // 1. 创建 kafka 消费者配置类
        Properties properties = new Properties();
        // 2. 添加配置参数
        // 添加连接
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,
                "hadoop102:9092");
        // 配置序列化 必须
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.StringDeserializer");

        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.StringDeserializer");
        // 配置消费者组
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
        // 是否自动提交 offset
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,
                false);
        //3. 创建 kafka 消费者
        KafkaConsumer<String, String> consumer = new
                KafkaConsumer<>(properties);
        //4. 设置消费主题 形参是列表
        consumer.subscribe(Arrays.asList("first"));
        //5. 消费数据
        while (true) {
            // 读取消息
            ConsumerRecords<String, String> consumerRecords =
                    consumer.poll(Duration.ofSeconds(1));
            // 输出消息
            for (ConsumerRecord<String, String> consumerRecord :
                    consumerRecords) {
                System.out.println(consumerRecord.value());
            }
            // 同步提交 offset
            consumer.commitSync();
        }
    }
}

2)异步提交 offset

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

以下为异步提交 offset 的示例:

public class CustomConsumerByHandAsync {
    public static void main(String[] args) {
        // 1. 创建 kafka 消费者配置类
        Properties properties = new Properties();
        // 2. 添加配置参数
        // 添加连接
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,
                "hadoop102:9092");
        // 配置序列化 必须
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.StringDeserializer");

        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.StringDeserializer");
        // 配置消费者组
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
        // 是否自动提交 offset
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,
                "false");
        //3. 创建 Kafka 消费者
        KafkaConsumer<String, String> consumer = new
                KafkaConsumer<>(properties);
        //4. 设置消费主题 形参是列表
        consumer.subscribe(Arrays.asList("first"));
        //5. 消费数据
        while (true) {
            // 读取消息
            ConsumerRecords<String, String> consumerRecords =
                    consumer.poll(Duration.ofSeconds(1));
            // 输出消息
            for (ConsumerRecord<String, String> consumerRecord :
                    consumerRecords) {
                System.out.println(consumerRecord.value());
            }
            // 异步提交 offset
            consumer.commitAsync();
        }
    }
}

5.5.4 指定 Offset 消费

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

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

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

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

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

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

public class CustomConsumerSeek {
    public static void main(String[] args) {
        // 0 配置信息
        Properties properties = new Properties();
        // 连接
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,
                "hadoop102:9092");
        // key value 反序列化

        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
                StringDeserializer.class.getName());

        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
                StringDeserializer.class.getName());
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test2");
        // 1 创建一个消费者
        KafkaConsumer<String, String> kafkaConsumer = new
                KafkaConsumer<>(properties);
        // 2 订阅一个主题
        ArrayList<String> topics = new ArrayList<>();
        topics.add("first");
        kafkaConsumer.subscribe(topics);
        Set<TopicPartition> assignment = new HashSet<>();
        while (assignment.size() == 0) {
            kafkaConsumer.poll(Duration.ofSeconds(1));
            // 获取消费者分区分配信息(有了分区分配信息才能开始消费)
            assignment = kafkaConsumer.assignment();
        }
        // 遍历所有分区,并指定 offset 从 1700 的位置开始消费
        for (TopicPartition tp : assignment) {
            kafkaConsumer.seek(tp, 1700);
        }
        // 3 消费该主题数据
        while (true) {
            ConsumerRecords<String, String> consumerRecords =
                    kafkaConsumer.poll(Duration.ofSeconds(1));
            for (ConsumerRecord<String, String> consumerRecord :
                    consumerRecords) {
                System.out.println(consumerRecord);
            }
        }
    }
}

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

他这个指定位置,这个位置的数据 没有消费的话,这个信息会进行消费,如果这个位置的信息如果消费过了的话,你指定的这个位置的信息就不会才被消费了,所以需要重新定义消费者组或者topic吧,我这块理解可能有问题

5.5.5 指定时间消费

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

操作步骤:

public class CustomConsumerForTime {
    public static void main(String[] args) {
        // 0 配置信息
        Properties properties = new Properties();
        // 连接
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,
                "hadoop102:9092");
        // key value 反序列化

        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
                StringDeserializer.class.getName());

        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
                StringDeserializer.class.getName());
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test2");
        // 1 创建一个消费者
        KafkaConsumer<String, String> kafkaConsumer = new
                KafkaConsumer<>(properties);
        // 2 订阅一个主题
        ArrayList<String> topics = new ArrayList<>();
        topics.add("first");
        kafkaConsumer.subscribe(topics);
        Set<TopicPartition> assignment = new HashSet<>();
        while (assignment.size() == 0) {
            kafkaConsumer.poll(Duration.ofSeconds(1));
            // 获取消费者分区分配信息(有了分区分配信息才能开始消费)
            assignment = kafkaConsumer.assignment();
        }
        HashMap<TopicPartition, Long> timestampToSearch = new
                HashMap<>();
        // 封装集合存储,每个分区对应一天前的数据
        for (TopicPartition topicPartition : assignment) {
            timestampToSearch.put(topicPartition,
                    System.currentTimeMillis() - 1 * 24 * 3600 * 1000);
        }
        // 获取从 1 天前开始消费的每个分区的 offset
        Map<TopicPartition, OffsetAndTimestamp> offsets =
                kafkaConsumer.offsetsForTimes(timestampToSearch);
        // 遍历每个分区,对每个分区设置消费时间。
        for (TopicPartition topicPartition : assignment) {
            OffsetAndTimestamp offsetAndTimestamp =
                    offsets.get(topicPartition);
            // 根据时间指定开始消费的位置
            if (offsetAndTimestamp != null) {
                kafkaConsumer.seek(topicPartition,
                        offsetAndTimestamp.offset());
            }
        }
        // 3 消费该主题数据
        while (true) {
            ConsumerRecords<String, String> consumerRecords =
                    kafkaConsumer.poll(Duration.ofSeconds(1));
            for (ConsumerRecord<String, String> consumerRecord :
                    consumerRecords) {
                System.out.println(consumerRecord);
            }
        }
    }
}

5.5.6 漏消费和重复消费

重复消费:已经消费了数据,但是 offset 没提交。

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

这里面有的弹幕说先消费,再提交,这个问题感觉就跟自动提交一样了,会出现重复消费的问题,就是你在提交offset的过程,kafka挂了,重新的读offset的时候,读的还是老的offset

思考:怎么能做到既不漏消费也不重复消费呢?详看消费者事务

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

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

6. Kafka-Eagle 监控

Kafka-Eagle 框架可以监控 Kafka 集群的整体运行情况,在生产环境中经常使用

6.1 MySQL 环境准备

Kafka-Eagle 的安装依赖于 MySQL,MySQL 主要用来存储可视化展示的数据。如果集群中之前安装过 MySQL 可以跨过该步

6.2 Kafka 环境准备

1)关闭 Kafka 集群

[atguigu@hadoop102 kafka]$ kf.sh stop

2)修改/opt/module/kafka/bin/kafka-server-start.sh 命令中

[atguigu@hadoop102 kafka]$ vim bin/kafka-server-start.sh

修改如下参数值:

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

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

注意:修改之后在启动 Kafka 之前要分发之其他节点

[atguigu@hadoop102 bin]$ xsync kafka-server-start.sh

6.3 Kafka-Eagle 安装

0)官网:EFAK

1)上传压缩包 kafka-eagle-bin-2.0.8.tar.gz 到集群/opt/software 目录

2)解压到本地

[atguigu@hadoop102 software]$ tar -zxvf kafka-eagle-bin-
2.0.8.tar.gz

3)进入刚才解压的目录

[atguigu@hadoop102 kafka-eagle-bin-2.0.8]$ ll
总用量 79164
-rw-rw-r--. 1 atguigu atguigu 81062577 10 月 13 00:00 efak-web-
2.0.8-bin.tar.gz

4)将 efak-web-2.0.8-bin.tar.gz 解压至/opt/module

[atguigu@hadoop102 kafka-eagle-bin-2.0.8]$ tar -zxvf efak-web-
2.0.8-bin.tar.gz -C /opt/module/

5)修改名称

[atguigu@hadoop102 module]$ mv efak-web-2.0.8/ efak

6)修改配置文件 /opt/module/efak/conf/system-config.properties

[atguigu@hadoop102 conf]$ vim system-config.properties
######################################
# multi zookeeper & kafka cluster list
# Settings prefixed with 'kafka.eagle.' will be deprecated, use 'efak.' 
instead
######################################
efak.zk.cluster.alias=cluster1
cluster1.zk.list=hadoop102:2181,hadoop103:2181,hadoop104:2181/kafka
######################################
# zookeeper enable acl
######################################
cluster1.zk.acl.enable=false
cluster1.zk.acl.schema=digest
cluster1.zk.acl.username=test
cluster1.zk.acl.password=test123
######################################
# 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.truststor
e
cluster1.efak.jmx.truststore.password=ke123456
######################################
# kafka offset storage
######################################
# offset 保存在 kafka
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.ScramL
oginModule 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.PlainL
oginModule 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=
######################################
######################################
# 配置 mysql 连接
efak.driver=com.mysql.jdbc.Driver
efak.url=jdbc:mysql://hadoop102:3306/ke?useUnicode=true&characterEncoding=UT
F-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://127.0.0.1:3306/ke?useUnicode=true&characterEncoding=U
TF-8&zeroDateTimeBehavior=convertToNull
#efak.username=root
#efak.password=123456

7)添加环境变量

[atguigu@hadoop102 conf]$ sudo vim /etc/profile.d/my_env.sh
# kafkaEFAK
export KE_HOME=/opt/module/efak
export PATH=$PATH:$KE_HOME/bin

注意:source /etc/profile

[atguigu@hadoop102 conf]$ source /etc/profile

8)启动

(1)注意:启动之前需要先启动 ZK 以及 KAFKA

[atguigu@hadoop102 kafka]$ kf.sh start

(2)启动 efak

[atguigu@hadoop102 efak]$ bin/ke.sh start
Version 2.0.8 -- Copyright 2016-2021
*****************************************************************
* EFAK Service has started success.
* Welcome, Now you can visit 'http://192.168.10.102:8048'
* Account:admin ,Password:123456
*****************************************************************
* <Usage> ke.sh [start|status|stop|restart|stats] </Usage>
* <Usage> https://www.kafka-eagle.org/ </Usage>
*****************************************************************

说明:如果停止 efak,执行命令

[atguigu@hadoop102 efak]$ bin/ke.sh stop

6.4 Kafka-Eagle 页面操作

1)登录页面查看监控数据

http://192.168.10.102:8048/

7. Kafka-Kraft 模式

7.1 Kafka-Kraft 架构

左图为 Kafka 现有架构,元数据在 zookeeper 中,运行时动态选举 controller,由controller 进行 Kafka 集群管理。右图为 kraft 模式架构(实验性),不再依赖 zookeeper 集群,而是用三台 controller 节点代替 zookeeper,元数据保存在 controller 中,由 controller 直接进行 Kafka 集群管理

这样做的好处有以下几个:

  • Kafka 不再依赖外部框架,而是能够独立运行;

  • controller 管理集群时,不再需要从 zookeeper 中先读取数据,集群性能上升;

  • 由于不依赖 zookeeper,集群扩展时不再受到 zookeeper 读写能力限制;

  • controller 不再动态选举,而是由配置文件规定。这样我们可以有针对性的加强controller 节点的配置,而不是像以前一样对随机 controller 节点的高负载束手无策

7.2 Kafka-Kraft 集群部署

1)再次解压一份 kafka 安装包

[atguigu@hadoop102 software]$ tar -zxvf kafka_2.12-3.0.0.tgz -C 
/opt/module/

2)重命名为 kafka2

​
[atguigu@hadoop102 module]$ mv kafka_2.12-3.0.0/ kafka2

​

3)在 hadoop102 上修改/opt/module/kafka2/config/kraft/server.properties 配置文件

[atguigu@hadoop102 kraft]$ vim server.properties
#kafka 的角色(controller 相当于主机、broker 节点相当于从机,主机类似 zk 功 能)
process.roles=broker, controller
#节点 ID
node.id=2
#controller 服务协议别名
controller.listener.names=CONTROLLER
#全 Controller 列表
controller.quorum.voters=2@hadoop102:9093,3@hadoop103:9093,4@hado
op104:9093
#不同服务器绑定的端口
listeners=PLAINTEXT://:9092,CONTROLLER://:9093
#broker 服务协议别名
inter.broker.listener.name=PLAINTEXT
#broker 对外暴露的地址
advertised.Listeners=PLAINTEXT://hadoop102:9092
#协议别名到安全协议的映射
listener.security.protocol.map=CONTROLLER:PLAINTEXT,PLAINTEXT:PLA
INTEXT,SSL:SSL,SASL_PLAINTEXT:SASL_PLAINTEXT,SASL_SSL:SASL_SSL
#kafka 数据存储目录
log.dirs=/opt/module/kafka2/data

4)分发 kafka2

[atguigu@hadoop102 module]$ xsync kafka2/
  • 在 hadoop103 和 hadoop104 上 需 要 对 node.id 相应改变 , 值 需 要 和controller.quorum.voters 对应

  • 在 hadoop103 和 hadoop104 上需要 根据各自的主机名称,修改相应的advertised.Listeners 地址

5)初始化集群数据目录

(1)首先生成存储目录唯一 ID

[atguigu@hadoop102 kafka2]$ bin/kafka-storage.sh random-uuid
J7s9e8PPTKOO47PxzI39VA

(2)用该 ID 格式化 kafka 存储目录(三台节点)。

[atguigu@hadoop102 kafka2]$ bin/kafka-storage.sh format -t 
J7s9e8PPTKOO47PxzI39VA -c 
/opt/module/kafka2/config/kraft/server.properties
[atguigu@hadoop103 kafka2]$ bin/kafka-storage.sh format -t 
J7s9e8PPTKOO47PxzI39VA -c 
/opt/module/kafka2/config/kraft/server.properties
[atguigu@hadoop104 kafka2]$ bin/kafka-storage.sh format -t 
J7s9e8PPTKOO47PxzI39VA -c 
/opt/module/kafka2/config/kraft/server.properties

6)启动 kafka 集群

[atguigu@hadoop102 kafka2]$ bin/kafka-server-start.sh -daemon 
config/kraft/server.properties
[atguigu@hadoop103 kafka2]$ bin/kafka-server-start.sh -daemon 
config/kraft/server.properties
[atguigu@hadoop104 kafka2]$ bin/kafka-server-start.sh -daemon 
config/kraft/server.properties

7)停止 kafka 集群

 
[atguigu@hadoop102 kafka2]$ bin/kafka-server-stop.sh
[atguigu@hadoop103 kafka2]$ bin/kafka-server-stop.sh
[atguigu@hadoop104 kafka2]$ bin/kafka-server-stop.sh

1)在/home/atguigu/bin 目录下创建文件 kf2.sh 脚本文件

[atguigu@hadoop102 bin]$ vim kf2.sh

脚本如下:

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

2)添加执行权限

[atguigu@hadoop102 bin]$ chmod +x kf2.sh

3)启动集群命令

[atguigu@hadoop102 ~]$ kf2.sh start

4)停止集群命令

[atguigu@hadoop102 ~]$ kf2.sh stop

10. 番外补充

1. 消费策略:

sticky:尽量保证上次消费者消费分区,对所有的分区和topic进行重新平衡

2. 确定coordinator

消费者组会向节点发送请求 GroupCoordinatorRequest,返回最小负载的broker节点的id,这个id就是coordinator,就是每个partition都有coordinator,那个负载最小就选哪个当

消费者向coordinator发送JoinGroup请求,有group.id消费者组id、member.id消费者id、protocol metadata消费者定义信息发送过去,会选举出消费者leader,broker会回馈lead_id,会告诉每个消费者lead_id是谁,就知道每个消费者组里面哪个是消费者组leader,还有确定的分区策略,consumer leader来做这个策略方案,这个方案再告诉coordinator,coordinator再将这个方案告诉每个消费者消费策略

然后进入synchronizing group state,消费者向broker发送consumser syncGroupRequest请求,

  • 1
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值