Apache Kafka 之所以能够处理百万级的 TPS(Transactions Per Second)并保持极低的延迟,其核心在于对操作系统底层机制的深度利用。它打破了"磁盘很慢"的传统认知,通过巧妙的设计将磁盘 I/O 性能发挥到了极致。
以下是 Kafka 实现高性能 I/O 的四大核心技术:
1. 顺序读写 (Sequential I/O)
这是 Kafka 高性能的基石。在传统的认知中,磁盘读写(尤其是机械硬盘 HDD)非常慢,但这通常是指随机读写。
Kafka 采用 Append-only Log(追加写日志) 的方式存储消息。新的消息总是被追加到文件的末尾,而不是随机写入文件的某个位置。
操作系统每次从磁盘读写数据的时候,需要先寻址,也就是先要找到数据在磁盘上的物理位置,然后再进行数据读写。如果是机械硬盘,这个寻址需要比较长的时间,因为它要移动磁头,这是个机械运动,顺序读写相比随机读写省去了大部分的寻址时间,它只要寻址一次,就可以连续地读写下去,所以说,性能要比随机读写要好很多。
2. 页缓存 (Page Cache)
在 Kafka 中,它会利用 Page Cache 加速消息读写。Page Cache 是现代操作系统都具有的一项基本特性。通俗地说,Page Cache 就是操作系统在内存中给磁盘上的文件建立的缓存。无论我们使用什么语言编写的程序,在调用系统的 API 读写文件的时候,并不会直接去读写磁盘上的文件,应用程序实际操作的都是 Page Cache,也就是文件在内存中缓存的副本。
- 机制:
- Kafka 将消息写入文件时,实际是写入操作系统的 Page Cache(内核空间的内存)。
- 操作系统负责在后台将 Page Cache 里的数据"刷盘"到物理磁盘(Flush)。
- 当消费者读取数据时,如果数据刚写入不久,直接从 Page Cache 读取,完全不涉及磁盘 I/O。
- 读取数据时,如果 Page Cache 中没有数据,这时候操作系统会引发一个缺页中断,应用程序的读取线程会被阻塞,操作系统把数据从文件中复制到 Page Cache 中,然后应用程序再从 Page Cache 中继续把数据读出来,这时会真正读一次磁盘上的文件,这个读的过程就会比较慢。
- 用户的应用程序在使用完某块 Page Cache 后,操作系统并不会立刻就清除这个 Page Cache,而是尽可能地利用空闲的物理内存保存这些 Page Cache,除非系统内存不够用,操作系统才会清理掉一部分 Page Cache。清理的策略一般是 LRU 或它的变种算法。
3. 零拷贝 (Zero Copy)
这是 Kafka 在网络传输层面实现高性能的关键。它极大地减少了 CPU 的上下文切换和内存拷贝次数。
在服务端,处理消费的大致逻辑是这样的:
- 首先,从文件中找到消息数据,读到内存中;
- 然后,把消息通过网络发给客户端。
这个过程中,数据实际上做了 2 次或者 3 次复制:
- 从文件复制数据到 Page Cache 中,如果命中 Page Cache,这一步可以省掉;
- 从 Page Cache 复制到应用程序的内存空间中;
- 从应用程序的内存空间复制到 Socket 的缓冲区,这个过程就是我们调用网络应用框架的 API 发送数据的过程。
Kafka 使用零拷贝技术可以把这个复制次数减少一次,上面的 2、3 步骤两次复制合并成一次复制。直接从 Page Cache 中把数据复制到 Socket 缓冲区中,这样不仅减少一次数据复制,更重要的是,由于不用把数据复制到用户内存空间,DMA 控制器可以直接完成数据复制,不需要 CPU 参与,速度更快。
下面是这个零拷贝对应的系统调用:
c
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
它的前两个参数分别是目的端和源端的文件描述符,后面两个参数是源端的偏移量和复制数据的长度,返回值是实际复制数据的长度。
这样数据直接在内核空间流转,数据根本不经过应用程序(用户空间)。CPU 几乎不参与数据搬运,全部交给 DMA(Direct Memory Access)处理。
4. 批量处理
为了减少网络请求的 RTT(往返时延)和磁盘 I/O 次数,Kafka 使用了批处理。
Batching(批处理): Producer 客户端不会产生一条消息就发一条,而是将消息累积到一个 Batch 中,然后一次性发送给 Broker。Broker 也是批量将数据写入磁盘。在 Broker 整个处理流程中,无论是写入磁盘、从磁盘读出来、还是复制到其他副本这些流程中,批消息都不会被解开,一直是作为一条"批消息"来进行处理的。Consumer 也是批量拉取数据,Consumer 从 Broker 拉到一批消息后,在客户端把批消息解开,再一条一条交给用户代码处理。
总结
Kafka 的高性能 I/O 并非依赖某种神奇的算法,而是对计算机体系结构的深刻理解:
- 磁盘层面: 强制 顺序读写,规避机械硬盘的物理劣势。
- 内存层面: 借力 Page Cache减少 IO 并提升读性能。
- 网络层面: 使用 Zero Copy,解放 CPU。
- 传输层面: 全链路 Batching,以吞吐量换低延迟。