kafka概述&快速入门

1. Kafka 概述

Kafka传统定义:

Kafka是一个分布式的基于发布/订阅模式的消息队列(Message Queue)。主要用于大数据实时处理领域。

什么是发布/订阅:

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

Kafka最 新定义 :

Kafka是一个开源的分布式事件流平台(Event Streaming
Platform),被数千家公司用于高性能数据管道流分析数据集成关键任务应用

2. 消息队列

2.1 为什么使用消息队列?

比如我们有这样一个场景:在注册账号的时候要发送短信给用户。这里比较使用MQ之后前后的区别。其中MQ的好处不止如此。但是比较核心的有 3 个:解耦异步削峰
在这里插入图片描述
解耦
这里我们发现注册信息发送短信是通过调用短信接口,如果此时发送短信系统崩溃了,我们的注册用户也会失败。此时两个系统存在严重的耦合

如果使用 MQ,注册信息系统产生一条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。这样下来,注册信息系统压根儿不需要去考虑要给谁发送数据,不需要维护这个代码,也不需要考虑人家是否调用成功、失败超时等情况。

总结:
通过一个MQ,使用发布订阅这个消息模型,注册信息系统就可以和发送短信系统解耦

异步

再来看上图中的场景,注册信息系统接收一个请求,需要在自己本地写库,还需要调用短信接口,自己本地写库要 300ms,短信接口3s。最终请求总延时是 300 + 3000 = 3300ms,接近 4s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求,等待个 4s,这几乎是不可接受的。

一般互联网类的企业,对于用户直接的操作,一般要求是每个请求都必须在 200 ms 以内完成,对用户几乎是无感知的。

如果使用 MQ,那么注册信息系统发送 1条消息到 MQ 队列中,假如耗时 50ms,注册信息系统从接受一个请求到返回响应给用户,总时长是 300 + 50 = 350ms,对于用户而言,其实感觉上就是点个按钮,350ms以后就直接返回了,用户体验就非常好。

削峰
每天 0:00 到 12:00,注册信息系统风平浪静,每秒并发请求数量就 50 个,结果每次一到 12:00 ~ 13:00 ,每秒并发请求数量突然会暴增到 5k+ 条。但是系统是直接基于 MySQL 的,大量的请求涌入 MySQL,每秒钟对 MySQL 执行约 5k 条 SQL。

一般的 MySQL,扛到每秒 2k 个请求就差不多了,如果每秒请求到 5k 的话,可能就直接把 MySQL 给打死了,导致系统崩溃,用户也就没法再使用系统了。

但是高峰期一过,到了下午的时候,就成了低峰期,可能也就 1w 的用户同时在网站上操作,每秒中的请求数量可能也就 50 个请求,对整个系统几乎没有任何的压力。
在这里插入图片描述
如果使用 MQ,每秒 5k 个请求写入 MQ,注册信息系统每秒钟最多处理 2k 个请求,因为 MySQL 每秒钟最多处理 2k 个。注册信息系统从 MQ 中慢慢拉取请求,每秒钟就拉取 2k 个请求,不要超过自己每秒能处理的最大请求数量就 ok,这样下来,哪怕是高峰期的时候,注册信息 系统也绝对不会挂掉。而 MQ 每秒钟 5k 个请求进来,就 2k 个请求出去,结果就导致在中午高峰期(1 个小时),可能有几十万甚至几百万的请求积压在 MQ 中。
)

这个短暂的高峰期积压是 ok 的,因为高峰期过了之后,每秒钟就 50 个请求进 MQ,但是 注册信息系统 系统依然会按照每秒 2k 个请求的速度在处理。所以说,只要高峰期一过,注册信息系统 系统就会快速将积压的消息给解决掉。

2.2 消息队列有什么优缺点?

优点就是我们上面说的:在其特殊场景下有其对应的好处。

  1. 解耦
  2. 异步
  3. 削峰

虽然使用MQ有好处,但是也有其缺点。如下

  1. 系统的可用性降低:
    系统引入的外部依赖越多,越容易挂掉,本来只需要为何注册信息系统和发送短信系统,如果我们引入了MQ,万一 MQ 挂了咋整?MQ 一挂,整套系统崩溃。如何保证消息队列的高可用是我们学习的一大重点。
  2. 系统复杂性提高
    如果我们引入了MQ,你就需要保证消息没有重复消费,怎么处理消息丢失的情况,怎么保证消息传递的顺序性。问题一大堆,令人头疼。
  3. 一致性问题
    注册信息系统处理完直接返回成功了,人都以为你这个请求就成功了;但是问题是,发送短信系统失败了。你这数据就不一致了。

所以消息队列实际是一种非常复杂的架构,你引入它有很多好处,但是也得针对它带来的坏处做各种额外的技术方案和架构来规避掉,做好之后,你会发现,妈呀,系统复杂度提升了一个数量级,也许是复杂了 10 倍。但是关键时刻,用,还是得用的。

2.3 如何选择合适的消息队列?

Kafka、ActiveMQ、RabbitMQ、RocketMQ 有什么优缺点?

特性ActiveMQRabbitMQRocketMQKafka
单机量万级,比Kafka和RocketMQ低一个数量级同ActiveMQ10万级,支持高吞吐10万级,支持高吞吐,一般配合大数据类的系统来进行实时数据计算,日志采集等场景
top数量对吞吐量的影响topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topictopic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源
时效性ms 级微秒级,这是 RabbitMQ 的一大特点,延迟最低ms 级延迟在 ms 级以内
可用性高,基于主从架构实现高可用同 ActiveMQ非常高,分布式架构非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用
消息可靠性有较低的概率丢失数据基本不丢经过参数优化配置,可以做到 0 丢失同 RocketMQ
功能支持MQ 领域的功能极其完备基于 erlang 开发,并发能力很强,性能极好,延时很低MQ 功能较为完善,还是分布式的,扩展性好功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用

综上,各种对比之后,有如下建议:

一般的业务系统要引入 MQ,最早大家都用 ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了吧,我个人不推荐用这个了。

后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的 Java 工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,活跃度也高。

不过现在确实越来越多的公司会去用 RocketMQ,确实很不错,毕竟是阿里出品,但社区可能有突然黄掉的风险(目前 RocketMQ 已捐给 Apache,但 GitHub 上的活跃度其实不算高)对自己公司技术实力有绝对自信的,推荐用 RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝对不会黄。

所以中小型公司,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;大型公司,基础架构研发实力较强,用 RocketMQ 是很好的选择。

如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。

3. 消息队列的两种模式

3.1 点对点模式

消息生产者生产消息发送到队列(Queue)中,然后消息消费者从Queue中取出并且消费消息。消息被消费后,Queue中不再有存储。所以消息消费者不可能消费到已经被消费的消息,Queue 支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费。
在这里插入图片描述
总结:
简单来说,就是一对一,消费者主动拉取数据,消息收到后就把消息删除。

3.2 发布/订阅模式

消息生产者(发布)将消息发布到topic中,同时有多个消息消费者(订阅)消费该消息。和点对点模式不同,发布到topic的消息会被所有订阅者消费。
在这里插入图片描述
总结:
简单来说,就是一对多,消费者消费消息后不会清除。

3. Kafka基础架构

在这里插入图片描述

  • Producer:消息生产者,就是向Kafka broker发消息的客户端。
  • Consumer:消息消费者,向Kafka broker获取消息的客户端。
  • Consumer Group(CG):消费者组,由多个consumer组成,消费者组内每个消费者负责消费不同分区的数据。一个分区只能由一个组内消费者消费,消费者组之间互不影响,所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者
  • Broker:一台Kafka服务器就是一个broker,一个集群有多个broker组成,一个broker可以容纳多个topic。
  • Topic:可以理解为一个队列,生产者和消费者面向的都是一个topic。
  • Partition:为了实现扩展性,一个非常大的的Topic可以分布到多个broker(即服务器)上,一个topic可以分为多个partition,每个partition是一个有序的队列
  • Replica:副本, 为保障集群中的某个节点发生故障时,该节点上的partition数据不丢失,且Kafka仍然能够继续工作,Kafka提供了副本机制。一个topic的每个分区都有若干个副本,一个Leader和若干个Follower
  • Leader:每个分区多个副本的"主",生产者发生数据的对象,以及消费者消费数据的对象都是Leader。
  • Follower:每个分区多个副本中的"从",实时从leader中同步数据,保持和 leader 数据的同步。leader 发生故障时,某个 follower 会成为新的 leader。

4. Kafka 快速入门

4.1 jar 包下载

官网地址:http://kafka.apache.org/downloads.html
在这里插入图片描述

4.2 搭建集群

这里使用的是最新的Kafka3.0

4.2.1 三台服务器搭建集群

4.2.1.1 集群规划
hadoop102hadoop103hadoop104
zkzkzk
kafkakafkakafka
4.2.1.2 集群部署
  1. 解压按照包
tar -zvxf kafka_2.12-3.0.0.tgz -C /opt/module
  1. 修改解压后的文件名称
mv kafka_2.12-3.0.0/ kafka
  1. 在/opt/module/kafka 目录下创建 logs 文件夹
mkdir logs
  1. 到Kafka目录下查看目录
    在这里插入图片描述
  • bin:主要存放命令脚本
    在这里插入图片描述

  • config:配置信息
    在这里插入图片描述

  • libs:一般是引入的第三方jar包
    在这里插入图片描述

  1. 修改配置文件

