kafka 是怎么做到基于磁盘却比内存还快的?
相信看见题目的同学都会很有疑问,甚至不服气,这都是基于个人对于 kafka 原理的理解,我可以说磁盘顺序写要比内存的随机读快吧。但是说到底,基于性能的优化方面,还是离不开内存的。
Kafka 作为一个支持大数据量写入写出的消息队列,由于是基于 Scala 和 Java 实现的,而 Scala 和 Java 均需要在 JVM 上运行,所以如果是基于内存的方式,即 JVM 的堆来进行数据存储则需要开辟很大的堆来支持数据读写,从而会导致 GC 频繁影响性能。考虑到这些因素,kafka 是使用磁盘存储数据的。
kafka 文件存储形式
Kafka 中消息是以 topic 进行分类的,生产者生产消息,消费者消费消息,都是面向 topic 的。topic 存储结构见下图:

由于生产者生产的消息会不断追加到 log 文件末尾,为防止 log 文件过大导致数据定位效率低下,Kafka 采取了分片和索引机制,将每个 partition 分为多个 segment。每个 segment 对应两个文件——“.index”文件和“.log”文件。
partition 文件夹命名规则
topic 名称+分区序号,举例有一个 topic 名称文“kafka”,这个 topic 有三个分区,则每个文件夹命名如下:
index 和 log 文件的命名规则
1)partition 文件夹中的第一个 segment 从 0 开始,以后每个 segement 文件以上一个 segment 文件的最后一条消息的 offset+1 命名(当前日志中的第一条消息的 offset 值命名)。
2)数值最大为 64 位 long 大小。19 位数字字符长度,没有数字用 0 填充。
举例,有以下三对文件:
以第二个文件为例看下对应的数据结构:

稀疏索引需要注意下。
消息查找过程:
找 message-2589,即 offset 为 2589:
1)先定位 segment 文件,在 0000000000000002584 中。
2)计算查找的 offset 在日志文件的相对偏移量 offset - 文件名的数量 = 2589 - 2584 = 5; 在 index 文件查找第一个参数的值,若找到,则获取到偏移量,通过偏移量到 log 文件去找对应偏移量的数据即可; 本例中没有找到,则找到当前索引中偏移量的上线最接近的值,即 3,偏移量文 246;然后到 log 文件中从偏移量为 246 数据开始向下寻找。
实现磁盘快于内存的原理
简单了解了 kafka 在数据存储方面的知识,线面我们具体分析下为什么 kafka 基于磁盘却快于内存。
顺序写磁盘
在前面了解存储结构过程中,我们发现 kafka 记录 log 日志使用的结尾追加的方式,即顺序写。这样要比随机写块很多,这与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间。
MMAP(Memory Mapped Files)
mmap,简单描述其就是将磁盘文件映射到内存, 用户通过修改内存就能修改磁盘文件。
即便是顺序写磁盘,磁盘的读写速度任然比内存慢慢的多得多,好在操作系统已经帮我们解决这个问题。在 Linux 操作系统中,Linux 会将磁盘中的一些数据读取到内存当中,我们称之为内存页。当需要读写硬盘的时候,都优先在内存页中进行处理。当内存页的数据比硬盘数据多的时候,就形成了脏页,当脏页达到一定数量,操作系统会进行刷脏,即将内存也数据写到磁盘。
问题:不可靠,写到 mmap 中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用 Flush 的时候才把数据真正的写到硬盘。
Kafka 提供了一个参数 producer.type 来控制是不是主动 Flush:
如果 Kafka 写入到 mmap 之后就立即 Flush,然后再返回 Producer 叫同步 (Sync)。
如果 Kafka 写入 mmap 之后立即返回 Producer 不调用 Flush 叫异步 (Async)。
零拷贝技术
零拷贝并不是不需要拷贝,而是减少不必要的拷贝次数,通常使用在 IO 读写过程中。
传统 io 过程

如上图所示,上图共经历了四次拷贝的过程:
1)数据到内核态的 read buffer;
2)内核态的 read buffer 到用户态应用层的 buffer;
3)用户态到内核态的 socket buffer;
4)socket buffer 到网卡的 buffer(NIC)。
DMA 引入 DMA 技术,是指外部设备不通过 CPU 而直接与系统内存交换数据的接口技术,网卡等硬件设备支持 DMA 技术。

如上图所示,上图共经历了两次拷贝的过程。
sendfile 在内核版本 2.1 中,引入了 Sendfile 系统调用,以简化网络上和两个本地文件之间的数据传输。同时使用了 DMA 技术。

如上图所示,上图共经历了一次拷贝的过程。
sendfile( DMA 收集拷贝) 之前我们是把页缓存的数据拷贝到 socket 缓存中,实际上,我们仅仅需要把缓冲区描述符传到 socket 缓冲区,再把数据长度传过去,这样 DMA 控制器直接将页缓存中的数据打包发送到网络中就可以了。

如上图所示,最后一次的拷贝也被消除了,数据->read buffer->NIC。
kafka 实现零拷贝
kafka 通过 java 和 scala 实现,而 Java 对 sendfile 是通过 NIO 的 FileChannel (java.nio.channels.FileChannel )的 transferTo 和 transferFrom 方法实现零拷贝。
注: transferTo 和 transferFrom 并不保证一定能使用零拷贝。实际上是否能使用零拷贝与操作系统相关,如果操作系统提供 sendfile 这样的零拷贝系统调用,则这两个方法会通过这样的系统调用充分利用零拷贝的优势,否则并不能通过这两个方法本身实现零拷贝。
作者:我犟不过你
链接:https://juejin.cn/post/7068090666969989151
来源:稀土掘金
评论