Kafka
Kafka简介
消息队列
消息队列,消息队列的英文名是 Message Queue
,缩写为 MQ。消息队列是一种用来存储消息的 队列
。
消息队列的中间件:
Kafka
、Active MQ、RabbitMQ、RocketMQ、ZeroMQ
消息队列的应用场景
异步处理
:将耗时的操作和任务转移到消息队列中进行异步处理,以提高系统的响应性能和吞吐量
流量削峰
:当系统面临大量并发请求时,通过消息队列将请求发送到消息队列中,然后按照系统的处理能力逐个进行处理,防止系统被过载
应用解耦
:通过消息队列将不同系统组件解耦,使得系统变得松散耦合、灵活可扩展
日志处理
:将系统日志发送到消息队列中,实现异步的日志处理和分析
消息队列的两种模式
- 点对点模式
- 发布-订阅模式
点对点模式:消息发送者生产消息发送到消息队列中,然后消息接收者从消息队列中取出并且消费消息。
发布订阅模式:消息发布者将消息发送到消息队列中,订阅者可以选择订阅自己感兴趣的消息主题,并从消息队列中接收相应的消息
点对点模式 VS 发布订阅模式
- 点对点模式每个消息只能有一个接收者(
一对一
);发布订阅模式每个消息可以有多个订阅者(一对多
) - 点对点模式消息被消费后就
会被删除
;发布订阅模式消息被订阅后不会被删除
可以被其他订阅者订阅 - 点对点模式的消息
不存在主题区分
;发布订阅模式的消息存在主题区分
,订阅者可以根据主题订阅指定消息
Kafka介绍
Kafka是一个分布式流平台,具有以下三个功能:
发布和订阅
数据流,类似于消息队列或者是企业消息传递系统- 以容错的持久化方式
存储
数据流 处理
数据流
Kafka重要概念
Broker 节点
Broker指Kafka集群中的节点,是运行Kafka服务器的服务器实例
。每个Broker都负责接收和处理客户端的请求
,并存储和复制数据
。
每个Borker在启动时会获得一个标识
(BorkerId),Kafka集群由多个Broker组成,构成一个高可用、支持水平扩展的分布式系统。
Broker的作用(以下作用不理解没关系,等看完整篇文章后再来理解即可)
- 负责存储和复制消息日志(Log)
- 负责维护Topic和分区的元数据信息
- 负责处理消费者的拉取请求,提供消息给消费者进行消费
- 负责处理分区的Leader选举
Zookeeper
Zookeeper提供分布式系统所需的协调
和同步机制
,确保集群的高可用性、容错性和一致性
,帮助Kafka实现分布式消息传递
和数据持久化
的功能。
Kafka正在逐步想办法将Zookeeper剥离,维护两套集群成本较高,"Kafka on Kafka"------由Kafka自己管理元数据。
Producer 生产者
生产者向Kafka集群发送消息的客户端应用程序,负责生产
和发送
消息到Kafka集群中的指定Topic(主题)。
Consumer 消费者
消费者从Kafka集群中消费消息的客户端应用程序,负责接收、处理和消费Kafka集群中指定Topic中的消息。
ConsumerGroup 消费者组
Consumer Group是多个消费者实例的逻辑组合,一个Consumer Group可以包含多个消费者,共同协作消费一组Topic中的消息。
Partitions 分区
每个Topic可以被划分为一个或多个Partitions(分区),并在Kafka集群中的多个Broker上进行分布存储。
Replicas 副本
每个Partitions(分区)可以有多个Replicas(副本),并分布在Kafka集群中的不同Broker节点上,保证Kafka高可用性
。
Topic 主题
Topic(主题)是消息的逻辑分类单位,代表一类具有相同特性的消息。每个消息都会被发布到一个特定的Topic中,并根据Topic进行发布和订阅。
Topic是一个逻辑概念,生产者发布消息到指定主题中,消费者从指定主题中拉取消息,每个主题都有一个唯一的标识符,一般一个主题包含某一类消息。
Offset 偏移量
Offset(偏移量)用来唯一标识一个消费者在一个特定分区中所消费的消息的位置,每个分区都有自己独立的Offset。
Kafka基本使用
Kafka目录结构
目录名称 | 说明 |
---|---|
bin | Kafka的所有执行脚本,比如:启动Kafka服务器 |
config | Kafka的所有配置文件 |
libs | 运行Kafka所需要的JAR包 |
logs | Kafka的所有日志文件 |
site-docs | Kafka的网站帮助文件 |
Kafka集群搭建
-
准备3台服务器
-
3台服务器安装Kafka,将压缩包上传到三台服务器上并解压 解压命令:
tar -xvzf kafka_2.12-2.4.1.tgz
-
修改Kafka配置文件,配置文件路径:
\config\server.properties
properties# 指定broker的id,Kafka节点的broker的id不能重复,需要唯一 broker.id=0 # 指定 listeners=PLAINTEXT://hadoop1:9092 # 指定Kafka消息数据的位置 log.dirs=/wenxuan/server/kafka_2.12-2.4.1/data # 配置zk的三个节点 zookeeper.connect=hostname:port,hostname:port,hostname:por
-
配置Kafka环境变量
shellvim /etc/profile export KAFKA_HOME=/wenxuan/server/kafka_2.12-2.4.1 export PATH=:$PATH:${KAFKA_HOME} # 使环境变量生效 source /etc/profile
-
启动Kafka
shell# 启动ZooKeeper nohup bin/zookeeper-server-start.sh config/zookeeper.properties & # 启动Kafka cd /wenxuan/server/kafka_2.12-2.4.1 nohup bin/kafka-server-start.sh config/server.properties & # 测试Kafka集群是否启动成功 bin/kafka-topics.sh --bootstrap-server hostname:port --list
Kafka基础操作
-
创建主题(topic)
shell# 创建名为test的主题 ./kafka-topics.sh --create --bootstrap-server node1.itcast.cn:9092 --topic test # 查看目前Kafka中的主题 ./kafka-topics.sh --list --bootstrap-server node1.itcast.cn:9092
-
生产消息到Kafka
shell# 生产消息到test主题 ./kafka-console-producer.sh --broker-list node1.itcast.cn:9092 --topic test
-
从Kafka消费消息
shell# 从test主题中消费消息 ./kafka-console-consumer.sh --bootstrap-server node1.itcast.cn:9092 --topic test --from-beginning
SpringBoot集成Kafka
- 添加Kafka的maven依赖
properties
<!-- kafka客户端工具 -->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>2.8.0</version>
</dependency>
- 配置Kafka
properties
sping:
kafka:
# Kafka的服务地址
bootstrap-servers: localhost:9092
consumer:
# 消费者组ID
group-id: group-name
# 消息偏移量
auto-offset-reset: earliest
producer:
# 消息序列化程序
value-serializer: org.apache.kafka.common.serialization.StringSerializer
key-serializer: org.apache.kafka.common.serialization.StringSerializer
- 创建生产者
编写生产者配置类
java
@Configuration // 声明这是一个配置类,用于定义和组织Bean
public class KafkaProducerConfig {
// 从配置文件中获取Kafka服务器的地址,并将其赋值给bootstrapServers变量
@Value("${spring.kafka.bootstrap-servers}")
private String bootstrapServers;
// 创建一个Map对象,用于存储Kafka生产者的配置信息
@Bean
public Map<String, Object> producerConfigs() {
// 创建一个HashMap对象,用于存储配置项
Map<String, Object> props = new HashMap<>();
// 将Kafka服务器地址添加到配置项中
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
// 将键的序列化类添加到配置项中
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
// 将值的序列化类添加到配置项中
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
// 返回包含配置信息的Map对象
return props;
}
// 创建一个Kafka生产者工厂对象,用于创建Kafka生产者实例
@Bean
public ProducerFactory<String, String> producerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfigs());
}
// 创建一个Kafka模板对象,用于发送消息到Kafka
@Bean
public KafkaTemplate<String, String> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
}
编写生产者接口
java
@RestController
public class KafkaController {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@PostMapping("/send")
public void sendMessage(@RequestBody String message) {
kafkaTemplate.send("my-topic", message);
}
}
- 创建消费者
消费者配置类
java
@Configuration
// 启用对Kafka的支持
@EnableKafka
public class KafkaConsumerConfig {
@Value("${spring.kafka.bootstrap-servers}")
private String bootstrapServers;
@Value("${spring.kafka.consumer.group-id}")
private String groupId;
@Bean
public Map<String, Object> consumerConfigs() {
Map<String, Object> props = new HashMap<>();
// 配置Kafka服务器地址
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
// 组ID
props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
// 偏移量重置策略
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
// key和value的序列化方式
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
return props;
}
@Bean
public ConsumerFactory<String, String> consumerFactory() {
// 创建Kafka消费者配置工厂对象
return new DefaultKafkaConsumerFactory<>(consumerConfigs());
}
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
// 创建消费者工厂对象
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
return factory;
}
}
消费者类
java
@Service
public class KafkaConsumer {
// 配置消费者的消费指定主题的消息,并且属于指定消费组
@KafkaListener(topics = "my-topic", groupId = "my-group-id")
public void consume(String message) {
System.out.println("Received message: " + message);
}
}
幂等性
幂等性是什么
幂等性指对同一个操作的多次执行产生的结果是一致的,也就是说无论对一个操作进行多少次重复执行,结果都是相同的
。
幂等性举例:
- 银行转账操作,当对同一个转账请求进行多次提交时,多次提交转账请求所产生的结果是一致的。比如用户A给用户B转账100元这个操作,可能因为网络延迟原因,响应不及时,导致提交多次转账请求,但是这个操作成功之后的结果只会是A的余额减少100元,B的余额增加100元。
- 表单请求操作,多次表单请求所产生的结果是一致的。比如用户再浏览器页面上点击多次提交订单按钮,只会在后台生成一个该订单数据。
Kafka生产者幂等性
Kafka未开启生产者幂等性的后果:
- 生产者向 broker 发送生产消息的请求
- broker 保存消息
- broker 向 生产者发送ACK确认,但是ACK确认在返回期间丢失了
- 生产者没有收到ACK确认,再次向 broker发送生产消息的请求
- broker 再次保存同一条消息,导致
partitions 重复保存相同的消息
Kafka生产者幂等性原理
- 生产者向 broker 发送生产消息的请求,同时携带
生产者唯一编号(pid)
和消息递增序列(sequeue number)
- broker 接收到请求后,检查 pid 对应的 sequeue number消息是否已生产
- 如果未生产,则生产该消息,并记录 pid 和 sequence number
- broker 向 生产者发送ACK确认,但是ACK在网络传输中丢失了
- 生产者未收到ACK确认,再次向 broker 发送生产消息的请求,同时携带 pid 和 sequeue number
- broker 收到生产消息的请求后,检查 pid 对应的sequeue number消息是否已生产
- 如果已生产,则不会生产该消息,直接向生产者发送ACK确认
生产者如何检查PID 对应的 SEQUEUE NUMBER消息已生产?
SEQUEUE NUMBER 是一个
递增的序号
,如果生产消息请求的 SEQUEUE NUMBER 大于 BROKER中记录的SEQUEUE NUMBER,则表示该消息未生产;否则表示该消息已生产保存了。
Kafka如何开启生产者幂等性?
通过设置 enable.idempotence
参数为 true,则表示开启 Kafka的生产者幂等性
上面 SpringBoot集成Kafka中的application配置文件中添加以下配置即可
properties
spring.kafka.producer.enable-idempotence=true
Kafka事务
Kafka事务用于保证消息在生产者和消费者之间的原子性
和一致性
的机制。通过事务可以保证一组消息要么全部提交成功,要么全部提交失败。
Kafka事务API
Producer接口中定义了五个事务相关方法:
- initTransactions:初始化事务
- beginTransaction:启动事务
- sendOffsetsToTransaction:提交偏移量
- commitTransaction:提交事务
- abortTransaction:取消事务
Kafka事务原理
事务协调器(Transaction Coordinator)
:Kafka集群中的一个特殊角色,负责协调事务的进行。每个消费者组都有一个对应的事务协调器。它负责为每个事务生成唯一的事务ID,并管理事务的状态和元数据。
生产者事务:
开启事务
:当生产者调用开启事务的API后,事务协调器会分配一个唯一的事务ID给该生产者,并将其状态设置为"开启"。发送消息
:生产者可以使用正常的发送消息API向Kafka发送消息,这些消息会被缓存在事务缓冲区中,并关联到事务ID。提交事务
:当生产者调用提交事务的API后,它会将事务ID提交给事务协调器。协调器会对事务中的消息进行预写日志,并将所有消息的偏移量记录到事务元数据日志中。异常处理
:如果在事务提交期间,发生任何错误,比如消息发送失败或消费者组出现异常,生产者可以选择回滚事务,导致事务缓冲区中的所有消息被丢弃。
消费者事务:
开启事务
:消费者在加入消费者组时,可以选择使用事务。消费者将自己的消费进度和事务状态进行关联,这样在事务提交或回滚时可以正确处理消息的偏移量。读取消息
:消费者可以使用正常的拉取消息API来读取消息。在事务中,消费者只能消费已经被提交的消息。提交位移
:消费者可以通过提交位移的API将消息的位移提交给事务协调器。这样在事务提交时,事务协调器知道哪些消息已经被消费了。
分区管理
:Kafka将每个主题划分为多个分区,事务协调器通过分区来进行事务管理。具体来说,事务协调器会为每个分区维护一个事务日志,记录该分区的最新已提交的事务ID。
Rebalance机制
Rebalance机制介绍
Rebalance称之为再均衡,当指定时机
发生时,系统自动重新分配分区给各个消费者实例,确保 Consumer Group
下所有的 Consumer
分区负载的均衡。
Rebalance触发时机:
- 消费者数量发生变化
- 主题数量发生变化
- 分区数量发生变化
Rebalance流程
- 当有消费者实例加入或离开消费者组时,协调器(Coordinator)会检测到这个变化,并触发Rebalance。
- 协调器会先获取所有消费者实例的订阅信息,包括它们订阅的主题和分区。
- 协调器根据配置的分配策略(如Range范围分配策略或RoundRobin轮询分配策略)以及其他因素,计算出新的分区分配方案。
- 协调器会将新的分区分配方案通知给消费者实例。
- 消费者实例根据新的分区分配方案,重新分配自己负责的分区。
- 当所有消费者实例完成分区分配后,Rebalance过程结束,消费者实例开始处理分配给它们的分区。
Rebalance的不良影响
消费者消费停顿
:在Rebalance过程中,消费者实例需要重新分配负责的分区,会导致消费者停止消费。
分区策略
生产者分区写入策略
生产者将消息写入到指定Topic中,Kafka按照一定策略将数据分配到主题的不同分区中
轮询分区策略
:生产者按照轮询方式将消息依次写入每个分区随机分区策略
:生产者随机将消息分区到每个分区(基本不使用)按key分区策略
:生产者按照消息的key放到对应的分区自定义分区策略
:使用接口配置自定义分区策略
轮询分区策略
轮询分区策略:生产者按照轮询的方式将消息依次写入每个分区,这是默认的策略,可以最大限度保证所有消息平均分配到每个分区。
随机分区策略
随机分区策略:生产者随机将消息分区到每个分区,随机分区策略无法保证消息能够均匀分配到各个分区,轮询分区策略表现比随机分区策略更好,所以基本上很少使用随机分区策略。
按key分区策略
按key分区策略:生产者按照消息的key计算得到分区号,可能会导致 数据倾斜
,如果某个key包含大量的数据,就会导致这一批数据全部分配到同一个分区中,造成该分区的消息数量远大于其他分区消息数量。
自定义分区策略
自定义分区策略:使用接口配置自定义分区策略
实现自定义分区策略
创建一个配置类,用于定义生产者的自定义分区策略: ```java @Configuration public class KafkaProducerConfig {
typescript
@Bean
public Partitioner customPartitioner() {
return (topic, partitionKey, keyBytes, allPartitions) -> {
// 自定义分区逻辑,根据partitionKey返回分区编号
// 示例:根据字符串长度计算分区编号
int length = new String(keyBytes).length();
return length % allPartitions.size();
};
}
}
```
消费者分区分配策略
消费者从Topic中的哪些 Partitions 中消费消息,由消费者分区分配策略决定
Range范围分配策略
:基于当前可用的分配范围来平衡分区的分配RoundRobin轮询分配策略
:简单的按照顺序将分区分配给不同的消费者实例Stick粘性分配策略
:没有发生Rebalance时,按照轮询分配策略分配分区;当发生Rebalance时,将未分配的分区均匀的分配给消费者。
Range范围分配策略
Range范围分配策略:基于当前可用的分配范围来平衡分区的分配,确保每个消费者实例消费的分区范围尽可能均匀。
Range范围分配策略将每个主题的分区按照分区ID进行排序,并将拍寻后的分区列表划分为大致相等的几个范围
(range)。然后每个消费者实例被分配其中一个范围作为其负责消费的分区集合。
RoundRobin轮询分配策略
RoundRobin(轮询)分配策略是一种用于消费者组中消费者实例之间分配分区的策略。它简单地按照顺序
将分区分配给不同的消费者实例,以达到均匀分配的目的。
Stick粘性分配策略
Stick粘性分配策略:没有发生Rebalance时,按照轮询分配策略分配分区;当发生Rebalance时,将未分配的分区均匀的分配给消费者。
Stick粘性分配策略有两个目标:
- 分区的分配要尽可能均匀
- 分区的分配尽可能与Rebalance前分配的保持相同
Stick粘性分配只会将Rebalance后未分配的分区均衡分配给消费者,Rebalance后已分配的分区分配的消费者不会发生改变。
副本机制
副本机制是什么
Kafka副本机制指在Kafka集群中将每个分区的数据进行复制,提供数据冗余
和容错能力
。每个分区可以有多个副本,其中一个是领导者(Leader)
,其他副本是追随者(Follower)
。领导者负责处理读写请求,追随者负责备份和同步数据。
副本机制的作用
高可用性
:如果某个副本发生故障或不可用,可以通过将领导者切换到其他副本来保证分区数据的可用性,从而实现高可用性。数据冗余
:每个副本包含相同的数据,如果领导者副本发生故障,追随者副本可以接替,确保数据的可用性。
副本的分类
根据追随者(Follower)与领导者(Leader)数据同步情况,可以将副本划分为以下三种:
AR(Assigned Replicas)
:分区中所有的副本ISR(In-Sync Replicas)
:所有与Leader保持一定程度数据同步的副本OSR(Out-of-Sync Replias)
:与Leader同步滞后过多的副本
ISR包含Leader副本,同时
AR = ISR + OSR
正常情况下,所有的Follwer副本都与Leader副本保持同步,即AR = ISR,OSR集合为空。只有当部分Follwer副本出现故障或者网络延迟时OSR集合不为空。
副本消息确认
副本之间数据同步后向生产者发送确认数通过 acks
参数设置
acks=0
:生产者发送消息后不等待任何确认,直接发送下一条消息acks=1
:生产者发送消息后等待Leader分区收到消息的确认后再发送下一条消息acks=all(或-1)
:生产者发送消息后等待所有ISR(In-Sync-Replicas)副本都成功收到消息后并确认,才会发送下一条消息
acks不同取值之间的比较
acks取值 | 数据可靠性 | 性能 |
---|---|---|
-1 | 最强 | 最差 |
0 | 最弱 | 最好 |
1 | 一般 | 一般 |
业务要求性能高,可以接受部分数据丢失,选择配置 0或1。业务要求数据不能丢失,选择配置 -1。
Leader-Follower
Leader-Follower介绍
在Kafka中,Leader和Follower是分区的两种角色,每个分区可以有多个副本,会有一个副本被选举为Leader,其余的副本为Follower。Leader负责处理所有的读写请求,Follower负责备份和同步数据
。
当Leader副本发生故障时,Kafka会从Follower中选举
一个副本成为新的Leader,来保证分区的可用性
。其余Follower会通过与新的Leader副本同步数据,使得所有副本数据保持一致。
Leader的负载均衡
分区可以存在多个副本,多个分区副本在一个broker上,如果没有实现Leader的负载均衡就可能导致所有Leader在同一个broker上,使得所有分区(Leader)的读写都在同一个broker
中,导致Kafka没有起到集群的作用。
Kafka是通过动态地调整
分区的Leader副本来实现Leader负载均衡,当Kafka集群中的Broker数量发生变化或某些Broker的负载状态发生变化时,Kafka会对分区的Leader副本继续宁重新分配以实现负载均衡。
Leader的选举
Leader的选举触发时机
启动时
:Kafka集群启动时,会对所有分区进行Leader选举故障恢复
:当Leader副本发生故障或不可用时,Kafka会进行Leader选举扩容与缩容
:当Kafka集群中新增或删除Broker时,可能会触发分区的重新分配和Leader的选举
Leader的选举过程:
确定ISR
:Kafka首先从每个分区的副本集合中确定属于ISR的副本集合选择Leader
:Kafka从ISR中选择一个副本作为新的Leader更新分区的元数据
:选举完成后,Kafka会更新分区的元数据,将新的Leader副本信息记录下来
ISR集合中确定Leader的依据
ISR中的首选副本
:Kafka优先选择ISR列表中的首选副本作为新的Leader,首选副本通常是与Leader保持最高的同步进度的副本副本的健康状态
:Kafka选择健康状态最好的ISR副本作为新的Leader副本的优先级
:副本可以配置优先级来影响Leader的选举
Controller角色
Controller介绍
Controller角色:Kafka启动时,会在所有的Broker中选择一个作为Controller,负责管理整个Kafka集群的元数据和协调器,创建Topic、添加分区、修改副本数量之类的管理任务都是由Controller完成。
Controller的选举:在Kafka集群启动的时候,每个Broker都会尝试去Zookeeper上注册成为Controller(临时节点),但只有一个竞争成功,其他的Broker会注册该节点的监视器。
Controller是高可用的,如果Controller崩溃,其他的Broker会竞选注册为Controller。
Controller标识
controller和controller_epoch是Controller角色的两个标识,用来维护和管理集群状态
/controller
:临时节点,记录当前的Controller的brokerId以及选举时间/controller_epoch
:持久节点,记录控制器发生变更的次数,即记录当前的控制器是第几代控制器。
controller_epoch的作用:controller_epoch是一个整数值
,用于表示Controller的版本或轮次
。每次Controller选举过程中,当新的Controller产生时,会将controller_epoch递增。可以保证不同的Controller在不同的轮次工作,避免潜在的冲突的混乱
。
Controller选举
Controller选举:当每个Broker创建自己的临时顺序节点后,根据节点的序号顺序
决定谁将成为新的Controller。
- 所有Broker根据创建的临时顺序节点的序号进行排序,确定谁是下一个Controller的候选者。
- 如果某个Broker创建的节点序号最小,则它赢得了Controller选举,成为新的Controller。
- 如果选中的Broker由于故障或其他原因不可用,Zookeeper将删除其节点,并选择下一个节点序号最小的Broker作为新的Controller。
Controller选举Leader
Controller负责管理Leader选举过程,确保每个分区都有一个Leader,并将相关的元数据信息更新到Zookeeper。
- 当一个Broker接收到请求(生产或消费请求),并且请求针对的分区没有Leader时,会
触发Leader选举过程
。 - Broker向Controller发送
Leader选举请求
。 - Controler根据存储在Zookeeper中的分区和副本的元数据信息,进行
Leader选举算法
。 - Leader选举算法根据
副本的状态和配置
,选择新的Leader。 - Controller将新的Leader选举结果发送给相关的Broker。
- 相关的Broker根据接收到的Leader选举结果更新自己的元数据信息,并通知其他Broker更新副本的状态。
为什么不能通过Zookeeper的方式来选举Leader?
过多的Zookeeper操作
:在Zookeeper中进行Leader选举需要频繁的读取和写入Zookeeper的节点,对Zookeeper集群的造成较大的压力。耦合性过高
:通过Zookeeper选举Leader会增加系统和Zookeepoer的耦合性。过多的Leader选举请求
:每个Broker都要向Zookeeper发送Leader选举请求,Zookeeper可能会收到过多的请求,导致性能下降。
Kafka原理
Kafka高可用性原理
Kafka实现高可用性主要依赖以下几个原理:
副本机制
:Kafka每个主题的分区可以配置多个副本,这些副本位于不同的Broker上。即使Leader副本发生故障,系统会选举出新的Leader,系统仍然能保持可用,并且Follower副本可以将数据同步到新的Leader。ISR机制
:ISR是一组与Leader副本保持同步的副本集合,只有ISR中的副本都成功接收并复制消息后,消息才被视为已提交。Controller角色
:Kafka集群中,有一个Broker会被选举为Controller角色,负责管理整个集群的元数据和协调器。健康检测和故障转移
:Kafka集群会定期检测Broker的健康状况,包括心跳检测和副本状态检测等。
Kafka数据可靠性原理
Kafka数据可靠性通过以下几个方面来保障
持久化存储
:Kafka使用磁盘上的持久化性存储保存消息数据,消息写入到磁盘上的日志文件不会立即删除或覆盖,而是以追加的方式写入。副本机制
:Kafka采用副本机制来实现数据的高可靠性,每个主题的分区可以配置多个副本,这些副本位于不同的Broker上。ISR机制
:ISR是一组与Leader副本保持数据同步的副本集合,这样可以确保消息不仅在Leader副本中可用,同时在ISR中的副本处也可用。同步和异步写入
:Kafka支持同步和异步的消息写入方式
同步写入
:在消息写入时等待所有副本都成功接收到消息,才进行下一步操作。可以确保数据的可靠性,但是会降低吞吐量。
异步写入
:将消息写入到本地日志后立即返回,不等待副本的接收确认。可以提高吞吐量,但是可能会导致一定的数据丢失。
Kafka数据持久化原理
Kafka数据存储形式
Kafka数据存储涉及三个文件:
日志文件(Log Files)
:日志文件是Kafka中存储实际消息数据
的文件。以顺序追加
的方式保存消息,Kafka将消息按主题和分区划分为多个日志文件,每个日志文件包含一定范围的消息。索引文件(Index Files)
:索引文件用于加速消息的查找
,存储每个消息在日志文件中的偏移量和对应的物理文件位置的映射关系。索引文件以稀疏索引
的方式组织,每个索引条目包含一定范围的消息偏移量和文件位置。- 时间索引文件(Time Index Files):时间索引文件存储每个消息的时间戳和对应偏移量的映射关系。提供以时间为条件进行消息查找的功能,使得可以
按时间范围快速查找消息
。
消息写入Log日志文件
:新的消息总是写入到最后一个日志文件中,并且日志文件的大小存在限制,当日志文件达到限制时,会新建一个日志文件并将消息滚动到新的日志文件中。
读取Log日志消息的过程
:
- 使用索引文件定位到包含指定偏移量(
全局偏移量
)的日志文件 - 读取日志文件,根据索引文件中的偏移量计算出消息的存储位置(
局部偏移量
) - 通过存储位置使用二分查找定位日志文件中指定消息,
- 读取该消息,并返回读取到的消息
Kafka高性能原理
Kafka高性能基于以下几种原因
分布式架构
:Kafka采用分布式架构,将数据分为多个分区并存储在多个Broker上。分布式设计允许Kafka能够水平扩展,提高整体的吞吐量和处理能力。零拷贝机制
:Kafka使用零拷贝机制来避免数据在内核空间和用户空间之间的多次复制。当消息被写入Kafka或者cKafka读取时,数据可以直接在磁盘和网络之间进行传输,避免不必要的内存拷贝,提升IO性能和吞吐量。磁盘顺序写入
:Kafka将消息持久化存储在磁盘上,采用顺序写入的方式将消息追加到日志文件中,提高写入的效率。高效的消息索引
:Kafka使用索引来快速定位分区中的信息,维护一个索引文件,记录消息在日志文件中的位置信息。
Kafka工作流程
Kafka读取消息
Kafka写入消息
Kafka写入消息的过程:
写入消息到Producer端缓冲区
:当Producer发送消息时,首先将消息吸入到Producer端的缓冲区,等待发送到Kafka集群。发送消息到Kafka服务端
:Producer将消息发送到目标Kafka Broker节点,目标节点可根据消息的分区键或分区策略确定。分区选择
:Kafka根据消息的分区键或分区策略确定消息存储的分区。消息写入到Leader的日志文件
:Producer将消息写入目标分区的Leader副本的日志文件中。Follower消息复制
:Follower副本向Leader副本拉取消息,将消息复制到Follower副本的日志文件中。ACK确认
:Kafka向Producer返回一个ACK确认消息写入成功。
Kafka数据清理
Kafka的消息保存在磁盘中,为了控制磁盘占用空间,Kafka需要对消息进行清理。Kafka提供两种日志清理方式:
- 日志删除:按照指定的策略直接删除不符合条件的日志
- 日志压缩:按照消息的key进行整合,有相同key的但有不同的value值的消息,只保留最后一个版本。
日志删除
Kafka日志管理器中会有一个专门的日志删除任务来定期检测和删除不符合保留条件的日志分段文件
,这个周期可以通过broker端参数log.retention.check.interval.ms
来配置,默认值为300,000,即5分钟。当前日志分段的保留策略有3种:
- 基于时间的保留策略:可以通过参数设置不符合条件的日志保留时间(
log.retention.hours
,log.retention.minutes
,log.retention.ms
) - 基于日志大小的保留策略:日志删除任务会检查当前日志的大小是否超过设定的阈值来寻找可删除的日志分段的文件集合。
- 基于日志起始偏移量的保留策略:每个segment日志都有它的起始偏移量,如果起始偏移量小于 logStartOffset,那么这些日志文件将会标记为删除。
日志压缩
Log Compaction是默认的日志删除之外的清理过时数据的方式。它会将相同的key对应的数据只保留一个版本。
文章参考资料:
- 黑马程序员 Kafka:www.bilibili.com/video/BV19y...