Kafka 以其卓越的伸缩性和容错能力,确立了在分布式系统中的核心地位,成为消息队列领域的佼佼者。接下来,我将带领大家深入了解 Kafka 的高性能原理。在本节内容中,我们将介绍 Kafka 与 RocketMQ 之间的差异和联系,这将有助于我们更清晰地认识到 Kafka 的独特之处。同时,我们还将详细分析 Kafka 实现高性能的四个关键因素:磁盘顺序读写、页缓存、零拷贝技术以及批量处理机制。
Kafka 和 RocketMQ 对比
要介绍 Kafka,首先无法绕开的一个话题,那就是 Kafka 与 RocketMQ 的对比,因为 RocketMQ 的很多设计都参考了 Kafka 的架构;如果对 RocketMQ 原理不熟悉的读者可以看之前发过的文章。
尽管 RocketMQ 在某些方面与 Kafka 相似,但由于它们解决的问题和业务出发点不同,我们不能简单地将它们划等号。我来举个形象的例子简单说一下二者本质的区别。
RocketMQ 很像是消防员使用的高压水枪,速度快、喷头比较小。而 Kafka 更像是急流,速度较高压水枪稍慢,但流量非常大。因此,每当我们提到 Kafka,总会和它的高吞吐量联系到一起。即使工作在普通的硬件上也能支持每秒数百万条消息的传输,并且 Kafka 天然支持 Hadoop 技术栈。
看到这里,也许你还会想,Kafka 能做的事情,RocketMQ 好像也可以做。为了帮你看清 Kafka 和普通消息队列的区别,我们还是从业务需求出发看个例子。
在许多大型企业中,业务数据量巨大,通常存储在 HDFS 或其他大数据存储介质中。面对如此庞大的数据量,如何快速将数据传递给下游系统是一个需要解决的问题。Kafka 的高性能特性使其在处理大规模数据传输时具有明显优势。例如,在磁盘上每小时产生数 TB 数据的场景下,我需要在 1 小时之内将这些数据批量搬走,显然这种情况下,我希望保证流速的情况下,"水流"的横截面积越大越好。这时,RocketMQ 很显然已经无法满足相关需求,而 Kafka 的高 吞吐能力就显现出了优势。
看到这里,你是否对 Kafka 性能的不可替代性已经有了基础的认识呢。那么,接下来,我将逐一带你分析 Kafka 高性能的关键点。
磁盘顺序读写
Kafka 是将消息记录持久化到本地磁盘中的,是的,你没听错,是磁盘。而且如果服务器不太好的话,甚至会选择机械硬盘。
此时,你可能会对 Kafka 的性能有所怀疑,人们普遍认为磁盘,尤其是廉价的机械磁盘,读写速度和内存有天壤之别,然而事实如何呢?以下截图来自 ACM 2009 年的论文《The Pathologies of Big Data》。
黄色的部分自上而下分别是,随机访问场景下机械磁盘、固态磁盘和内存的读写速度对比。这个对比结果还是很符合经验的:内存最快,固态磁盘次之,机械磁盘最差。然而在蓝色部分代表的顺序访问中,机械磁盘、固态磁盘和内存速度竟然差不多。
实际上不论什么存储介质,读写速度的核心在于访问的方式,也就是顺序读写还是随机读写。我们可以看到磁盘的随机读写很差,但是顺序读写比内存差不了多少。在某些服务器上盘顺序读写性能甚至要高于内存随机读写。
此外,现在的操作系统也针对磁盘的顺序读写做了大量优化,Kafka 就是基于磁盘的顺序读写来实现高性能的。Kafka 会把到来的消息不断追加到磁盘文件的末尾,这样 Kafka 的写入吞吐量就会极高。
上图是 Kafka 写入流程,其中每一个 Partition 是一个分区,可以理解为 RocketMQ 中的队列,每个 Partition 是磁盘上的一个文件,收到消息后,Kafka 会把数据插入到文件的末尾。
这种情况并不是很完美,当你需要删除数据的时候,就会破坏这个结构,那么怎么解决呢?Kafka 给出的答案就是,不删除数据。Kafka 会为每一个消费者保留一个偏移量,这点和 RocketMQ 几乎一样。
我们稍微回顾一下 RocketMQ 主题的实现,我们把下图中的队列换成 Partition,几乎就是 Kafka 了,是不是非常像。需要注意的是,Kafka 对 Partition 做了分段处理,这点很像 JDK7 时期的 ConcurrentHashMap,从而进一步提升了并发度。
当然如果一直不删除数据,硬盘肯定会被撑满,所以 Kakfa 支持基于时间和 Partition 文件大小来对历史数据做删除。具体可以去看它的配置文档。
页缓存
为了进一步优化读写性能,Kafka 还利用了操作系统本身的页缓存,也就是直接利用操作系统自身的内存而不是 JVM 内存。
这样我们可以绕开 GC,如果将数据放在 JVM 内存中,随着 JVM 中数据不断增多,垃圾回收将会变得复杂与缓慢,甚至出现 Stop The World 问题。
同时我们也绕开了对象的创建,我们知道对象有一些上下文、对象头等元数据信息,不使用对象也让数据的存储成本更小了,页缓存的空间利用率会更高,因为存储的都是紧凑的字节结构而不是独立的对象。
再者,即使程序进程重启,系统缓存依然不会消失,避免了重建缓存的过程。因此,通过直接引用操作系统的页缓存,Kafka 的读写速度得到了进一步提升。
零拷贝
那么,是否还有针对页缓存的其他优化方式呢?答案是肯定的。Linux 操作系统 "零拷贝"技术允许内核中页缓存直接发送到 Socket 缓冲区,这样绕开了内核态和用户态之间的数据交换。
我们先看看,如果 Kafka 不使用零拷贝技术,那么会经历这样的一个过程:
1.操作系统将数据从磁盘读入到内核态的页缓存中。
2.Kafka 从页缓存将数据拷贝到用户空间的内存中。
3.Kafka 将数据从用户空间内存再写回到内核态的 Socket 缓冲区中。
从图中可以看到,我们的数据在内核态和用户态之间移动了两次,那么能否避免这个过程呢?当然可以,Kafka 使用了零拷贝技术,也就是直接将数据从内核态的页缓存直接拷贝到内核态的 Socket 缓冲区,避免在内核态和用户态之间移动数据。
注意,这里的零拷贝并非指 0 次拷贝,而是避免了在内核态和用户态之间的拷贝。
批量操作
Kafka 提升性能的最后一个要素就是批量操作,Kafka 提供了很多批量操作 API 以提升吞吐量,在很多情况下,系统的瓶颈不是 CPU 或磁盘,而是网络 IO。
批量的数据传输可以高效地利用网络带宽,但是数据太多也会造成拥堵,Kafka 很自然地想到了批量压缩,Kafka 高性能的另一个原因在于,它把所有的消息都变成批量的文件,并且进行批量压缩,最大程度减少网络 IO 损耗。
总结
这次我们共同探索了 Kafka 实现高性能的秘诀。通过本篇内容,我们了解到 Kafka 采用批量处理机制来优化数据处理流程,并且通过磁盘顺序读写技术,以较低的成本实现了高效的性能表现。我们还揭示了操作系统中隐藏的两个强大工具:页缓存和零拷贝技术。希望大家不仅能够理解 Kafka 是如何达成其高性能的,而且能够在自己的项目中灵活运用这些关键技术。