Kafka传统定义:
Kafka
是一个分布式
的基于发布/订阅
模式的消息队列(Message Queue)。主要用于大数据实时处理领域。
什么是发布/订阅:
消息的发布者不会将消息直接发送给特定的订阅者,而是将
发布的消息分为不同的类别
,订阅者只接受感兴趣的消息
。
Kafka最 新定义 :
Kafka是一个开源的
分布式事件流平台
(Event Streaming
Platform),被数千家公司用于高性能数据管道
、流分析
、数据集成
和关键任务应用
。
比如我们有这样一个场景:在注册账号的时候要发送短信给用户。这里比较使用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 个请求的速度在处理。所以说,只要高峰期一过,注册信息系统 系统就会快速将积压的消息给解决掉。
优点就是我们上面说的:在其特殊场景下有其对应的好处。
虽然使用MQ有好处,但是也有其缺点。如下
如何保证消息队列的高可用是我们学习的一大重点。
消息没有重复消费
,怎么处理消息丢失
的情况,怎么保证消息传递的顺序性
。问题一大堆,令人头疼。所以消息队列实际是一种非常复杂的架构,你引入它有很多好处,但是也得针对它带来的坏处做各种额外的技术方案和架构来规避掉,做好之后,你会发现,妈呀,系统复杂度提升了一个数量级,也许是复杂了 10 倍。但是关键时刻,用,还是得用的。
Kafka、ActiveMQ、RabbitMQ、RocketMQ 有什么优缺点?
特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka |
---|---|---|---|---|
单机量 | 万级,比Kafka和RocketMQ低一个数量级 | 同ActiveMQ | 10万级,支持高吞吐 | 10万级,支持高吞吐,一般配合大数据类的系统来进行实时数据计算,日志采集等场景 |
top数量对吞吐量的影响 | topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic | topic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,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 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。
消息生产者
生产消息发送到队列
(Queue)中,然后消息消费者从Queue中取出并且消费消息。消息被消费后,Queue中不再有存储。所以消息消费者不可能消费到已经被消费的消息,Queue 支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费。
总结:
简单来说,就是一对一,消费者主动拉取数据,消息收到后就把消息删除。
消息生产者(发布)将消息发布到topic中,同时有多个消息消费者(订阅)消费该消息。和点对点模式不同,发布到topic的消息会被所有订阅者消费。
总结:
简单来说,就是一对多,消费者消费消息后不会清除。
消费者组内每个消费者负责消费不同分区的数据。一个分区只能由一个组内消费者消费,消费者组之间互不影响
,所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者
。生产者和消费者面向的都是一个topic。
一个topic可以分为多个partition
,每个partition是一个有序的队列
。Leader
和若干个Follower
。副本的"主"
,生产者发生数据的对象,以及消费者消费数据的对象都是Leader。副本中的"从"
,实时从leader中同步数据,保持和 leader 数据的同步。leader 发生故障时,某个 follower 会成为新的 leader。官网地址:http://kafka.apache.org/downloads.html
这里使用的是最新的Kafka3.0
hadoop102 | hadoop103 | hadoop104 |
---|---|---|
zk | zk | zk |
kafka | kafka | kafka |
tar -zvxf kafka_2.12-3.0.0.tgz -C /opt/module
mv kafka_2.12-3.0.0/ kafka
mkdir logs
进入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
sudo vi /etc/profile
#KAFKA_HOME
export KAFKA_HOME=/opt/module/kafka
export PATH=$PATH:$KAFKA_HOME/bin
修改完重新编译
source /etc/profile
xsync kafka/
注意:分发之后记得配置其他机器的环境变量
注:broker.id 不得重复
#启动
/opt/module/zookeeper-3.5.7/bin/zkServer.sh start
#关闭
/opt/module/zookeeper-3.5.7/bin/zkServer.sh stop
#!/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
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
bin/kafka-server-stop.sh stop
bin/kafka-server-stop.sh stop
bin/kafka-server-stop.sh stop
#! /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 进程了。
如何在一台服务器部署kafka集群,Kafka多节点配置,可以向zookeeper一样把软件目录copy多份,修改各自的配置文件。这里介绍另外一种方式:同一个软件目录程序,但使用不同的配置文件启动
使用不同的配置文件启动多个broker节点,这种方式只适合一台机器下的伪集群搭建,在多台机器的真正集群就没有意义了
#日志路径
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
#整个集群内唯一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
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
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连接,进程就有可能结束
ps -ef |grep kafka
#整个集群内唯一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
bin/zookeeper-server-start.sh -daemon config/zookeeper.properties
bin/kafka-server-start.sh -daemon config/server.properties
bin/kafka-topics.sh
参数 | 描述 |
---|---|
- -bootstrap-server |
连接得Kafka Broker主机名称和端口号 |
- -topic |
操作得topic名称 |
- -create | 创建主题 |
- - delete | 删除主题 |
- -alter | 修改主题 |
- -list | 查看所有主题 |
- -describe | 查看主题详细描述 |
- -partitions |
设置分区数 |
- -replication-factor |
设置分区副本 |
- -config |
更新系统默认的配置 |
bin/kafka-topics.sh --bootstrap-server 124.221.5.51:9092 --list
bin/kafka-topics.sh --bootstrap-server 124.221.5.51:9092 --topic first --describe
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个数。
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$)
bin/kafka-console-producer.sh
参数 | 描述 |
---|---|
- - bootstrap-server |
连接的 Kafka Broker 主机名称和端口号 |
- -topic |
操作的 topic 名称 |
bin/kafka-console-producer.sh --bootstrap-server 124.221.5.51:9092 --topic first
>hello world
>kafka
bin/kafka-console-consumer.sh
参数 | 描述 |
---|---|
- - bootstrap-server |
连接的 Kafka Broker 主机名称和端口号 |
- -topic |
操作的 topic 名称 |
- -from-beginning | 从头开始消费 |
- -group |
指定消费者组名称 |
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
在消息发送得过程中,涉及到两个线程——main线程和Sender线程。在main线程中创建一个双端队列RecordAccumulator。mian线程将消息发送给RecordAccumulator,Sender线程不断从RecordAccumulator中拉取消息发送到Kafka Broker。
参数名称 | 描述 |
---|---|
bootstrap.servers | 生产者连接集群所需的broker地址清单。例如hadoop102:9092,hadoop103:9092,hadoop104:9092可以设置1个或者多个,中间用逗号隔开。注意这里并非需要所有的broker地址,因为生产者从给定的broker里查找到其他broker信息 |
key.serializer 和 value.serializer | 指定发送消息的 key 和 value 的序列化类型。一定要写全类名 |
buffer.memory | RecordAccumulator 缓冲区总大小,默认32m |
batch.size | 缓冲区一批数据最大值,默认16k 。适当增加该值,可以提高吞吐量,但是如果该值设置太大,会导致数据传输延迟增加。 |
linger.ms | 如果数据迟迟未达到 batch.size,sender 等待 linger.time之后就会发送数据。单位 ms,默认值是 0ms ,表示没有延迟。生产环境建议该值大小为 5-100ms 之间。 |
acks | 0:生产者发送过来的数据,不需要等数据落盘应答。 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。 |
<dependencies>
<dependency>
<groupId>org.apache.kafkagroupId>
<artifactId>kafka-clientsartifactId>
<version>3.0.0version>
dependency>
dependencies>
com.dhx.kafka.producer;
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();
}
}
bin/kafka-console-consumer.sh --bootstrap-server 124.221.5.51:9092 --topic first
回调函数会在 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
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();
}
}
bin/kafka-console-consumer.sh --bootstrap-server 124.221.5.51:9092 --topic first
只需在异步发送的基础上,再调用一下 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
按照分区
切割成一块一块数据存储在多台Broker上。合理控制分区的任务,可以实现负载均衡
的效果。以分区为单位
发送数据,消费者可以以分区为单位
进行消费数据。在IDEA中全局查找(ctrl +n)ProducerRecord
类,在类中可以看到如下构造方法:
指明partition的情况下,直接将指明的值作为partition值
,例如partition=1,所有数据写入分区1。没有指明partition值但有key的情况下
,将key的hash值
与topic的partition数
进行取余得到partition值
,例如:key1的hash值=5, key2的hash值=6 ,topic的partition数=2,那么key1 对应的value1写入1号分区,key2对应的value2写入0号分区。备注:kafka-python中没有指定partition和key,分区是随机的
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();
}
bin/kafka-console-consumer.sh --bootstrap-server 124.221.5.51:9092 --topic first
hello0
hello1
hello2
hello3
hello4
主题:first->分区:0
主题:first->分区:0
主题:first->分区:0
主题:first->分区:0
主题:first->分区:0
// 依次指定 key 值为 a,b,f ,数据 key 的 hash 值与 3 个分区求余,
//分别发往 1、2、0
kafkaProducer.send(new ProducerRecord<>("first","f", "hello" + i), new Callback() {
主题:first->分区:1
主题:first->分区:1
主题:first->分区:1
主题:first->分区:1
主题:first->分区:1
主题:first->分区:2
主题:first->分区:2
主题:first->分区:2
主题:first->分区:2
主题:first->分区:2
主题:first->分区:0
主题:first->分区:0
主题:first->分区:0
主题:first->分区:0
主题:first->分区:0
我们可以根据自己得业务需求,来重新实现分区器,例如我们实现一个分区器实现,发送过来的数据中如果包含 dhx,就发往 0 号分区,不包含 dhx,就发往 1 号分区。
实现步骤:
/**
* 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) {
}
}
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();
}
}
代码案例:
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();
}
}
测试:
bin/kafka-console-consumer.sh --bootstrap-server 124.221.5.51:9092 --topic first
hello0
hello1
hello2
hello3
hello4
问题: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,一般用于传输普通日志,允许丢个别数据。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();
}
}
ACK级别设置为-1
+分区副本大于等于2
+ISR里应答的最小副本数量大于等于2
总结:
如果我们需要精确一次怎么办呢?
幂等性:是指Producer不论向Broker发送多少次重复的数据,Broker端都只会持久化一条,保证了不重复。
精确一次(Exactly Once) = 幂等性 + 至少一次( ack=-1 + 分区副本数>=2 + ISR最小副本数量>=2) 。
重复数据的判断标准:具有<PID,Partition,SeqNumber
>相同主键的消息提交时,Broker只会持久化一条,其中PID是Kafka每次重启都会分配一个新的,Partition 表示分区号,Sequence Number是单调自增的。
所以幂等性只能保证的是单分区单会话内不重复
。
java客戶端:开启参数 enable.idempotence 默认为 true,false 关闭。
幂等性只能保证在单分区单会话内不重复,开启幂等性能保证客户端重启也能保证仅一次发送。
说明:开启事务,必须开启幂等性。
唯一的 transactional.id
。有了 transactional.id,即使客户端挂掉了,它重启后也能继续处理未完成的事务。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;
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();
}
}
}
单分区内,有序(有条件的,详见下节);多分区,分区与分区间无序;
//不需要考虑是否开启幂等性
max.in.flight.requests.per.connection=1
//不需要考虑是否开启幂等性
max.in.flight.requests.per.connection=1
开启幂等性
max.in.flight.requests.per.connection需要设置小于等于5。
原因说明:因为在kafka1.x以后,启用幂等后,kafka服务端会缓存
producer发来的最近5个request的元数据,故无论如何,都可以保证最近5个request的数据都是有序的。
启动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相关信息:
这里解释一些专业名字:
模拟 Kafka 上下线,Zookeeper 中数据变化
[zk: localhost:2181(CONNECTED) 2] ls /kafka/brokers/ids
[0, 1, 2]
[zk: localhost:2181(CONNECTED) 3] get /kafka/controller
{"version":1,"brokerid":0,"timestamp":"1651496005332"}
[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]}
# 停止节点2上的 kafka
bin/kafka-server-stop.sh
[zk: localhost:2181(CONNECTED) 5] ls /kafka/brokers/ids
[0, 1]
[zk: localhost:2181(CONNECTED) 6] get /kafka/controller
{"version":1,"brokerid":0,"timestamp":"1651496005332"}
[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]}
参数名称 | 描述 |
---|---|
replica.lag.time.max.ms | ISR 中,如果 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.bytes | Kafka 中 log 日志是分成一块块存储的,此配置是指 log 日志划分 成块的大小,默认值 1G 。 |
log.index.interval.bytes | 默认 4kb ,kafka 里面每当写入了 4kb 大小的日志(.log),然后就往 index 文件里面记录一个索引。 |
log.retention.hours | Kafka 中数据保存的时间,默认 7 天 。 |
log.retention.minutes | Kafka 中数据保存的时间,分钟级别,默认关闭 。 |
log.retention.ms | Kafka 中数据保存的时间,毫秒级别,默认关闭 。 |
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。一般不建议修改,交给系统自己管理。 |
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
rm -rf /opt/module/kafka/logs /opt/module/logs
/bin/kafka-server-start.sh -daemon config/server.properties
$ vim topics-to-move.json
{
"topics": [
{
"topic": "first"
}
],
"version": 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"]}]}
$ 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"]}]}
bin/kafka-reassign-partitions.sh --bootstrap-server 124.221.5.51:9092 --reassignment-json-file increase-replication-factor.json --execute
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
执行负载均衡操作
先按照退役一台节点,生成执行计划,然后按照服役时操作流程执行负载均衡。
$ vim topics-to-move.json
{
"topics": [
{
"topic": "first"
}
],
"version": 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"]}]}
vim increase-replication-factor.json
{"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,0,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[0,1,2],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[1,2,0],"log_dirs":["any","any","any"]}]}
bin/kafka-reassign-partitions.sh --bootstrap-server 124.221.5.51:9092 --reassignment-json-file increase-replication-factor.json --execute
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
bin/kafka-server-stop.sh
AR = ISR + OSR
管理集群broker 的上下线
,所有 topic 的分区副本分配
和 Leader 选举
等工作。bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --create --topic atguigu1 --partitions 4 --replication-factor 4
Created topic atguigu1.
[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
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
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
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
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
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
LEO(Long End Offset):每个副本的最后一个offset,LEO其实就是最新的offset+1。
HW(High Watermark):所有副本中最小的LEO。
注意:
这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。
在生产环境中,每台服务器的配置和性能不一致,但是Kafka只会根据自己的代码规则创建对应的分区副本,就会导致个别服务器存储压力较大。所有需要手动调整分区副本的存储。
需求:创建一个新的topic,4个分区,两个副本,名称为three。将该topic的所有副本都存储到broker0和broker1两台服务器上。
手动调整分区副本存储的步骤如下:
bin/kafka-topics.sh --bootstrap-server 124.221.5.51:9092 --create --topic three --partitions 4 --replication-factor 2
bin/kafka-topics.sh --bootstrap-server 124.221.5.51:9092 --describe --topic three
vim increase-replication-factor.json
输入如下内容:
{
"version": 1,
"partitions": [{"topic": "three", "partition": 0, "replicas": [0, 1]},
{"topic": "three", "partition": 1, "replicas": [0, 1]},
{"topic": "three", "partition": 2, "replicas": [1, 0]},
{"topic": "three", "partition": 3, "replicas": [1, 0]}]
}
bin/kafka-reassign-partitions.sh --bootstrap-server 124.221.5.51:9092 --reassignment-json-file increase-replication-factor.json --execute
bin/kafka-reassign-partitions.sh --bootstrap-server 124.221.5.51:9092 --reassignment-json-file increase-replication-factor.json --verify
bin/kafka-topics.sh --bootstrap-server 124.221.5.51:9092 --describe --topic three
参数名称 | 描述 |
---|---|
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负载是否平衡的间隔时间。 |
在生产环境当中,由于某个主题的重要等级需要提升,我们考虑增加副本。副本数的
增加需要先制定计划,然后根据计划执行。
bin/kafka-reassign-partitions.sh --bootstrap-server 124.221.5.51:9092 --create --partitions 3 --replication-factor 1 --topic second
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
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
每一个partition对应于一个log文件
,该log文件中存储的就是Producer生产的数据。Producer生产的数据会被不断的追加到该log文件末端
,为放止log文件过大导致数据定位效率低下,Kafka采取了分片
和索引
机制。".index"文件
,".log"文件
,".timeindex"
等文件。/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
cat 00000000000000000000.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
说明:日志存储参数配置
参数 | 描述 |
---|---|
log.segment.bytes | Kafka 中 log 日志是分成一块块存储的,此配置是指 log 日志划成块的大小,默认值 1G。 |
log.index.interval.bytes | 默认 4kb,kafka 里面每当写入了 4kb 大小的日志(.log),然后就往 index 文件里面记录一个索引。 稀疏索引。 |
Kafka中默认的日志保存时间为7天,可以通过调整如下参数修改保存时间。
参数名称 | 描述 |
---|---|
log.retention.hours | 最低优先级小时,默认是7天 |
log.retention.minutes | 分钟 |
log.retention.ms | 最高优先级毫秒 |
log.retention.check.interval.ms | 负责设置检查周期,默认 5 分钟。 |
那么日志一旦超过了设置的时间,怎么处理呢? Kafka 中提供的日志清理策略有 delete 和 compact 两种。
如果我们配置了log.cleanup.policy = delete
,所有数据采用删除策略。
注意:如果一个 segment 中有一部分数据过期,一部分没有过期,将不会删除。
如果我们配置了log.cleanup.policy = compat
,所有数据采用压缩策略。
compact日志压缩:对于相同key的不同value值,只保留最后一个版本。
压缩之后的offset可能不是连续的,比如上图中没有6,当从这些offset消费消息时,将会拿到比这个offset大的offset对应的消息,实际上会拿到offset为7的消息,并从这个位置开始消费。
这种策略只适合特殊场景,比如消息的key是用户ID,value是用户的资料,通过这种压缩策略,整个消息集里就保存了所有用户最新的资料。
Kafka 本身是分布式集群,可以采用分区技术,并行度高。
数据采用稀疏索引,可以快速定位要消费的数据。
顺序写磁盘
Kafka 的 producer 生产数据,要写入到 log 文件中,写的过程是一直追加到文件末端,为顺序写
。官网有数据表明,同样的磁盘,顺序写能到 600M/s,而随机写只有 100K/s。这与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间。
页缓存 + 零拷贝技术
参数 | 描述 |
---|---|
log.flush.interval.messages | 强制页缓存刷写到磁盘的条数,默认是 long 的最大值,9223372036854775807。一般不建议修改,交给系统自己管理。 |
log.flush.interval.ms | 每隔多久,刷数据到磁盘,默认是 null。一般不建议修改,交给系统自己管理。 |
为什么Kafka没有采用push模式呢?
不过pull模式也有不足之处,如果Kafka没有数据,消费者可能会
陷入循环中
,一直返回空数据。
Kafka Broker:
消费者:
Consumer Group(CG):消费者组,由多个consumer组成,形成一个消费者组的条件,是所有消费者的groupid相同。
如果向消费者组中添加更多的消费者,
超过主题分区数量,则有部分消费者就会闲置,不会接受任何消息。
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。
参数名称 | 描述 |
---|---|
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.ms | Kafka 消费者和 coordinator 之间的心跳时间,默认 3s 。该条目的值必须小于 session.timeout.ms ,也不应该高于session.timeout.ms 的 1/3。不建议修改 |
session.timeout.ms | Kafka 消费者和 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 条。 |
需求:创建一个独立消费者,消费first主题中数据。
注意:
实现步骤
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);
}
}
}
}
[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)
需求:创建一个独立消费者,消费 first 主题 0 号分区的数据。
实现步骤
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);
}
}
}
}
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)
需求:测试同一个主题的分区数据,只能由一个消费者组中的一个消费。
实现步骤:
注意,要保持groupid相同
properties.put(ConsumerConfig.GROUP_ID_CONFIG,“test”);
Range
,RoundRobin
,Sticky
,CooperativeSticky
。partition.assignment.strategy
,修改分区的分配策略。Range
+ CooperativeSticky
。Kafka可以同时使用Range是对每个topic而言的。
分区按照序号进行排序
,并对消费者按照字母顺序排序
。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 个分区。容易产生数据倾斜!
bin/kafka-topics.sh --bootstrap-server 124.221.5.51:9092 --alter --topic first --partitions 7
注意:分区数可以增加,但是不能减少。
会整体被分配到
1 号消费者或者 2 号消费者。RoundRobin针对集群中所有的topic
而言。
RoundRobin轮询分区策略,是把所有的partition和所有的consumer都列出来
,然后按照hashCode值进行排序
,最后通过轮询算法
来分配partition给到各个消费者。
// 修改分区分配策略
properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, RoundRobinAssignor.class.getName())
停止掉 0 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)。
1 号消费者:消费到 2、5 号分区数据
2 号消费者:消费到 4、1 号分区数据
0 号消费者的任务会按照 RoundRobin
的方式,把数据轮询分成 0 、6 和 3 号分区数据,分别由 1 号消费者或者 2 号消费者消费。
说明:0 号消费者挂掉后,消费者组需要按照超时时间 45s 来判断它是否退出,所以需要等待,时间到了 45s 后,判断它真的退出就会把任务分配给其他 broker 执行。
再次重新发送消息观看结果(45s 以后)。
1 号消费者:消费到 0、2、4、6 号分区数据
2 号消费者:消费到 1、3、5 号分区数据
说明:消费者 0 已经被踢出消费者组,所以重新按照 RoundRobin 方式分配。
粘性分区定义:可以理解为分配的结果带有"粘性的",即在执行一次新的分配之前,考虑上一次分配的结果,尽量少的调整分配的变动,可以节省大量的开销。
粘性分区是 Kafka 从 0.11.x 版本开始引入这种分配策略,首先会尽量均衡的放置分区到消费者上面,在出现同一消费者组内消费者出现问题的时候,会尽量保持原有分配的分区不变化。
需求:设置主题为 first,7 个分区;准备 3 个消费者,采用粘性分区策略,并进行消费,观察消费分配情况。然后再停止其中一个消费者,再次观察消费分配情况。
实现步骤:
// 修改分区分配策略
properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, StickyAssignor.class.getName())
注意:3 个消费者都应该注释掉,之后重启 3 个消费者,如果出现报错,全部停止等会再重启,或者修改为全新的消费者组。
4. _consumer_offsets主题里面采用key和value的方式存储数据。
5. key是group_id+topic+分区号。value就是当前的offset的值。
6. 每隔一段时间,Kafka内部会对这个topic进行compact。也就是每个 group.id+topic+分区号就保留最新数据。
消费 offset 案例
bin/kafka-console-producer.sh --bootstrap-server 124.221.5.51:9092 --topic three
bin/kafka-console-consumer.sh --bootstrap-server 124.221.5.51:9092 --topic three --group test
注意:指定消费者组名称,更好观察数据存储位置(key 是 group.id+topic+分区号)。
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)
为了使我们能够专注于自己的业务逻辑,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);
}
}
}
}
虽然自动提交offset十分便利,但由于其是基于时间提交的,开发人员难以把握offset提交的时机,因此Kafka还提供了手动提交offset的API。
手动提交offset的方法有两种,分别是commitSync(同步提交)和commitAsync(异步提交)。两者的相同点是,都会将本次提交的一批数据最高的偏移量提交,不同点是,同步提交阻塞当前线程,一直到提交成功,并且会自动失败重试(由不可控因素导致,也会出现提交失败);而异步提交则没有失败重试机制,故有可能提交失败。
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();
}
}
}
当 Kafka 中没有初始偏移量(消费者组第一次消费)或服务器上不再存在当前偏移量时(例如该数据已被删除),该怎么办?
可以使用auto.offset.reset = earliest | latest | none 默认是 latest。
指定策略开始消费
//自动将偏移量重置为最早的偏移量,–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);
}
}
}
}
需求:在生产环境中,会遇到最近消费的几个小时数据异常,想重新按照时间消费。例如要求按照时间消费前一天的数据,怎么处理?
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);
}
}
}
}
重复消费:已经消费了数据,但是 offset 没提交。
漏消费:先提交 offset 后消费,有可能会造成数据的漏消费。
如果想完成Consumer端的精准一次性消费,那么需要Kafka消费端将消费过程和提交offset过程做原子绑定。
此时我们需要将Kafka的offset保存到支持事务的自定义介质(比如MySQL)。
如果是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 条 |
左图为 Kafka 现有架构,元数据在 zookeeper 中,运行时动态选举 controller,由controller 进行 Kafka 集群管理。右图为 kraft 模式架构(实验性),不再依赖 zookeeper 集群,而是用三台 controller 节点代替 zookeeper,元数据保存在 controller 中,由 controller 直接进行 Kafka 集群管理。
这样做的好处有以下几个:
#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
/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
/usr/local/kafka/kafka_kraft/bin/kafka-server-start.sh -daemon /usr/local/kafka/kafka_kraft/config/kraft/server.properties
/usr/local/kafka/kafka_kraft/bin/kafka-server-stop.sh
#! /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
sh kf_kraft.sh start
sh kf_kraft.sh stop