今天聊聊 Kafka 的存储系统架构设计。说到存储系统,大家可能对 MySQL 比较熟悉,也知道 MySQL 是基于 B+ 树来作为它的索引数据结构。
Kafka 又是基于什么机制来存储?为什么要设计成这样?它解决了什么问题?又是如何解决的?里面又用到了哪些高大上的技术?
带着这些疑问,我们就来和你聊一聊 Kafka 存储架构设计背后的深度思考和实现原理。
认真读完这篇文章,相信你会对 Kafka 存储架构有更加深刻的理解,也能有思路来触类旁通其他存储系统的架构。
1 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 就是为了解决大数据的实时日志流而生的,,每天要处理的日志量级在千亿规模。对于日志流的特点主要包括数据实时产生与海量数据存储与处理。所以,它必然要面临分布式系统遇到的高并发、高可用、高性能等三高挑战。
通过上面的背景可以得出:一切脱离业务场景谈架构设计都是耍流氓。
综上我们看对于 Kafka 的存储需求来说,要保证以下几点:
-
存储的主要是消息流,可以是简单的文本格式也可以是其他格式。对于 Broker 存储来说,它并不关心数据本身;
-
要支持海量数据的高效存储、高持久化,保证重启后数据不丢失;
-
要支持海量数据的高效检索,消费的时候可以通过 offset 或者时间戳高效查询并处理;
-
要保证数据的安全性和稳定性、故障转移容错性。
2 Kafka 存储选型
有了上面的场景需求分析后, 我们接下来分析看看 Kafka 到底基于什么机制来存储的?能否直接用现有我们了解到的关系型数据库来实现呢?我们接着继续深度分析。
2.1 存储基本知识
我们先来了解下存储的基本知识或者常识。在我们的认知中,对于各个存储介质的速度大体同下图中看到的那样,层级越高代表速度越快。很显然,磁盘处于一个比较尴尬的位置。然而,事实上磁盘可以比我们预想的要快,也可能比我们预想的要慢,这完全取决于我们如何使用它。
各存储介质对比分布(图来自网络)
关于磁盘和内存的 IO 速度,我们可以从下图性能测试的结果看出普通机械磁盘的顺序 I/O 性能指标是 53.2M value/s,而内存的随机 I/O 性能指标是 36.7M value/s。由此似乎可以得出结论:磁盘的顺序 I/O 性能要强于内存的随机 I/O 性能。
磁盘和内存的 IO 速度对比(图片来自网络)
另外从整个数据读写性能方面,有不同的实现方式,要么提高读速度,要么提高写速度。
-
提高读速度:利用索引来提高查询速度。但是有了索引,大量写操作都会维护索引,那么会降低写入效率。常见的如关系型数据库 MySQL 等;
-
提高写速度:这种一般是采用日志存储,通过顺序追加写的方式来提高写入速度。因为没有索引,无法快速查询,最严重的只能一行行遍历读取。常见的如大数据相关领域的基本都基于此方式来实现。
2.2 Kafka 存储方案剖析
上面从存储基础知识,以及存储介质 IO 速度、读写性能方面剖析了存储类系统的实现方式,那么我们来看看 Kafka 的存储到底该采用哪种方式来实现呢?
对于 Kafka 来说, 它主要用来处理海量数据流。这个场景的特点主要包括:
-
写操作:写并发要求非常高,基本得达到百万级 TPS 顺序追加写日志即可,无需考虑更新操作;
-
读操作:相对写操作来说比较简单,只要能按照一定规则高效查询即可(offset 或者时间戳)。
根据上面两点分析,对于写操作来说直接采用顺序追加写日志的方式就可以满足 Kafka 对于百万 TPS 写入效率要求。
但是如何解决高效查询这些日志呢?
直接采用 MySQL 的 B+ 树数据结构存储是否可以?
我们来逐一分析下:
如果采用 B+ 树索引结构来进行存储,那么每次写都要维护索引,还需要有额外空间来存储索引,更会出现关系型数据库中经常出现的“数据页分裂”等操作。对于 Kafka 这种高并发的系统来说,这些设计都太重了,所以并不适合用。
但是在数据库索引中,似乎有一种索引看起来非常适合此场景,即哈希索引(底层基于Hash Table 实现)。为了提高读速度, 我们只需要在内存中维护一个映射关系即可。每次根据 Offset 查询消息的时候,从哈希表中得到偏移量,再去读文件就可以快速定位到要读的数据位置。但是哈希索引通常是需要常驻内存的,