进入config目录

cd config/
vi server.properties

修改server.properties配置文件

# 修改以下内容:
#----------------
#broker的全局唯一编号,不能重复
broker.id=0
#删除topic功能开启
delete.topic.enable=true
#处理网络请求的线程数量
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 运行日志存放的路径
log.dirs=/opt/module/logs
#top 在当前broker上的分区个数
num.partitions=1
#用来恢复和清理 data 下数据的线程数量
num.recovery.threads.per.data.dir=1
#segment 文件保留的最长时间,超时将被删除
log.retention.hours=168
#配置连接 Zookeeper 集群地址
zookeeper.connect=hadoop102:2181,hadoop103:2181,hadoop104:2181
  1. 配置环境变量
sudo vi /etc/profile

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

修改完重新编译

source /etc/profile
  1. 分发安装包
xsync kafka/

注意:分发之后记得配置其他机器的环境变量

  1. 分别在 hadoop103 和 hadoop104 上修改配置文件/opt/module/kafka/config/server.properties中的broker.id=1、broker.id=2

注:broker.id 不得重复

  1. 启动Kafka集群之前,要先启动zk集群。
    单个启动
#启动
/opt/module/zookeeper-3.5.7/bin/zkServer.sh start
#关闭
/opt/module/zookeeper-3.5.7/bin/zkServer.sh stop
  1. zk集群群起脚本
#!/bin/bash

case $1 in
"start"){
	for i in hadoop102 hadoop103 hadoop104
	do
		echo  ------------- zookeeper $i 启动 ------------
		ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh start"
	done
}
;;
"stop"){
	for i in hadoop102 hadoop103 hadoop104
	do
		echo  ------------- zookeeper $i 停止 ------------
		ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh stop"
	done
}
;;
"status"){
	for i in hadoop102 hadoop103 hadoop104
	do
		echo  ------------- zookeeper $i 状态 ------------
		ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh status"
	done
}
;;
esac
  1. 启动集群:依次在 hadoop102、hadoop103、hadoop104 节点上启动 kafka
bin/kafka-server-start.sh -daemon config/server.properties
bin/kafka-server-start.sh -daemon config/server.properties
bin/kafka-server-start.sh -daemon config/server.properties
  1. 关闭集群
bin/kafka-server-stop.sh stop
bin/kafka-server-stop.sh stop
bin/kafka-server-stop.sh stop
  1. kafka 群起脚本
#! /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

添加执行权限

chmod +x kf.sh

启动集群命令

 kf.sh start

停止集群命令

kf.sh stop

注意:停止 Kafka 集群时,一定要等 Kafka 所有节点进程全部停止后再停止 Zookeeper集群。因为 Zookeeper 集群当中记录着 Kafka 集群相关信息,Zookeeper 集群一旦先停止,Kafka 集群就没有办法再获取停止进程的信息,只能手动杀死 Kafka 进程了。

4.2.2 一台Linux主机搭建kafka集群

4.2.2.2 集群规划

如何在一台服务器部署kafka集群,Kafka多节点配置,可以向zookeeper一样把软件目录copy多份,修改各自的配置文件。这里介绍另外一种方式:同一个软件目录程序,但使用不同的配置文件启动

使用不同的配置文件启动多个broker节点,这种方式只适合一台机器下的伪集群搭建,在多台机器的真正集群就没有意义了

4.2.2.2 集群部署
  1. 把kafka根目录下的config/zookeeper.properties copy两份,名称分别修改为:zookeeper.properties-1.properties 、zookeeper.properties-2.properties。各自修改他们的配置如下:
    zookeeper.properties.properties配置如下
#日志路径
dataDir=/opt/module/kafka/zookeeper 
#端口号
clientPort=2181

zookeeper.properties-1.properties配置如下

#日志路径
dataDir=/opt/module/kafka/zookeeper1 
#端口号
clientPort=2182

zookeeper.properties-2.properties配置如下

#日志路径
dataDir=/opt/module/kafka/zookeeper2 
#端口号
clientPort=2183
  1. 把kafka根目录下的config/server.properties copy两份,名称分别修改为:server-1.properties 、server-2.properties。各自修改他们的配置如下:
    server.properties
 #整个集群内唯一id号,整数,一般从0开始
broker.id=0
#协议、当前broker机器ip、端口,此值可以配置多个,应该跟SSL等有关系,更多用法尚未弄懂,这里修改为ip和端口。
listeners=PLAINTEXT://192.168.6.56:9092 
#broker端口
port=9092
#broker 机器ip
host.name=192.168.6.56 
#kafka存储数据的目录
log.dirs=/opt/module/logs
#zookeeper 集群列表
zookeeper.connect=192.168.6.56:2181,192.168.6.56:2182,192.168.6.56:2183 

server-1.properties

broker.id=1 
listeners=PLAINTEXT://192.168.6.56:9093
port=9093 
host.name=192.168.6.56
log.dirs=/opt/module/logs
zookeeper.connect=192.168.6.56:2181,192.168.6.56:2182,192.168.6.56:2183

server-2.properties

broker.id=2 
listeners=PLAINTEXT://192.168.6.56:9094
port=9094 
host.name=192.168.6.56
log.dirs=/opt/module/logs
zookeeper.connect=192.168.6.56:2181,192.168.6.56:2182,192.168.6.56:2183
  1. zookeeper集群启动
    进入到kafka安装目录,分别启动自带zk
bin/zookeeper-server-start.sh -daemon config/zookeeper.properties
bin/zookeeper-server-start.sh -daemon config/zookeeper-1.properties
bin/zookeeper-server-start.sh -daemon config/zookeeper-2.properties

  1. kafka 集群启动
    进入到kafka目录,执行
bin/kafka-server-start.sh -daemon config/server.properties
bin/kafka-server-start.sh -daemon config/server-1.properties
bin/kafka-server-start.sh -daemon config/server-2.properties

“-daemon” 参数代表以守护进程的方式启动kafka server。
官网及网上大多给的启动命令是没有"-daemon"参数,如:“bin/kafka-server-start.sh config/server.properties &”,但是这种方式启动后,如果用户退出的ssh连接,进程就有可能结束

  1. 我们可以通过以下命令查看kafka server是否已启动
ps -ef |grep kafka

4.2.3 一台Linux主机搭建kafka单节点

  1. 解压完毕后,进入安装目录/config目录
    在这里插入图片描述

  2. 修改zookeeper.properties
    在这里插入图片描述

  3. 修改server.properties

#整个集群内唯一id号,整数,一般从0开始
broker.id=0
#协议、当前broker机器ip、端口,此值可以配置多个,应该跟SSL等有关系,更多用法尚未弄懂,这里修改为ip和端口。
listeners=PLAINTEXT://:9092 
advertised.listeners=PLAINTEXT://124.221.5.51:9092
#kafka存储数据的目录
log.dirs=/opt/module/logs
#zookeeper 集群列表
zookeeper.connect=192.168.6.56:2181
  1. 启动zk
bin/zookeeper-server-start.sh -daemon config/zookeeper.properties
  1. 启动kafka
bin/kafka-server-start.sh -daemon config/server.properties

5. Kafka 命令行操作

5.1 主题命令行操作

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

在这里插入图片描述

  • 创建一个first topic
bin/kafka-topics.sh --bootstrap-server 124.221.5.51:9092 --topic first --create --partitions 1 --replication-factor 1 

选项说明:
bootstrap-server 124.221.5.51:9092:连接到Kafka主机
–topic first: 定义 topic 名
–create:创建主题
–partitions :定义分区数
–replication-factor 定义副本数,不能超过broker个数。

  • 删除opic
bin/kafka-topics.sh --bootstrap-server 124.221.5.51:9092 --topic first --delete
  • 修改分区数(注意:分区数只能增加,不能减少
bin/kafka-topics.sh --bootstrap-server 124.221.5.51:9092 --topic first --alter --partitions 3

如果我们修改分区数为3后,在修改为1就会报错

Error while executing topic command : Topic currently has 3 partitions, which is higher than the requested 1.
[2022-05-07 10:41:44,379] ERROR org.apache.kafka.common.errors.InvalidPartitionsException: Topic currently has 3 partitions, which is higher than the requested 1.
 (kafka.admin.TopicCommand$)

5.2 生产者命令行操作

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

5.3 消费者命令行操作

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

6. Kafka 生产者

6.1 生产者消息发送流程

6.1.1 发送原理

在消息发送得过程中,涉及到两个线程——main线程和Sender线程。在main线程中创建一个双端队列RecordAccumulator。mian线程将消息发送给RecordAccumulator,Sender线程不断从RecordAccumulator中拉取消息发送到Kafka Broker。
在这里插入图片描述

6.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当消息发送出现错误的时候,系统会重发消息。retrie表示重试次数。默认是 int 最大值,2147483647。如果设置了重试,还想保证消息的有序性,需要设置MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION=1 否则在重试此失败消息的时候,其他的消息可能发送成功了
retry.backoff.ms两次重试之间的时间间隔,默认是 100ms
enable.idempotence是否开启幂等性,默认 true,开启幂等性。
compression.type生产者发送的所有数据的压缩方式。默认是 none,就是不压缩。支持压缩类型:none、gzip、snappy、lz4 和 zstd。

6.2 异步发送 API

6.2.1 普通异步发送

  1. 需求:创建 Kafka 生产者,采用异步的方式发送到 Kafka Broker
  2. 创建工程 kafka,并导入依赖
<!--kafka客户端-->
    <dependencies>
        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka-clients</artifactId>
            <version>3.0.0</version>
        </dependency>
    </dependencies>
  1. 创建包名:com.dhx.kafka.producer;
  2. 创建类CustomProducer,编写不带回调函数的 API 代码
package com.dhx.kafka.producer;

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;

import java.util.Properties;

public class CustomProducer {
    public static void main(String[] args) {

        //1.创建kafka生产者得配置对象
        Properties properties=new Properties();

        //2.给kafka配置对象添加配置信息 bootstrap.servers
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"124.221.5.51:9092");
        //指定ke和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());

        //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", "dhx" + i));
        }

        //5.关闭资源
        kafkaProducer.close();
    }
}
  1. 测试:
  • 在任意一个kafka节点上开启 Kafka 消费者。
