文章目录
一.简介
1.什么是Kafka
- Kafka 是一个分布式的基于发布/订阅模式的消息队列(Message Queue),主要应用于大数据实时处理领域。
- 生产者往队列里写消息,消费者从队列里取消息进行业务逻辑处理。一般在架构设计中起到解耦、削峰、异步处理的作用。Kafka适合离线和在线消息消费。 Kafka消息保留在磁盘上,并在群集内复制以防止数据丢失。 Kafka构建在ZooKeeper同步服务之上。它与Apache Storm和Spark非常好地集成,用于实时流式数据分析。
- kafka和JMS(Java Message Service)实现(activeMQ)不同的是:即使消息被消费,消息仍然不会被立即删除.日志文件将会根据broker中的配置要求,保留一定的时间之后删除;比如log文件保留2天,那么两天后,文件会被清除,无论其中的消息是否被消费.kafka通过这种简单的手段,来释放磁盘空间,以及减少消息消费之后对文件内容改动的磁盘IO开支.
2.Kafka的使用场景
- 日志收集:一个公司可以用Kafka可以收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer,例如hadoop、Hbase、Solr等。
- 消息系统:解耦和生产者和消费者、缓存消息等。
- 用户活动跟踪:Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到hadoop、数据仓库中做离线分析和挖掘。
- 运营指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告。
- 流式处理:流行的框架(如Storm和Spark Streaming)从主题中读取数据,对其进行处理,并将处理后的数据写入新主题,供用户和应用程序使用。 Kafka的强耐久性在流处理的上下文中也非常有用。
- 事件源
3.Kafka的好处
-
解耦合:耦合的状态表示当你实现某个功能的时候,不是直接接入当前接口,而利用消息队列,可以将相应的消息发送到消息队列,这样的话,如果接口出了问题,将不会影响到当前的功能。
-
异步处理:异步处理替代了之前的同步处理,异步处理不需要让流程走完就返回结果,可以将消息发送到消息队列中,然后返回结果,剩下让其他业务处理接口从消息队列中拉取消费处理即可。
-
流量削峰:高流量的时候,使用消息队列作为中间件可以将流量的高峰保存在消息队列中,从而防止了系统的高请求,减轻服务器的请求处理压力。
4.消息队列的两种模式
- 点对点模式(一对一,消费者主动拉取数据,消息收到后消息清除)
消息生产者生产消息发送到Queue中,然后消息消费者从Queue中取出并且消费消息。消息被消费以后,queue 中不再有存储,所以消息消费者不可能消费到已经被消费的消息。Queue 支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费。
- 发布/订阅模式(一对多,消费者消费数据之后不会清除消息)
消息生产者(发布)将消息发布到 topic 中,同时有多个消息消费者(订阅)消费该消息。和点对点方式不同,发布到 topic 的消息会被所有订阅者消费。
二.Kafka的安装配置
1.jdk安装
- 1.去Oracle官网下载需要安装的jdk版本,我这里用的是jdk-8u181-linux-x64.tar.gz
- 2.将该压缩包放到/usr/local/jdk目录下,jdk目录需要自己手动创建,也可以叫java,名字自己随意取(见名知意),然后解压该压缩包,输入如下指令:tar zxvf jdk-8u181-linux-x64.tar.gz
- 接下来就该配置环境变量了,输入以下指令进行配置:vim /etc/profile
- 输入完毕并回车,在文件尾部添加如下信息:
export JAVA_HOME=/usr/local/jdk/jdk1.8.0_181
export CLASSPATH=$:CLASSPATH:$JAVA_HOME/lib/
export PATH=$PATH:$JAVA_HOME/bin
注意:第一行的JAVA_HOME=/usr/local/jdk/jdk1.8.0_181 此处等号右边的是自己的jdk实际解压目录。如果不是该目录则需要改成自己的实际目录,其他不变。
- 编辑完之后,保存并退出,然后输入以下指令,刷新环境配置使其生效:source /etc/profile
- 查看jdk是否安装成功,输入指令java -version即可。
2.zookeeper的安装配置
-
下载 zookeeper-3.4.12.tar.gz,并解压:
tar -zxvf zookeeper-3.4.12.tar.gz -
进入到 /usr/local/zookeeper/zookeeper-3.4.12/conf 目录中:
cd zookeeper-3.4.12/conf/ -
复制 zoo_sample.cfg 文件的并命名为为 zoo.cfg:
cp zoo_sample.cfg zoo.cfg -
用 vim 打开 zoo.cfg 文件并修改其内容为如下:
# 数据文件夹
dataDir=/usr/local/zookeeper/zookeeper-3.4.12/data
# 日志文件夹
dataLogDir=/usr/local/zookeeper/zookeeper-3.4.12/logs
- 保存并关闭 zoo.cfg 文件
- 默认情况下,Linux 系统中没有/tmp/zookeeper/data 和/tmp/zookeeper/log 这两个目录,所以接下来还要创建这两个目录:
mkdir -p /tmp/zookeeper/data
mkdir -p /tmp/zookeeper/log
- 进入到 /usr/local/zookeeper/zookeeper-3.4.11/bin 目录中:cd …/bin/
- 用 vim 打开 /etc/ 目录下的配置文件 profile:vim /etc/profile
并在其尾部追加如下内容:
export ZOOKEEPER_HOME=/usr/local/zookeeper/zookeeper-3.4.12/
export PATH=$ZOOKEEPER_HOME/bin:$PATH
export PATH
- 使 /etc/ 目录下的 profile 文件即可生效:source /etc/profile
- 启动 zookeeper 服务:zkServer.sh start
- 如打印如下信息则表明启动成功:
ZooKeeper JMX enabled by default
Using config: /usr/local/zookeeper/zookeeper-3.4.12/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
- 关闭 zookeeper 服务:zkServer.sh stop
以上是关于 ZooKeeper 单机模式的安装与配置,一般在生产环境中使用的都是集群模式,集群模式的配置也比较简单,相比单机模式而言只需要修改一些配置即可。下面以3台机器为例来配置一个 ZooKeeper 集群。首先在这3台机器的 /etc/hosts 文件中添加3台集群的IP地址与机器域名的映射,示例如下(3个IP地址分别对应3台机器):
192.168.0.2 node1
192.168.0.3 node2
192.168.0.4 node3
然后在这3台机器的 zoo.cfg 文件中添加以下配置:
server.0=192.168.0.2:2888:3888
server.1=192.168.0.3:2888:3888
server.2=192.168.0.4:2888:3888
为了便于讲解上面的配置,这里抽象出一个公式,即 server.A=B:C:D。其中A是一个数字,代表服务器的编号,就是前面所说的 myid 文件里面的值。集群中每台服务器的编号都必须唯一,所以要保证每台服务器中的 myid 文件中的值不同。B代表服务器的IP地址。C表示服务器与集群中的 leader 服务器交换信息的端口。D表示选举时服务器相互通信的端口。如此,集群模式的配置就告一段落,可以在这3台机器上各自执行 zkServer.sh start 命令来启动服务。
3. Kafka的安装与配置
在安装完 JDK 和 ZooKeeper 之后,就可以执行 Kafka broker 的安装了,首先也是从官网中下载安装包,示例中选用的安装包是 kafka_2.11-2.0.0.tgz,将其复制至/opt 目录下并进行解压缩,示例如下:
[root@node1 opt]# ll kafka_2.11-2.0.0.tgz
-rw-r--r-- 1 root root 55751827 Jul 31 10:45 kafka_2.11-2.0.0.tgz
[root@node1 opt]# tar zxvf kafka_2.11-2.0.0.tgz
# 解压之后当前/opt目录下生成一个名为kafka_2.11-2.0.0的文件夹
[root@node1 opt]# cd kafka_2.11-2.0.0
[root@node1 kafka_2.11-2.0.0]#
# Kafka的根目录$KAFKA_HOME即为/opt/kafka_2.11-2.0.0,可以将Kafka_HOME添加到/etc/profile文件中,具体做法可以参考前面JDK和ZooKeeper的安装示例
接下来需要修改 broker 的配置文件$KAFKA_HOME/conf/server.properties。主要关注以下几个配置参数即可:
#broker 的全局唯一编号,不能重复,只能是数字。
broker.id=0
#处理网络请求的线程数量
num.network.threads=3
#用来处理磁盘 IO 的线程数量
num.io.threads=8
#发送套接字的缓冲区大小
socket.send.buffer.bytes=102400
#接收套接字的缓冲区大小
socket.receive.buffer.bytes=102400
#请求套接字的缓冲区大小
socket.request.max.bytes=104857600
#kafka 运行日志(数据)存放的路径,路径不需要提前创建,kafka 自动帮你创建,可以配置多个磁盘路径,路径与路径之间可以用","分隔
log.dirs=/opt/module/kafka/datas
#topic 在当前 broker 上的分区个数
num.partitions=1
#用来恢复和清理 data 下数据的线程数量
num.recovery.threads.per.data.dir=1
# 每个 topic 创建时的副本数,默认时 1 个副本
offsets.topic.replication.factor=1
#segment 文件保留的最长时间,超时将被删除
log.retention.hours=168
#每个 segment 文件的大小,默认最大 1G
log.segment.bytes=1073741824
# 检查过期数据的时间,默认 5 分钟检查一次是否数据过期
log.retention.check.interval.ms=300000
#配置连接 Zookeeper 集群地址(在 zk 根目录下创建/kafka,方便管理)
zookeeper.connect=hadoop102:2181,hadoop103:2181,hadoop104:2181/kafka
如果是单机模式,那么修改完上述配置参数之后就可以启动服务。如果是集群模式,那么只需要对单机模式的配置文件做相应的修改即可:确保集群中每个 broker 的 broker.id 配置参数的值不一样,以及 listeners 配置参数也需要修改为与 broker 对应的IP地址或域名,之后就可以各自启动服务。注意,在启动 Kafka 服务之前同样需要确保 zookeeper.connect 参数所配置的 ZooKeeper 服务已经正确启动。
启动 Kafka 服务的方式比较简单,在$KAFKA_HOME 目录下执行下面的命令即可:
bin/kafka-server-start.sh config/server.properties
如果要在后台运行 Kafka 服务,那么可以在启动命令中加入 -daemon 参数或&字符,示例如下:
bin/kafka-server-start.sh –daemon config/server.properties
# 或者
bin/kafka-server-start.sh config/server.properties &
集群启停脚本
vim kf.sh
#! /bin/bash
case $1 in
"start"){
for i in hadoop102 hadoop103 hadoop104
do
echo " --------启动 $i Kafka-------"
ssh $i "/opt/module/kafka/bin/kafka-server-start.sh -
daemon /opt/module/kafka/config/server.properties"
done
};;
"stop"){
for i in hadoop102 hadoop103 hadoop104
do
echo " --------停止 $i Kafka-------"
ssh $i "/opt/module/kafka/bin/kafka-server-stop.sh "
done
};;
esac
4.docker-compose安装kafka
创建docker-compose.yml
version: '3'
services:
zookeeper:
image: wurstmeister/zookeeper
environment:
TZ: Asia/Shanghai
restart: always
ports:
- "2181:2181"
#消息队列
kafka:
image: wurstmeister/kafka
ports:
- 19092:19092
environment:
- KAFKA_LISTENERS=INTERNAL://:19093,CLIENT://:19092
- KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1
- KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,CLIENT:PLAINTEXT
- KAFKA_INTER_BROKER_LISTENER_NAME=INTERNAL
- ALLOW_PLAINTEXT_LISTENER=yes
- KAFKA_ADVERTISED_LISTENERS=INTERNAL://127.0.0.1:19093,CLIENT://127.0.0.1:19092
- KAFKA_ADVERTISED_HOST_NAME=kafka
- KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181
- KAFKA_AUTO_CREATE_TOPICS_ENABLE=true
- TZ=Asia/Shanghai
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /data2/wuyongyu/kafka1/logs:/kafka
depends_on:
- zookeeper
执行
docker-compose up -d
三.Kafka的基础架构和相关命令
1.角色
Kafka像其他Mq一样,也有自己的基础架构,主要存在生产者Producer、Kafka集群Broker、消费者Consumer、注册消息Zookeeper.
- Producer:消息生产者,向Kafka中发布消息的角色。
- Consumer:消息消费者,即从Kafka中拉取消息消费的客户端。
- Consumer Group:消费者组,消费者组则是一组中存在多个消费者,消费者消费Broker中当前Topic的不同分区中的消息,消费者组之间互不影响,所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。某一个分区中的消息只能够一个消费者组中的一个消费者所消费
- Broker:经纪人,一台Kafka服务器就是一个Broker,一个集群由多个Broker组成,一个Broker可以容纳多个Topic。
- Topic:主题,可以理解为一个队列,生产者和消费者都是面向一个Topic
- Partition:分区,为了实现扩展性,一个非常大的Topic可以分布到多个Broker上,一个Topic可以分为多个Partition,每个Partition是一个有序的队列(分区有序,不能保证全局有序)
- Replica:副本Replication,为保证集群中某个节点发生故障,节点上的Partition数据不丢失,Kafka可以正常的工作,Kafka提供了副本机制,一个Topic的每个分区有若干个副本,一个Leader和多个Follower
- Leader:每个分区多个副本的主角色,生产者发送数据的对象,以及消费者消费数据的对象都是Leader。
- Follower:每个分区多个副本的从角色,实时的从Leader中同步数据,保持和Leader数据的同步,Leader发生故障的时候,某个Follower会成为新的Leader。
2.数据可靠性保证
为保证producer发送的数据,能可靠的发送到指定的topic,topic的每个partition收到producer发送的数据后,都需要向producer发送ack(acknowledgement确认收到),如果producer收到ack,就会进行下一轮的发送,否则重新发送数据。
2.kafka启动命令
- 先启动zookeeper
- 单机启动kafka
./bin/kafka-server-start.sh -daemon ./config/server.properties
3.主题命令行操作
- 创建主题
./bin/kafka-topics.sh --bootstrap-server localhost:9092 --topic test --create --replication-factor 1 --partitions 1
选项说明:
–topic 定义 topic 名
–replication-factor 定义副本数
–partitions 定义分区数
- 查看操作主题命令参数
./bin/kafka-topics.sh --bootstrap-server 192.168.111.5:9092 --list
- 查看主题详情
./bin/kafka-topics.sh --bootstrap-server 192.168.111.5:9092 --describe --topic test
分区数为1,副本为1
副本的leader在0上面,对leader进行处理
- 修改主题分区数
./bin/kafka-topics.sh --bootstrap-server 192.168.111.5:9092 --alter --topic test --partitions 3
分区数只能增加,不能减少
- 删除主题
4.生产者和消费者命令行操作
生产者
./bin/kafka-console-producer.sh
- 发送消息
消费者
./bin/kafka-console-consumer.sh
- 消费消息
消费 sise主题中的数据
./bin/kafka-console-consumer.sh --bootstrap-server 192.168.111.5:9092 --topic sise
- 把主题中所有的数据都读取出来(包括历史数据)
./bin/kafka-console-consumer.sh --bootstrap-server 192.168.111.5:9092 --from-beginning --topic sise
无序是因为多分区,kafka只保证单分区的顺序性,不保证多分区聚合的顺序性
四.Kafka 生产者
1.原理
在消息发送的过程中,涉及到了两个线程——main 线程和 Sender 线程。在 main 线程 中创建了一个双端队列 RecordAccumulator。main 线程将消息发送给 RecordAccumulator, Sender 线程不断从 RecordAccumulator 中拉取消息发送到 Kafka Broker,Kafka Broker收到之后会进行副本备份,然后回应acks。
- 生产者重要参数列表
2.异步发送
异步发送是指不需要手动重试,只要把消息递交到dquene即可;消息发送失败会自动重试,不需要我们在回调函数中手动重试。
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1. 创建 kafka 生产者的配置对象
Properties properties = new Properties();
//给kafka配置对象添加配置信息:bootstrap.servers
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.111.5:9092");
// key,value 序列化(必须)
// properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
// properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
//创建kafka生产者对象
KafkaProducer<String,String> producer = new KafkaProducer<String, String>(properties);
//调用send方法 发送消息
for (int i = 0; i < 5; i++) {
producer.send(new ProducerRecord<>("sise","hello world "+i),new Callback(){
//该方法在 Producer 收到 ack 时调用,为异步调用
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (e ==null){
System.out.println("主题:"+recordMetadata.topic()+",分区:"+recordMetadata.partition());
} else {
e.printStackTrace();
}
}
});
}
producer.close();
}
问题
(localhost/127.0.0.1:9092) could not be established. Broker may not be availab
修改kafka配置文件config/server.properties
advertised.listeners才是真正的对外代理地址!那么listeners的作用就不是对外提供服务代理,而是监听
# 允许外部端口连接
listeners = PLAINTEXT://0.0.0.0:9092
# 外部代理地址
advertised.listeners = PLAINTEXT://192.168.66.151:9092
注意:先关掉kafka,再关掉zookeeper 并重启
3.同步发送
只需在异步发送的基础上,再调用一下 get()方法即可。
4.生产者选择分区发送数据
- 分区好处
(1)便于合理使用存储资源,每个Partition在一个Broker上存储,可以把海量的数据按照分区切割成一
块一块数据存储在多台Broker上。合理控制分区的任务,可以实现负载均衡的效果。
(2)提高并行度,生产者可以以分区为单位发送数据;消费者可以以分区为单位进行消费数据。
- 生产者发送消息的分区策略
默认的分区器 DefaultPartitioner
/**
* The default partitioning strategy:
* <ul>
* <li>If a partition is specified in the record, use it
* <li>If no partition is specified but a key is present choose a
partition based on a hash of the key
* <li>If no partition or key is present choose the sticky
partition that changes when the batch is full.
*
* See KIP-480 for details about sticky partitioning.
*/
public class DefaultPartitioner implements Partitioner {
… …
}
eg1:
指定数据发送到 2号分区,key 为空
eg2:
没有指明 partition 值但有 key 的情况下,将 key 的 hash 值与 topic 的 partition 数进行取余得到 partition 值。
- 自定义分区器
例如我们实现一个分区器实现,发送过来的数据中如果包含 sise,就发往 0 号分区,不包含 sise,就发往 1 号分区。
(1)定义类实现 Partitioner 接口。
(2)实现 3 个方法:partition,close,configure; 重写 partition()方法; 返回分区号
/*
* 返回信息对应的分区
* @param topic 主题
* @param key 消息的 key
* @param keyBytes 消息的 key 序列化后的字节数组
* @param value 消息的 value
* @param valueBytes 消息的 value 序列化后的字节数组
* @param cluster 集群元数据可以查看分区信息
* @return
*/
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
//获取消息
String msgValue = value.toString();
//创建partition
int partition;
//判断消息是否包含sise
if (msgValue.contains("sise")){
partition=0;
} else {
partition = 1;
}
//返回分区号
return partition;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
}
}
使用分区器的方法,在生产者的配置中添加自定义分区器
properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.sise.kafka.MyPartitioner");
5.生产者提高吞吐量
batch.size:批次大小,默认 16K properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
linger.ms:等待时间,默认linger.ms是0毫秒,也就是说没发布一条消息都会直接往broker发送,这样会影响效率,如果把linger.ms和batch.size调太大就会导致消息的延迟
compression.type: 把消息压缩后发送过去提高吞吐量,默认 none,可配置值 gzip、snappy、lz4 和 zstd
recordAccumulator调大,当分区很多时,如果缓存区太小会导致消息加到缓冲区太慢(?)
//batch.size:批次大小,默认 16K
properties.put(ProducerConfig.BATCH_SIZE_CONFIG,16384);
//linger.ms:等待时间,默认 0
properties.put(ProducerConfig.LINGER_MS_CONFIG,1);
//RecordAccumulator:缓冲区大小,默认 32M:buffer.memory
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,33554432);
//compression.type:压缩,默认 none,可配置值 gzip、snappy、lz4 和 zstd
properties.put(ProducerConfig.COMPRESSION_TYPE_CONFIG,"snappy");
6.数据可靠和数据重复
acks=0:表示producer发送消息后,不管broker是否接收到,都会继续发下一条消息,这样会导致消息丢失
acks=1: 表示producer发送消息后,broker的leader进行数据落盘后响应acks,代表这条消息发送完成,但是当leader对消息落盘后,还没有同步到follower就挂了,而且ack已经回复了,重新选举一个leader之后,这条消息也不会再发送了,这样就导致了消息也会丢失
acks=-1: 表示producer发送消息后,要等broker在leader进行数据落盘后,并且同步到follower成功后才返回ack,但是这样当同步到follower时,出现一个follower宕机,导致一直无响应ack,最后导致服务崩盘,事情更加严重
// 设置 acks
properties.put(ProducerConfig.ACKS_CONFIG, "all");
// 重试次数 retries,默认是 int 最大值,2147483647
properties.put(ProducerConfig.RETRIES_CONFIG, 3);
当acks=-1会存在数据重复问题
producer发送消息后,leader刚和其他follower同步完成后返回ack时挂了,导致producer没有收到ack,这是重新选举了leader,producer由于没有收到消息所以重新发送了一个消息,这是因为之前宕掉的leader已经同步过了,这是就导致了消息重复了
解决方法:使用幂等性
// 设置 acks
properties.put(ProducerConfig.ACKS_CONFIG, "all");
// 重试次数 retries,默认是 int 最大值,2147483647
properties.put(ProducerConfig.RETRIES_CONFIG, 3);
7.幂等性和事务
幂等性就是指Producer不论向Broker发送多少次重复数据,Broker端都只会持久化一条,保证了不重复。
精确一次(Exactly Once) = 幂等性 + 至少一次( ack=-1 + 分区副本数>=2 + ISR最小副本数量>=2) 。
重复数据的判断标准:具有<PID, Partition, SeqNumber>相同主键的消息提交时,Broker只会持久化一条。其中PID是Kafka每次重启都会分配一个新的;Partition 表示分区号;Sequence Number是单调自增的。
所以幂等性只能保证的是在单分区单会话内不重复。
-
如何使用幂等性
开启参数 enable.idempotence 默认为 true,false 关闭。 -
Kafka 的事务一共有如下 5 个 API
// 1 初始化事务
void initTransactions();
// 2 开启事务
void beginTransaction() throws ProducerFencedException;
// 3 在事务内提交已经消费的偏移量(主要用于消费者)
void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets,String consumerGroupId) throws ProducerFencedException;
// 4 提交事务
void commitTransaction() throws ProducerFencedException;
// 5 放弃事务(类似于回滚事务的操作)
void abortTransaction() throws ProducerFencedException;
- 单个 Producer,使用事务保证消息的仅一次发送
public class ProducerTransaction {
public static void main(String[] args) {
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
"192.168.9.50:9092");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
StringSerializer.class.getName());
// 设置事务 id(必须),事务 id 任意起名
properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG,
"transaction_id_0");
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
// 初始化事务
producer.initTransactions();
// 开启事务
producer.beginTransaction();
// 4. 调用 send 方法,发送消息
try {
for (int i = 0; i < 5; i++) {
producer.send(new ProducerRecord("first","atguigu " + i));
}
int i = 10/0;
// 提交事务
producer.commitTransaction();
} catch (Exception e){
// 终止事务
producer.abortTransaction();
} finally {
// 5. 关闭资源
producer.close();
}
}
}
8.数据乱序
五.Kafka Broker
1.Kafka Broker 工作流程
- Zookeeper中存储的Kafka 信息
- 工作流程
1、broker启动后注册id到zk
2、3、哪个broker先注册成为controller(只要一个),controller启动时就起一个监视器监视ZK/brokers/ids/子节点。当存在broker启动加入集群后都会在ZK/brokers/ids/增加一个子节点brokerId,控制器的监视器发现这种变化后,控制器开始执行broker加入的相关流程并更新元数据信息到集群。
4、controller决定了leader选举
5、controller将所有节点和leader上传至zk,也就是leader和isr(存活的节点包括leader本身)
6、Controller Follower节点从zk同步相关信息
producer发送消息后,broker会对数据精选副本备份,然后进行数据落盘
7、当 leader挂了,那么controller leader就会监听到节点变化,然后去获取isr进行选举新的leader(在isr存活为前提,按照ar排在前面的优先)
8、如果controller leader挂了那么参考下面 控制器Leader如何Failover
2.controller的作用
https://www.bilibili.com/read/cv15660873/
1、Kafka Controller是Kakfa服务端Broker的概念,Broker集群有多台,但只有一台Broker可以扮演控制器的角色;
2、某台Broker一旦成为Controller,它用于以下权力:完成对集群成员管理、主题维护和分区的管理,如集群broker信息、Topic维护、Partition维护、分区选举ISR、同步元信息给其他Broker等。
- 为何要引进controller ?
多个watch对zk负载太大
历史版本的实现架构如上图所示,对于分区和副本的状态的管理依赖于zookeeper的Watcher和队列:每一个broker都会在zookeeper注册Watcher,所以zookeeper就会出现大量的Watcher, 如果宕机的broker上的partition比较多,会造成多个Watcher触发,造成集群内大规模调整;每一个replica都要去再次zookeeper上注册监视器,当集群规模很大的时候,zookeeper负担很重。所以严重依赖zk实现元数据的协调同步,这样做的弊端有以下几点:
当Kafka集群规模很大的时候,zookeeper负担很重。因为所有Broker的所有主题分区都要与zk建立连接,分区上下线和Broker的宕机或重新启动,会触发多个watch,给zk带来大量的写压力;
zk压力大,容易引起网络风暴,不利于Kafka元数据的稳定性;
这种设计容易出现脑裂(zk集群的brain-split原理,请自行补充知识)和羊群效应。
- 优化后
Kafka官方团队也意识到严重依赖zk带来的弊端,于是对架构做了优化和调整。核心思想:
降低对zk的依赖程度,减少zk压力
元数据的控制和管理由中心化节点维护;
异步化同步元数据给集群所有节点,并做好同步的容错处理。
- 控制器的启动初始化
1、当broker启动的时候,都会创建KafkaController对象;
2、每个节点上的KafkaController会在指定的zookeeper路径下创建临时节点,只有第一个成功创建的节点的KafkaController才可以成为Leader,其余的都是follower;
3、集群中只能有一个Leader对外提供服务;
- 控制器运行时对元信息的处理流程
只有Controller Leader向zk注册watch并监听元数据的变化,其他broker不用监听zookeeper的状态变化。大概流程:
- 用户通过命令或控制台执行集群的管理操作,比如:扩锁容Broker集群、创建/删除Topic、扩/缩容Partition;
- 第一步会触发集群在zk对应路径上变更节点信息,比如/brokers/topics;
- Controller Leader收到watch的监听推送消息,通过监听接口收到对应的变更信息,比如主题信息、分区信息、集群Broker信息等;
- Controller Leader根据推送的事件类型,做不同的逻辑处理。比如:Broker加入或删除的相关流程、Topic新增或删除的相关流程、 分区重分配引起的重分配相关流程 、Partition扩展的相关流程(即分区Leader选举和ISR同步);
- Controller Leader同步元信息至集群的Controller Follower节点。
- 控制器Leader如何Failover
如果控制器Leader所在broker退出、崩溃或与ZK会话失效则ZK会删除/controller内该子节点,各个broker的监视器收到这种变化后,每个broker开始竞争直到有一个竞争成为新的控制器Leader,并向/controller注册子节点,以及向/controller_EPOCH注册子节点。此时,新一轮的controller周期开始运行,新的Leader节点继续肩负起监听元数据变化、处理元信息并同步给集群的任务。
3.Broker 重要参数
3.分区与副本
-
分区
首先,从数据组织形式来说,kafka有三层形式,kafka有多个主题,每个主题有多个分区,每个分区又有多条消息。
而每个分区可以分布到不同的机器上,这样一来,从服务端来说,分区可以实现高伸缩性,以及负载均衡,动态调节的能力。
当然多分区就意味着每条消息都难以按照顺序存储,那么是不是意味着这样的业务场景kafka就无能为力呢?不是的,最简单的做法可以使用单个分区,单个分区,所有消息自然都顺序写入到一个分区中,就跟顺序队列一样了。而复杂些的,还有其他办法,那就是使用按消息键,将需要顺序保存的消息存储的单独的分区,其他消息存储其他分区,这个在下面会介绍。
我们可以通过replication-factor指定创建topic时候所创建的分区数。
bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic test
比如这里就是创建了1个分区,的主题。值得注意的是,还有一种创建主题的方法,是使用zookeeper参数的,那种是比较旧的创建方法,这里是使用bootstrap参数的。
- 分区写入策略
所谓分区写入策略,即是生产者将数据写入到kafka主题后,kafka如何将数据分配到不同分区中的策略。
常见的有三种策略,轮询策略,随机策略,和按键保存策略。其中轮询策略是默认的分区策略,而随机策略则是较老版本的分区策略,不过由于其分配的均衡性不如轮询策略,故而后来改成了轮询策略为默认策略。
轮询策略
所谓轮询策略,即按顺序轮流将每条数据分配到每个分区中。
举个例子,假设主题test有三个分区,分别是分区A,分区B和分区C。那么主题对接收到的第一条消息写入A分区,第二条消息写入B分区,第三条消息写入C分区,第四条消息则又写入A分区,依此类推。
轮询策略是默认的策略,故而也是使用最频繁的策略,它能最大限度保证所有消息都平均分配到每一个分区。除非有特殊的业务需求,否则使用这种方式即可。
随机策略
随机策略,也就是每次都随机地将消息分配到每个分区。其实大概就是先得出分区的数量,然后每次获取一个随机数,用该随机数确定消息发送到哪个分区。
在比较早的版本,默认的分区策略就是随机策略,但其实使用随机策略也是为了更好得将消息均衡写入每个分区。但后来发现对这一需求而言,轮询策略的表现更优,所以社区后来的默认策略就是轮询策略了。
按键保存策略
按键保存策略,就是当生产者发送数据的时候,可以指定一个key,计算这个key的hashCode值,按照hashCode的值对不同消息进行存储。
至于要如何实现,那也简单,只要让生产者发送的时候指定key就行。欸刚刚不是说默认的是轮询策略吗?其实啊,kafka默认是实现了两个策略,没指定key的时候就是轮询策略,有的话那激素按键保存策略了。
上面有说到一个场景,那就是要顺序发送消息到kafka。前面提到的方案是让所有数据存储到一个分区中,但其实更好的做法,就是使用这种按键保存策略。
让需要顺序存储的数据都指定相同的键,而不需要顺序存储的数据指定不同的键,这样一来,即实现了顺序存储的需求,又能够享受到kafka多分区的优势,岂不美哉。
实现自定义分区
kafka提供了两种让我们自己选择分区的方法,第一种是在发送producer的时候,在ProducerRecord中直接指定,但需要知道具体发送的分区index,所以并不推荐。
第二种则是需要实现Partitioner.class类,并重写类中的partition(String topic, Object key, byte[] keyBytes,Object value, byte[] valueBytes, Cluster cluster) 方法。后面在生成kafka producer客户端的时候直接指定新的分区类就可以了。
- 副本
说完了分区,再来说说副本。先说说副本的基本内容,在kafka中,每个主题可以有多个分区,每个分区又可以有多个副本。这多个副本中,只有一个是leader,而其他的都是follower副本。仅有leader副本可以对外提供服务。
多个follower副本通常存放在和leader副本不同的broker中。通过这样的机制实现了高可用,当某台机器挂掉后,其他follower副本也能迅速”转正“,开始对外提供服务。
- 副本的作用
在kafka中,实现副本的目的就是冗余备份,且仅仅是冗余备份,所有的读写请求都是由leader副本进行处理的。follower副本仅有一个功能,那就是从leader副本拉取消息,尽量让自己跟leader副本的内容一致。
正常情况下,kafka里的数据都不能只有一份。假设我们保存了N个副本,即topic每个partition都有N个副本(Replica)。并且副本的个数一定小于broker个数。(因为每份数据的副本必须保存在不同的broker,否则没有意义,因为如果一份数据的副本保存在同一个broker,那么这个broker挂了,则数据依然丢失。)所以对于每个partition而言,每个broker上最多只有一个副本,因此我们常常使用broker-id表示副本。kafka还有个机制,就是会默认将副本均匀分布到所有的broker上。
- 说说follower副本为什么不对外提供服务?
这个问题本质上是对性能和一致性的取舍。试想一下,如果follower副本也对外提供服务那会怎么样呢?首先,性能是肯定会有所提升的。但同时,会出现一系列问题。类似数据库事务中的幻读,脏读。
比如你现在写入一条数据到kafka主题a,消费者b从主题a消费数据,却发现消费不到,因为消费者b去读取的那个分区副本中,最新消息还没写入。而这个时候,另一个消费者c却可以消费到最新那条数据,因为它消费了leader副本。
看吧,为了提高那么些性能而导致出现数据不一致问题,那显然是不值得的。
- leader副本挂掉后,如何选举新副本?
如果你对zookeeper选举机制有所了解,就知道zookeeper每次leader节点挂掉时,都会通过内置id,来选举处理了最新事务的那个follower节点。
从结果上来说,kafka分区副本的选举也是类似的,都是选择最新的那个follower副本,但它是通过一个In-sync(ISR)副本集合实现。
kafka会将与leader副本保持同步的副本放到ISR副本集合中。当然,leader副本是一直存在于ISR副本集合中的,在某些特殊情况下,ISR副本中甚至只有leader一个副本。
当leader挂掉时,kakfa通过zookeeper感知到这一情况,在ISR副本中选取新的副本成为leader,对外提供服务。
但这样还有一个问题,前面提到过,有可能ISR副本集合中,只有leader,当leader副本挂掉后,ISR集合就为空,这时候怎么办呢?这时候如果设置unclean.leader.election.enable参数为true,那么kafka会在非同步,也就是不在ISR副本集合中的副本中,选取出副本成为leader,但这样意味这消息会丢失,这又是可用性和一致性的一个取舍了。
- ISR副本集合保存的副本的条件是什么?
上面一直说ISR副本集合中的副本就是和leader副本是同步的,那这个同步的标准又是什么呢?
答案其实跟一个参数有关:replica.lag.time.max.ms。
前面说到follower副本的任务,就是从leader副本拉取消息,如果持续拉取速度慢于leader副本写入速度,慢于时间超过replica.lag.time.max.ms后,它就变成“非同步”副本,就会被踢出ISR副本集合中。但后面如何follower副本的速度慢慢提上来,那就又可能会重新加入ISR副本集合中了。
4. 节点服役和退役
服役新节点
执行负载均衡操作
- 创建一个要均衡的主题
vim topics-to-move.json
{
"topics": [
{"topic": "first"}
],
"version": 1
}
- 生成一个负载均衡的计划
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --topics-to-move-json-file
topics-to-move.json --broker-list "0,1,2,3" --generate
Current partition replica assignment
{"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[0,2,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[2,1,0],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[1,0,2],"log_dirs":["any","any","any"]}]}
Proposed partition reassignment configuration
{"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,3,0],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[3,0,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[0,1,2],"log_dirs":["any","any","any"]}]}
- 创建副本存储计划(所有副本存储在 broker0、broker1、broker2、broker3 中)。
vim increase-replication-factor.json
{"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,3,0],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[3,0,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[0,1,2],"log_dirs":["any","any","any"]}]}
- 执行副本存储计划。
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092
--reassignment-json-file increase-replication-factor.json --execute
- 验证副本存储计划。
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file
increase-replication-factor.json --verify
Status of partition reassignment:
Reassignment of partition first-0 is complete.
Reassignment of partition first-1 is complete.
Reassignment of partition first-2 is complete.
Clearing broker-level throttles on brokers 0,1,2,3
Clearing topic-level throttles on topic first
退役旧节点
1.执行负载均衡操作
先按照退役一台节点,生成执行计划,然后按照服役时操作流程执行负载均衡。
- 创建一个要均衡的主题
vim topics-to-move.json
{
"topics": [
{"topic": "first"}
],
"version": 1
}
- 创建执行计划。
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --topics-to-move-json-file
topics-to-move.json --broker-list "0,1,2" --generate
Current partition replica assignment
{"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,0,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[3,1,2],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[0,2,3],"log_dirs":["any","any","any"]}]}
Proposed partition reassignment configuration
{"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,0,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[0,1,2],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[1,2,0],"log_dirs":["any","any","any"]}]}
- 创建副本存储计划(所有副本存储在 broker0、broker1、broker2 中)。
vim increase-replication-factor.json
{"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,0,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[0,1,2],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[1,2,0],"log_dirs":["any","any","any"]}]}
- 执行副本存储计划。
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file
increase-replication-factor.json --execute
- 验证副本存储计划。
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file
increase-replication-factor.json --verify
```java
Status of partition reassignment:
Reassignment of partition first-0 is complete.
Reassignment of partition first-1 is complete.
Reassignment of partition first-2 is complete.
Clearing broker-level throttles on brokers 0,1,2,3
Clearing topic-level throttles on topic first
2.执行停止命令
在 hadoop105 上执行停止命令即可。
```java
bin/kafka-server-stop.sh
5.leader选举流程
Kafka 集群中有一个 broker 的 Controller 会被选举为 Controller Leader,负责管理集群 broker 的上下线,所有 topic 的分区副本分配和 Leader 选举等工作。
每个主题有多个分区,每个分区有多个副本,但是具体操作的还是leader副本
查看leader分配情况(leader的选举是按ar进行轮询选举的)
Replicas就是ar
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe
--topic atguigu1
Topic: atguigu1 TopicId: awpgX_7WR-OX3Vl6HE8sVg PartitionCount: 4 ReplicationFactor: 4
Configs: segment.bytes=1073741824
Topic: atguigu1 Partition: 0 Leader: 3 Replicas: 3,0,2,1 Isr: 3,0,2,1
Topic: atguigu1 Partition: 1 Leader: 1 Replicas: 1,2,3,0 Isr: 1,2,3,0
Topic: atguigu1 Partition: 2 Leader: 0 Replicas: 0,3,1,2 Isr: 0,3,1,2
Topic: atguigu1 Partition: 3 Leader: 2 Replicas: 2,1,0,3 Isr: 2,1,0,3
把 broker3关闭治好后,分区0的leader由3往后退,变成了0
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe
--topic atguigu1
Topic: atguigu1 TopicId: awpgX_7WR-OX3Vl6HE8sVg PartitionCount: 4 ReplicationFactor: 4
Configs: segment.bytes=1073741824
Topic: atguigu1 Partition: 0 Leader: 0 Replicas: 3,0,2,1 Isr: 0,2,
6.Follower 故障处理
7.leader故障处理
8.分区副本分配
如果 kafka 服务器只有 4 个节点,那么设置 kafka 的分区数大于服务器台数,在 kafka 底层如何分配存储副本呢?
1)创建 16 分区,3 个副本
每个的间隔都是有顺序的
9.手动调整分区副本存储
手动调整分区副本存储的步骤如下:
- 创建一个新的 topic,名称为 three。
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --create
--partitions 4 --replication-factor 2 --topic three
- 查看分区副本存储情况、
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic three
- 创建副本存储计划(所有副本都指定存储在 broker0、broker1 中)。
vim increase-replication-factor.json
{
"version":1,
"partitions":[
{"topic":"three","partition":0,"replicas":[0,1]},
{"topic":"three","partition":1,"replicas":[0,1]},
{"topic":"three","partition":2,"replicas":[1,0]},
{"topic":"three","partition":3,"replicas":[1,0]}]
}
- 执行副本存储计划。
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092
--reassignment-json-file increase-replication-factor.json --execute
- 验证副本存储计划。
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092
--reassignment-json-file increase-replication-factor.json --verify
- 查看分区副本存储情况
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic three
10.Leader Partition 负载平衡
如果集群中出现z机器宕机导致leader集中到了部分几台服务器就会出现有些机器压力过高,kafka提供了自平衡
12.新增保存副本因子
在生产环境当中,由于某个主题的重要等级需要提升,我们考虑增加副本。副本数的
增加需要先制定计划,然后根据计划执行。
创建 topic
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --create
--partitions 3 --replication-factor 1 --topic four
手动增加副本存储
- 创建副本存储计划(所有副本都指定存储在 broker0、broker1、broker2 中)。
vim increase-replication-factor.json
{"version":1,
"partitions":[
{"topic":"four","partition":0,"replicas":[0,1,2]},
{"topic":"four","partition":1,"replicas":[0,1,2]},
{"topic":"four","partition":2,"replicas":[0,1,2]}]}
- 执行副本存储计划
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092
--reassignment-json-file increase-replication-factor.json --execute
13.文件存储机制
14. 文件存储位置
kafka数据的存储位置,在config/server.properties中的log.dirs中配置;
本次演示kafka的日志存储配置为:log.dirs=/tmp/kafka-logs
15.分区与存储方式的关系
partition是以文件的形式存储在文件系统中,比如,创建了一个名为kafkaData的topic,有4个partition,那么在Kafka的数据目录中(由配置文件中的log.dirs指定的)中就有这样4个目录: kafkaData-0, kafkaData-1,kafkaData-2,kafkaData-3,其命名规则为<topic_name>-<partition_id>,里面存储的分别就是这4个partition的数据。
3、每个数据目录的子目录都有xx.index ,xx.log ,xx.timeindex三个文件组成
16.演示
1、创建一个主题
创建一个带有4个分区,2个副本的topic(kafkaData)
[root@master bin]# ./kafka-topics.sh --create --zookeeper master:2181,slaves1:2181,slaves2:2181 --replication-factor 2 --partitions 4 --topic kafkaData
Created topic "kafkaData".
2、查看数据目录中的效果
[root@master kafka-logs]# ls /tmp/kafka-logs/
kafkaData-0
kafkaData-1
[root@slaves1 bin]# ls /tmp/kafka-logs/
kafkaData-1
kafkaData-2
kafkaData-3
[root@slaves2 bin]# ls /tmp/kafka-logs/
kafkaData-0
kafkaData-2
kafkaData-3
由上可以看出kafka的第一个分区kafka-0的两个副本分别在master、slaves2两个节点上;其他同理;
命令查看
[root@master bin]# ./kafka-topics.sh --describe --zookeeper master:2181,slaves1:2181,slaves2:2181 --topic kafkaData
Topic:kafkaData PartitionCount:4 ReplicationFactor:2 Configs:
Topic: kafkaData Partition: 0 Leader: 2 Replicas: 2,0 Isr: 2,0
Topic: kafkaData Partition: 1 Leader: 0 Replicas: 0,1 Isr: 0,1
Topic: kafkaData Partition: 2 Leader: 1 Replicas: 1,2 Isr: 1,2
Topic: kafkaData Partition: 3 Leader: 2 Replicas: 2,1 Isr: 2,1
Leader: 指定主分区的broker id;
Replicas: 副本在那些机器上;
Isr : 可以做为主分区的broker id;
3、向此主题写入大批量数据
4、查看segment file
以kafkaData-0为例:
使用kafka安装bin目录下的kafka-run-class.sh分别查看这些文件的内容:
(1)查看log文件:
[root@master bin]# ./kafka-run-class.sh kafka.tools.DumpLogSegments --files /tmp/kafka-logs/kafkaData-0/00000000000000000000.log --print-data-log
...
offset: 7211 position: 448934 CreateTime: 1587632825139 isvalid: true payloadsize: 29 magic: 1 compresscodec: NONE crc: 995429819 payload: 阳光小区,11,1587632825139
offset: 7212 position: 448997 CreateTime: 1587632825139 isvalid: true payloadsize: 28 magic: 1 compresscodec: NONE crc: 2299568067 payload: 单身小区,5,1587632825139
offset: 7213 position: 449059 CreateTime: 1587632825139 isvalid: true payloadsize: 29 magic: 1 compresscodec: NONE crc: 2772987037 payload: 花花小区,12,1587632825139
offset: 7214 position: 449122 CreateTime: 1587632825139 isvalid: true payloadsize: 28 magic: 1 compresscodec: NONE crc: 2369864650 payload: 阳光小区,6,1587632825139
offset: 7215 position: 449184 CreateTime: 1587632825139 isvalid: true payloadsize: 28 magic: 1 compresscodec: NONE crc: 820724779 payload: 单身小区,4,1587632825139
...
**payload:**为消息体
(2)查看index文件:
[root@master bin]# ./kafka-run-class.sh kafka.tools.DumpLogSegments --files /tmp/kafka-logs/kafkaData-0/00000000000000000000.index --print-data-log
...
offset: 1269114 position: 79002134
offset: 1269231 position: 79009410
offset: 1269316 position: 79014708
offset: 1269456 position: 79023419
offset: 1269715 position: 79039540
offset: 1269838 position: 79047192
offset: 1269933 position: 79053095
offset: 1270083 position: 79062430
...
(3)查看timeindex文件
[root@master bin]# ./kafka-run-class.sh kafka.tools.DumpLogSegments --files /tmp/kafka-logs/kafkaData-0/00000000000000000000.timeindex --print-data-log
...
timestamp: 1587632824453 offset: 1867
timestamp: 1587632824473 offset: 1975
timestamp: 1587632824507 offset: 1987
timestamp: 1587632824658 offset: 2657
timestamp: 1587632824759 offset: 3057
timestamp: 1587632824810 offset: 3468
...
注意:
segment file 组成:由2部分组成,分别为index file和data file,这两个文件是一一对应的,后缀”.index”和”.log”分别表示索引文件和数据文件;
segment file 命名规则:partition的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset,ofsset的数值最大为64位(long类型),20位数字字符长度,没有数字用0填充。
17.数据存储原理分析
1、说明
(1)在生产环境中,kafkaData-0下不会只存在一个index、log、timeindex文件;而是像这样:
(2)、我们将index文件称为索引文件,里面存储着大量元数据;log文件称为数据文件,里面存储着大量消息;
2、数据文件建立索引原理
数据文件分段使得可以在一个较小的数据文件中查找对应offset的Message了,但是这依然需要顺序扫描才能找到对应offset的Message。为了进一步提高查找的效率,Kafka为每个分段后的数据文件建立了索引文件,文件名与数据文件的名字是一样的,只是文件扩展名为.index。
索引文件中包含若干个索引条目,每个条目表示数据文件中一条Message的索引。索引包含两个部分(均为4个字节的数字),分别为相对offset和position。
相对offset:因为数据文件分段以后,每个数据文件的起始offset不为0,相对offset表示这条Message相对于其所属数据文件中最小的offset的大小。举例,分段后的一个数据文件的offset是从20开始,那么offset为25的Message在index文件中的相对offset就是25-20 = 5。存储相对offset可以减小索引文件占用的空间。
position,表示该条Message在数据文件中的绝对位置。只要打开文件并移动文件指针到这个position就可以读取对应的Message了。
Kafka高效文件存储设计特点
(1)Kafka把topic中一个parition大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用。
(2)通过索引信息可以快速定位message和确定response的最大大小。
(3)通过index元数据全部映射到memory,可以避免segment file的IO磁盘操作。
(4)通过索引文件稀疏存储,可以大幅降低index文件元数据占用空间大小。
稀疏索引
为什么使用稀疏索引?
因为占用内存少,内存可是宝贵的资源,而且Kafka面对海量消息存储时,使用稠密索引带来的额外存储空间占用会给存储带来很大的挑战,与其这样不如牺牲一点检索效率换取更大的索引存储空间。这样可以避免索引文件占用过多的内存,从而可以在内存中保存更多的索引,而牺牲的检索效率对消息延迟影响不太大,换句话说业务能够容忍。Kafka对应的就是Broker 端参数log.index.interval.bytes 值,默认4KB,即4KB的消息建一条索引。
18.文件清理策略
Kafka 中默认的日志保存时间为 7 天,可以通过调整如下参数修改保存时间。
⚫ log.retention.hours,最低优先级小时,默认 7 天。
⚫ log.retention.minutes,分钟。
⚫ log.retention.ms,最高优先级毫秒。
⚫ log.retention.check.interval.ms,负责设置检查周期,默认 5 分钟。
那么日志一旦超过了设置的时间,怎么处理呢? Kafka 中提供的日志清理策略有 delete 和 compact 两种。
1)delete 日志删除:将过期数据删除
⚫ log.cleanup.policy = delete 所有数据启用删除策略
(1)基于时间:默认打开。以 segment 中所有记录中的最大时间戳作为该文件时间戳。 (2)基于大小:默认关闭。超过设置的所有日志总大小,删除最早的 segment。 log.retention.bytes,默认等于-1,表示无穷大。
思考:如果一个 segment 中有一部分数据过期,一部分没有过期,怎么处理?
答案1:看所有数据是否超过7天,超过7天就删掉。不超过7天就保留,通俗就是不求同年同月同日生,但求同年同月同日四
答案2:看日志大小,如果超过设置的所有日志总大小,则删除,没超过,则保留
2)compact 日志压缩:
19.高效读写数据(面试题)
1)Kafka 本身是分布式集群,可以采用分区技术,并行度高
2)读数据采用稀疏索引,可以快速定位要消费的数据
3)顺序写磁盘 Kafka 的 producer 生产数据,要写入到 log 文件中,写的过程是一直追加到文件末端, 为顺序写。官网有数据表明,同样的磁盘,顺序写能到 600M/s,而随机写只有 100K/s。这 与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间。
4)页缓存 + 零拷贝技术
六.Kafka 消费者
1.消费方式
kafka消费方式是pull模式
2.消费者工作流程
一个消费者可以消费多个分区的数据,但是每个分区的数据只能由一个消费组中一个消费者消费;kafka将消费偏移量offset保存到系统主题(之前是保存到zookeeper,但是这样会导致与zookeeper交互过于频繁,影响效率)
3.消费者组原理
4.消费者组初始化流程
1、consumer发送joinGroup请求
2、coordinator选出一个consumer作为leader
3、把要消费的topic元数据发送给leader消费者
4、consumer leader制定消费方案
5、把消费方案发给coordinator
6、coordinator把方案发给其他各个consumer
group发送sendFetches发送消费请求,CousumerNetworkClient去抓取数据放到队列,group去队列拉取消费数据反序列化然后处理
+消费者参数
5. 消费者 API
独立消费者案例(订阅主题)
-
需求
注意:在消费者 API 代码中必须配置消费者组 id。命令行启动消费者不填写消费者组
id 会被自动填写随机的消费者组 id。 -
消费者
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1. 创建 kafka 生产者的配置对象
Properties properties = new Properties();
//给kafka配置对象添加配置信息:bootstrap.servers
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.111.5:9092");
// key,value 反序列化(必须)
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
//配置消费者组
properties.put(ConsumerConfig.GROUP_ID_CONFIG,"dessw");
//创建消费者对象
KafkaConsumer<String,String> kafkaConsumer =new KafkaConsumer<String, String>(properties);
//注册要消费的主题(可以多个)
ArrayList<String> topics = new ArrayList<>();
topics.add("first");
topics.add("second");
kafkaConsumer.subscribe(topics);
//拉取数据打印
while (true) {
//设置1s消费一批数据
ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
//打印消费到的数据
for (ConsumerRecord<String,String> record : consumerRecords){
System.out.println(record);
}
}
}
- 生产者
- 消费
独立消费者案例(订阅分区) - 需求
- 消费者
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1. 创建 kafka 生产者的配置对象
Properties properties = new Properties();
//给kafka配置对象添加配置信息:bootstrap.servers
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.111.5:9092");
// key,value 反序列化(必须)
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
//配置消费者组
properties.put(ConsumerConfig.GROUP_ID_CONFIG,"dessw");
//创建消费者对象
KafkaConsumer<String,String> kafkaConsumer =new KafkaConsumer<String, String>(properties);
//消费某个主题的某个分区
ArrayList<TopicPartition> topicPartitions = new ArrayList<>();
topicPartitions.add(new TopicPartition("first",0));
kafkaConsumer.assign(topicPartitions);
//拉取数据打印
while (true) {
//设置1s消费一批数据
ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
//打印消费到的数据
for (ConsumerRecord<String,String> record : consumerRecords){
System.out.println(record);
}
}
}
消费者组案例
-
需求
-
生产者
-
消费者组
复制一份基础消费者的代码,在 IDEA 中同时启动,即可启动同一个消费者组中
的两个消费者。
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1. 创建 kafka 生产者的配置对象
Properties properties = new Properties();
//给kafka配置对象添加配置信息:bootstrap.servers
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.111.5:9092");
// key,value 反序列化(必须)
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
//配置消费者组
properties.put(ConsumerConfig.GROUP_ID_CONFIG,"dessw");
//创建消费者对象
KafkaConsumer<String,String> kafkaConsumer =new KafkaConsumer<String, String>(properties);
//注册要消费的主题(可以多个)
ArrayList<String> topics = new ArrayList<>();
topics.add("first");
topics.add("second");
kafkaConsumer.subscribe(topics);
//消费某个主题的某个分区
// ArrayList<TopicPartition> topicPartitions = new ArrayList<>();
// topicPartitions.add(new TopicPartition("first",0));
// kafkaConsumer.assign(topicPartitions);
//拉取数据打印
while (true) {
//设置1s消费一批数据
ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
//打印消费到的数据
for (ConsumerRecord<String,String> record : consumerRecords){
System.out.println(record);
}
}
}
- 结论
启动代码中的生产者发送消息,在 IDEA 控制台即可看到两个消费者在消费不同
分区的数据;
重新发送到一个全新的主题中,由于默认创建的主题分区数为 1,可以看到只能
有一个消费者消费到数据。
6.分区的分配以及再平衡
Range 分区策略原理
RoundRobin 以及再平衡
Sticky 以及再平衡
7.offset 位移
offset 的默认维护位置
__consumer_offsets 主题里面采用 key 和 value 的方式存储数据。key 是 group.id+topic+ 分区号,value 就是当前 offset 的值。每隔一段时间,kafka 内部会对这个 topic 进行 compact,也就是每个 group.id+topic+分区号就保留最新数据。
自动提交 offset
// 是否自动提交 offset
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
// 提交 offset 的时间周期 1000ms,默认 5s
properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 1000);
手动提交 offset
- 同步提交offset
由于同步提交 offset 有失败重试机制,故更加可靠,但是由于一直等待提交结果,提交的效率比较低。以下为同步提交 offset 的示例。
public class CustomConsumerByHandSync {
public static void main(String[] args) {
// 1. 创建 kafka 消费者配置类
Properties properties = new Properties();
// 2. 添加配置参数
// 添加连接
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
// 配置序列化 必须
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer");
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
// 配置消费者组
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
// 是否自动提交 offset
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
//3. 创建 kafka 消费者
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
//4. 设置消费主题 形参是列表
consumer.subscribe(Arrays.asList("first"));
//5. 消费数据
while (true){
// 读取消息
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
// 输出消息
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.println(consumerRecord.value());
}
// 同步提交 offset
consumer.commitSync();
}
}
}
- 异步提交offset
虽然同步提交 offset 更可靠一些,但是由于其会阻塞当前线程,直到提交成功。因此吞吐量会受到很大的影响。因此更多的情况下,会选用异步提交 offset 的方式。
public class CustomConsumerByHandAsync {
public static void main(String[] args) {
// 1. 创建 kafka 消费者配置类
Properties properties = new Properties();
// 2. 添加配置参数
// 添加连接
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
// 配置序列化 必须
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
// 配置消费者组
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
// 是否自动提交 offset
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
//3. 创建 Kafka 消费者
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
//4. 设置消费主题 形参是列表
consumer.subscribe(Arrays.asList("first"));
//5. 消费数据
while (true){
// 读取消息
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
// 输出消息
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.println(consumerRecord.value());
}
// 异步提交 offset
consumer.commitAsync();
}
}
}
指定 Offset 消费
auto.offset.reset = earliest | latest | none 默认是 latest。
当 Kafka 中没有初始偏移量(消费者组第一次消费)或服务器上不再存在当前偏移量 时(例如该数据已被删除),该怎么办?
(1)earliest:自动将偏移量重置为最早的偏移量,–from-beginning。
(2)latest(默认值):自动将偏移量重置为最新偏移量。
(3)none:如果未找到消费者组的先前偏移量,则向消费者抛出异常。
(4)任意指定 offset 位移开始消费
public class CustomConsumerSeek {
public static void main(String[] args) {
// 0 配置信息
Properties properties = new Properties();
// 连接
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
// key value 反序列化
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test2");
// 1 创建一个消费者
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties);
// 2 订阅一个主题
ArrayList<String> topics = new ArrayList<>();
topics.add("first");
kafkaConsumer.subscribe(topics);
Set<TopicPartition> assignment= new HashSet<>();
while (assignment.size() == 0) {
kafkaConsumer.poll(Duration.ofSeconds(1));
// 获取消费者分区分配信息(有了分区分配信息才能开始消费)
assignment = kafkaConsumer.assignment();
}
// 遍历所有分区,并指定 offset 从 1700 的位置开始消费
for (TopicPartition tp: assignment) {
kafkaConsumer.seek(tp, 1700);
}
// 3 消费该主题数据
while (true) {
ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.println(consumerRecord);
}
}
}
}
注意:每次执行完,需要修改消费者组名;
指定时间消费
需求:在生产环境中,会遇到最近消费的几个小时数据异常,想重新按照时间消费。
例如要求按照时间消费前一天的数据,怎么处理?
public class CustomConsumerForTime {
public static void main(String[] args) {
// 0 配置信息
Properties properties = new Properties();
// 连接
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
// key value 反序列化
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test2");
// 1 创建一个消费者
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties);
// 2 订阅一个主题
ArrayList<String> topics = new ArrayList<>();
topics.add("first");
kafkaConsumer.subscribe(topics);
Set<TopicPartition> assignment = new HashSet<>();
while (assignment.size() == 0) {
kafkaConsumer.poll(Duration.ofSeconds(1));
// 获取消费者分区分配信息(有了分区分配信息才能开始消费)
assignment = kafkaConsumer.assignment();
}
HashMap<TopicPartition, Long> timestampToSearch = new HashMap<>();
// 封装集合存储,每个分区对应一天前的数据
for (TopicPartition topicPartition : assignment) {
timestampToSearch.put(topicPartition, System.currentTimeMillis() - 1 * 24 * 3600 * 1000);
}
// 获取从 1 天前开始消费的每个分区的 offset
Map<TopicPartition, OffsetAndTimestamp> offsets = kafkaConsumer.offsetsForTimes(timestampToSearch);
// 遍历每个分区,对每个分区设置消费时间。
for (TopicPartition topicPartition : assignment) {
OffsetAndTimestamp offsetAndTimestamp = offsets.get(topicPartition);
// 根据时间指定开始消费的位置
if (offsetAndTimestamp != null){
kafkaConsumer.seek(topicPartition, offsetAndTimestamp.offset());
}
}
// 3 消费该主题数据
while (true) {
ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.println(consumerRecord);
}
}
}
}
漏消费和重复消费
重复消费:已经消费了数据,但是 offset 没提交。
漏消费:先提交 offset 后消费,有可能会造成数据的漏消费。
消费者事务
数据积压(消费者如何提高吞吐量)
七.Kafka-Kraft 模式
八.Kafka整合springboot
生产者
配置连接地址
消费者