对于特定的主题下的特定分区,可能存在多个副本,具体分为两类:
分区首领是同步副本,而对于跟随者而言,需要满足以下条件才能被认为是同步的:
一个滞后的同步副本会知道生产者和消费者变慢,因为生产者可能需要等待其同步,消费者需要确保都同步后才能收到标记为已提交的消息。
所有的同步副本信息维护在zookeeper的ISR中,每个topic分区都有自己的ISR列表。
先看副本的各个位置信息:
对于follow,每次从leader拉取数据时,会比较当前LEO和leaderHW,取两者中的小值为新的HW。follow的HW值不会超过leader HW值
对于leader,有4中情况尝试更新,不满足条件则不更新:
满足更新尝试条件时,leader会找出所有的同步副本,比较所有的LEO,取其中的最小值为HW。其中同步副本满足以下两个条件之一:
1. 在ISR中
2. 副本LEO落后于leader LEO的时长不大于replica.lag.time.max.ms(默认为10s),主要处理特殊时期下刚好追上leader进度,但不在ISR的情况
基于水印同步会引起两个问题:
在0.11.0.0版本后,kafka引入leader epoch替代HW,解决了水印备份复制机制的两个问题。
leader epoch实际为一对值(epoch,offset)。epoch表示leader的版本号,leader发生变化,则epoch+1。offset为epoch版本对应的leader写入第一条消息的偏移,假设存在两对值(0,0),(1,120)表示第一个leader从0开始写消息,共写了120调,第二个leader从120开始写消息。
每次副本重新成为leader会查询这部分缓存,后去对应leader版本的位移,以避免数据不一致和丢失的情况。
避免数据丢失:
避免数据不一致:
在配置Kafka时,通过log.dirs指定了存储分区的目录列表。在创建主题时,Kafka会决定如何在broker间分配分区。
假如有6个broker,创建一个包含10个分区的主题,并且复制系数时3,则每个分区有3个副本。在分配时有以下目标:
为分区和副本选好broker后,会将分区副本分配目录,规则是计算每个目录的分区数量,新的分区都会被创建到这个磁盘上。
对于每个分区日志,Kafka又会进一步细分成日志段文件(log segment file)。每个日志段文件又会有3个后缀文件对应包括.log为具体的消息日志段文件,.index/。timeindex为相应的索引文件
对于.log后缀的日志段文件,Kafka使用第一条消息记录对应的offset来命名该.log文件,放在 t o p i c − {topic}- topic−{分区号}下,Kafka使用20为标志偏移量,则第一个日志段文件为0000000000000000000.log
每个日志段文件会有上线大小,到达后会初始化新的日志段文件和对应的索引文件,这个过程叫做日志切分(log rolling)。其中正在写入的分区日志段文件被成为激活日志段(active log segment) 。
对于.index和.timeindex后缀文件,分别成为位移索引文件和时间戳文件。
两类索引文件都是稀疏索引文件(sparse index file),每个索引文件由若干条索引项(index entry)组成。Kafka不会对每条消息记录都保存对应的索引项,而是待写入若干条记录后才增加一个索引项。log.index.interval.bytes参数设置了这个间隔大小,默认4KB,即Kafka分区至少写入4KB数据后才仔索引文件增加一个索引项,因而本质上他们是稀疏的。
可以通过log.index.size.max.bytes配置每个索引文件的最大文件大小,默认为10MB。创建索引文件时,会预先分配10MB大小,在进行切分时,会裁剪到真实大小,故正在写入的索引文件大小为10MB,而已切分的索引文件往往小于10MB。
位移索引文件的索引项格式如下所示:
每个索引项固定8字节物理空间,Kafka强制要求索引文件必须是索引项的整数倍,如果配置log.index.size.max.bytes=20,则该文件的大小为16。
索引项的具体内容包括相对位移和文件物理位置:
时间戳索引文件的索引项格式如下所示:
每个索引项固定占用12字节的物理空间,同样的Kafka强制要求索引文件必须是索引项大小的整数倍。
索引项的具体内容包括8字节的时间戳和4字节的相对位移。查找过程是先根据时间戳找到最近的相对位移,再通过.index文件,通过相对位移找到实际的消息物理位置。
Kafka存储的消息数据会有一个保留规则:
默认情况下,一个分区的数据会被分成若干个片段,每个片段包含1GB或一周的数据,以较小的为准。在broker网分区写入数据时,如果达到片段上线,就关闭当前文件,打开新文件。当前写入的数据片段称为活跃片段,活跃片段永远不会被删除。
kafka接收来自生产者的消息保存到文件,再使用零复制技术给消费者发送消息,期间消息格式不会发生变化,避免了对生产者已经压缩的消息进行解压和再压缩。
除了键值、偏移量外,消息还包含消息大小、校验和、消息格式版本号、压缩算法和时间戳。如果生产者发送压缩过的消息,则同一批次的消息会被压缩再一起,被当作“包装消息”发送,如下所示:
log compaction确保kafka topic下每个分区的每条具有相同key的消息都至少保存最新value的消息,一个应用场景如用户修改了三次信息,发了三次消息,只需保留最新信息对应的消息,则可以以用户id为key,kafka会定期压实保留最新key的value。
具体实现原理:每个日志段会分为两部分:
kafka如果启用了清理功能(log.cleaner.enabled=true且log.cleanup.policy=compact)。会在清理时初始化一个map,映射关系是键的散列值和消息的偏移量。在开始清理时,会从干净的片段读取消息,从前往后不断添加/覆盖,到最后,map存储的是所有键最新的偏移量。而后对照原来的片段,对旧值进行清理,如下所示:
Kafka协议的所有请求和响应都具有统一的格式,即Size+Request/Response,其中Size是int32,表征请求或响应的长度。
请求可划分为请求头部和请求体,请求头部的结构是固定的,由以下4个字段组成:
响应同样包含响应头部和响应体。响应头部只有一个字段:
这个请求包含客户端感兴趣的主题列表,broker响应消息里指明了这些主题所包含的分区、每个分区都有哪些副本,以及哪个副本是首领。这个请求可以发送给任意broker,因为所有broker都缓存了这些信息。
客户端获取元数据后,会进行缓存,并时不时地刷新以获取最新数据。当生产者和消费者发送请求到指定的首领时,如果首领已经发生变化,则发送请求的客户端会收到一个“非分区首领”的错误响应,然后会尝试重发元数据请求,擦好姑娘是获取最新的元数据。
包含首领副本的broker在收到生产请求,会做一些校验:
验证后,消息会被写入本地磁盘或本地文件系统缓存,而后判断如果acks为all,则会缓存请求,知道首领发现所有粉碎者副本都复制了消息,响应此阿辉返回给客户端
消费者尝试获取消息,会指定主题、分区、消费的起始偏移量等,还会指定从一个分区里返回的最大数据,避免客户端内存不足。
首领节点在接受请求时,会校验请求是否有效,如指定的偏移量在分区是否存在。
Kafka采用零复制技术想客户端发送消息,Kafka直接把消息从文件(Linux文件系统缓存)里发送到网络通道,不经过任何中间缓冲区,以避免字节复制和管理内存缓冲区,获取更好的性能。
分区首领在将消息发送给客户端前,需要保证消息已经被写入所有同步副本。
控制器本身是一个broker,除此外还负责分区多副本的首领选举。集群里第一个启动的broker通过在Zookeeper里创建一个临时节点/controller让自己称为控制器,其他并发节点会创建失败。而后会在控制器节点创建一个监听器。
在控制器节点宕机后,会再次尝试注册让自己称为新的控制器。当新的控制器节点诞生后,会在zookeeper拿到一个数值更大的controller epoch。当其他broker知道当前的controller epoch后,如果收到较旧的epoch消息,就会忽略。控制器使用epoch来避免脑裂(指两个节点同时认为自己是当前的控制器)
控制器的职责包括:
客户端可以向任意一台broker发送元数据,随着集群的运行,元数据信息可能发生变化,controller负责监听变更的消息,封装成UpdateMetadataRequests请求发送给每个broker,以同步最新的集群元数据。
controller会在ZooKeeper创建一个监听器,监听ZooKeeper节点/brokers/topics下节点的变更情况,当新主题创建后,/brokers/topics节点下新增一个znode。controller会为新建的topic的每个分区确定leader和IDR,然后更新集群的元数据信息。
当Kafka触发删除topic操作时,会在ZooKeeper的/admin/delete_topics下新建一个znode。controller启动会创建一个监听器监听该路径下的子节点变更情况,一旦发现有新增节点,则开启删除topic逻辑,这会触发两个操作:
通常由kafka集群管理员发起,对topic的所有分区重新分配副本所在broker位置,管理员需要手动指定分配方案并按指定格式写入ZooKeeper的/admin/reassign_partitions节点下。具体重分配流程为:
假如原来broker1为partition1的leader副本,broker3存在partion1的follow副本,partition2的leader副本,当broker1宕机后,partition1的leader会转移到broker2,这时会导致broker1闲置,broker2繁忙,造成资源分配不均,可以配置preferred leader副本,如partition1的prefered leader为broker1,则在broker1恢复后,会重新将partition1的leader副本转移回broker1。
这可以通过配置auto.leader.rebalance.enable=true实现,controller会定时调整preferred leader。然后再去/admin/preferred_replica_election写入数据,controller监听到会调整副本leader,再广播出去。
在新增分区后,会在/brokers/topics/节点下写入新的分区目录,controller会监听到分区变化,执行对应的分区创建任务(如选举leader和ISR),之后会更新集群元数据信息。
新的broker加入/退出集群时,会在/broker/ids下创建,并写入broker信息或会话过期删除一个znode,controller会监听到配置变化,执行对应的broker启动/退出任务,之后更新集群元数据信息并广播。
broker通过脚本等方式被关闭时,会与controller建立RPC请求,同步堵塞等待controller响应,controller会在处理完必要的leader重选举和ISR收缩调整后,给broker发送响应,然后broker完成正常退出。
当发生以下种情景,会触发controller重选举
所有broker会监听/controller节点,当节点发生变化时,所有broker会争抢创建该节点,并存储所在broker.id,创建成功的broker成为controller,同时会增加/controller_epoch节点的值。
当控制器发现一个broker离开节点(监听的zokeeper路径发生变化),且这个broker负责的分区恰好是首领分区,那么失去首领的分区需要一个新首领,控制器会遍历分区,并确定分区的新首领(简单来说就是分区副本列表的下一个副本)。然后向所有包含新首领或现有跟随者的broker发送请求,包括首领和追随者信息。
当控制器发现一个broker加入集群,会检查新broker的brokerId是否包含现有分区的副本。如果有,控制器就把变更通知发给新加入的broker和其他broker,新broker的副本从首领哪里复制消息
kafka基于以下保证消息的可靠性: