kafka 数据持久化、高性能、读写原理、高可用、扩缩容总结

2,252 阅读7分钟

前言

此文对 kafka 的工作原理做一个高度的抽象总结,便于回顾,不适合用来入门

数据持久化

元数据持久化

Kafka 中的元数据需要保存到 zookeeper 中,如topic 和 partition 信息需要保存为持久化节点、broker 信息需要保存为临时节点等

数据持久化

Kafka 中的一个 topic 旗下划分了多个 partition,每个 partition 存储一部分数据

每个 partition 会存储多个 replica 在其它 Broker 节点

每个 replica 代表了单个 partition 在当前机器的存储

将一个 partition 划分为了多个 LogSegment 分段

每个 LogSegment 分段下面保存有日志文件、索引文件信息,每个 .log 日志文件为 1GB 索引文件是存储的稀疏索引每隔 4096 字节写入一行索引

图片.png

为什么 kafka 在分区数量过多的时候性能会大幅度下降

在分区设计和持久化这里就能看出端倪,每个 partition 存储完整消息内容(不像 rocketmq 存储的是索引所以 rocketmq 在同等配置能支撑更多的队列),虽然每个分区文件通过 mmap 维护映射关系,数据写入 PageCache 然后定时从 PageCache 中刷入磁盘,但是 Partition 存储的是真实数据,当分区过多的时候,每个 partition 能够 mmap 映射的虚拟内存是非常小的,导致了接近于每隔几次写入或者是每次都需要刷盘或者再次从磁盘读取数据

高性能

从读取的角度来看

broker 在处理拉取请求的时候,先根据偏移量定位到 LogSegment,然后在其中根据偏移量在所引文件中进行二分查找,找到之后基于索引位置执行的物理位置加载一块缓冲到 PageCache 中,那么这里高效点在于

(1)索引文件每隔 4096 字节写入一个行数据,通过 mmap 映射在虚拟内存中,减少了取磁盘 IO 开销和 DMA 拷贝开销

(2)在索引中二分查找出来磁盘物理位置之后加载一块缓冲数据到 PageCache 中,如果后续的消费没有太高的延迟,那么都是基于 Cache 进行消费非常快速

在读取的时候基于 mmap 索引和 PageCache 中的部分数据快速定位到要拉取的数据真实的内存地址,然后构建一个 FileMessageSet 其中返回的是 FileChannel 、物理位置、拉取消息的长度,然后关注 write 事件,通过 PlaintextTransportLayer 调用 FileChannel 通过 sendfile 进行传输

从写入的角度来看

所有的写入都是顺序写入,所以可以充分利用 PageCache,将数据写入 PageCache 然后异步刷入磁盘,此时读取请求如果没有延迟也都是从 PageCache 中进行读取

那么为什么顺序写入 PageCache 就快呢? 以随机写入为例,需要先从用户态切换到内核态,然后进行磁盘 seek 找到要写入的位置才能写入

如果是顺序写入,利用 PageCache 机制,在一次 seek 的时候映射一整块的磁盘空间到 PageCache 虚拟内存中,后续操作 PageCache 完成后异步刷盘即可,数据在磁盘物理位置都是一一对应的

高可用

元数据高可用

元数据都是存储在 zookeeper 中的,高可用依赖于 zookeeper 自身,zookeeper 的过半写入和只要过半节点存活就能选举出新 Leader 的机制保证 zookeeper 的可靠性,同时 zookeeper 只是维护一些元数据以及和 broker 的心跳信息不会承载过高的 IO 压力

数据高可用

broker 端会维护一个 isr 列表,这个列表表示在 rerplica.lag.time.max.ms=10000 进行通信的副本节点,如果超出这个时间会被移除 isr 列表中

其中 broker 可以配置 min-isr = 2 要求 isr 列表中至少需要有 2 台节点,这个配置一般会和客户端发送 acks=all/-1 一起使用表示,写入到 broker 中的数据至少要有 2 台服务同步完成才响应成功,如果 isr 列表中不满足 2 台节点就写入失败,用此来保证写入数据不丢失

