前言:我们都知道 Kafka 是基于磁盘进行存储的,但 Kafka 官方又称其具有高性能、高吞吐、低延时的特点,其吞吐量动辄几十上百万。小伙伴们是不是有点困惑了,一般认为在磁盘上读写数据是会降低性能的,因为寻址会比较消耗时间。那 Kafka 又是怎么做到其吞吐量动辄几十上百万的呢?
Kafka 高性能,是多方面协同的结果,包括宏观架构、分布式 partition 存储、ISR 数据同步、以及“无所不用其极”的高效利用磁盘和操作系统特性。
一、磁盘顺序读写磁盘的顺序读写的情况下,磁盘的顺序读写速度和内存随机读写持平。
在磁盘上顺序读写,不需要移动磁盘臂来寻找数据位置,并且操作系统对于线性读写也做了很多优化,比如预读(read-ahead)技术,提前将一个比较大的磁盘读入内存和后写(write-behind),将很多小的逻辑写操作后合并组成一个大的物理写操作。
因为磁盘是机械结构,每次读写都会寻址->写入,其中寻址是一个“机械动作”。为了提高读写磁盘的速度,Kafka 就是使用顺序 I/O。
Kafka 利用了一种分段式的、只追加 (Append-Only) 的日志,基本上把自身的读写操作限制为顺序 I/O,也就使得它在各种存储介质上能有很快的速度。一直以来,有一种广泛的误解认为磁盘很慢。实际上,存储介质 (特别是旋转式的机械硬盘) 的性能很大程度依赖于访问模式。在一个 7200 转/分钟的 SATA 机械硬盘上,随机 I/O 的性能比顺序 I/O 低了大概 3 到 4 个数量级。此外,一般来说现代的操作系统都会提供预读和延迟写技术:以大数据块的倍数预先载入数据,以及合并多个小的逻辑写操作成一个大的物理写操作。正因为如此,顺序 I/O 和随机 I/O 之间的性能差距在 flash 和其他固态非易失性存储介质中仍然很明显,尽管它远没有旋转式的存储介质那么明显。
二、页缓存技术
即便是顺序写入硬盘,硬盘的访问速度还是不可能追上内存。所以 Kafka 的数据并不是实时的写入硬盘 ,它充分利用了现代操作系统页缓存来利用内存提高 I/O 效率。kafka在写数据的时候,会先将数据写入到页缓存,满足一定条件后刷写到磁盘上,可以保证更高的读写性能。它的工作原理是直接利用操作系统的 Page Cache 来实现磁盘文件到物理内存的直接映射,完成映射之后你对物理内存的操作会被同步到硬盘上(操作系统在适当的时候)
Kafka 是基于操作系统的页缓存技术来实现文件写入的,将磁盘的数据缓存到内存中,把对磁盘的访问变为对内存的访问。
操作系统本身有一层缓存,叫做page cache,是在内存里的缓存,我们也可以称之为os cache,意思就是操作系统自己管理的缓存。你在写入磁盘文件的时候,可以直接写入这个os cache里,也就是仅仅写入内存中,接下来由操作系统自己决定什么时候把os cache里的数据真的刷入磁盘文件中。
仅仅这一个步骤,就可以将磁盘文件写性能提升很多了,因为其实这里相当于是在写内存,不是在写磁盘,大家看下图。
Kafka 接收来自 socket buffer 的网络数据,应用进程不需要中间处理、直接进行持久化时。可以使用mmap 内存文件映射。
Memory Mapped Files
简称 mmap,简单描述其作用就是:将磁盘文件映射到内存,用户通过修改内存就能修改磁盘文件。
通过 mmap,进程像读写硬盘一样读写内存(当然是虚拟机内存)。使用这种方式可以获取很大的 I/O 提升,省去了用户空间到内核空间复制的开销。
三、零拷贝技术
通过零拷贝技术,可以去掉那些没有必要的数据复制操作,同时减少内核态和用户态上下文的切换次数。零拷贝方式相较于传统IO方式的性能提升幅度在 2 到 3 倍。
设想一种情况,需要将服务器磁盘的一个文件展示给用户。
传统IO的做法是:
-
调用read(),将文件A的内容复制到内核态模式的ReaderBuffer中;
-
CPU控制将内核模式数据复制到用户模式中;
-
调用write(),将用户模式下的内容复制到内核模式下的SocketBuffer中;
-
将内核模式下的SocketBuffer的数据复制到网卡设备中进行发送;
整个过程,一共进行了4次复制,内核和用户模式的上下文切换也是4次;
3.1、采用零拷贝如果不拷贝到用户空间,有内核模式的ReaderBuffer直接拷贝到SocketBuffer是否可行呢?
其实是可行的,但这依次有3次复制。直到引入DMA技术,才将拷贝减少到2次:
-
使用DMA技术将文件内容复制都内核的ReaderBuffer中
-
DMA将数据从内核的ReaderBuffer直接发送到网卡设备
整个过程,进行了2次复制,上下文切换也是2次。针对内核而言,数据在内核模式下实现了零拷贝。
3.2、DMA(Direct Memroy Access,直接内存读取)DMA就是为了解决批量数据的IO问题,允许不同速度的硬件沟通。不需要依赖CPU来进行复制,只需要CPU初始化这个传输动作,而传输本身是有DMA控制器来实现和完成的,无需CPU直接控制传输,也没有终端处理方式那样保留现场和恢复现场的过程,通过硬件为RAM和IO设备开辟一条直接传输数据的通道,是的CPU效率大大提升。
如果不使用DMA,CPU需要从来源把数据复制到暂存器,再将其写会新的地方,这个时间内,CPU无法处理其他工作。
-
数据较大,读写较慢,追求速度
-
内存不足,不能加载太大数据
-
宽带不足,存在其他线程大量的IO操作,导致宽带不足
四、Broker 性能
4.1 、日志记录批处理
顺序 I/O 在大多数的存储介质上都非常快,几乎可以和网络 I/O 的峰值性能相媲美。在实践中,这意味着一个设计良好的日志结构的持久层将可以紧随网络流量的速度。事实上,Kafka 的瓶颈通常是网络而非磁盘。因此,除了由操作系统提供的底层批处理能力之外,Kafka 的 Clients 和 Brokers 会把多条读写的日志记录合并成一个批次,然后才通过网络发送出去。日志记录的批处理通过使用更大的包以及提高带宽效率来摊薄网络往返的开销。
4.2、 批量压缩
当启用压缩功能时,批处理的影响尤为明显,因为压缩效率通常会随着数据量大小的增加而变得更高。特别是当使用 JSON 等基于文本的数据格式时,压缩效果会非常显著,压缩比通常能达到 5 到 7 倍。此外,日志记录批处理在很大程度上是作为 Client 侧的操作完成的,此举把负载转移到 Client 上,不仅对网络带宽效率、而且对 Brokers 的磁盘 I/O 利用率也有很大的提升。
4.3 、非强制刷新缓冲写操作
另一个助力 Kafka 高性能、同时也是一个值得更进一步去探究的底层原因:Kafka 在确认写成功 ACK 之前的磁盘写操作不会真正调用 fsync 命令;通常只需要确保日志记录被写入到 I/O Buffer 里就可以给 Client 回复 ACK 信号。这是一个鲜为人知却至关重要的事实:事实上,这正是让 Kafka 能表现得如同一个内存型消息队列的原因 —— 因为 Kafka 是一个基于磁盘的内存型消息队列 (受缓冲区/页面缓存大小的限制)。
另一方面,这种形式的写入是不安全的,因为副本的写失败可能会导致数据丢失,即使日志记录似乎已经被确认成功。换句话说,与关系型数据库不同,确认一个写操作成功并不等同于持久化成功。真正使得 Kafka 具备持久化能力的是运行多个同步的副本的设计;即便有一个副本写失败了,其他的副本(假设有多个)仍然可以保持可用状态,前提是写失败是不相关的(例如,多个副本由于一个共同的上游故障而同时写失败)。因此,不使用 fsync 的 I/O 非阻塞方法和冗余同步副本的结合,使得 Kafka 同时具备了高吞吐量、持久性和可用性。
五、Kafak吞吐量高的原因总结 5.1、 mmap 和 sendfile
-
Linux 内核提供、实现零拷贝的 API。
-
mmap 将磁盘文件映射到内存,支持读和写,对内存的操作会反映在磁盘文件上。
-
sendfile 是将读到内核空间的数据,转到 socket buffer,进行网络发送。
-
RocketMQ 在消费消息时,使用了 mmap;Kafka 使用了 sendfile。
-
Partition 顺序读写,充分利用磁盘特性,这是基础。
-
Producer 生产的数据持久化到 Broker,采用 mmap 文件映射,实现顺序的快速写入。
-
Customer 从 Broker 读取数据,采用 sendfile,将磁盘文件读到 OS 内核缓冲区后,直接转到 socket buffer 进行网络发送。
-
Broker 性能优化:日志记录批处理、批量压缩、非强制刷新缓冲写操作等。
-
流数据并行
如果你之前一直想知道 Kafka是如何拥有其现如今公认的高性能标签,那么相信你现在应该有了所需的答案。这里必须说明一下 Kafka 并还不是最快的消息中间件,还有其他具有更大吞吐量的消息中间件。Apache Pulsar 是一项极具前景的技术,它具备可扩展性,在提供相同的消息顺序性和持久性保证的同时,还能实现更好的吞吐量-延迟效果。使用 Kafka 的根本原因是,它作为一个完整的生态系统仍然是无与伦比的。它展示了卓越的性能,同时提供了一个丰富和成熟而且还在不断进化的生态环境,尽管 Kafka 的规模已经相当庞大了,但仍以一种令人羡慕的速度在成长。
参考链接,拜谢大佬:
老周聊架构:聊聊 Kafka: Kafka 为啥这么快?
艾小仙:什么是mmap?
moon聊技术 :妹妹10分钟就玩懂了零拷贝和NIO,也太强了



