kafka的介绍
Kafka是一款分布式消息发布和订阅系统,它的特点是高性能、高吞吐量。
最早设计的目的是作为LinkedIn的活动流和运营数据的处理管道。这些数据主要是用来对用户做用户画 像分析以及服务器性能数据的一些监控
所以kafka一开始设计的目标就是作为一个分布式、高吞吐量的消息系统,所以适合运用在大数据传输场景。
Kafka本身的架构
一个典型的kafka集群包含若干Producer,若干个Broker(kafka支持水平扩展)、若干个Consumer Group,以及一个 zookeeper集群。
kafka通过zookeeper管理集群配置及服务协同。Producer使用push模式将消息发布 到broker,consumer通过监听使用pull模式从broker订阅并消费消息。
多个broker协同工作,producer和consumer部署在各个业务逻辑中。三者通过zookeeper管理协调请 求和转发。
producer 发送消息到broker的过程是push,而 consumer从broker消费消息的过程是pull,主动去拉数据。而不是broker把数据主动发送给consumer
Topic
在kafka中,topic是一个存储消息的逻辑概念,可以认为是一个消息集合。每条消息发送到kafka集群的 消息都有一个类别。
物理上来说,不同的topic的消息是分开存储的, 每个topic可以有多个生产者向它发送消息,也可以有多个消费者去消费其中的消息。
Partition
每个topic可以划分多个分区(每个Topic至少有一个分区),同一topic下的不同分区包含的消息是不同 的。
每个消息在被添加到分区时,都会被分配一个offset(称之为偏移量),它是消息在此分区中的唯 一编号,kafka通过offset保证消息在分区内的顺序,offset的顺序不跨分区,即kafka只保证在同一个 分区内的消息是有序的。
Offset
每个消息进入 , 在分区中追加得到一个offset , 代表当前消息在此分区中的消费位置
只要kafka 消息没被删, 根据offset 还可以重复消费 .
kafka 实现不同group 之间可以消费相同的消息 就是因为不同group , 管理着自己的 offset :
mygroup_mytopic => offset = 50
mygroup666_mytopic => offset = 20
不同group的消费情况放在不同的文件中记录 , 根据groupId % 文件数 (默认offset 50个文件)
Math.abs(“groupid”.hashCode())%groupMetadataTopicPartitionCount
生产者
producer在发送消息时 , 默认情况下,kafka采用的是hash取模的分区算法。如果Key为null,则会给消息随机分配一个分区。
如果消息中带有key , 会key进行hash , 对相同的key会分配到同一个分区中.
消费者
kafka存在consumer group的概念,也就是group.id一样的 consumer,这些consumer属于一个consumer group,组内的所有消费者协调在一起来消费订阅主题 的所有分区。
每一个分区只能由同一个消费组内的consumer来消费,那么同一个consumer group里面的consumer是怎么去分配该消费哪个分区里的数据的呢?
对于上面这个图来说,这3个消费者会分别消费test这个topic 的3个分区,也就是每个consumer消费一 个partition。
分区分配消费
C : 消费者
P : 一个分区 Partition
结果:
1、3个C , 3个P 每一个 Consumer 绑定到一个 partition上
2、2个C , 3个P C1 绑定 2个分区 , C2绑定1个分区.
3、4个C , 3个P 每一个Consumer 绑定到一个 partition上, 多余了一个Consumer在挂机(什么事都没干)
建议 :
1、当Consumer 大于 分区数的时候是没意义的, 因为kafka 在设计上不允许 partition并发.
2、分区数最好是 Consumer 的整数倍, 这样在绑定时候 能让consumer 都绑定差不多数量的分区
在kafka中,存在三种分区分配策略
1: Range (范围分区) [默认]
2: RoundRobin(轮询)、
将所有的 分区 和 消费者 列出来 , 通过轮询个每个消费依次分配 分区
假如按照hashCode排序完的topic-partitions组依次为T1-5, T1-3, T1-0, T1-8, T1-2, T1-1, T1-4, T1-7, T1-6, T1-9,我们的消费者线程排序为C1-0, C1-1, C2-0, C2-1,最后分区分配的结果
C1-0将消费T1-5, T1-2, T1-6分区;
C1-1将消费T1-3, T1-1, T1-9分区;
C2-0将消费T1-0, T1-4分区;
C2-1将消费T1-8, T1-7分区;
3: StickyAssignor(粘性)。
Consumer上下线? 分区由谁消费?
kafka 提供了让一个Broker 成为 Coordinator 来执行 consumser的Group 管理
kafka引入协调器有其历史过程,原来consumer信息依赖于zookeeper存储,当代理或消费者发生变化时,引发消费者平衡,此时消费者之间是互不透明的,每个消费者和zookeeper单独通信,容易造成羊群效应和脑裂问题。
总结 : 防止每个消费者与zookeeper进行通信 .
一: 确定Broker管理者
1、消费者向任意 kafka 节点发送一个请求, kafka集群挑选一台 负载最低的节点ID 返回给此consumer.
2、此台Broker 就作为了管理者 .
二: 消费者加入 group 并进行 消费者 leader选举
选出了管理者后, 这个group中的所有 Consumer 都要和这个管理者通信 , 一旦所有的成员请求了管理者, 管理者会选举出一消费者leader , 第一个加入的消费者就是leader , 并告诉所有Consumer.
注意 : 每个消费者 都有自己的 分区分配策略, 管理者进行统计, 哪个多应用哪个
三: 同步
Broker管理者 将所有分区 和 consumer信息告诉 Consumer Group Leader
Consumer Leader 节点计算每一个Consumer 应该 消费哪些分区, 得出结果后发给管理者 , 管理再将节点发送给所有的consumer.
总结: Consumer Leader计算出分区分配的结果并利用管理者告诉所有Consumer .
其他 :
1、当Consumer 进行增加减少时, 再触发动态分配
2、consumer向管理者 发送心跳, 当consumer 挂了管理者要进行重新分配
3、consumer向管理者 发送心跳, 当管理者异常的时候 , consumer 要重新加入一个管理者
分区的副本
kafka 的每个topic 都可以分为多个 partition , 每个partition 会分散在分布式集群的各个broker 节点上 , 对于每个partition来说都是单点的 .
如果没有副本 , 一个节点挂了, 上面的数据就全都访问不到了. 因此需要一种高可用的机制 .
每个分区可以有多个副本, 但是只会存在一个leader副本 , 用于 读和写 , 其他的相当于备份. 当leader副本的节点挂了之后, 剩余副本会进行新的选举 , 选举出新的分区leader 提供服务.
总结 : 一定要有一个Leader 副本, 提供写服务 , follower 副本用来同步数据. 副本就是一个BackUP 一个备份, 不能读, 更加不能直接写.
创建了一个 3个分区、3个副本的topic , 标红的是一个topic的3个分区, 分别在 102,103,104上 , 而0号分区有2个followerfu'be
Kafka Controller选举过程
为什么要选出一个controller ? 因为需要一个Broker 来进行 zookeeper 写操作. 难道N个broker一起写zookeeper (混乱) ?
Kafka 集群会利用 Zookeeper 为一个Broker 选出一个 KafkaController , 同时别的Broker 有监听器, 如果 控制器挂了可以接替.
KafkaController会监听ZooKeeper的/brokers/ids节点路径 , 判断各个broker有无宕机 .
作用: 分区和副本 的状况管理
1、有副本leader 挂了, 由controller 进行重新选举
2、ISR 信息变更 , 由controller 通知所有 broker 进行更新信息
ISR 是什么 ?
get /brokers/topics/secondTopic/partitions/1/state 或者
sh kafka-topics.sh --zookeeper 192.168.13.106:2181 --describe --topic test_partition
可以看到一个topic 的分区所在的broker位置 , 以及分区的leader是哪一个broker .
Replicas : 说明了该分区的副本所在的broker
ISR: 优质的备胎副本 (延迟较低的) In-Sync Replicas
何为优质的备胎, 比如一个副本同步了 leader 99%的数据 那就是优质的备胎, 另一个副本只同步了 leader 80%的数据 那就不是一个好备胎~
而ISR 就是保存了当前分区的相对优质的副本 .
ISR用处 : 对于生产者Producer的ack机制 , 根据策略 , 比如ISR 中的节点都完成了数据同步, kafka才会确认生产者的消息发送成功.
min.insync.replicas=n 配置参数表示 当满足了n个副本的消息确认(n默认为1,最好大于1,因为leader 也在isr 列表中),才认为这条消息是发送成功的
min.insync.replicas 参数只有配合request.required.acks =-1 时才能达到最大的可靠性 (全部都返回成功)
request.required.acks 的参数说明:
-1:只有isr 中的n-1个副本(leader 除外所以n-1)都同步了消息 此消息才确认发送成功
0:生产者只管发送,不管服务器,消费者是否收到信息
1:只有当leader 确认了收到消息,才确认此消息发送成功
如果 follower的数据一直追不上leader , 一段时间后 (replica.lag.time.max.ms) replica.lag.time.max.ms: 就会被踢出ISR
副本leader选举过程
一个分区的leader副本挂了, 需要选出一个副本leader .
Kafka Controller 发现, 有副本leader 挂了后, 会让分区副本重新进行选举
1、优先从 ISR 列表中选取一个作为 leader 副本
2、如果ISR 列表为空 , 查看该topic的unclean.leader.election.enable配置 , 如果是true 说明允许 非ISR的节点竞选leader .
副本数据同步一致性
生产者写数据主流程:
Producer 在发布消息到某一个 Partition 时 :
1、通过zookeeper 找到这个分区的leader [ /brokers/topics/<topic>/partitions/2/state ], producer将信息只会发到分区的leader上
2、leader 将信息写入分区日志, 每个follower 都从 leader上take() 数据 [主动阻塞pull , 类似 blockQueue Take() ]
3、follower 在拉取到了数据, 写入到自己的副本log 中, 向 leader 发送ACK
4、一旦 leader 收到了ack , 根据request.required.acks 策略判断接收到ack的个数是满足, 满足的话向producer 发送ACK .
一致性原理:
LEO : 当前副本的日志 末端偏移, 记录是当前最大偏移 +1 , 也就是下一条记录应该获取的offsetId
HW : 水位值 , 对于同一个分区而言 , 认为所有副本都备份ok了的数据
例子 : leader 一直在被producer 写数据 , 写到第7条了 LEO = 7, follower1 在向leader同步数据, 同步到第5条了, LEO = 5 , 向leader返回LEO =5 ,
follower2 也在向leader 同步数据 , 同步到第3条了, LEO = 3 向 leader返回 LEO = 3 , 这时leader 进行比较 [7 ,5, 3] , 选取HW = 3 , 消费者最多只能消费到第3条数据.
[前提是在ISR]
为什么要HW? 为什么要日志截断?
HW的意义 => 消费者消费的一致性 :
假设没有HW, 消费者 消费leader能获取到 18, 然而同步到 follower 只有同步到 15 .
接下来消费者要去消费19, Leader 挂了 , 但发现 follower 只有到15? 出现问题了 => 因为kafka 数据不一致导致报错
而HW 保证所有的副本都同步到到的一个offset , 哪一个leader挂了 都可以继续消费
存储数据的一致性:
leader1 挂了 ,
follower3 成为了leader , 然后leader1 又活了 ! 如果又发来一个消息 , 是第follower3的16 , 是follower1 的20 ??? 导致了混乱
因此其他follwer 一刀切到 HW , 再从 leader进行同步 [网上都称日志截断]
一刀切到 HW 导致 数据丢失场景
Leader副本高水位更新 和 Follower副本高水位更新在时间上存在错配 , follower 副本最新的Offset 是在下次 fetch() 的时候传过来
follower3 重启后 会将 lEO 的值 , 置为和HW 一样, 目的是 重新从 Leader1 拉取信息 .
但如果这个时候 Leader1 也重启了, follower3 就会成为Leader , 这个时候 LEO因为置为和HW一样, 导致了数据的丢失
使用epoch 解决数据丢失
Epoch 是和zookeeper 一样的机制, 每次进行选举后, epoch加1 , 代表了不同朝代
broker中会保存这样一个缓存,并且定期写入到checkpoint文件中 当leader写log时它会尝试更新整个缓存:
如果这个leader首次写消息,则会在缓存中追加<epoch , startOffset>
有了Epoch 后 :
流程变化 一刀切至HW 转变=> follower3 挂了, 并且重启了, 先不将LEO 置为HW, 先向Leader 节点先发送一个OffsetsForLeaderEpochRequest请求, leader返回当前LEO值.
1、leader 没变
因为leader 没有挂, epoch 是不变的, leader1 的LEO 肯定是大于 follower3 的, 因此不用截断
2、leader 变了
如果Leader1 挂了, follower3 现在成为了新的Leader, epoch +1 , 当leader1 重启回来后, 也要向follower3 发一个OffsetsForLeaderEpochRequest请求
follower3 从缓存中找 leader1 传来的epoch+1 的startOffset , 发送给leader1 , leader1将数据从这截断 , 并开始从leaderB同步.
细节:
1、follower 的fetch() 操作如果请求 leader 没有新数据, 会阻塞 .
2、follower 的fetch 操作会携带上自己的LEO , 用于让leader 更新 remoteLEO , 同时leader会返回新计算的HW让 follower 更新HW.
持久化
消息的存储
消息发送到Broker 后, 持久化使用日志的方式
Kafka 通过分段的方式 将一个分区的Log 分成了多份 (log.segment.bytes=107370 (设置分段大小),默认是1gb )
LogSegment
一份 Index + 一份Log 称之为LogSegment
假如一个分区 只有一个日志文件, 随着数据量的增大 , 日志文件会变的非常庞大.因此kafka 将日志做了分割.
方便清理
Segment 以 offset 的起始值命名 , 可以用二分查找 快速查找一个offset 在哪一个日志文件中.
segment中index和log的对应关系
物理偏移量(position)为1600. position是ByteBuffer的指针位置 , 根据文件位置 偏移查找数据.
索引文件 是一个稀疏索引, 没有必要存储每一条记录的位置, 利用二分查找可以快速找到记录的大概位置 .
日志的清除
过久前的消息存放没有意义, 会进行清除 , 因为kafka 是分段存储日志的, 因此清理起来也更加方便
策略1 : 删除一定时间前的消息 . log.retention.hours 默认是7天
策略 2: topic 的日志存量太大时 . log.retention.bytes
日志的压缩
类似redis 的 AOF 重写日志功能
场景 : 做实时计算时,需要长期在内存里面维护一些数据,这些数据可能是通过聚合了一天或者一周的日志得到的,这些数据一旦由于异常因素(内存、网络、磁盘等)崩溃了,从头开始计算需要很长的时间。一个比较有效可行的方式就是定时将内存里的数据备份到外部存储介质中,当崩溃出现时,再从外部存储介质中恢复并继续计算。
我们现在大部分企业仍然用的是机械结构的磁盘,如果把消息以随机的方式写入到磁盘,那么磁盘首先 要做的就是寻址,也就是定位到数据所在的物理地址,在磁盘上就要找到对应的柱面、磁头以及对应的 扇区;这个过程相对内存来说会消耗大量时间,为了规避随机读写带来的时间消
kafka 对日志的存储是顺序写的 , 是追加到文件末尾 递增offset 的形式, 这也提升了性能.
零拷贝 =>零拷贝?
Broker 维护的消息日志文身就文件目录 , 生产者 和 消费者使用相同的格式处理 .
在Broker 将消息 发送给消费者的时候 按照常理需要 磁盘=>内存=>socket=>网卡
通过零拷贝可以省去一些 数据复制的过程 例如kafka 使用 sendFile 对应javaApi FileChannel.transfer
页缓存 => pageCache/buffCache
页缓存是为了减少磁盘IO , 操作系统的一个内存缓存.
当一个进程准备 读取磁盘文件的时候, 操作系统会先查看 物理数据的页在不在 页缓存中 ,
如果在页缓存中, 可以直接返回 , 避免了IO .
如果不存在, 需要将数据从磁盘读到页缓存, 并返回给进程.
Kafka中大量使用了页缓存, 这是Kafka实现高吞吐的重要因素之 一 。
对于Produce请求:将请求中的数据写入到操作系统的PageCache后立即返回,当消息条数到达一定阈值后,Kafka应用本身或操作系统内核会触发强制刷盘操作.
对于Consume请求:主要利用了操作系统的ZeroCopy机制,当Kafka Broker接收到读数据请求时,会向操作系统发送sendfile系统调用,操作系统接收后,首先试图从PageCache中获取数据;如果数据不存在,会触发缺页异常中断将数据从磁盘读入到临时缓冲区中,随后通过DMA操作直接将数据拷贝到网卡缓冲区中等待后续的TCP传输。
虽然消息都是先被写入页缓存, 然后由操作系统负责具体的刷盘任务的, 但在Kafka中同样提供了同步刷盘及间断性强制刷盘(fsync), 可以通过 log.flush.interval.messages 和 log.flush.interval.ms 参数来控制。
同步刷盘能够保证消息的可靠性,避免因为宕机导致页缓存数据还未完成同步时造成的数据丢失。但是 实际使用上,我们没必要去考虑这样的因素以及这种问题带来的损失,消息可靠性可以由多副本来解决,同步刷盘会带来性能的影响。 刷盘的操作由操作系统去完成即可