需要注意的是消息在 broker 和副本节点写入都是写入 Page Cache 中的,用以来提升写入性能,除非多台副本节点同时宕机,不然消息就不会丢失

broker 节点在启动后都会向 zookeeper 进行注册,同时选举 controller,一个集群只有一个 controller 来监听 zookeeper 中数据变化(topic、partition、broker等等)做相应处理来保持模型简单(仅仅是 controller 来监听处理而不是每个 broker 都这么做)

当 broker 节点宕机后,其中的大量的 leader partition 副本需要进行选主,controller 的监听器会感知到 broker 宕机,然后在 isr 列表中选举出来一个 partition 的 follower 作为 leader 提供服务,然后将新选举出来的 Leader 副本对应的 broker 地址返回给消费者,消费者基于新的地址进行消费

读写原理

写原理

客户端通过配置的 Broker 地址感知到集群所有的 broker 信息,基于指定分区、key、lb 算法选择一个分区,将数据写入到对应 broker 分区中

数据写入 broker leader 之后,当前写入的文件最大偏移量叫做 leo,当前消费者能够读取的偏移量叫做 hw

写入 leader 后为了保证高可用一般会同步到 isr follower 中的至少 1 个副本节点中的 os cache 中,follower 通过定时拉取机制拉取 Leader 信息到本地,当 isr 检测到存在 follower 超过了配置时间默认为 1S 没有拉取数据就将其移出 isr,当检测到有副本跟上的时候就将其加入到 isr 列表中

每次写入完成之后就将 isr 中最小的 leo 作为 Leader 的 hw

读原理

当消费者启动的时候连接到 broker 后需要选择一个 broker 作为当前消费组对应的 group coordinator

Utils.abs(groupId.hashCode) % groupMetadataTopicPartitionCount

groupId 的 hashcode % topic 的分区总数,得到的分区所在的 leader 节点就作为 coordinator

group coordinator 创建完成之后会返回给 consumber,consumber 发起加入 group 请求,最先到达 coordinator 的成为 consumber leader,consumber leader 会收到来自于 coordinator 关于当前 topic、分区、消费组、broker 节点等完整信息之后,基于 lb 算法来进行分配每个节点的消费策略,然后将这个策略返回给 coordinator,然后 coordinator 将消费信息同步给所有的消费者,然后接收 rebalance 阶段,消费者开始消费

消费者消费完成后可以自动提交偏移量,也可以手动提交偏移量

提交偏移量到 group coordinator 节点,coordinator 节点负责记录信息到__consumer_offsets 这个 topic 中,用以记录消费组消费到哪一个 partition 的哪一个 offset 处,以便消费者重启后可以接着消费 这里需要注意 offsets.topic.replication.factor 这个配置默认为 1 表示只有1个副本节点即 leader 节点,当 broker 宕机后可能导致消费者消费失败,因为无法找到消费的位置信息,可以将其配置为 2

扩缩容

当消费者宕机后或者新加入消费者后

group coordinator 感知到后,下发指令所有消费者进入 rebalance 阶段,重新partition 重新消费分配

新增 partition

当原本的 topic 分区数量不够,导致的单点 broker io 压力过大,此时可以通过脚本去动态修改分区的数量,修改完成之后,kafka controller 通过 zookeeper 监听器感知到变化后会通知所有的 broker 这时对应的 topic 消费又会再次进入到 rebalance 阶段

但是不能减少分区的数量,减少会涉及到减少分区数据分配,__consumer_offsets 数据存储问题等等

新增 broker

当单台 broker 的硬盘存储达到上限可以通过纵向扩容再度提升硬盘存储空间,或者新增几台 broker 节点,创建新的 topic 将分区数量增多,均匀的写到更多的 broker 中,基于开关控制,老的 topic 消费完毕之后再去消费新的 topic,写消息直接写入新的 topic