bin/kafka-console-consumer.sh --bootstrap-server 124.221.5.51:9092 --topic first
  1. 在 IDEA 中执行代码,观察 124.221.5.51:9092 控制台中是否接收到消息。
    在这里插入图片描述

6.2.2 带回调函数的异步发送

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

  1. 注意:消息发送失败会自动重试,不需要我们在回调函数中手动重试。
public class CustomProducerCallback {
    public static void main(String[] args) throws InterruptedException {

        //1.创建kafka生产者得配置对象
        Properties properties=new Properties();

        //2.给kafka配置对象添加配置信息 bootstrap.servers
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"124.221.5.51:9092");
        //指定ke和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());

        //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", "dhx" + i), new Callback() {
                /**
                 *
                 * @param metadata  元数据信息 RecordMetadata
                 * @param exception               异常信息  Exception
                 */
                @Override
                public void onCompletion(RecordMetadata metadata, Exception exception) {
                    if (exception==null){
                        //没有异常,输出信息到控制台
                        System.out.println("主题:" + metadata.topic() + "->" + "分区:" + metadata.partition());
                    }else {
                        // 出现异常打印、
                        exception.printStackTrace();
                    }

                }
            });
            // 延迟一会会看到数据发往不同分区
            Thread.sleep(2);
        }

        //5.关闭资源
        kafkaProducer.close();
    }
}
  1. 测试:
  • 在任意一个kafka节点上开启 Kafka 消费者。
bin/kafka-console-consumer.sh --bootstrap-server 124.221.5.51:9092 --topic first
  1. 在 IDEA 中执行代码,观察 124.221.5.51:9092 控制台中是否接收到消息。
    在这里插入图片描述
  2. 在 IDEA 控制台观察回调信息。

在这里插入图片描述

6.2.3 同步发送 API

只需在异步发送的基础上,再调用一下 get()方法即可。

public class CustomProducerSync {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1.创建kafka生产者得配置对象
        Properties properties=new Properties();

        //2.给kafka配置对象添加配置信息 bootstrap.servers
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"124.221.5.51:9092");
        //指定ke和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());

        //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", "kafka" + i));
            // 同步发送
            kafkaProducer.send(new ProducerRecord<>("first", "kafka" + i)).get();
        }

        //5.关闭资源
        kafkaProducer.close();
    }
}

测试:
在任意一个kafka节点上开启 Kafka 消费者。

bin/kafka-console-consumer.sh --bootstrap-server 124.221.5.51:9092 --topic first

在 IDEA 中执行代码,观察 124.221.5.51:9092 控制台中是否接收到消息。

bin/kafka-console-consumer.sh --bootstrap-server 124.221.5.51:9092 --topic first
kafka0
kafka1
kafka2
kafka3
kafka4

6.3 生产者分区

6.3.1 分区好处

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

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

6.3.2.1 常见的分区策略

在IDEA中全局查找(ctrl +n)ProducerRecord类,在类中可以看到如下构造方法:
在这里插入图片描述

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

备注:kafka-python中没有指定partition和key,分区是随机的

6.3.2.2 案例1–指定partition
  1. 将数据发往指定 partition 的情况下,例如,将所有数据发往分区 0 中。
  2. 代码实例:
public class CustomProducerCallbackPartitions {
    public static void main(String[] args) throws InterruptedException {

        //1.创建kafka生产者得配置对象
        Properties properties=new Properties();

        //2.给kafka配置对象添加配置信息 bootstrap.servers
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"124.221.5.51:9092");
        //指定ke和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());

        //3.创建kafka生产者对象
        KafkaProducer<String,String> kafkaProducer=new KafkaProducer<String, String>(properties);

        // 4、调用send方法,发送消息
        for (int i = 0; i < 5; i++) {
            // 数据发送到1号分区,key为空
            kafkaProducer.send(new ProducerRecord<>("first",0,"", "hello" + i), new Callback() {
                /**
                 *
                 * @param metadata  元数据信息 RecordMetadata
                 * @param exception               异常信息  Exception
                 */
                @Override
                public void onCompletion(RecordMetadata metadata, Exception exception) {
                    if (exception==null){
                        //没有异常,输出信息到控制台
                        System.out.println("主题:" + metadata.topic() + "->" + "分区:" + metadata.partition());
                    }else {
                        // 出现异常打印、
                        exception.printStackTrace();
                    }

                }
            });
        }

        //5.关闭资源
        kafkaProducer.close();
    }
  1. 测试:
  • 在任意一个kafka节点上开启 Kafka 消费者。
bin/kafka-console-consumer.sh --bootstrap-server 124.221.5.51:9092 --topic first
hello0
hello1
hello2
hello3
hello4
  • 在 IDEA 中执行代码,观察控制台中是否接收到消息。
  • 在 IDEA 控制台观察回调信息。
主题:first->分区:0
主题:first->分区:0
主题:first->分区:0
主题:first->分区:0
主题:first->分区:0
6.3.2.3 案例2–指定key值
  1. 没有指明 partition 值但有 key 的情况下,将 key 的 hash 值与 topic 的 partition 数进行余得到partition 值。
  2. 代码实例:
			 // 依次指定 key 值为 a,b,f ,数据 key 的 hash 值与 3 个分区求余,
            //分别发往 1、2、0
            kafkaProducer.send(new ProducerRecord<>("first","f", "hello" + i), new Callback() {
  1. 测试:
  • key="a"时,在控制台查看结果。
主题:first->分区:1
主题:first->分区:1
主题:first->分区:1
主题:first->分区:1
主题:first->分区:1
  • key="b"时,在控制台查看结果。
主题:first->分区:2
主题:first->分区:2
主题:first->分区:2
主题:first->分区:2
主题:first->分区:2
  • key="f"时,在控制台查看结果。
主题:first->分区:0
主题:first->分区:0
主题:first->分区:0
主题:first->分区:0
主题:first->分区:0

6.3.3 自定义分区器

我们可以根据自己得业务需求,来重新实现分区器,例如我们实现一个分区器实现,发送过来的数据中如果包含 dhx,就发往 0 号分区,不包含 dhx,就发往 1 号分区。

实现步骤:

  1. 定义一个MyPartitioner类实现Partitioner接口并重新其partition()方法。
/**
 * 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;
        if (msgValue.contains("dhx")){
            partition=0;
        }else{
            partition=1;
        }
        return partition;
    }

    @Override
    public void close() {

    }

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

    }
}
  1. 如何配置,使自定义的分区策略生效
public class CustomProducerCallbackPartitions {
    public static void main(String[] args) throws InterruptedException {

        //1.创建kafka生产者得配置对象
        Properties properties=new Properties();

        //2.给kafka配置对象添加配置信息 bootstrap.servers
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"124.221.5.51:9092");
        //指定ke和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());

        //添加自定义分区器
        properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,MyPartitioner.class.getName());

        //3.创建kafka生产者对象
        KafkaProducer<String,String> kafkaProducer=new KafkaProducer<String, String>(properties);

        // 4、调用send方法,发送消息
        for (int i = 0; i < 5; i++) {
            /// value 中有关键字dhx,发往分区0,没有关键字dhx,发往分区1
            kafkaProducer.send(new ProducerRecord<>("first","f", "hellodhx" + i), new Callback() {
                /**
                 *
                 * @param metadata  元数据信息 RecordMetadata
                 * @param exception               异常信息  Exception
                 */
                @Override
                public void onCompletion(RecordMetadata metadata, Exception exception) {
                    if (exception==null){
                        //没有异常,输出信息到控制台
                        System.out.println("主题:" + metadata.topic() + "->" + "分区:" + metadata.partition());
                    }else {
                        // 出现异常打印、
                        exception.printStackTrace();
                    }

                }
            });
        }

        //5.关闭资源
        kafkaProducer.close();
    }
}

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

  • batch.size:批次大小,默认是16k(16384)
  • linger.ms:等待时间,默认是0ms,修改为5-100ms
  • compression.type:生产者发送的所有数据的压缩方式。默认是 none,就是不压缩。支持压缩类型:none、gzip、snappy、lz4 和 zstd。使用压缩类型snappy
  • buffer.memory:RecordAccumulator 缓冲区总大小,默认32m,修改为64M。

代码案例:

