目录
kafka消费模式
kafka架构
kafka生产者消息发送流程
文件存储机制
kafka生产者分区策略
kafka数据可靠性与一致性
Exactly Once
kafka消费者分区策略
consumer offset的维护
kafka读写效率为什么这么高
zookeeper在kafka中的作用
kafka事务
producer事务
consumer事务
kafka是一个分布式的、基于发布/订阅模式的消息队列。
基于发布、订阅模式的消息队列有两种消费模式:
一种是pruducer发送数据后,由消息队列push数据给到consumer,由于消息队列控制着数据传输的速率,而不同consumer消费速率不一致,当消费速率低于生产速率时,consumer就忙不过来了。
kafka采用的另一种消费模式,pruducer把数据push到消息队列,然后consumer从消息队列中pull数据。由consumer自己来决定消费速率。当 consumer 速率落后于 producer 时,可以在适当的时间赶上来。这种模式也有其不足之处,在没有消息时,consumer一直在空轮询,等待数据的到来。
总体架构图:
broker:一个kafka服务器就是一个broker,kafka集群由多个broker组成。一个broker下可以有多个topic
producer:消息生产者,向kafka broker发消息的客户端,直接发送数据到主分区的broker上,不需要经过任何中间路由
consumer:消息消费者,从kafka broker 中pull消息的客户端
topic:消息的分类,生产者 和消费者都是面向topic
partition:分区,一个topic可以分为多个partition,每个partion是一个有序的队列
replica:partition的副本,kafka为了防止数据丢失,每个partion会有多个副本,一个Leader和多个Follower,分布到不同的broker中副本的数量不能超过broker的数量。
leader:partition主分区,producer发送消息和consumer消费消息的对象
follower:从分区,从leader中同步数据,当leader出现故障时,follower会成为新的leader
consumer group:消费者组,由多个consumer组成。同一消费组内,每个分区只能由某一个消费者消费;多个消费者组可以同时消费。
生产者消息写入流程:
kafka以topic对消息进行分类,一个topic可以有多个partition,每个partition物理上对应一个文件夹,其文件夹的命名
规则为:topic 名称+分区序号,例如topicA有3个分区,对应文件夹名称为:topicA-0,topicA-1,topicA-2。文件夹中存储了该partition的所有消息数据和索引文件,producer发送的消息数据存储在文件夹下的log文件中,producer发送的数据会不断向log文件追加。
为了防止log文件过大导致数据访问效率低下,kafka采取了分片和索引的机制,每个partition分成多个segment,每个segment对应有2个文件:.index 和 .log文件
00000000000000000000.index
00000000000000000000.log
00000000000000184635.index
00000000000000184635.log
00000000000000324567.index
00000000000000324567.log
log数据文件和index索引文件的命名规则:上一个 segment文件最后一条消息的 offset 值进行递增。
log文件存储的是massage数据,index文件存储的是相对offset和position物理偏移量。
segment index file采取稀疏索引存储方式,并没有为数据文件中的每条message建立索引,每隔一定字节的数据建立一条索引。减少了索引文件占用空间,可以通过mmap直接在内存当中操作索引,减少系统调用的次数,提高查询效率。稀疏索引为数据文件的message设置了一个元数据指针(position),它比稠密索引节省了更多的存储空间,但查找起来需要消耗更多的时间。
message物理结构:
参数如下:
关键字 | 解释说明 |
---|---|
8 byte offset | 在parition(分区)内的每条消息都有一个有序的id号,这个id号被称为偏移(offset),它可以唯一确定每条消息在parition(分区)内的位置。即offset表示partiion的第多少message |
4 byte message size | message大小 |
4 byte CRC32 | 用crc32校验message |
1 byte “magic" | 表示本次发布Kafka服务程序协议版本号 |
1 byte “attributes" | 表示为独立版本、或标识压缩类型、或编码类型。 |
4 byte key length | 表示key的长度,当key为-1时,K byte key字段不填 |
K byte key | 可选 |
value bytes payload | 表示实际消息数据。 |
segment下,index索引和log数据文件对应关系如下:
在partition中通过offset查找message的步骤:
例如,查找offset为368773的message:
1.为什么要分区?
2.分区的原则
kafka 是根据message key 来决定message落到哪个分区上的
kafka多分区副本架构是可靠性保证的核心,每个partion多份数据备份存储在不同机器上,保证消息的持久化。
1.多副本数据同步策略
为保障producer发送的消息能可靠的发送到指定的topic,topic下的每个partition接收到消息后,都要向producer发送ack,确认收到消息。如果producer收到ack,就会进行下一轮消息发送,否则,重新发送消息。
方案 | 优点 | 不足 |
半数以上完成同步,就发 送ack |
延迟低 | 选举新的leader 时,容忍n 台节点的故障,需要2n+1 个副 本 |
全部完成同步,才发送 ack |
选举新的leader 时,容忍n 台节点的故障,需要n+1 个副 本 |
延迟高 |
kafka选用的是第二种方案,对于kafka而言,每个partition都有大量数据,使用方案一,会造成大量数据冗余,占用资源成本高
2.同步副本ISR
kafka要确保所有follower完成同步后,leader才会发送ack,设想:有一个follower,发生故障,迟迟不能与leader 进行同步,那leader 就要一直等下去,这个问题怎么解决?
这就引出了ISR(in-sync replica set),也叫同步副本,每个分区的leader维护了一个动态的ISR,意为和leader 保持同步的follower 集
合。只有跟的上leader的follower才能加入ISR,如果follower长时间未向leader 同步数据, 则该follower 将被踢出ISR,该时间阈值由replica.lag.time.max.ms 参数设定。Leader 发生故障之后,就会从ISR 中选举新的leader。
3.ack 应答机制
有些场景下,对数据的可靠性要求不是很高,能够容忍数据的少量丢失,所以没必要等ISR 中的follower 全部接收成功。
所以Kafka 为用户提供了三种可靠性级别,用户根据对可靠性和延迟的要求进行权衡,自行选择。
4.故障处理
kafka的每个分区副本都有两个重要的属性:LEO和HW。注意是所有的副本,不只是leader副本。
kafka有两套follower副本LEO:
那么为什么要保存两套呢?
因为kafka要使用第一套LEO帮助follower更新其HW值,使用第二套LEO帮助leader更新HW。
producer 发送消息到partition leader,leader收到消息并成功写入log后,leader LEO就+1
follower发送fetch请求并得到leader的数据响应,成功写入log后,follower LEO+1
再比较本地LEO值和leader中HW值,选则小的作为follower中的HW值
leader收到follower的fetch请求后,拉取对应消息的log,在响应follower前,remote LEO+1(leader上保存的follower LEO)。
leader会筛选出ISR中所有符合条件的partition'副本,比较所有的LEO,选择最小的LEO值作为HW。
(1)follower故障
follower发生故障后会被踢出ISR,等到该follower恢复后,会读取本地磁盘上记录的HW,将log文件中高于HW的部分截取掉,从HW开始向leader同步,等该follower的LEO大于等于partition的HW,HW,即follower 追上leader 之后,就可以重新加入ISR
(2)leader故障
leader发生故障之后,会从ISR中选出一个新的leader,新的leader会尝试去更新分区HW,再通知其它的follower把各自的log文件中高于HW部分截取掉,之后从新的leader同步数据
将kafka的ack级别设置为-1,可以保证producer到broker之间不会丢失睡觉,即At Least Once。
将kafka的ack级别设置为0,可以保证生产者每条消息至多发送一次,即At Most Once。
At Least Once可以保证数据不丢失,但不能保证数据不重复。At Most Once 可以保证数据不重复,不能保证数据不丢失。但最好的效果当然是数据不重复也不丢失,即Exactly Once语义。
0.11版本后,kafka推出一重大新特性:幂等性。即同一条消息,不论producer向broker发送多少次数据,broker都只会持久化一条。在ack设置为-1的前提下,幂等性结合At Least Once 就构成了Exactly Once语义:At Least Once + 幂等性 = Exactly Once
要启用幂等机制,需要将producer的参数enable.idompotence设置为true,开启幂等性后,producer在初始化会分配一个PID,发往同一partition的消息会携带Sequence Number,broker对
注意:0.11版本之前,在producer客户端重启后broker会分配新的PID。
kafka的consumer采用pull的方式从broker中拉取消息,如果kafka 没有数据,消费者可能会陷入循环中,一直返回空数
据。针对这种场景,kafka在消费消息时会传入一个timeout参数,如果当前没有可供消费,那么consumer会等待timeout时长之后再返回。
在kafka中,一个consumer group由一到多个consumer组成。而producer发送给topic的数据是落到多个partition上的,一个partition只能被同一consumer group中的某一个consumer消费。consumer要消费消息,必然面临partition分配问题。
kafka消费者分区策略有三种:RoundRobin、Range、Sticky。当consumer的数量发生变动的时候,会触发消费者partition重新分配
RoundRobin就是轮询的意思,这种策略以consumer group为整体,拿到consumer group中所有Consumer 订阅的 TopicPartition后,根据TopicAndPartition的hash值对partition进行排序,按照顺序分发给consumer,如果组内每个消费者的topic是同样的,那么partition的分配是均匀的。如果组内每个consumer订阅的topic不相同,可能会造成消费数据混乱(没有订阅该topic的consumer却有可能消费到该topic的消息)
Range是以每个topic作为一个单独的整体,只有订阅了该topic的consumer才能消费到该topic的消息。用partition的数量除以consumer的数量,按照平均范围分配给Consumer,因为分区数可能无法被消费者数量整除,多出的parition追加到 前面的consumer,可能造成分区分配不均匀。Kafka默认采用RangeAssignor的分配算法
Kafka从0.11 版本开始引入Sticky,主要有两大特性:
1.主题分区的分配要尽可能的均匀;
2.当Rebalance 发生时,尽可能保持上一次的分配方案。
这样一种实现可以使消息分配更加均匀,减少了不必要的操作节约系统资源。
由于consumer在消费过程可能会出现宕机等故障,为了保证consumer在恢复后,能从上一次消费的位置继续消费,kafka需要把消费的offset存储起来。offset是以group+topic+partition存储的,在0.9版本以前,offset是存储在zookeeper中的,0.9版本之后,offset是存储在内部一个topic中的,该topic 为__consumer_offsets
要消费kafka内置topic需要修改配置文件consumer.properties:exclude.internal.topics=false
kafka数据是以文件的形式存储在磁盘中的,是如何做到如此高效的呢?
Kafka读写效率高的秘诀在于,它把所有的消息都存储在文件中。通过mmap提高I/O写入速度,写入数据的时候它是末尾添加所以速度很快;同时为了减少磁盘的写入的次数,broker会将消息暂存到buffer中,当消息数量到达一定阈值的时候,再flush到磁盘,这样更减少了磁盘IO的调用次数。而consumer端也是批量fetch多条消息,读取数据的时候配合sendfile直接输出
kafka生产消息使用的是顺序写,数据进入到分区的消息队列尾部,这样的磁盘顺序写比传统的BTREE随机写性能高了很多。磁盘顺序写的速度甚至比内存随机写都快。
消费者与生产者互相不干扰,消费者读取消息队列的头部,生产者读取消息队列的尾部。 这样的方式无写锁,读锁。性能非常高。
mmap:内存映射文件
写入数据:应用程序调用了mmap()之后,会打通用户空间和内核空间,用户空间和内核空间共享一块缓冲区,并且MMAP直接映射到磁盘上的某个文件,完成映射之后对物理内存的操作会被同步到硬盘上
客户端发送数据到达系统内核缓冲区,数据从内核拷贝到用户空间程序,程序处理完之后,直接将数据放入共享缓冲区,写入到磁盘
如果没有MMAP,要写入数据,客户端先发送数据到达系统内核,数据从内核拷贝到用户空间程序,程序处理完之后,会调用系统内核的write写,数据先写入到内存,再落到磁盘上
读取数据:用户空间调用系统内核sendfile指令,内核读取到数据后直接通过网卡发送给客户端。
如果不使用零拷贝技术,要读取数据,用户空间程序先调用系统内核的read指令,数据从磁盘上拷贝到内存,内核读取数据,拷贝到用户空间程序,程序调用内核write写将数据通过网卡发送给客户端
kafka集群是分布式部署,broker之间相互独立,这个时候就需要有一个系统来管理broker节点,这个时候就用到了zookeeper。所有的kafka broker节点一起去zookeeper上注册一个临时节点,只有一个broker能够注册成功,这个broker会成功kafka集群的Controler,负责管理集群broker 的上下线。负责topic分区副本分配,leader选举等工作。
Kafka 从0.11 版本开始引入了事务支持。事务可以保证Kafka 在Exactly Once 语义的基础上,生产和消费可以跨分区和会话,要么全部成功,要么全部失败。
为了实现跨分区会话的事务,需要引入一个全局唯一的Transaction ID,这个Transaction ID是由客户端定义并传入的,再将producer的PID和Transaction ID绑定,这样一来,producer在重启后,就能通过正在进行的Transaction ID获得原来的PID,而不用重新向broker申请一个PID,通过这种机制,kafka就能保证跨分区跨会话的Exactly Once
为了管理Transaction ,kafka引入了新组件Transaction Coordinator,Producer和Transaction Coordinator交互,获得Transaction ID对应的任务状态,并且Transaction Coordinator还负责将事务写入kafka内部的一个topic,即使整个服务重启,由于事务状态保存起来了,进行中的事务状态还是可以得到恢复,继续进行下去
对于consumer而言,事务的保证相对弱些,尤其是无法保证commit的消息被精确消费,这是由于consumer可以通过offset访问任意消息,而不同segment file的生命周期不一样,同一事务的消息可能会出现重启后被删除的情况。