聊聊 Kafka 的存储架构上篇

392 阅读8分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第10天

你好, 我是华仔,又和大家见面了。

从这篇文章开始,我将对 Kafka 专项知识进行深度剖析, 今天我就来聊聊 kafka 的存储系统架构设计, 说到存储系统,大家可能对 MySQL 比较熟悉,也知道 MySQL 是基于 B+ tree 来作为它的索引数据结构。

Kafka 又是基于什么机制来存储?为什么要设计成这样?它解决了什么问题?又是如何解决的?里面又用到了哪些高大上的技术?

带着这些疑问,我们就来和你聊一聊 Kafka 存储架构设计背后的深度思考和实现原理。

认真读完这篇文章,我相信你会对 Kafka 存储架构,有更加深刻的理解。也能有思路来触类旁通其他存储系统的架构。

图1:kafka 存储架构大纲


01 kafka 存储场景剖析

在讲解 Kafka 的存储方案之前,我们先来看看 Kafka 官网给的定义:

Apache Kafka is an open-source distributed event streaming platform used by thousands of companies for high-performance data pipelines, streaming analytics, data integration, and mission-critical applications.

翻译成中文如下:

Apache kafka 是一个开源的分布式事件流处理平台,由成千上万的公司用于高性能的数据管道流分析、数据集成和关键任务的应用程序。

了解 Kafka 的老司机都知道它是从 Linkedin 内部孵化的项目,从一开始,Kafka 就是为了解决大数据的实时日志流而生的, 每天要处理的日志量级在千亿规模。

对于日志流的特点主要包括

1)、数据实时产生。

2)、海量数据存储与处理,所以它必然要面临分布式系统遇到的高并发、高可用、高性能等三高挑战。

通过上面的背景可以得出:一切脱离业务场景谈架构设计都是耍流氓


综上我们看对于 Kafka 的存储需求来说,要保证以下几点:

  1. 存储的主要是消息流(可以是简单的文本格式也可以是其他格式,对于 Broker 存储来说,它并不关心数据本身)

  2. 要支持海量数据的高效存储、高持久化(保证重启后数据不丢失)

  3. 要支持海量数据的高效检索(消费的时候可以通过offset或者时间戳高效查询并处理)

  4. 要保证数据的安全性和稳定性、故障转移容错性


02 kafka 存储选型

有了上面的场景需求分析后, 我们接下来分析看看 Kafka 到底基于什么机制来存储的,能否直接用现有我们了解到的关系型数据库来实现呢?我们接着继续深度分析。


存储基本知识

我们先来了解下存储的基本知识或者常识, 在我们的认知中,对于各个存储介质的速度大体同下图所示的,层级越高代表速度越快。很显然,磁盘处于一个比较尴尬的位置,然而,事实上磁盘可以比我们预想的要快,也可能比我们预想的要慢,这完全取决于我们如何使用它。

图2:各存储介质对比分布(来自网络)

关于磁盘和内存的 IO 速度,我们可以从下图性能测试的结果看出普通机械磁盘的顺序I/O性能指标是53.2M values/s,而内存的随机I/O性能指标是36.7M values/s。由此似乎可以得出结论:磁盘的顺序I/O性能要强于内存的随机I/O性能。

图3:磁盘和内存的 IO 速度对比(来自网络)

另外从整个数据读写性能方面,有不同的实现方式,要么提高读速度,要么提高写速度。

  1. 提高读速度:利用索引,来提高查询速度,但是有了索引,大量写操作都会维护索引,那么会降低写入效率。常见的如关系型数据库:mysql等。

  2. 提高写速度:这种一般是采用日志存储, 通过顺序追加写的方式来提高写入速度,因为没有索引,无法快速查询,最严重的只能一行行遍历读取。常见的如大数据相关领域的基本都基于此方式来实现。


Kafka 存储方案剖析

上面从存储基础知识,以及存储介质 IO 速度、读写性能方面剖析了存储类系统的实现方式,那么我们来看看 Kafka 的存储到底该采用哪种方式来实现呢?

对于 Kafka 来说, 它主要用来处理海量数据流,这个场景的特点主要包括:

  1. 写操作:写并发要求非常高,基本得达到百万级 TPS,顺序追加写日志即可,无需考虑更新操作

  2. 读操作:相对写操作来说,比较简单,只要能按照一定规则高效查询即可(offset或者时间戳)