public class CustomProducerParameters {
    public static void main(String[] args) {
        // 1. 创建 kafka 生产者的配置对象
        Properties properties=new Properties();

        // 2. 给 kafka 配置对象添加配置信息:bootstrap.servers
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"124.221.5.51:9092");
        //指定ke和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());

        //batch.size  批次大小,默认为16k
        properties.put(ProducerConfig.BATCH_SIZE_CONFIG,16384);

        //linger.ms 等待时间,默认为0 ,改为5ms
        properties.put(ProducerConfig.LINGER_MS_CONFIG,5);

        //buffer.memory RecordAccumulator的缓冲区大小,默认 32M:改为64M
        properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,67108864);

        // compression.type:压缩,默认 none,可配置值 gzip、snappy、lz4 和 zstd
        properties.put(ProducerConfig.COMPRESSION_TYPE_CONFIG,"snappy");

        //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","hello"+i));
        }

        //5.关闭资源
        kafkaProducer.close();
    }
}

测试:

  • 在任意一个kafka节点上开启 Kafka 消费者。
  • 在 IDEA 中执行代码,观察控制台中是否接收到消息。
bin/kafka-console-consumer.sh --bootstrap-server 124.221.5.51:9092 --topic first
hello0
hello1
hello2
hello3
hello4

6.5 生产经验——数据可靠性(ack 应答原理)

在这里插入图片描述

问题:Leader收到数据,所有Follower都开始同步数据,但有一个Follower,因为某种故障,迟迟不能与Leader进行同步,那这个问
题怎么解决呢?

Leader维护了一个动态的in-sync-replica set(ISR),意为和Leader保持同步的Follower+Leader集合(leader:0,isr:0,1,2)。如果Follower长时间未向Leader发送通信请求或者同步数据,则该Follower将会被踢出ISR。该时间阈值由replica.lag.time.max.ms参数设定,默认是30s,例如2超时,(leader:0, isr:0,1)。这样就不用等长期联系不上或者已经故障的节点。

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

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

可靠性总结:

  • acks=0,生产者发送过来数据就不管了,可靠性差,效率高。
  • acks=1,生产者发送过来数据Leader应答,可靠性中等,效率中等。
  • acks=-1,生产者发送过来数据Leader和ISR队列里的所有Follower应答,可靠性高,效率低。

在生产环境中,acks=0很少使用,acks=1,一般用于传输普通日志,允许丢个别数据。acks=-1,一般用于传输和钱相关的数据,对可靠性要求比较高的场景。

数据重复分析:
acks=-1(all):生产者发送过来的数据,Leader和ISR队列里面的所有节点收齐数据后应答。如果Leader收到后,Follower1已同步,Follower2未同步完,Leader挂了,Follower1变成Leader,并且接收了消息。客户端未收到ack,以为发送失败,再次发送,导致现在的Leader接收了两次消息,重复了。具体如何解决数据重复?下回分解。

在这里插入图片描述
代码实例:

public class CustomProducerAcks {
    public static void main(String[] args) {

        //1.创建kafka生产者得配置对象
        Properties properties=new Properties();

        //2.给kafka配置对象添加配置信息 bootstrap.servers
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"124.221.5.51:9092");
        //指定ke和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 2.x默认是1,3.0.0默认是all
        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", "dhx" + i));
        }

        //5.关闭资源
        kafkaProducer.close();
    }
}

6.6 生产经验——数据去重

6.6.1 数据传递语义

  • 至少一次(At Least Once) = ACK级别设置为-1+分区副本大于等于2+ISR里应答的最小副本数量大于等于2
  • 最多一次(At Most Once)=ACK级别设置为0

总结:

  • At Least Once可以保证数据不丢失,但是不能保证数据不重复。
  • At Most Once可以保证数据不重复,但是不能保证数据不丢失。

如果我们需要精确一次怎么办呢?

  • 精确一次(Exactly Once):对于一些非常重要的信息,比如和钱相关的数据,要求数据既不能重复也不丢失。Kafka 0.11版本以后,引入了一项重大特性:幂等性和事务。

6.6.2 幂等性

6.6.2.1 幂等性原理

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

重复数据的判断标准:具有<PID,Partition,SeqNumber>相同主键的消息提交时,Broker只会持久化一条,其中PID是Kafka每次重启都会分配一个新的,Partition 表示分区号,Sequence Number是单调自增的。
所以幂等性只能保证的是单分区单会话内不重复
在这里插入图片描述

6.6.2.2 如何使用幂等性

java客戶端:开启参数 enable.idempotence 默认为 true,false 关闭。

6.6.3 生产者事务

幂等性只能保证在单分区单会话内不重复,开启幂等性能保证客户端重启也能保证仅一次发送。

6.6.3.1 Kafka 事务原理

说明:开启事务,必须开启幂等性。

在这里插入图片描述

  • Producer 在使用事务功能前,必须先自定义一个唯一的 transactional.id。有了 transactional.id,即使客户端挂掉了,它重启后也能继续处理未完成的事务。
6.6.3.2 Kafka事务的API

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;
6.6.3.3 单个 Producer,使用事务保证消息的仅一次发送
public class CustomProducerTransactions {
    public static void main(String[] args) {

        //1.创建kafka生产者得配置对象
        Properties properties=new Properties();

        //2.给kafka配置对象添加配置信息 bootstrap.servers
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"124.221.5.51:9092");
        //指定ke和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());
        // 设置事务 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", "dhx" + i));
            }
            //测试异常是否终止
            //int k = 1 / 0;//构造异常,可以发现事务不能提交,消息未发送
            //提交事务
            kafkaProducer.commitTransaction();
        }catch (Exception e){
            //终止事务
            kafkaProducer.abortTransaction();
        }finally {
            //5.关闭资源
            kafkaProducer.close();
        }
    }
}

6.7 生产经验——数据有序

单分区内,有序(有条件的,详见下节);多分区,分区与分区间无序;
在这里插入图片描述

6.8 生产经验——数据乱序

  1. Kafka在1.x版本之前保证数据单分区有序,条件如下
//不需要考虑是否开启幂等性
max.in.flight.requests.per.connection=1
  1. kafka在1.x及以后版本保证数据单分区有序,条件如下:
    未开启幂等性:
//不需要考虑是否开启幂等性
max.in.flight.requests.per.connection=1

开启幂等性

max.in.flight.requests.per.connection需要设置小于等于5。

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

7. Kafka Broker

7.1 Kafka Broker 工作流程

7.1.1 Zookeeper 存储的 Kafka 信息

启动Kafka自带的Zookeeper 客户端,通过 ls 命令可以查看 kafka 相关信息。

zookeeper-shell.sh 124.221.5.51:2181
ls /
[admin, brokers, cluster, config, consumers, controller, controller_epoch, feature, isr_change_notification, latest_producer_id_block, log_dir_event_notification, zookeeper]

在这里插入图片描述
启动 Zookeeper 客户端,通过 ls 命令可以查看 kafka 相关信息。

# bin/zkCli.sh
[zk: localhost:2181(CONNECTED) 1] ls /kafka 
[admin, brokers, cluster, config, consumers, controller, controller_epoch, feature, isr_change_notification, latest_producer_id_block, log_dir_event_notification]

在zookeeper的服务端存储的Kafka相关信息:

  1. /kafka/brokers/ids [0,1,2] :记录有哪些服务器
  2. /kafka/brokers/topics/first/partitions/0/state:{“leader”:1 ,“isr”:[1,0,2] } 记录谁是Leader,有哪些服务器可用
  3. /kafka/controller {“brokerid”:0} 辅助选举Leader
    在这里插入图片描述
    当然我们可以使用一些工具来查看,例如PrettyZoo 等等。
    在这里插入图片描述

7.1.2 Kafka Broker 总体工作流程

在这里插入图片描述

  1. broker启动后在zk中注册。所以要先启动zk集群。
  2. zk集群中controller谁先注册,谁说了算。
  3. 由选举出来的Controller监听brokers节点变化。
  4. Controller决定Leader的选举
  5. Controller将节点信息上传到zk
  6. 其他Controller从zk同步相关信息
  7. 假设Broker1中Leader挂了.
  8. Controller监听到节点变化。
  9. 从zk节点获取ISR信息。
  10. 选举新的Leader(在ISR中存活为前提,按照AR中排在前面的优先)。
  11. 更新Leader及ISR。

这里解释一些专业名字:

  • ISR :Leader保持同步的Follower+Leader集合(leader:0,isr:0,1,2)
  • AR:Kafka分区中的所有副本统称
  • 选举规则:在ISR中存活为前提,按照AR中排在前面的优先。例如ar[1,0,2],isr[1,0,2]。那么Leader就会按照1,0,2的顺序进行轮询。

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

  1. 查看/kafka/brokers/ids 路径上的节点。
[zk: localhost:2181(CONNECTED) 2] ls /kafka/brokers/ids 
[0, 1, 2]
  1. 查看/kafka/controller 路径上的数据。
[zk: localhost:2181(CONNECTED) 3] get /kafka/controller
{"version":1,"brokerid":0,"timestamp":"1651496005332"}
  1. 查看/kafka/brokers/topics/first/partitions/0/state 路径上的数据。
[zk: localhost:2181(CONNECTED) 4] get /kafka/brokers/topics/first/partitions/0/state
{"controller_epoch":6,"leader":2,"version":1,"leader_epoch":8,"isr":[1,2,0]}
  1. 停止节点2上的 kafka
