1. 基础概念
1.1 概述
kafka是一个基于zooKeeper协调的分布式消息系统,如今的定义是:一个分布式流处理平台。
1.2 特点
- 高吞吐量:多生产者、多消费者带来高吞吐量
- 支持水平扩展:水平添加broker简单做到水平扩展
- 可持久化:数据存储在磁盘
- 高性能:页缓存技术、磁盘顺序写、零拷贝技术做到轻松支持百万千万级数据流,并且做到亚秒级消息延迟
1.3 架构组成
kafka的架构组成包括:
- zooKeeper集群:负责保存broker元数据
- 生产者:生产消息,可以存在多个生产者
- broker:kafka消息队列服务器,负责接收来自生产者的消息,为消息在分区中设置偏移量,为消费者提供读取消息的服务
- topic:主题,kafka中消息存放的形式,它是一个逻辑上的分类。
kafka中topic的角色与rabbitMQ中的topic角色有所不同:kafka中的topic是消息存在的形式,不同消息存放在不同的topic中,而rabbitMQ中的topic是消息匹配队列的方式,在topic模式下,消息通过routing key 匹配到对应的队列中 - 分区:每个主题下有多个分区(具体有几个分区通过分区因子这个属性设置),分区是数据的逻辑划分单位,每个分区中存有各自的消息,消息设置有偏移量,分区中的消息可追加不可变更
- 分区副本:每个分区可以在其他broker的同topic下存在多个副本,其中有leader和follower之分,leader只有一个,负责消息的读写工作,剩下的是follower,负责数据备份的工作
- 消息:存放在分区中,设置有唯一偏移量
- 消费者:消费消息,可以存在多个消费者
总结:一个broker下可以有多个topic,topic属于逻辑上的分类,一个topic分类下可以有多个分区,分区中可以有多个设置了偏移量的消息,一个分区,在其他broker下的同topic中有其分区副本,分区副本有leader和follower之分,leader负责读写请求,follower负责备份
1.4 “高性能”特点涉及技术
-
页缓存(pageCache)
pageCache由操作系统管理,通常存在于内核态空间,以页的形式存在,用于缓存从磁盘读取到内存中的数据,可以提高读写性能 -
顺序写
kafka在写数据时,以磁盘顺序写的方式来写,仅仅将数据追加到文件末尾,比随机写性能更高 -
零拷贝
kafka从磁盘读取数据到下游消费者的过程需要经过数据拷贝传统的拷贝过程如下:
①:CPU发起io请求,执行read方法,用户态切换为内核态
②:DMA向磁盘发起io请求,开始拷贝(第一次)磁盘上的数据到pageCache上
③:数据读取完成后,磁盘向DMA发送传输完成信号,DMA向数据拷贝(第二次)到用户缓存区,内核态切换为用户态
④:数据存储到用户缓存区后,CPU通过轮询的方式获取时间片后,执行write方法,尝试将数据拷贝(第三次)到socket缓存区,用户态切换为内核态
⑤:拷贝完成后,DMA将数据拷贝(第四次)到网卡设备
⑥:拷贝完成后,网卡设备发出写完成信号,write方法执行完成,内核态切换为用户态涉及名词解释:
- DMA:直接内存访问技术,一种允许外设绕过CPU直接访问内存的技术,可以提高数据的传输效率
- 用户缓存区:操作系统为应用程序划分的数据缓存区,可以隔离用户态和内核态的数据
- socket缓存区:操作系统为套接字通信划分的缓存区,用来缓存发送到网络的数据或接收来自网络的数据
零拷贝过程如下:
①:CPU发起io请求,执行mmap方法,在用户空间和内核空间之间建立地址映射,用于在虚拟内存空间映射pageCache上存放的数据的地址,这样就可以直接通过内存来访问文件,用户态切换为内核态
②:DMA读取磁盘上的数据到pageCache中(第一次拷贝),拷贝完成后,内核态切换为用户态
③:CPU执行write方法,通过地址映射将pageCache中的数据拷贝(第二次)到socket缓存区中,用户态切换为内核态
④:最后从socket缓存区拷贝(第三次)到网卡设备,最后write方法执行完成,内核态切换为用户态
2. kafka与zooKeeper的关系
kafka本身是一个分布式消息中间件,需要进行各种协调和管理工作,而zooKeeper就是一个高性能的分布式协调服务,它提供了可靠的协调和一致性管理功能。每一个broker服务启动时都会在zooKeeper上注册,zooKeeper会在broker服务器节点列表中记录新注册的服务,这些节点是临时的,会随broker服务器的下线被清除,可以根据节点的变化来表征服务的可用性,与Eureka和Nacos的服务注册与发现功能相同。以下是zooKeeper在kafka中的作用
- 协调服务:zooKeeper为kafka提供分布式协调功能,如:检查broker上下线、leader选举、topic和分区元数据管理
- 配置管理:zooKeeper可以存储kafka的一些配置,与Nacos的配置中心有相同的功能,如:broker列表、topic配置
- 集群管理:检测broker健康状态、维护集群的一致性
3. 消息相关
3.1 消息发送的过程
- 生产者创建消息
- 消息通过过滤器、序列器、分区器到达broker
①:消息需要通过序列器序列号为字节数组进行传输;
②:然后使用分区器确定在topic中的分区,对于key不为空的消息(消息以key-value形式存在),通过hash算法(murmur 的hash算法,具有高性能低碰撞特点)计算它们的分区信息,对于key为空的消息,则通过轮询的方式选择分区信息
如果在发送消息时,指定了分区信息,那么,分区器就不需要决定将消息发送到哪个分区,但是它仍然进行工作,比如:验证指定的分区ID是否有效,是否在主题分区内等等,如果分区ID无效,分区器还可以抛出异常
- 分区选择好后,将消息添加到记录批次中,一个记录批次中的消息都会被发送到同一个分区中
- 将记录批次发送到broker,如果broker写入成功,则返回一个rollbackMetaData对象,它包含消息的主题、分区、偏移量信息
- 如果写入失败,则返回错误异常,由生产者重试发送,多次重试失败则返回错误信息
3.2 消息分区的过程
-
如何确保同一类型的消息进入同一分区:
①:如果消息已经指定了分区,那么就根据分区ID放入到分区中,无需进行额外计算
②:如果未指定分区,那么分区器通过hash算法计算出键的hash值(也就是偏移量)
③:根据偏移量对主题的分区数量进行取模,如果几个消息的键相同(类型相同),那么取模结果也会相同,也就会进入同一个分区 -
消息分区策略:
①:轮询策略:kafka Java 生产者API的默认分区策略
②:随机策略
③:按key分区策略
3.3 消息持久化的存储结构
消息持久化的存储结构从上往下为:
- 主题:topic
- 分区:主题下可以有多个分区
- segment:分区下有多个segment
- 三个文件:segment下包含三个文件:log、index、time index
① log:实际的消息数据,以追加写的方式存储消息。每个segment对应有一个log文件,当文件达到一定大小或时间阈值,就会新创建一个segment,从而产生新的log文件
② index:用于加速消息的查找,它存储了消息在日志文件(log)中的偏移量和消息的物理位置之间的映射关系,通过这个索引文件可以快速找到消息的位置,提高消息的查找效率
③ time index:时间索引文件同样用于加速消息的查找速度,它存储消息在日志文件中的偏移量与消息时间戳之间的映射关系
消息进入集群的topic中之后,会被写入日志持久化到磁盘上,并在分区的监控和管理下,在各个副本之间进行同步
3.4 消息检索的过程
- kafka可以根据偏移量快速定位到消息所在的segment
- 再根据偏移量和索引文件,找到log文件中消息的位置
- 读取log文件中的消息
3.5 消息的顺序消费
如何保证消息的顺序消费:
- topic中只有一个leader分区负责写消息,而这个写消息的过程是只可追加不可修改的,因此就形成了一个天然的先进先出队列,其中的消息本身就是有序的
- 在消费时,kafka可以确保一个分区中的消息只能被一个消费者组中的一个消费者消费,由此可以保证消息的顺序消费
需要注意的是:一个分区中的消息可以保证顺序消费,但是如果存在多个分区,需要保证全局有序的话,则需要只设置一个生产者,一个分区,一个消费者,这会影响kafka性能和扩展性,违背了其分布式的设计特征,如果需要保证全局有序,可以尝试从业务逻辑层面解决,而不是依赖于kafka自身来保证
3.6 消息的删除策略
- 基于时间的删除策略:可以配置消息在topic中保留的最长时间,超过则被删除
- 基于大小的删除策略:可以配置topic中的最大消息,超过则被删除
4. 机制相关
4.1 ack应答机制
4.1.1 kafka的ack应答机制
ack应答机制可以用来保证生产者生产的消息被正确送达;生产者在发送消息后,会根据 acks 参数等待对应数量副本确认收到消息了才算消息发送成功,具体参数对应效果如下:
- acks = 0;此时不需要分区副本确认,生产者可以直接再发送消息,特点是高吞吐量,但是有消息丢失的风险
- acks = 1;此时只需要leader分区收到消息就算消息发送成功,此时在follower分区未同步之前,leader分区发生故障,会有消息丢失的风险,而吞吐量主要受生产者发送消息的类型决定(由producer.type = sync / async 参数决定),在同步发送的情况下,生产者需要等待成功响应再继续发送消息,而异步发送情况下生产者可以在收到消息发送成功信号之前就发送下一条消息
- acks = -1;此时需要等待所有参与消息复制的分区都收到消息后才算消息发送成功
4.1.2 kafka与rabbitMQ应答机制的区别
关于ack应答机制,不同消息中间件中的应用有所不同,在rabbitMQ中,ack应答机制在生产者发送消息和消费者消费消息的过程中都有使用,而kafka的消费者消费消息过程却不需要ack的参与:
- rabbitMQ:消费者消费消息后,向broker发送消费成功信号,broker收到ack信号后,从队列中删除消息,消费完成,如果没有收到ack信号,就会根据配置采取重试或投递给其他消费者来继续消费这个消息
- kafka:kafka的消息存放在分区中,通过 “消费位移” 的来确认消息的消费进度。消费位移类似一个位移指针,可以把它理解成一个在分区中的消息上面移动的指针,它指示了消费者在分区中的下一个消费位置,可以表示消费者在分区中下一个要消费的消息是哪个;当消费者拉取消息并成功消费后,会提交消费位移,并将位移指针向前移动到下一个待消费的位置,这种方式可以保证不漏掉消费消息,也不会重复消费消息。(下图有误,应该是先消费消息3,然后指针移动到)
4.2 分区副本机制
每个分区都可以在其他broker下的同topic中有自己的分区副本,分区副本中有一个leader分区,剩下的是follower分区,follower分区、定期从leader分区中同步数据,用于备份和容错功能,通常不提供读写服务,只有在leader分区发生故障的时候,自己又被zooKeeper选举为新的leader分区时,才会提供读写服务,分区副本机制是保证kafka可靠性的核心
4.3 ISR机制
ISR是一个副本集合,它由leader分区进行维护,里面动态记录了与leader分区保存同步了的分区副本(包括leader分区本身),所谓动态是指,当一个副本超过一定时间(这个阈值可以通过参数设置)没有响应同步的ack,没有向leader进行同步,就会被从ISR集合中删除,被删除的副本不会被立即停用,只要它在一定时间内(一样通过允许的最大滞后时间和消息数参数设置:replica.lag.max.messages / replica.lag.time.max.ms)拉取赶上leader的数据,就可以向leader发送同步的ack,从而被拉回ISR集合中
4.4 broker控制器的选举机制
broker的控制器节点负责管理分区的分配、副本的分配、故障转移等工作
选举过程:
- 第一个broker,broker1启动时,会向zooKeeper注册登记自己的信息,zooKeeper会为这个broker创建一个 broker/ids/1 目录,创建这个目录的目的是维护这个broker的元数据,包括ID、主题分区的分配情况等,方便在选举时协调获取这些broker的必要信息
- broker1会尝试通过zooKeeper创建一个临时的控制节点(controller),创建成功后,这个临时节点的value值就会变成 broker1的broker ID
- 同时,zooKeeper还会创建一个永久节点,用来记录controller_epoch的值,这个值是递增的,因为broker是第一个controller,所以这个值现在是1
这是解决“脑裂”问题的关键,如果broker1异常导致控制器节点发生变化,产生了新的控制器,那么controller_epoch的值就会增加1,变成2;假如broker1仍然向其他broker(如broker3)发送指令,同时新的controller(如broker2)也向broker3发送指令,那么broker就会根据这个controller_epoch的值,判断broker2才是新的控制器,执行broker2的指令
- 当其他的broker进来时,zooKeeper同样会根据他们的broker ID 为它们创建对应的目录
- 这些broker也会尝试创建控制器节点,但是因为此时broker1已经成为了控制节点,所以它们不能创建成功,但是它们会创建一个监听对象(watch),轮询监听这个临时节点,一旦这个临时节点发生变化,它们就会尝试成为新的控制器,这是一种抢占式的选举方式
5. kafka事务
kafka事务指的是在消息的发送和消费过程中,要求一组消息要么全部发送(或消费)成功,要么全部发送(或消费)失败
- 发送消息
在生产者发送消息的过程中,我们可以通过创建事务性的生产者发送消息,启动事务后执行发送操作,如果中途发生异常,就可以回滚事务,这个过程是通过创建代理对象来完成的
public class TransactionalProducerExample {
public static void main(String[] args) {
// 关键步骤
String transactionalId = "my-transactional-id"; // 告诉生产者使用这个ID来标识事务
Properties props = new Properties();
props.put("transactional.id", transactionalId); // 设置事务ID
Producer<String, String> producer = new KafkaProducer<>(props);
producer.initTransactions(); // 初始化事务
try {
producer.beginTransaction(); // 开启事务
// 发送消息
ProducerRecord<String, String> record1 = new ProducerRecord<>(topicName, "key1", "value1");
producer.send(record1);
ProducerRecord<String, String> record2 = new ProducerRecord<>(topicName, "key2", "value2");
producer.send(record2);
producer.commitTransaction(); // 提交事务
} catch (ProducerFencedException | OutOfOrderSequenceException | AuthorizationException e) {
// 处理异常
producer.close();
} catch (KafkaException e) {
producer.abortTransaction(); // 中止事务
// 处理异常
producer.close();
}
producer.close();
}
}
-
消费消息
消费者消费消息时的事务,依靠消费位移的偏移量提交机制来保证:①:假设这么一个场景,有一组消息(m1、m2、m3),我们需要保证这三条消息消费时,要么全部消费成功,要么全部消费失败;
②:当一个消费者实例从kafka中消费一条消息时,它会将消费的偏移量提交回kafka,这个偏移量的提交是原子性的,也就是说,只要成功提交了这个偏移量就证明这条消息被成功消费了,接下来要保证这三条消息都能被提交消费位移,就算是满足了这一组消息消费的原子性了
③:消费者组中有一个专门的消费者作为协调者管理各个消费者的消费位移,当一个消费者实例消费完一个消息,它就会将自己的消息偏移量提交给协调者,由协调者持久化到kafka中,当所有消息的偏移量都被提交以后,就可以做到全部提交成功了
但是这更多是监管,不能保证每个消费者都能成功消费消息并提交偏移量,比如,有两个消费者实例A和B,消费者A消费完m1后,迟迟不能消费m2,而此时消费者B已经消费完成m3了,这种情况被称为消费者实例故障,或者消费者实例失效,针对这个问题,有以下机制可以处理:
📌1. 心跳机制:消费者客户端定期向服务器发送心跳,如果消费者故障,长时间没有发送心跳,那么kafka可以将其标记为失效,并把它的分区分配给其他消费者
📌2. 消费者自动提交消费位移,消费者可以自己提交偏移量,这样当消费者故障重启后还是可以从原来的地方继续处理消息尽管如此,消费者事务的保证还是相对较弱的,重启后再处理消息也是重启后的事情了
6. 消费相关
6.1 消费模式
当我们谈论 Kafka 消费消息时的提交模式时,实际上讨论的是消费者提交偏移量的方式:
-
At Most Once 模式(最多一次):在这种模式下,消费者会在读取消息后先提交偏移量,然后才处理消息。这意味着消息可能会丢失,因为如果在消息处理之前发生故障,那么重新启动后,不会再处理这个消息,而是根据提交的偏移量从下一个偏移量开始读取消息。因此,这种模式保证了消息不会重复处理,最多会被消费一次,但可能会丢失。
-
At Least Once 模式(至少一次):消费者会先处理消息,然后再提交偏移量。这样做可以确保消息不会丢失,因为消息已经处理,即便在提交偏移量的过程中出现异常,重启后也会从出现异常的那个偏移量位置开始消费消息,所以能保证消息至少被消费一次,但可能会导致消息重复处理。
-
Exactly Once 模式(精确传递一次):消费者会将消息的处理和偏移量的提交作为一个原子操作来执行,即消息的偏移量和消费者处理消息的输出结果要一起更新和输出,确保消息只会被处理一次。这种模式要求消息系统和消费者能够确保处理的原子性,通常需要借助事务或幂等性操作来实现。因此,这种模式可以保证消息不会丢失也不会重复。
6.2 如何防止消息丢失与重复消费
Kafka 通过以下方式来保证消息不丢失、不重复消费:
-
持久化:Kafka 使用持久化存储来保证消息不丢失。消息首先被写入到磁盘上的日志文件中,然后才会被消费者消费。即使消费者出现故障,消息仍然会被保留在日志中,不会丢失。
-
偏移量:Kafka 使用偏移量来跟踪每个消费者组在每个分区上消费的位置。消费者消费消息后,会提交偏移量到 Kafka,Kafka 会记录每个消费者组在每个分区上的偏移量信息。这样即使在消费者出现故障后,Kafka 也能够根据提交的偏移量信息来确保消息不会被重复消费。