根据上面两点分析,对于写操作来说,直接采用顺序追加写日志的方式就可以满足 Kafka 对于百万TPS写入效率要求。但是如何解决高效查询这些日志呢? 直接采用 MySQL 的 B+ tree 数据结构存储是否可以?我们来逐一分析下:

如果采用 B+ tree 索引结构来进行存储,那么每次写都要维护索引,还需要有额外空间来存储索引、更会出现关系型数据库中经常出现的“数据页分裂”等操作, 对于 Kafka 这种高并发的系统来说,这些设计都太重了,所以并不适合用。

但是在数据库索引中,似乎有一种索引看起来非常适合此场景,即:哈希索引【底层基于Hash Table 实现】 ,为了提高读速度, 我们只需要在内存中维护一个映射关系即可,每次根据 Offset 查询消息的时候,从哈希表中得到偏移量,再去读文件就可以快速定位到要读的数据位置。但是哈希索引通常是需要常驻内存的,对于Kafka 每秒写入几百万消息数据来说,是非常不现实的,很容易将内存撑爆, 造成 oom。

这时候我们可以设想把消息的 Offset 设计成一个有序的字段,这样消息在日志文件中也就有序存放了,也不需要额外引入哈希表结构, 可以直接将消息划分成若干个块,对于每个块,我们只需要索引当前块的第一条消息的 Offset ,这个是不是有点二分查找算法的意思。 即先根据 Offset 大小找到对应的块, 然后再从块中顺序查找。如下图所示:

图4:kafka 稀疏索引查询示意图

这样就可以快速定位到要查找的消息的位置了,在 Kafka 中,我们将这种索引结构叫做 “稀疏索引”。

03 kafka 存储架构设计

上面从 Kafka 诞生背景、 存储场景分析、存储介质 IO 对比、以及 Kafka 存储方案选型等几个方面进行深度剖析, 得出了 Kafka 最终的存储实现方案, 即基于顺序追加写日志 + 稀疏哈希索引。


接下来我们来看看 Kafka 日志存储结构:

图5:kafka日志存储结构

从上图可以看出来,Kafka 是基于「主题 + 分区 + 副本 + 分段 + 索引」的结构:

  1. kafka 中消息是以主题 Topic 为基本单位进行归类的,这里的 Topic 是逻辑上的概念,实际上在磁盘存储是根据分区 Partition 存储的, 即每个 Topic 被分成多个 Partition,分区 Partition 的数量可以在主题 Topic 创建的时候进行指定。

  2. Partition 分区主要是为了解决 Kafka 存储的水平扩展问题而设计的, 如果一个 Topic 的所有消息都只存储到一个 Kafka Broker上的话, 对于 Kafka 每秒写入几百万消息的高并发系统来说,这个 Broker 肯定会出现瓶颈, 故障时候不好进行恢复,所以 Kafka 将 Topic 的消息划分成多个 Partition, 然后均衡的分布到整个 Kafka Broker 集群中。

  3. Partition 分区内每条消息都会被分配一个唯一的消息 id,即我们通常所说的 偏移量 Offset, 因此 kafka 只能保证每个分区内部有序性,并不能保证全局有序性。

  4. 然后每个 Partition 分区又被划分成了多个 LogSegment,这是为了防止 Log 日志过大,Kafka 又引入了日志分段(LogSegment)的概念,将 Log 切分为多个 LogSegement,相当于一个巨型文件被平均分割为一些相对较小的文件,这样也便于消息的查找、维护和清理。这样在做历史数据清理的时候,直接删除旧的 LogSegement 文件就可以了。

  5. Log 日志在物理上只是以文件夹的形式存储,而每个 LogSegement 对应磁盘上的一个日志文件和两个索引文件,以及可能的其他文件(比如以".snapshot"为后缀的快照索引文件等)

也可以直接看之前写的 Kafka 基础入门篇 中的存储机制部分,也有详细的说明。

至此,kafka 架构存储上篇剖析完成。

坚持总结, 持续输出高质量文章 关注我: 华仔聊技术 回复 kafka 有惊喜哦。