# 停止节点2上的 kafka
bin/kafka-server-stop.sh
  1. 再次查看/kafka/brokers/ids 路径上的节点
[zk: localhost:2181(CONNECTED) 5] ls /kafka/brokers/ids 
[0, 1]
  1. 再次查看/kafka/controller 路径上的数据。
[zk: localhost:2181(CONNECTED) 6] get /kafka/controller
{"version":1,"brokerid":0,"timestamp":"1651496005332"}
  1. 再次查看/kafka/brokers/topics/first/partitions/0/state 路径上的数据。
[zk: localhost:2181(CONNECTED) 7] get /kafka/brokers/topics/first/partitions/0/state
{"controller_epoch":6,"leader":1,"version":1,"leader_epoch":9,"isr":[1,0]}
  1. 启动节点2上的 kafka,再次观察1、2、3步骤中的内容。

7.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。一般不建议修改,交给系统自己管理。

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

7.2.1 服役新节点

7.2.1.1 新节点准备
  1. 将节点 0 关机,并右键执行克隆操作,克隆出节点 3。
  2. 开启节点3,并修改 IP 地址。设置 -> 网络 -> 有线连接 -> IPV4
    在这里插入图片描述
  3. 修改节点 3 中 kafka 的 broker.id 为 3,监听地址改为192.168.228.150。
broker.id=3
listeners=PLAINTEXT://192.168.228.150:9092
zookeeper.connect=192.168.6.56:2181,192.168.6.56:2182,192.168.6.56:2183 
  1. 删除节点 3 中 kafka 下的 datas 和 logs。
rm -rf  /opt/module/kafka/logs  /opt/module/logs  
  1. 重启节点3
  2. 启动节点0、节点1、节点2上的 kafka 集群。
  3. 单独启动 hadoop105 中的 kafka。
/bin/kafka-server-start.sh -daemon config/server.properties
7.2.1.2 执行负载均衡操作
  1. 创建一个要均衡的主题。
$ vim topics-to-move.json
{
	"topics": [
		{
			"topic": "first"
		}
	],
	"version": 1
}
  1. 生成一个负载均衡的计划。
bin/kafka-reassign-partitions.sh --bootstrap-server 124.221.5.51:9092 --topics-to-move-json-file topics-to-move.json --broker-list "0,1,2,3" --generate

