一、什么是Kafka?
Kafka使用Java和Scale语言开发的,是一种高吞吐量的分布式消息发布订阅系统。主要特点是:高吞吐、高并发、高可靠、低延迟等
二、Kafka基本概念和常见组件
- 生产者(Producer):负责将消息发送到Broker
- 消费者(Consumer):负责从Broker拉取消息
- 消费者组(Consumer Group):由多个消费者组成。一个Partition只能被同一个消费者组中的一个消费者消费,同一个消费者可以消费多个不同的分区
- 服务器(Broker):Kafka服务节点,主要职责是存储消息、分区管理、处理请求、数据复制管理、管理消费者组、垃圾回收等
- 主题(Topic):主要是对消息进行逻辑上的分类,一个Topic可以有多个Partition,一个分区可以有多个Replica
- 分区(Partition):为了实现高扩展性、缓解单Topic在单Broker的负载问题,Kafka可以将一个Topic的数据放在不同的Broker,一个Topic可以分成多个Partition,分区均匀分布在多个Broker上
- 副本(Replication):解决多分区可能存在的数据丢失问题,Kafka将分区数据进行多个备份,备份就是副本
- Leader&Follower:副本类型,多个副本会划分为1个Leader+多个Follower,Leader负载数据的读写,Follower只做备份使用,Leader和Follower被放在不同的Broker
- Controller:Kafka集群管理者,一个特殊的Broker节点,主要负责管理和协调Kafka集群的运行状态
- ZooKeeper:在Kafka4.0版本之前(4.0之后更换为KRaft),ZooKeeper作为分布式协调服务,用于管理Kafka集群元数据、协调Broker之间的通信、Broker的注册和Controller选择等
- 数据文件(Log):Kafka早期主要是用于日志传输和存储,所以数据都存在log文件中
- 数据偏移量(Offset):Kafka每个分区的数据都是有序的,数据偏移量用于快速定位数据的标识,从0开始;类似Java数组中的索引
- 数据起始偏移量(LSO):用于记录每个分区副本数据的起始偏移位置,默认为0,一般不会修改,当数据过期或者删除的时候会更新副本的LSO
- 数据末端偏移量(LEO):用于记录每个分区副本数据的下一个数据的写入位置
- 高水位线(HW):同一个分区的所有副本数据对外可见的偏移量,消费者只能拉取HW之前的数据,同时解决Kafka不同副本之间数据同步问题
- 消费者组协调者(Group Coordinator):Group Coordinator是Broker上的一个组件,用于管理和调度消费者组的消费者、分区分配、状态、偏移量等信息。每个Broker都有一个Group Coordinator,管理多个消费者组,一个消费者组只有一个Group Coordinator。Kafka会创建一个内部主题__consumer_offsets,Group Coordinator将消费者组偏移量等信息持久化到这个主题管理
- 事务管理器(TransactionCoodinator):TransactionCoodinator是Broker上的一个组件,用于管理和协调事务的生命周期,每个Broker都有一个TransactionCoodinator。Kafka会创建一个内部主题**__transaction_state**,TransactionCoodinator将事务状态信息持久化到这个内部主题管理
- Kafka应用接口组件(KafkaApis):Broker内部的一个组件,用于处理生产者发送消息、消费者拉取消息、事务、副本同步等各种请求
- 副本管理器(ReplicaManager):Broker内部的一个组件,主要负责数据的读写和副本的数据同步
三、Kafka生产者相关
1.生产者生产消息的过程
1.1.设置生产者连接配置(序列化),创建生产者连接,准备发送的数据、主题、分区等
1.2.消息先经过拦截器列表进行处理,然后消息key和value进行序列化,再经过分区器确定发送的分区编号
- 拦截器:对数据进行校验等统一操作,比如校验数据大小是否符合要求等;可以自定义拦截器,实现ProducerInterceptor接口,重写onSend方法,然后创建生产者的时候配置自定义的拦截器即可
- 序列化:确保消息通过字节流的形式在网络中高效传输和存储
- 分区器:确定消息发送的分区编号,可以自定义分区器,实现Partition接口,重写partition方法,创建生产者时配置自定义的分区器即可
- 生产者发送消息时有指定分区编号则直接使用
- 有自定义分区器时使用分区器产生的分区编号
- 没有指定编号也没有自定义分区器,消息有key的情况下,Kafka会根据key进行非加密散列计算后得到散列值(类似hash),然后散列值对主题分区数量进行取取余,得到的就是分区编号
- 没有指定编号也没有自定义分区器,消息没有key的情况下,kafka会采用粘性分区策略。第一次先随机选择一个分区数据发送,后面会根据分区中数据量判断选择分区
1.3.消息保存到数据收集缓存区中
数据收集缓存区中会有很多批次队列用来缓存数据,缓存区默认大小是32MB,一个批次队列默认大小是16K;批次队列并不是固定存满16K数据就不存,批次队列会保证数据的完整性,如果一条消息大于16K,kafka也算一个批次数据;批次队列是以topic进行区分的,同一个批次队列的数据都是同一个topic的
1.4.Sender定时从缓存区中读取满足要求的消息批次队列
数据从缓存区进入Sender发送线程会根据Broker进行区分,不再以topic进行区分,方便消息投递到broker
1.5.消息进入在途请求列表,在途请求列表长度默认是5,也就是可以同时处理5个请求,然后通过Kafka Channel发送消息到Leader所在的Broker
1.6.Broker进行ack响应,生产者根据ack确认消息发送是否成功,如果失败可能会进行重试,重新把数据发送到缓存区中,然后再进行上面的步骤
2.Ack应答机制
Ack是生产者发送数据后Kafka的接收确认方式,确认方式有3种,默认是ack=-1(all)
- ack=0,生产者发送消息请求进入网络客户端的缓存区,Kafka就会向生产者进行应答。这种方式消息可靠性很低,无法保证数据是否保存成功
- ack=1,生产者发送消息成功到达Leader,并且Leader成功将数据写入log文件,Kafka就会向生产者进行应答。这种方式会导致数据发送的效率降低,但会提高消息可靠性
- ack=-1(all),Kafka默认应答方式,生产者发送消息成功到达Leader,Leader成功写入数据到log文件,ISR列表中的所有Follower从Leader成功同步数据并写入log文件,Kafka就会向生产者进行应答。数据发送效率最低,消息可靠性最高
3.如何保证生产者消息不重复发送以及消息有序
- 消息重复发送产生的原因:生产者开启消息重试机制时,ack应答响应可能由于网络问题导致生产者无法收到ack,但实际消息已经发送成功,这时候生产者会重新发送收不到ack的消息,因此产生消息重复发送问题
- 消息乱序问题产生的原因:生产者开启消息重试机制时,本来按照顺序发送的data1、data2、data3消息,由于data2消息发送的时候出现问题导致生产者重新发送data2,导致最终消息实际发送顺序是data1、data3、data2
3.1.解决方案一:生产者开启消息重试机制、幂等性配置、ack=-1(保证同一个生产者会话同一个分区消息不重复发送和消息有序)
开启幂等性配置后,Kafka会给生产者分配一个producerId,加上消息唯一序列号sequenceNum,producerId +sequenceNum可以保证同一个分区的消息是唯一且有序的。因为Broker会缓存最近5个批次的数据,如果新的消息跟Broker缓存中一致就会被忽略,如果新消息与缓存中的消息对比顺序不连续的话会让生产者重试消息发送,直到消息有序,如果新消息在缓存中没有且有序就移除缓存中最前面的消息
3.2.解决方案二:生产者事务、ack=-1、重试机制、幂等性配置(保证同一个生产者同一个分区消息不重复发送和消息有序,可以跨生产者会话幂等)
由于解决方案一只能保证同一个生产者会话同一个分区消息不重复以及有序,当Producer挂掉重启的时候会重新分配新的producerId,导致跟原来挂掉的生产者是两个独立的生产者
开启生产者事务之后,Kafka的事务管理器TransactionCoodinator会创建一个内部主题**__transaction_state**用来管理事务id和producerId,从而解决跨生产者幂等性问题。
4.数据传输语义
- at most once:数据最多传输一次,不管数据是否收到。例子:ack=0
- at least once:数据最少传输一次,数据收不到就一直重试,数据不会丢失,但是会出现数据重复、乱序问题。例子:ack=-1
- exactly once:精准一次,数据最多传输一次,数据不会丢也不会重复以及乱序。例子:事务+幂等性+重试+ack=-1
四、Kafka消费者相关
1. Kafka的分区分配策略,默认分配策略是【范围分配RangeAssignor、协作粘性分配CooperativeStickyAssignor】,开始是RangeAssignor,满足一定条件会从RangeAssignor升级到CooperativeStickyAssignor
1.1. RoundRobinAssignor(轮询分配策略):每个消费者都会自动生成一个UUID作为memberId,轮询策略对所有消费者根据memberId进行排序,按照消费者订阅的主题依次进行排序,没有订阅当前轮询主题的消费者会跳过。例子:假设存在消费者【aaa、ccc】订阅3个分区的主题test1【test1-0、test1-1、test1-2】,消费者【aaa、bbb】订阅3个分区的主题test2【test2-0、test2-1、test2-2】
轮询策略中会将每个消费者按照memberid进行排序,所有member消费的主题分区根据主题名称进行排序
将主题分区轮询分配给对应的订阅用户,注意未订阅当前轮询主题的消费者会跳过
消费者aaa分配3个分区、bbb分配2个分区、ccc分配1个分区,明显分区分配不均衡
轮询的缺点:消费者分区分配不均衡
1.2. RangeAssignor(范围分配策略): 一个主题的分区尽可能平均分配给消费者,如果不能平均分,则顺序向前分配补齐
范围分配的优缺点:
优点:在单topic情况下比较均衡;
缺点:多topic情况下,排序靠前的消费者负载比靠后的多很多、消费者新增或者减少都重新建立分区节点的连接,影响一定的效率
1.3. StickyAssignor(粘性分配):在第一次分配后,每个组成员保留分配给自己的分区信息,有新增或减少消费者的情况下,进行再次分配时,尽可能保留原有消费者分区作最小改变的前提下进行再次分配
粘性分区的优缺点:分区分配更加均匀和高效
1.4.CooperativeStickyAssignor(协作粘性分配):一种优化后的粘性分配策略,上面三种分配策略在分配的时候消费者都需要进行分区连接的关闭,在粘性分配的基础上,使用COOPERATIVE协议优化了重分配的过程
2. Kafka的Rebalance机制(分区分配机制):Rebalance机制是消费者组负载均衡和容错的核心机制,主要是分配消费者和分区之间的对应关系,确保消费者之间尽量负载均衡
2.1. Rebalance触发时机
2.1.1. 消费者数量变化
- 新增消费者:消费者组加入新的消费者实例
- 消费者退出:消费者主动关闭或者崩溃,导致与协调者Group Coordinator失去连接
- 消费者被踢出:消费者心跳信息发送出现问题导致超时,协调者Group Coordinator将消费者踢出
2.1.2. 主题分区数量变化
- 新增分区:管理员增加已创建的topic分区数量
2.1.3. 消费者订阅主题关系发生变化
- 消费者新增主题订阅
- 消费者取消主题订阅
2.1.4. 协调者发生变化
- 协调者Group Coordinator所在broker发生故障导致消费者组需要重新选举新的协调者Group Coordinator
2.2. Rebalance执行过程
2.2.1.消费者组暂停消费:协调者Group Coordinator检测到满足Rebalance条件时,通知消费者提交offset后暂停消费,进入Rebalance状态
2.2.2.消费者加入消费者组
- 每个消费者向协调者Group Coordinator发送JoinGroup请求,加入消费者组
- 协调者Group Coordinator为每个消费者组中的消费者选择出一个Leader消费者,其它为Folower消费者(通常是第一个加入的消费者为leader)
2.2.3.Leader消费者分配分区
- Leader消费者根据当前消费者数量、分区数量和分配策略,为每个消费者分配分区
- Leader消费者将分配结果同步给Group Coordinator
2.2.4.协调者Group Coordinator同步分配结果
- 协调者Group Coordinator将Leader分配的结果广播给消费者组的所有消费者
- 每个消费者更新自己的分区消费关系
2.2.5.消费者恢复消息消费
- 消费者根据重新分配的分区进行消息消费
2.3. Rebalance负面影响
- 消息处理延迟:Rebalance的时候,消费者会短暂暂停消息消费
- 重复消费或消息丢失:消费者在Rebalance前提交offset出现问题,可能导致消息丢失或者重复消费
- 性能开销:Rebalance过程中消费者需要和协调者Group Coordinator进行多次通信,增加网络开销和协调者的负载
2.4. Rebalance优化策略
- 合理设置消费者数量:消费者数量应该与分区数量匹配,避免消费者数量过多变化触发频繁的Rebalance
- 选择合适的分区分配策略
- 尽量避免topic订阅的频繁变更
- 调整消费者参数:合理设置session.timeout.ms和heartbeat.interval.ms,适当延长心跳超时时间,避免消费者心跳超时被协调者Group Coordinator踢出
3. Offset(偏移量):消费者用于定位数据位置的标志,Kafka默认从当前主题最后Offset开始消费消息,如果生产者先生产消息再启动消费者,会导致消费者启动之前生产者生产的消息无法被消费,需要根据情况调整消费者偏移量相关参数auto.offset.reset
3.1. 起始偏移量(auto.offset.reset),消费者未提交Offset之前按照这个参数进行消息消费,提交之后按照Broker保存的Offset进行消息消费
- earliest:新启动的消费者组都会从头开始消费topic的所有消息,包含历史消息
- latest(默认情况):消费者组启动后只消费新产生的消息,不管历史消息
3.2. 指定偏移量消费:消费者可以根据集群分区信息手动设置topic的消息偏移量
3.3. 偏移量提交:生产环境中,消费者可能因为某些问题重新启动,如果不知道之前消费数据的位置,就会导致消息重复消费或丢失。所以消费者每次消费都需要提交Offset给Broker保存,这样消费者重启之后就能按照之前提交的Offset继续消费消息。Offset提交分为自动提交和手动提交
- 自动提交(Kafka默认消费者消费消息机制):默认情况下,消费者会定时自动提交Offset给Broker,默认时间是5000ms,可以通过配置参数修改时间间隔。因为网络和提交时间间隔问题,自动提交可能会导致消息重复消费
- 手动提交:由于自动提交Offset容易导致消息重复消费,Kafka提供手动提交机制,需要禁用自动提交auto.offset.reset=false开启手动提交,手动提交也可能导致消息重复消费,不过相对自动提交概率小一些,手动提交分为同步和异步两种方式,前提都是关闭自动提交,设置auto.offset.reset=false:
- 异步提交:消息消费后消费者手动执行commitAsync()方法,异步提交消费者无需等待Broker响应就可以消费下一条消息
- 同步提交:消息消费后消费者手动执行commitSync()方法,同步提交消费者需等待Broker响应才能消费下一条消息
消费者如何保证消息不重复消费:在生产者生产消息幂等性的前提下,消费者为了保障消息不重复消费可以选择手动提交offset,同时另外维护一个已处理的消息列表,每次消息消费时先进行去重判断
3.4. 偏移量保存:Kafka会创建一个内部主题__consumer_offsets(默认50个分区),用于保存消费者提交的offset,至于保存到哪个分区Kafka是通过hash(consumerGroupId)%50得到
4. 消费者事务隔离级别:生产者开启事务之后,可能存在一些事务未提交成功的数据,消费者默认是已提交读事务隔离级别
- 已提交读事务隔离级别(默认配置):只读取事务提交成功的数据
- 未提交读事务隔离级别:读取事务提交成功和事务未提交成功的数据
5. 消费者消费消息的过程
5.1. 服务端获取消费者拉取数据的请求
消费者向Broker发送拉取数据的请求FetchRequest,请求携带topic、fetchOffset等相关信息,Broker接收到请求后交由KafkaApis进行处理
5.2. 通过副本管理器(ReplicaManager)拉取数据
副本管理器先确定要拉取的数据分区,然后再进行数据读取操作
5.3. 判断首选副本
Kafka2.4版本之前,读写都是通过Leader进行的,从2.4版本开始后,支持Follower读取数据,主要原因是跨机房或跨数据中心的场景,为了节约流量,可以从当前机房或数据中心的副本中获取数据,称之为首选副本
5.4. 拉取分区数据
Kafka底层读取数据是通过日志段LogSegmenet对象进行操作
5.5. 零拷贝
Kafka为了提高读取效率,底层使用NIO的FiLeChannel零拷贝技术,直接在操作系统内核进行数据传输,提高数据拉取的效率
6. 消费者为什么poll不用push的方式获取数据
6.1. 消费者的消费能力是有限的,如果采用Broker主动push给消费者的方式可能会导致消费者消息积压,从而造成服务器网络、存储等资源压力很大
6.2. 采用消费者主动poll的方式,不同消费能力的消费者可以根据自身去拉取数据,提高消息处理的效率
五、Kafka消息存储相关
1.数据存储的日志文件
每个主题的每个分区都会创建一个文件夹存储数据,每个分区都会创建多个文件段(Segment),每个Segment创建下面3个文件用于存储数据:
- 数据文件xxx.log:实际存储数据的文件,数据以二进制的方式存储,每个消息的偏移量、创建时间等信息都会被记录。可以通过
log.segment.bytes设置文件大小,默认是1G,超过这个值就会创建新的log文件 - 数据索引文件xxx.index:数据索引文件(稀疏索引),主要是加速数据的检索。存储的是offset和position的关系。可以通过
log.index.interval.bytes来决定索引的密度 - 时间索引文件xxx.timeindex:时间索引文件(稀疏索引),存储的是时间和偏移量之间的关系。可以通过
log.index.interval.bytes来决定索引的密度
2.数据的刷写
数据的刷写基于操作系统的刷盘机制实现的,以Linux为例,当我们把数据写入文件系统之后,其实数据是在操作系统的页缓存(PageCache),并没有写入磁盘中,如果操作系统挂了,数据有可能就丢失了。操作系统本身会定时刷盘;此外Kafka也提供手动刷盘的配置:
- log.flush.interval.messages:达到多少消息数量时强制刷盘
- log.flush.interval.ms:消息缓存多少时间(ms)强制刷盘
- flush.scheduler.interval.ms:间隔多少时间(ms)强制刷盘
3.副本数据同步
Kafka中只有Leader副本负责数据的写入,其它follower副本需要定时同步Leader副本的数据写入到自己本地日志,保证所有副本上的数据是一致的
3.1.Follower启动数据同步线程
创建topic的完成之后,被副本管理器分配为Follower的副本就会启动数据同步线程
3.2.Follower定时发起数据同步请求
Follower启动数据同步线程之后,就会定时向Leader请求数据同步
3.3.Leader处理数据同步请求,Leader更新HW、LEO偏移量
Leader接收到Follower数据同步请求,将数据同步给Follower,同时更新HW、LEO偏移量
4.Kafka如何保证不同副本之间的数据一致性
不同副本同步数据速度是不一致的,有可能存在Leader副本挂掉,然后重新选了一个Leader出来,此时新Leader的数据跟原来的不一致
所以Kafka引入高水位线标志(HW),在副本同步数据的时候,不同Follower同步的数据量不同,也就是不同的副本LEO偏移量可能不同,HW取所有副本的最小值(类似水桶理论,最短的木板决定装水的高度),消费者只能消费到HW线以下的数据
5.AR、ISR、OSR是什么
- AR(All Replication):Topic所有副本集合
- ISR(In Sync Replication):Topic正在同步数据的副本集合,集合的第一个副本就是Leader
- OSR(Out Sync Replication):Topic没有同步数据的副本集合
6.ISR列表的收缩与扩张
OSR中的副本拉取数据满足一定条件就会从OSR移除加入ISR,ISR中的副本没有拉取数据满足一定条件就会从ISR移除加入OSR
7.日志清理和压缩
-
日志清理,log.cleanup.policy=delete,有两种策略,默认是基于时间删除数据
- 基于时间删除过期数据:默认配置7天,超过保留时间的数据被删除
- 基于数据大小删除:默认值是-1,表示数据无穷大,永不删除数据 如果一个segment(日志段)中有一部分数据过期,一部分没有过期,怎么处理?Kafka会等待整个日志段数据过期才删除数据
-
日志压缩(会造成数据丢失),log.cleanup.policy=compact
相同key的数据只保留最新的一条数据,其它删除
8.页缓存(PageCache)
页缓存是操作系统的一种磁盘缓存,主要作用是减少磁盘IO操作。就是将磁盘数据缓存到内存中,把对磁盘的访问转成对内存的访问,提升数据读取效率。
- 数据读取的时候会先判断PageCache是否命中数据,命中则直接返回;否则向磁盘发起读取请求将数据加载到PageCache中,再返回数据
- 数据写入的时候会先判断PageCache是否存在,不存在则新增缓存页,然后将数据写入对应的页,被修改后的页就变成脏页,操作系统定时将脏页的数据写入磁盘
9.零拷贝
零拷贝不是指不用数据拷贝,而是不需要用户态的数据拷贝,数据直接在操作系统内核完成数据的传输。由于传统数据传输需要在用户态和系统内核态来回切换,这种切换是很耗费时间和资源的。零拷贝通过减少数据IO操作,大大提升了数据传输效率
六、Kafka其它常见问题
1.Controller是如何选举的?
Controller是Kafka集群管理者,一个特殊的Broker节点,主要负责管理和协调Kafka集群的运行状态。Controller的选择依赖ZooKeeper的选举机制,所有Broker都会监听Controller临时节点,如果该节点不存在,Broker就会申请创建,所有Broker都会发起创建请求,谁先创建成功谁就是Controller,Controller创建成功之后就会更新Controller纪元字段(类似Controller的版本号)
2.集群脑裂问题
Broker1是集群的Controller,由于某些问题导致挂掉,这时候会重新选举出一个新的Controller,然后Broker1又恢复正常,这时候Broker1仍认为自己是集群的Controller,那么就会出现两个Controller向其它Broker同步集群信息(相当于多个大脑,所以叫脑裂问题),其它Broker究竟听哪个Controller的?很明显Broker1同步的集群信息是错误的,由于Controller选择成功后会更新保存在ZooKeeper的Controller纪元参数,集群根据判断Controller的纪元参数跟ZooKeeper是否一致,一致就是正确的Controoler
3.Broker的上下线
Controller创建的时候会注册一个监听Broker节点变化的监听器,每一个Broker上线会在ZooKeeper中注册一个节点brokerId,表示有新的Broker上线;如果有节点消失,表示Broker下线。从而Controller可以掌握整个集群的Broker节点变化并同步给其它Broker
4.如何删除Topic
- 使用AdminClient删除,adminClient.deleteTopics()
- 使用Kafka管理工具,例如Kafka tool等
- 使用kafka-topics.sh命令行删除
5.Kafka如何保证有序
- 生产有序:事务、ack=-1、重试、幂等性(保障同一个分区的数据有序)
- 存储有序:Kafka数据写入日志文件的时候是顺序写入(保障同一个分区的数据有序)
- 消费有序:消费者组会保存每个消费者访问数据的偏移量,消费者按照偏移量读取数据就不会乱序