kafka设计和原理解析

文章目录

  • 副本与ISR设计
    • follower 副本同步
      • 基于水印备份复制
        • LEO更新机制
        • HW更新机制
        • 缺陷
      • 基于leader epoch
  • 物理存储
    • 分区分配
    • 日志段文件
    • 索引文件
      • 位移索引文件
      • 时间戳索引文件
    • 日志留存
    • 文件格式
    • log compaction(日志压实)
  • 处理请求
    • 元数据请求
    • 生产请求
    • 获取请求
  • 控制器
    • 控制器职责
      • 更新集群元数据信息
      • 创建topic
      • 删除topic
      • 分区重分配
      • preferred leader选举
      • topic分区拓展
      • broker加入/退出集群
      • 受控关闭
      • controller leader选举
  • 可靠性保证
  • 参考

副本与ISR设计

对于特定的主题下的特定分区,可能存在多个副本,具体分为两类:

  1. 首领副本,每个分区有一个首领副本,所有生产者和消费者请求都会经过这个副本。首领需要确保跟随者和自己的状态是一致的。为了保持和首领同步,跟随者向首领发送获取数据的请求,会附带想要获取的消息的偏移量,这个偏移量表示消费者的消费进度。如果跟随者在10s内没有请求任何消息或者请求最新的数据,则会被认为是不同步的。
  2. 跟随者副本,跟随者不处理来自客户端的请求,只负责从首领复制消息,保持与首领一直的状态。如果首领发生崩溃,其中的一个和首领状态完全同步的跟随者会被提升为新首领。

分区首领是同步副本,而对于跟随者而言,需要满足以下条件才能被认为是同步的:

  1. 与Zookeeper保持一个活跃的会话,在过去配置时间内向Zookeeper发送心跳
  2. 在过去配置时间内从首领那里获取过消息
  3. 在过去配置时间内从首领那里获取过最新的消息。

一个滞后的同步副本会知道生产者和消费者变慢,因为生产者可能需要等待其同步,消费者需要确保都同步后才能收到标记为已提交的消息。

所有的同步副本信息维护在zookeeper的ISR中,每个topic分区都有自己的ISR列表。

follower 副本同步

基于水印备份复制

先看副本的各个位置信息:

  1. 起始位移(base offset):表示该副本当前所含第一条消息的offset
  2. 高水印值(high watermark,HW)保存该副本最新一条已提交消息的位移。确定了consumer能够获取的消息上限。超过HW的消息对消费者来说都是不可见的
  3. 日志末端位移(log end offset, LEO):副本日志下一待写入消息的offset。所有副本的LEO信息可能会不一样。分区的HW即为所有副本中的最小的LEO。

LEO更新机制

  1. 对于leader端的LEO更新时机为每次写log的时候
  2. 对于follow端,会有两个副本LEO,分别存在leader和follow中,即leader存储了所有follow的LEO副本,基于这些副本来帮助leader更新HW。每次follow往leader拉取消息时,会同步更新leader端的follow LEO和follow端的LEO。

HW更新机制

  1. 对于follow,每次从leader拉取数据时,会比较当前LEO和leaderHW,取两者中的小值为新的HW。follow的HW值不会超过leader HW值

  2. 对于leader,有4中情况尝试更新,不满足条件则不更新:

    1. 分区leader发生变化,此时leader副本会尝试更新HW
    2. broker出现崩溃导致副本被踢出ISR时:若有broker崩溃,会检查是否波及当前分区
    3. producer向leader副本写入消息时:会更新leader的LEO,会查看HW值是否需要更新
    4. leader处理follower拉取请求时:会从底层的log读取数据,然后尝试更新HW值

    满足更新尝试条件时,leader会找出所有的同步副本,比较所有的LEO,取其中的最小值为HW。其中同步副本满足以下两个条件之一:
    1. 在ISR中
    2. 副本LEO落后于leader LEO的时长不大于replica.lag.time.max.ms(默认为10s),主要处理特殊时期下刚好追上leader进度,但不在ISR的情况