Current partition replica assignment
{"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[1,0,2],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[0,2,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[2,1,0],"log_dirs":["any","any","any"]}]}

Proposed partition reassignment configuration
{"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[0,1,2],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[1,2,3],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[2,3,0],"log_dirs":["any","any","any"]}]}
  1. 创建副本存储计划(所有副本存储在 broker0、broker1、broker2、broker3 中)
$ vim increase-replication-factor.json

输入如下内容:这里是上面生成一个负载均衡的计划内容。

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

Clearing broker-level throttles on brokers 0,1,2,3
Clearing topic-level throttles on topic first

7.2.2 退役旧节点

执行负载均衡操作

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

  1. 创建一个要均衡的主题。
$ vim topics-to-move.json
{
	"topics": [
		{
			"topic": "first"
		}
	],
	"version": 1
}
  1. 创建执行计划。
bin/kafka-reassign-partitions.sh --bootstrap-server 124.221.5.51:9092  --topics-to-move-json-file topics-to-move.json --broker-list "0,1,2" --generate
Current partition replica assignment
{"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[0,1,2],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[1,2,3],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[2,3,0],"log_dirs":["any","any","any"]}]}

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

Clearing broker-level throttles on brokers 0,1,2
Clearing topic-level throttles on topic first
  1. 在需要停止的服务器上执行停止命令即可。
bin/kafka-server-stop.sh

7.3 Kafka 副本

7.3.1 副本基本信息

  • Kafka副本作用:提高数据可靠性。
  • Kafka默认副本1个,生产环境一般配置2个,保证数据可靠性。太多副本会增加磁盘存储空间,增加网络上数据传输,降低效率。
  • Kafka中副本分为:Leader和Follower。Kafka生产者只会把数据发往Leader。然后Follower找Leader进行同步数据。
  • 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副本同步时,延迟过多的副本。

7.3.2 Leader 选举流程

  • Kafka 集群中有一个 broker 的 Controller 会被选举为 Controller Leader,负责管理集群broker 的上下线,所有 topic 的分区副本分配Leader 选举等工作。
  • Controller 的信息同步工作是依赖于 Zookeeper 的。
  • Leader 选举流程图如下:
    在这里插入图片描述
    接下来我们验证一下Leader的选举
  1. 创建一个新的 topic,4 个分区,4 个副本
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --create --topic atguigu1 --partitions 4 --replication-factor 4
Created topic atguigu1.
  1. 查看 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
  1. 停止掉 hadoop105 的 kafka 进程,并查看 Leader 分区情况
bin/kafka-server-stop.sh

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
  1. 停止掉 hadoop104 的 kafka 进程,并查看 Leader 分区情况
 bin/kafka-server-stop.sh
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
  1. 启动 hadoop105 的 kafka 进程,并查看 Leader 分区情况
 bin/kafka-server-start.sh -daemon config/server.properties

 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
  1. 启动 hadoop104 的 kafka 进程,并查看 Leader 分区情况
bin/kafka-server-start.sh -daemon config/server.properties

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
  1. 停止掉 hadoop103 的 kafka 进程,并查看 Leader 分区情况
 bin/kafka-server-stop.sh

 bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic atguigu1
  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

7.3.3 Leader 和 Follower 故障处理细节

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

7.3.3.1 Follower故障
  1. Follower发生故障后会被临时踢出ISR。
  2. 这个期间Leader和Follower继续接受数据。
  3. 待该Follower恢复后,Follower会读取本地磁盘记录的上次的HW。并将log文件高于HW的部分截取掉,从HW开始向Leader进行同步。
  4. 等该Follower的LEO大于等于该partition的HW后,即Follower追上Leader之后,就可以重新加入ISR了。
    在这里插入图片描述
7.3.3.2 Leader故障
  1. Leader发生故障只会,会从ISR中选出一个新的Leader。
  2. 为保证多个副本之间的数据一致性,其余的Follower会先将各自的log文件高于HW的部分截掉,然后从新的Leader同步数据。

注意:这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。

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

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

需求:创建一个新的topic,4个分区,两个副本,名称为three。将该topic的所有副本都存储到broker0和broker1两台服务器上。
在这里插入图片描述
手动调整分区副本存储的步骤如下:

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

输入如下内容:

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

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

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

在这里插入图片描述

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

下面拿一个主题举例说明,假设集群只有一个主题如下图所示:
在这里插入图片描述

  • 针对broker0节点,分区2的AR优先副本是0节点,但是0节点却不是Leader节点,所以不平衡数加1,AR副本总数是4。所以broker0节点不平衡率为1/4>10%,需要再平衡。
  • broker2和broker3节点和broker0不平衡率一样,需要再平衡。
  • Broker1的不平衡数为0,不需要再平衡

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

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

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

输入如下内容:

{"version":1,"partitions":[
{"topic":"second","partition":0,"replicas":[0,1,2]},
{"topic":"second","partition":1,"replicas":[0,1,2]},
{"topic":"second","partition":2,"replicas":[0,1,2]}]
}
  • 执行副本存储计划。
bin/kafka-reassign-partitions.sh --bootstrap-server 124.221.5.51:9092  --reassignment-json-file increase-replication-factor.json --execute

  1. 这样就可以增加topic seond的副本数量
    调整前
Topic: second	TopicId: uGNISi4DR4aMlM75YOCl2g	PartitionCount: 3	ReplicationFactor: 1 Configs: segment.bytes=1073741824
	Topic: second	Partition: 0	Leader: 2	Replicas: 2	Isr: 2
	Topic: second	Partition: 1	Leader: 1	Replicas: 1	Isr: 1
	Topic: second	Partition: 2	Leader: 0	Replicas: 0	Isr: 0

调整后

Topic: second	TopicId: uGNISi4DR4aMlM75YOCl2g	PartitionCount: 3	ReplicationFactor: 3 Configs: segment.bytes=1073741824
	Topic: second	Partition: 0	Leader: 2	Replicas: 0,1,2	Isr: 2,0,1
	Topic: second	Partition: 1	Leader: 1	Replicas: 0,1,2	Isr: 1,2,0
	Topic: second	Partition: 2	Leader: 0	Replicas: 0,1,2	Isr: 0,1,2

7.4 文件存储

7.4.1 文件存储机制

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

在这里插入图片描述

7.4.1.2 Topic 数据存储在什么位置?
  1. 查看任意一节点的/opt/module/logs路径上的文件。(就是我们前面server.properties配置文件配置的log.dirs=/opt/module/logs)。
cd /opt/module/logs

在这里插入图片描述
查看first-0(first-0、first-2)路径上的文件。

cd /opt/module/logs/first-0

在这里插入图片描述
2. 直接查看 log 日志,发现是乱码。

cat 00000000000000000000.log 

在这里插入图片描述
3. 通过工具查看 index 和 log 信息。

$ /opt/module/kafka/bin/kafka-run-class.sh kafka.tools.DumpLogSegments --files /opt/module/logs/first-0/00000000000000000000.index

Dumping /opt/module/logs/first-0/00000000000000000000.index
offset: 0 position: 0
$ /opt/module/kafka/bin/kafka-run-class.sh kafka.tools.DumpLogSegments --files /opt/module/logs/first-0/00000000000000000000.log

Dumping /opt/module/logs/first-0/00000000000000000000.log
Starting offset: 0
baseOffset: 0 lastOffset: 0 count: 1 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position: 0 CreateTime: 1651903975789 size: 71 magic: 2 compresscodec: none crc: 102744555 isvalid: true
baseOffset: 1 lastOffset: 5 count: 5 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position: 71 CreateTime: 1651903989793 size: 116 magic: 2 compresscodec: none crc: 3901175619 isvalid: true
baseOffset: 6 lastOffset: 10 count: 5 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position: 187 CreateTime: 1651904008498 size: 116 magic: 2 compresscodec: none crc: 1731399155 isvalid: true
baseOffset: 11 lastOffset: 15 count: 5 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position: 303 CreateTime: 1651904165621 size: 116 magic: 2 compresscodec: none crc: 1229183665 isvalid: true
baseOffset: 16 lastOffset: 20 count: 5 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position: 419 CreateTime: 1651904699227 size: 116 magic: 2 compresscodec: none crc: 703395200 isvalid: true

带上 --print-data-log 表示查看消息内容。若是要查看多个log文件能够用逗号分隔

/opt/module/kafka/bin/kafka-run-class.sh kafka.tools.DumpLogSegments --files /opt/module/logs/first-0/00000000000000000000.log --print-data-log

Dumping /opt/module/logs/first-0/00000000000000000000.log
Starting offset: 0
| offset: 7 CreateTime: 1651904008498 keySize: -1 valueSize: 4 sequence: -1 headerKeys: [] payload: dhx1
| offset: 8 CreateTime: 1651904008498 keySize: -1 valueSize: 4 sequence: -1 headerKeys: [] payload: dhx2
| offset: 9 CreateTime: 1651904008498 keySize: -1 valueSize: 4 sequence: -1 headerKeys: [] payload: dhx3
| offset: 10 CreateTime: 1651904008498 keySize: -1 valueSize: 4 sequence: -1 headerKeys: [] payload: dhx4
baseOffset: 11 lastOffset: 15 count: 5 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position: 303 CreateTime: 1651904165621 size: 116 magic: 2 compresscodec: none crc: 1229183665 isvalid: true
7.4.1.3 index 文件和 log 文件详解

在这里插入图片描述
注意:

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

说明:日志存储参数配置

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

7.4.2 文件清理策略

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

参数名称描述
log.retention.hours最低优先级小时,默认是7天
log.retention.minutes分钟
log.retention.ms最高优先级毫秒
log.retention.check.interval.ms负责设置检查周期,默认 5 分钟。

那么日志一旦超过了设置的时间,怎么处理呢? Kafka 中提供的日志清理策略有 delete 和 compact 两种。

7.4.2.1 delete 日志删除:将过期数据删除

如果我们配置了log.cleanup.policy = delete ,所有数据采用删除策略。

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

注意:如果一个 segment 中有一部分数据过期,一部分没有过期,将不会删除。

7.4.2.2 compact 日志压缩

如果我们配置了log.cleanup.policy = compat ,所有数据采用压缩策略。

compact日志压缩:对于相同key的不同value值,只保留最后一个版本。
在这里插入图片描述
压缩之后的offset可能不是连续的,比如上图中没有6,当从这些offset消费消息时,将会拿到比这个offset大的offset对应的消息,实际上会拿到offset为7的消息,并从这个位置开始消费。

这种策略只适合特殊场景,比如消息的key是用户ID,value是用户的资料,通过这种压缩策略,整个消息集里就保存了所有用户最新的资料。

7.5 高效读写数据

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

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

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

  4. 页缓存 + 零拷贝技术

  • 零拷贝:Kafka的数据加工处理操作交由Kafka生产者(Producer)和Kafka 消费者(Consumer)处理,Kafka Broker应用层不关心存储的数据,所以就不用走应用层,传输效率高。
  • PageCache页缓存:Kafka重度依赖底层操作系统提高的PageCache功能,当上层有写操作时,操作系统只是将数据写入PageCache,当读操作发生时,先从PageCache中查找,如果查找不到,再去磁盘中读取,实际上PageCache是把尽可能多的空闲内存都当做了磁盘缓存来使用。
    在这里插入图片描述
参数描述
log.flush.interval.messages强制页缓存刷写到磁盘的条数,默认是 long 的最大值,9223372036854775807。一般不建议修改,交给系统自己管理。
log.flush.interval.ms每隔多久,刷数据到磁盘,默认是 null。一般不建议修改,交给系统自己管理。

8. Kafka 消费者

8.1 Kafka 消费方式

  • pull(拉)模式:consumer采取从broker中主动拉取。Kafka采用这种方式。
  • push(推)模式:由broker决定消息发送频率。

为什么Kafka没有采用push模式呢?

  • 因为由broker决定消息发送速率,很难适应所有消费者的消费速率。
  • 例如推送的速度是50m/s,Consumer1、Consumer2就来不及处理消息。如下图
    在这里插入图片描述

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

8.2 Kafka 消费者工作流程

8.2.1 消费者总体工作流程

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

  • 不断的生产数据按照分区规则写入到Kafka集群中。

Kafka Broker:

  • 负责生产者数据的持久化。
  • 选举Leader和同步Follower副本数据。
  • 每个消费者的offset由消费者提交到系统主题保存。

消费者:

  • 一个消费者可以消费多个分区数据。
  • 每个分区的数据只能由消费者组中一个消费者消费。
  • 消费者组之间是互不影响的。

8.2.2 消费者组原理

8.2.2.1 消费者组

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

  • 消费者组内每个消费者负责消费不同分区的数据,但是一个分区只能由一个组内消费者消费。
  • 消费者组之间互不影响,所有的消费者都属于某个消费者组,即消费者组是一个逻辑上的一个订阅者。
    在这里插入图片描述
    在这里插入图片描述
    注意:

如果向消费者组中添加更多的消费者,超过主题分区数量,则有部分消费者就会闲置,不会接受任何消息。

8.2.2.2 消费者组初始化流程

coordinator:辅助实现消费者组的初始化和分区操作

coordinator节点选择=groupid的hashCode值%50(_consumer_offsets的分区数量)

首先会对groupid进行hash计算获得值,接着对_consumer_offsets的分区数量取模,默认是50,_consumer_offsets的分区数可以通过offsets.topic.num.partitions来设置,找到分区以后,这个分区所在的broker机器就是coordinator机器。

比如说:groupId,“myconsumer_groupid” -> hash值(数字)-> 对50取模 ->3__consumer_offsets 这个主题的3号分区在哪台broker上面,那一台就是coordinator 就知道这个consumer group下的所有的消费者提交offset的时候是往哪个分区去提交offset。

作为这个消费者组的老大。消费者组下的所有的消费者提交offset的时候就往这个分区去提交offset。
在这里插入图片描述

  1. 每个consumer都发送JoinGroup请求。
  2. 选出一个consumer作为Leader。
  3. 把要消费的topic情况,发送给Leader消费者。
  4. Leader会负责制定消费方案。
  5. 把消费方案发给coordinator。
  6. Coordinator就把消费方案下发给各个consumer。
  7. 每个消费者都会和coordinator保持心跳(默认是3s),一旦超时((session.timeout.ms=45s),该消费者会被移除。并触发再平衡,或者消费者处理消息的时间过长(max.poll.interval.ms5分钟),也
    会触发再平衡
8.2.2.2 消费者组详细消费流程

在这里插入图片描述

8.2.2 消费者重要参数

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

8.3 消费者 API

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

需求:创建一个独立消费者,消费first主题中数据。
在这里插入图片描述
注意:

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

实现步骤

  1. 在com.dhx.kafka.consumer包中创建CustomConsumer类
public class CustomConsumer {
    public static void main(String[] args) {
        // 1、创建消费者的配置对象
        Properties properties=new Properties();
        // 2、给消费者配置对象添加参数  bootstrap.servers
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"124.221.5.51: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);

        // 注册要消费的主题(可以消费多个主题)
        List list=new ArrayList();
        list.add("first");
        kafkaConsumer.subscribe(list);

        // 拉取数据打印
        while (true){
            // 设置1s中消费一批数据
            ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
            // 打印消费到的数据
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.println(consumerRecord);
            }
        }
    }
}

  1. 测试:在 IDEA 中执行消费者程序。
    在 Kafka 集群控制台,创建 Kafka 生产者,并输入数据。
[root@VM-4-13-centos kafka]# bin/kafka-console-producer.sh --bootstrap-server 124.221.5.51:9092 --topic first
>hello dhx
>

在 IDEA 控制台观察接收到的数据。

ConsumerRecord(topic = first, partition = 0, 
leaderEpoch = 1, offset = 83, CreateTime = 1652103473334,
serialized key size = -1, serialized value size = 9, 
headers = RecordHeaders(headers = [], 
isReadOnly = false), key = null, value = hello dhx)

也可以使用我们之前写的生产者API代码测试,比如CustomProducer.java。运行之后在 IDEA 控制台观察接收到的数据。

ConsumerRecord(topic = first, partition = 2, leaderEpoch = 1, offset = 31, CreateTime = 1652103657987, serialized key size = -1, serialized value size = 4, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = dhx0)
ConsumerRecord(topic = first, partition = 2, leaderEpoch = 1, offset = 32, CreateTime = 1652103657998, serialized key size = -1, serialized value size = 4, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = dhx1)
ConsumerRecord(topic = first, partition = 2, leaderEpoch = 1, offset = 33, CreateTime = 1652103657998, serialized key size = -1, serialized value size = 4, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = dhx2)
ConsumerRecord(topic = first, partition = 2, leaderEpoch = 1, offset = 34, CreateTime = 1652103657998, serialized key size = -1, serialized value size = 4, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = dhx3)
ConsumerRecord(topic = first, partition = 2, leaderEpoch = 1, offset = 35, CreateTime = 1652103657998, serialized key size = -1, serialized value size = 4, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = dhx4)

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

需求:创建一个独立消费者,消费 first 主题 0 号分区的数据。
在这里插入图片描述
实现步骤

  1. 创建类CustomConsumerPartition
public class CustomConsumerPartition {
    public static void main(String[] args) {
        // 1、创建消费者的配置对象
        Properties properties=new Properties();
        // 2、给消费者配置对象添加参数  bootstrap.servers
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"124.221.5.51: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);

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

        // 拉取数据打印
        while (true){
            // 设置1s中消费一批数据
            ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
            // 打印消费到的数据
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.println(consumerRecord);
            }
        }
    }
}
  1. 测试:在 IDEA 中执行消费者程序。
    在 IDEA 中执行生产者程序 CustomProducerCallback()在控制台观察生成几个 0 号分区的数据。
    在这里插入图片描述
    在 IDEA 控制台,观察接收到的数据,只能消费到 0 号分区数据表示正确。
ConsumerRecord(topic = first, partition = 0, leaderEpoch = 1, offset = 89, CreateTime = 1652106013974, serialized key size = 1, serialized value size = 4, headers = RecordHeaders(headers = [], isReadOnly = false), key =  , value = dhx0)
ConsumerRecord(topic = first, partition = 0, leaderEpoch = 1, offset = 90, CreateTime = 1652106013986, serialized key size = 1, serialized value size = 4, headers = RecordHeaders(headers = [], isReadOnly = false), key =  , value = dhx1)
ConsumerRecord(topic = first, partition = 0, leaderEpoch = 1, offset = 91, CreateTime = 1652106013989, serialized key size = 1, serialized value size = 4, headers = RecordHeaders(headers = [], isReadOnly = false), key =  , value = dhx2)
ConsumerRecord(topic = first, partition = 0, leaderEpoch = 1, offset = 92, CreateTime = 1652106013992, serialized key size = 1, serialized value size = 4, headers = RecordHeaders(headers = [], isReadOnly = false), key =  , value = dhx3)
ConsumerRecord(topic = first, partition = 0, leaderEpoch = 1, offset = 93, CreateTime = 1652106013995, serialized key size = 1, serialized value size = 4, headers = RecordHeaders(headers = [], isReadOnly = false), key =  , value = dhx4)

8.3.3 消费者组案例

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

实现步骤:

  1. 复制两份消费者(前面写的CustomConsumer)的代码,在 IDEA 中同时启动,即可启动同一个消费者组中的三个消费者。

注意,要保持groupid相同
properties.put(ConsumerConfig.GROUP_ID_CONFIG,“test”);

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

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

  • 一个consumer group中有多个consumer组成,一个topic有多个partition组成,现在问题是,到底由那个consumer来消费那个partition的数据。
  • Kafka有四种主流的分区分配策略,RangeRoundRobinStickyCooperativeSticky
  • 可以通过配置参数partition.assignment.strategy,修改分区的分配策略。
  • 默认策略是Range + CooperativeSticky。Kafka可以同时使用
    多个分区分配策略。

8.4.1 Range 以及再平衡

8.4.1.1 Range 分区策略原理

Range是对每个topic而言的。

  • 首先对同一个topic里面的分区按照序号进行排序,并对消费者按照字母顺序排序
  • Range之分区分配策略:通过partitions数/consumer数来决定每个消费者应该消费几个分区,如果除不尽,那么前面几个消费者将会多消费1个分区。

假如现在有7个分区,3个消费者,排序后的分区将会是0,1,2,3,4,5,6;消费者排序完之后将会是C0,C1,C2。例如,7/3 = 2 余 1 ,除不尽,那么 消费者 C0 便会多消费 1 个分区。 8/3=2余2,除不尽,那么C0和C1分别多消费一个。

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

8.4.1.2 Range 分区分配策略案例
  1. 修改主题 first 为 7 个分区。
bin/kafka-topics.sh  --bootstrap-server 124.221.5.51:9092 --alter --topic first --partitions 7

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

  1. 复制 CustomConsumer 类,创建CustomConsumer1, CustomConsumer2。这样可以由三个消费者CustomConsumer、CustomConsumer1、CustomConsumer2 组成消费者组,组名都为“test”,同时启动 3 个消费者。
  2. 启动 CustomProducer 生产者,发送 50 条消息,随机发送到不同的分区。
  3. 观看 3 个消费者分别消费哪些分区的数据。
    在这里插入图片描述
  • 说明:Kafka 默认的分区分配策略就是 Range + CooperativeSticky,所以不需要修改策略。
  • 可以看到CustomConsumer1消费了三个区,这里注意下我们命名的规则跟Range分配的名称排序并不是一致的。
8.4.1.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 方式分配。

8.4.2 RoundRobin 以及再平衡

8.4.2.1 RoundRobin 分区策略原理

RoundRobin针对集群中所有的topic而言。

RoundRobin轮询分区策略,是把所有的partition和所有的consumer都列出来,然后按照hashCode值进行排序,最后通过轮询算法来分配partition给到各个消费者。

在这里插入图片描述

8.4.2.2 RoundRobin 分区分配策略案例
  1. 依次在 CustomConsumer、CustomConsumer1、CustomConsumer2 三个消费者代码中修改分区分配策略为RoundRobin。
// 修改分区分配策略
properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, RoundRobinAssignor.class.getName())
  1. 重启 3 个消费者,重复发送消息的步骤,观看分区结果。
    在这里插入图片描述
8.4.2.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 方式分配。

8.4.3 Sticky 以及再平衡

8.4.3.1 Sticky 分区原理

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

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

8.4.3.2 Sticky 分区分配策略案例

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

实现步骤:

  1. 修改分区分配策略为粘性。
// 修改分区分配策略
properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, StickyAssignor.class.getName())

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

  1. 使用同样的生产者发送 50 条消息。
    在这里插入图片描述