缺陷

基于水印同步会引起两个问题:

  1. 数据丢失,基于以下图示来分析:

    在开始时候,A.LEO=1,A.HW=0,B.HW=0,B.LEO=1。B向A发出拉取请求,此时A会更新自身HW=1(并通知生产者消费成功),B会尝试更新自身HW=min(A.HW=1,B.LEO=1)=1,但这个时候B故障重启,重启后会调整LEO=HW=0,导致HW未更新,而后A副本挂掉,B称为leader,导致了第一条消息丢失。
  2. 数据不一致/数据离散,基于以下图示分析:

    开始A是leader,B是follow。消息情况如图所示,而后A、B挂掉,B称为leader,接收生产者消息3,但实际更新了自身的偏移wei位2,而后A恢复称为follow,此时从外部看A,B是同步的,但实际上A的第二条消息和B的第二条消息不是同一条消息,导致了数据不一致的情况。

基于leader epoch

在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个副本。在分配时有以下目标:

  1. 在broker间平均地分布分区副本,即确保每个broker可以分到5个副本
  2. 确保每个分区的每个副本分布在不同的broker
  3. 如果为broker指定了机架信息,呀哦尽可能把每个分区的副本分配到不同机架的broker上,以保证一个机架的不可用不会导致整体的分区不可用。

为分区和副本选好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后缀文件,分别成为位移索引文件和时间戳文件。

  1. 位移索引文件按照位移顺序保存,可以帮助broker更快定位记录所在的物理文件位置。
  2. 时间戳索引文件按照时间戳顺序保存,根据给定的时间戳查找对应的位移信息。
    kafka基于二分查找目标索引项,整体时间复杂度为O(lgN)

两类索引文件都是稀疏索引文件(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。

索引项的具体内容包括相对位移和文件物理位置:

  1. 文件物理位置:记录某条消息相对文件起始的偏移字节数。
  2. 相对位移:索引项中相对位移记录的是和索引文件其实位移的差值(即索引文件名),索引项中的位移都是生序排序的,以此来保证查找的性能。通过位移索引文件,broker可根据指定位移快速定位到记录的物理文件位置,或至少定位出离目标记录最近的低位文件位置。位移索引查找过程如下所示:

    假如要查找位移为7000的消息先定位大小于7000的最大索引项(2650,1150100)而后从1150100字节开始顺序查找记录。

时间戳索引文件

时间戳索引文件的索引项格式如下所示:
每个索引项固定占用12字节的物理空间,同样的Kafka强制要求索引文件必须是索引项大小的整数倍。

索引项的具体内容包括8字节的时间戳和4字节的相对位移。查找过程是先根据时间戳找到最近的相对位移,再通过.index文件,通过相对位移找到实际的消息物理位置。

日志留存

Kafka存储的消息数据会有一个保留规则:

  1. 规定数据的保留时长
  2. 规定数据的保留数据量大小

默认情况下,一个分区的数据会被分成若干个片段,每个片段包含1GB或一周的数据,以较小的为准。在broker网分区写入数据时,如果达到片段上线,就关闭当前文件,打开新文件。当前写入的数据片段称为活跃片段,活跃片段永远不会被删除。

文件格式

kafka接收来自生产者的消息保存到文件,再使用零复制技术给消费者发送消息,期间消息格式不会发生变化,避免了对生产者已经压缩的消息进行解压和再压缩。

除了键值、偏移量外,消息还包含消息大小、校验和、消息格式版本号、压缩算法和时间戳。如果生产者发送压缩过的消息,则同一批次的消息会被压缩再一起,被当作“包装消息”发送,如下所示:

log compaction(日志压实)

log compaction确保kafka topic下每个分区的每条具有相同key的消息都至少保存最新value的消息,一个应用场景如用户修改了三次信息,发了三次消息,只需保留最新信息对应的消息,则可以以用户id为key,kafka会定期压实保留最新key的value。

具体实现原理:每个日志段会分为两部分:

  1. 干净的部分,之前没清理过,每个健保留最新的一条消息
  2. 污浊的部分,在清理之后写入的,未被清理过,具体又会分成两部分:
    1. 可被清理的
    2. 不可被清理的,通过配置log.cleaner.min.compaction.lag.ms,表示最新配置时间内的日志不会被请求

kafka如果启用了清理功能(log.cleaner.enabled=true且log.cleanup.policy=compact)。会在清理时初始化一个map,映射关系是键的散列值和消息的偏移量。在开始清理时,会从干净的片段读取消息,从前往后不断添加/覆盖,到最后,map存储的是所有键最新的偏移量。而后对照原来的片段,对旧值进行清理,如下所示:

处理请求

Kafka协议的所有请求和响应都具有统一的格式,即Size+Request/Response,其中Size是int32,表征请求或响应的长度。

请求可划分为请求头部和请求体,请求头部的结构是固定的,由以下4个字段组成:

  1. api_key: 请求类型,int16整数
  2. api_version: 请求版本,int16整数
  3. correlation_id: 与响应关联的对应编号,方便用户调试和排错,int32整数
  4. client_id,表示发出此请求的client ID,非空字符串。

响应同样包含响应头部和响应体。响应头部只有一个字段:

  1. correlation_id:就是上面请求头部中的correlation_id,和请求建立关联关系

元数据请求

这个请求包含客户端感兴趣的主题列表,broker响应消息里指明了这些主题所包含的分区、每个分区都有哪些副本,以及哪个副本是首领。这个请求可以发送给任意broker,因为所有broker都缓存了这些信息。

客户端获取元数据后,会进行缓存,并时不时地刷新以获取最新数据。当生产者和消费者发送请求到指定的首领时,如果首领已经发生变化,则发送请求的客户端会收到一个“非分区首领”的错误响应,然后会尝试重发元数据请求,擦好姑娘是获取最新的元数据。

生产请求

包含首领副本的broker在收到生产请求,会做一些校验:

  1. 发送数据的用户是否有主题写入权限
  2. acks值是否有效(0,1或all)
  3. 如果acks=all,是否有足够多的同步副本保证消息已经被安全写入

验证后,消息会被写入本地磁盘或本地文件系统缓存,而后判断如果acks为all,则会缓存请求,知道首领发现所有粉碎者副本都复制了消息,响应此阿辉返回给客户端

获取请求

消费者尝试获取消息,会指定主题、分区、消费的起始偏移量等,还会指定从一个分区里返回的最大数据,避免客户端内存不足。

首领节点在接受请求时,会校验请求是否有效,如指定的偏移量在分区是否存在。

Kafka采用零复制技术想客户端发送消息,Kafka直接把消息从文件(Linux文件系统缓存)里发送到网络通道,不经过任何中间缓冲区,以避免字节复制和管理内存缓冲区,获取更好的性能。

分区首领在将消息发送给客户端前,需要保证消息已经被写入所有同步副本。

控制器

控制器本身是一个broker,除此外还负责分区多副本的首领选举。集群里第一个启动的broker通过在Zookeeper里创建一个临时节点/controller让自己称为控制器,其他并发节点会创建失败。而后会在控制器节点创建一个监听器。

在控制器节点宕机后,会再次尝试注册让自己称为新的控制器。当新的控制器节点诞生后,会在zookeeper拿到一个数值更大的controller epoch。当其他broker知道当前的controller epoch后,如果收到较旧的epoch消息,就会忽略。控制器使用epoch来避免脑裂(指两个节点同时认为自己是当前的控制器)

控制器职责

控制器的职责包括:

  1. 更新集群元数据信息
  2. 创建topic
  3. 删除topic
  4. 分区再均衡
  5. preferred leader副本玄虚
  6. topic分区拓展
  7. broker加入集群
  8. broker崩溃
  9. 受控关闭
  10. controller leader选举

更新集群元数据信息

客户端可以向任意一台broker发送元数据,随着集群的运行,元数据信息可能发生变化,controller负责监听变更的消息,封装成UpdateMetadataRequests请求发送给每个broker,以同步最新的集群元数据。

创建topic

controller会在ZooKeeper创建一个监听器,监听ZooKeeper节点/brokers/topics下节点的变更情况,当新主题创建后,/brokers/topics节点下新增一个znode。controller会为新建的topic的每个分区确定leader和IDR,然后更新集群的元数据信息。

删除topic

当Kafka触发删除topic操作时,会在ZooKeeper的/admin/delete_topics下新建一个znode。controller启动会创建一个监听器监听该路径下的子节点变更情况,一旦发现有新增节点,则开启删除topic逻辑,这会触发两个操作:

  1. 停止所有副本运行
  2. 删除所有副本的日志数据
    完成后controller会移除/admin/delete_topics/<待删除topic>节点,表示topic删除操作完成。

分区重分配

通常由kafka集群管理员发起,对topic的所有分区重新分配副本所在broker位置,管理员需要手动指定分配方案并按指定格式写入ZooKeeper的/admin/reassign_partitions节点下。具体重分配流程为:

  1. 在ZooKeeper上创建/admin/reassign_partitions节点,存入分配方案。
  2. controller监听到/admin/reassign_partitions节点变更,controller获取该列表。
  3. 对列表中的所有partition,controller会做如下操作:
    1. 启动RAR-AR中的Replica,即新分配的Replica。(RAR = Reassigned Replicas, AR = Assigned Replicas)
    2. 等待新的Replica与Leader同步
        3. 如果Leader不在RAR中,从RAR中选出新的Leader
        4. 停止并删除AR-RAR中的Replica,即不再需要的Replica
        5. 删除/admin/reassign_partitions节点

preferred leader选举

假如原来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,再广播出去。

topic分区拓展

在新增分区后,会在/brokers/topics/节点下写入新的分区目录,controller会监听到分区变化,执行对应的分区创建任务(如选举leader和ISR),之后会更新集群元数据信息。

broker加入/退出集群

新的broker加入/退出集群时,会在/broker/ids下创建,并写入broker信息或会话过期删除一个znode,controller会监听到配置变化,执行对应的broker启动/退出任务,之后更新集群元数据信息并广播。

受控关闭

broker通过脚本等方式被关闭时,会与controller建立RPC请求,同步堵塞等待controller响应,controller会在处理完必要的leader重选举和ISR收缩调整后,给broker发送响应,然后broker完成正常退出。

controller leader选举

当发生以下种情景,会触发controller重选举

  1. 关闭controller所在broker,或broker宕机崩溃
  2. 手动删除zk的/controller节点,或在ck的/controller节点下写入新的broker.id

所有broker会监听/controller节点,当节点发生变化时,所有broker会争抢创建该节点,并存储所在broker.id,创建成功的broker成为controller,同时会增加/controller_epoch节点的值。

当控制器发现一个broker离开节点(监听的zokeeper路径发生变化),且这个broker负责的分区恰好是首领分区,那么失去首领的分区需要一个新首领,控制器会遍历分区,并确定分区的新首领(简单来说就是分区副本列表的下一个副本)。然后向所有包含新首领或现有跟随者的broker发送请求,包括首领和追随者信息。

当控制器发现一个broker加入集群,会检查新broker的brokerId是否包含现有分区的副本。如果有,控制器就把变更通知发给新加入的broker和其他broker,新broker的副本从首领哪里复制消息

可靠性保证

kafka基于以下保证消息的可靠性:

  1. 保证分区消息的有序性,在同一个分区内,先生产投递的消息总是会被先消费
  2. 只有当消息被写入分区的所有副本时(但不一定是磁盘),才被认为是“已提交”的
  3. 消费者只能读取“已提交”消息
  4. 只要还有一个副本是活跃的,那么已经提交的消息就不会丢失

参考

  1. Kafka设计解析:Replication工具
  2. Kafka权威指南
  3. Apache Kafka实战

你可能感兴趣的:(kafka)