8.4.3.3 Sticky 分区分配再平衡案例
  1. 停止掉 2 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)。
    0 号消费者:消费到 0、1、2 号分区数据。
    1 号消费者:消费到 4、5 号分区数据。
    2 号消费者的任务会按照粘性规则,尽可能均衡的随机分成 3 和 6 号分区数据,分别由 0 号消费者或者 1 号消费者消费。
  2. 再次重新发送消息观看结果(45s 以后)。
    0 号消费者:消费到 0、1、2、6 号分区数据。
    1 号消费者:消费到 3、4、5 号分区数据。
    说明:消费者 2 已经被踢出消费者组,所以重新按照粘性方式分配。

8.5 offset 位移

8.5.1 offset 的默认维护位置

在这里插入图片描述
4. _consumer_offsets主题里面采用key和value的方式存储数据。
5. key是group_id+topic+分区号。value就是当前的offset的值。
6. 每隔一段时间,Kafka内部会对这个topic进行compact。也就是每个 group.id+topic+分区号就保留最新数据。

消费 offset 案例

  1. 思考:__consumer_offsets 为 Kafka 中的 topic,那就可以通过消费者进行消费。
  2. 在配置文件 config/consumer.properties 中添加配置 exclude.internal.topics=false,默认是 true,表示不能消费系统主题。为了查看该系统主题数据,所以该参数修改为 false。
  3. 启动生产者往 topic three生产数据。
bin/kafka-console-producer.sh  --bootstrap-server 124.221.5.51:9092 --topic three
  1. 启动消费者消费 three数据。
bin/kafka-console-consumer.sh  --bootstrap-server 124.221.5.51:9092 --topic three  --group test

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

  1. 查看消费者消费主题__consumer_offsets。
bin/kafka-console-consumer.sh  --topic __consumer_offsets --bootstrap-server 124.221.5.51:9092 --formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter" --from-beginning
[test,three,0]::OffsetAndMetadata(offset=0, leaderEpoch=Optional.empty, metadata=, commitTimestamp=1652358232398, expireTimestamp=None)
[test,three,1]::OffsetAndMetadata(offset=1, leaderEpoch=Optional[0], metadata=, commitTimestamp=1652358232398, expireTimestamp=None)
[test,three,2]::OffsetAndMetadata(offset=0, leaderEpoch=Optional.empty, metadata=, commitTimestamp=1652358232398, expireTimestamp=None)
[test,three,3]::OffsetAndMetadata(offset=1, leaderEpoch=Optional.empty, metadata=, commitTimestamp=1652358232398, expireTimestamp=None)

8.5.2 自动提交 offset

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

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

在这里插入图片描述
代码实例:

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

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


        //配置消费者组(组名任意起)必须
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"test3");
        // 创建消费者对象
        KafkaConsumer<String,String> kafkaConsumer=new KafkaConsumer<String, String>(properties);

        // 注册要消费的主题(可以消费多个主题)
        List list=new ArrayList();
        list.add("first");
        kafkaConsumer.subscribe(list);

        // 拉取数据打印
        while (true){
            // 设置1s中消费一批数据
            ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
            // 打印消费到的数据
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.println(consumerRecord);
            }
        }
    }
}

8.5.3 手动提交 offset

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

手动提交offset的方法有两种,分别是commitSync(同步提交)和commitAsync(异步提交)。两者的相同点是,都会将本次提交的一批数据最高的偏移量提交,不同点是,同步提交阻塞当前线程,一直到提交成功,并且会自动失败重试(由不可控因素导致,也会出现提交失败);而异步提交则没有失败重试机制,故有可能提交失败。

  • commitSync(同步提交):必须等待offset提交完毕,再去消费下一批数据。
  • commitAsync(异步提交) :发送完提交offset请求后,就开始消费下一批数据了。
    在这里插入图片描述
  • 同步提交 offset:
    由于同步提交 offset 有失败重试机制,故更加可靠,但是由于一直等待提交结果,提交的效率比较低。
  • 异步提交 offset:
    虽然同步提交 offset 更可靠一些,但是由于其会阻塞当前线程,直到提交成功。因吞吐量会受到很大的影响。因此更多的情况下,会选用异步提交 offset 的方式。
public class CustomConsumerByHandSync {
    public static void main(String[] args) {
        // 1、创建消费者的配置对象
        Properties properties=new Properties();
        // 2、给消费者配置对象添加参数  bootstrap.servers
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"124.221.5.51:9092");
        // 配置序列化,必须
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());

        // 是否自动提交偏移量,true为自动提交
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);



        //配置消费者组(组名任意起)必须
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"test3");
        // 创建消费者对象
        KafkaConsumer<String,String> kafkaConsumer=new KafkaConsumer<String, String>(properties);

        // 注册要消费的主题(可以消费多个主题)
        List list=new ArrayList();
        list.add("first");
        kafkaConsumer.subscribe(list);

        // 拉取数据打印
        while (true){
            // 设置1s中消费一批数据
            ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
            // 打印消费到的数据
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.println(consumerRecord);
            }
            // 同步提交 offset
            kafkaConsumer.commitAsync();
            // 异步提交 offset
            // kafkaConsumer.commitSync();
        }
    }
}

8.5.4 指定 Offset 消费

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

可以使用auto.offset.reset = earliest | latest | none 默认是 latest。

  • earliest :自动将偏移量重置为最早的偏移量,–from-beginning。
  • latest(默认值):自动将偏移量重置为最新的偏移量。
  • none:如果未找到消费者组的先前偏移量,则向消费者抛出异常。

指定策略开始消费

//自动将偏移量重置为最早的偏移量,–from-beginning。
 properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");

任意指定 offset 位移开始消费

public class CustomConsumerSeek {
    public static void main(String[] args) {
        // 1、创建消费者的配置对象
        Properties properties=new Properties();
        // 2、给消费者配置对象添加参数  bootstrap.servers
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"124.221.5.51: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,"test1");
        // 创建消费者对象
        KafkaConsumer<String,String> kafkaConsumer=new KafkaConsumer<String, String>(properties);

        // 注册要消费的主题(可以消费多个主题)
        List list=new ArrayList();
        list.add("first");
        kafkaConsumer.subscribe(list);

        Set<TopicPartition> assignment=new HashSet<>();
        while (assignment.size()==0){
            kafkaConsumer.poll(Duration.ofSeconds(1));
            //获取消费者分区分配信息,有了分区分配信息才能开始消费
            assignment=kafkaConsumer.assignment();
        }

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

        // 拉取数据打印
        while (true){
            // 设置1s中消费一批数据
            ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
            // 打印消费到的数据
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.println(consumerRecord);
            }
        }
    }
}

8.5.5 指定时间消费

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

public class CustomConsumerForTime {
    public static void main(String[] args) {
        // 1、创建消费者的配置对象
        Properties properties=new Properties();
        // 2、给消费者配置对象添加参数  bootstrap.servers
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"124.221.5.51: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,"test6");
        // 创建消费者对象
        KafkaConsumer<String,String> kafkaConsumer=new KafkaConsumer<String, String>(properties);

        // 注册要消费的主题(可以消费多个主题)
        List list=new ArrayList();
        list.add("first");
        kafkaConsumer.subscribe(list);

        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());
            }
        }

        // 拉取数据打印
        while (true){
            // 设置1s中消费一批数据
            ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
            // 打印消费到的数据
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.println(consumerRecord);
            }
        }
    }
}

8.5.5 漏消费和重复消费

重复消费:已经消费了数据,但是 offset 没提交。
漏消费:先提交 offset 后消费,有可能会造成数据的漏消费。
在这里插入图片描述

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

如果想完成Consumer端的精准一次性消费,那么需要Kafka消费端将消费过程和提交offset过程做原子绑定。此时我们需要将Kafka的offset保存到支持事务的自定义介质(比如MySQL)。
在这里插入图片描述

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

如果是Kafka的消费能力不足,则可以考虑增加topic的分区数,并且同时提升消费组的消费者数量,消费者数=分区数。(两者缺一不可)。如下图如果只有一个consumer,那么其消费速度肯定赶不上生产速度。所以要增加消费者数量。
在这里插入图片描述
如果是下游的数据处理不及时,提高每批次拉取的数量,批次拉取数据过少(拉取数据/处理时间<生产速度),使处理的数据小于生产的数据,也会造成数据积压。

在这里插入图片描述
可以通过下面的参数来增加吞吐量:

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

9 Kafka-Kraft 模式

9.1 Kafka-Kraft 架构

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

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

  • Kafka 不再依赖外部框架,而是能够独立运行;
  • controller 管理集群时,不再需要从 zookeeper 中先读取数据,集群性能上升;
  • 由于不依赖 zookeeper,集群扩展时不再受到 zookeeper 读写能力限制;
  • controller 不再动态选举,而是由配置文件规定。这样我们可以有针对性的加强;
  • controller 节点的配置,而不是像以前一样对随机 controller 节点的高负载束手无策;

9.2 Kafka-Kraft 集群部署

  1. 再次解压一份 kafka 安装包到目录/usr/local/kafka,重命名为kafka_kraft
  2. 在节点1上修改/usr/local/kafka/kafka_kraft/config/kraft/server.properties配置文件
#kafka 的角色(controller 相当于主机、broker 节点相当于从机,主机类似 zk 功能)
process.roles=broker,controller
#节点 ID
node.id=1
#全 Controller 列表
controller.quorum.voters=1@192.168.228.147:9093,2@192.168.228.148:9093,3@192.168.228.149:9093
#不同服务器绑定的端口
listeners=PLAINTEXT://:9092,CONTROLLER://:9093
# broker 服务协议别名
inter.broker.listener.name=PLAINTEXT
# broker 对外暴露的地址
advertised.listeners=PLAINTEXT://192.168.228.147:9092
# controller 服务协议别名
controller.listener.names=CONTROLLER
# 协议别名到安全协议的映射
listener.security.protocol.map=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,SSL:SSL,SASL_PLAINTEXT:SASL_PLAINTEXT,SASL_SSL:SASL_SSL
# kafka 数据存储目录
log.dirs=/usr/local/kafka/kafka_kraft/kafka_logs
  1. 节点2,节点3相同的操作
    在节点2和节点3上需要对node.id相应改变,值需要和controller.quorum.voters对应。
    在节点2和节点3上需要根据各自的主机ip,修改相应的advertised.Listeners地址。
  2. 初始化集群数据目录
    首先生成存储目录唯一 ID。
/usr/local/kafka/kafka_kraft/bin/kafka-storage.sh random-uuid
mHm4I8YRQoKZp1iD7ucyNw

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

/usr/local/kafka/kafka_kraft/bin/kafka-storage.sh format -t mHm4I8YRQoKZp1iD7ucyNw -c /usr/local/kafka/kafka_kraft/config/kraft/server.properties
  1. 启动 kafka 集群
    分别字三个节点执行启动脚本,拉起kafka
/usr/local/kafka/kafka_kraft/bin/kafka-server-start.sh -daemon /usr/local/kafka/kafka_kraft/config/kraft/server.properties
  1. 停止 kafka 集群
/usr/local/kafka/kafka_kraft/bin/kafka-server-stop.sh

9.3 Kafka-Kraft 集群启动停止脚本

#! /bin/bash
passwd=xxxxxx

case $1 in
"start"){
for i in 192.168.228.147 192.168.228.148 192.168.228.149
	do
		echo " --------启动 $i Kafka-------"
		sshpass -p $passwd ssh -p 22 root@$i /usr/local/kafka/kafka_kraft/bin/kafka-server-start.sh -daemon /usr/local/kafka/kafka_kraft/config/kraft/server.properties
	done
};;
"stop"){
	for i in 192.168.228.147 192.168.228.148 192.168.228.149
	do
		echo " --------停止 $i Kafka-------"
		sshpass -p $passwd ssh -p 22 root@$i /usr/local/kafka/kafka_kraft/bin/kafka-server-stop.sh
	done
};;
esac
  1. 启动集群命令
sh kf_kraft.sh start
  1. 停止集群命令
sh kf_kraft.sh stop
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值