大数据 -71 Kafka 从 sendfile 到 mmap:高性能背后的 I/O 技术全解析

点一下关注吧!!!非常感谢!!持续更新!!!
🚀 AI 篇持续更新中!(长期更新)
AI 炼丹日志-31- 千呼万唤始出来 GPT-5 发布!“快的模型 + 深度思考模型 + 实时路由”,持续打造实用 AI 工具指南!📐🤖
💻 Java 篇正式开启!(300 篇)
目前 2025 年 08 月 18 日更新到:Java-100 深入浅出 MySQL 事务隔离级别:读未提交、已提交、可重复读与串行化 MyBatis 已完结,Spring 已完结,Nginx 已完结,Tomcat 已完结,分布式服务正在更新!深入浅出助你打牢基础!
📊 大数据板块已完成多项干货更新(300 篇):
包括 Hadoop、Hive、Kafka、Flink、ClickHouse、Elasticsearch 等二十余项核心组件,覆盖离线+实时数仓全栈!大数据-278 Spark MLib - 基础介绍 机器学习算法 梯度提升树 GBDT 案例 详解

章节内容
上节我们完成了如下内容:
日志删除 日志清理
基于时间删除、基于日志大小、基于偏移量
日志压缩、压缩细节、清理器配置

磁盘存储
零拷贝技术详解
零拷贝(Zero-copy)是一种高效的数据传输技术,它通过减少 CPU 在数据传输过程中的拷贝次数,显著提升 I/O 性能。这项技术在现代高性能系统中扮演着关键角色。
零拷贝的本质
零拷贝并非完全消除数据拷贝,而是通过优化数据传输路径来最小化不必要的数据拷贝操作。在传统 I/O 流程中,数据通常需要在多个缓冲区之间来回拷贝:
磁盘/网卡缓冲区
内核空间缓冲区
用户空间缓冲区
这种多次拷贝会消耗宝贵的 CPU 资源和内存带宽,而零拷贝技术通过以下方式优化这个过程:
Kafka 中的零拷贝实现
Kafka 作为高性能消息系统,充分利用了零拷贝技术:
生产者到 Broker:使用
sendfile
系统调用,直接将消息从生产者网络缓冲区传输到磁盘文件Broker 到消费者:通过
sendfile
将日志段文件直接发送到网络套接字内存映射文件:使用 mmap 将磁盘文件映射到内存地址空间,避免用户空间和内核空间的数据拷贝
具体实现中,Kafka 结合了 Java NIO 的FileChannel.transferTo()
方法,该方法在底层会调用操作系统的零拷贝机制。
Nginx 中的零拷贝应用
Nginx 作为高性能 Web 服务器,同样采用了零拷贝技术:
静态文件传输:使用
sendfile
系统调用直接将文件内容发送到网络套接字大文件处理:对于大文件采用异步 I/O 结合零拷贝的方式
TCP 优化:通过设置
TCP_NODELAY
和TCP_CORK
等选项优化网络传输
Nginx 的零拷贝实现使其在处理静态内容时能够达到极高的吞吐量,特别是在高并发场景下优势明显。
操作系统支持
现代操作系统提供了多种零拷贝机制:
sendfile:Linux 2.4+引入,支持文件到套接字的直接传输
splice:Linux 2.6+引入,支持任意两个文件描述符之间的数据传输
mmap:内存映射文件,减少用户空间和内核空间的数据拷贝
Direct I/O:绕过页面缓存,直接操作存储设备
应用场景
零拷贝技术特别适用于以下场景:
大文件传输(如视频流媒体)
高吞吐量消息系统(如 Kafka)
静态 Web 内容服务(如 Nginx)
数据库系统(如 MySQL 的大查询结果传输)
网络代理和网关
性能对比
在实际测试中,使用零拷贝技术可以带来显著的性能提升:
文件传输吞吐量可提高 30%-50%
CPU 利用率降低 20%-40%
内存带宽压力显著减小
这些优化对于构建高性能分布式系统至关重要,特别是在处理海量数据时效果尤为明显。
传统 IO
比如:读取文件,Socket 发送传统实现方式:先读取、再发送、实际经过 1-4 次 Copy
第一次:将磁盘文件,读取到操作系统内核缓冲区
第二次:将内核缓冲区的数据,Copy 到 Application 应用程序的 Buffer
第三次:将 Application 应用程序 Buffer 中的数据,Copy 到 Socket 网络发送缓冲区(数据操作系统内核的缓冲区)
第四次:将 Socket Buffer 的数据,Copy 到网络协议栈,由网卡进行网络传输。

实际 IO 读写,需要进行 IO 中断,需要 CPU 响应中断(内核态到用户态转换),尽管引入 DMA(Direct Memory Access,直接存储器访问)来接管 CPU 的中断请求,但四次 copy 是存在不必要拷贝的。实际上并不需要第二个和第三个副本,数据可以直接从读缓存区传输到套接字缓存。
Kafka 的两个过程:
网络数据持久化到磁盘(Producer 到 Broker)
磁盘文件通过网络发送(Broker 到 Consumer)数据落盘通常都是非实时的,Kafka 的数据并不是实时写入磁盘,它充分利用了现代操作系统分页存储来利用内存提高 IO 效率。
磁盘文件通过网络发送
Broker 到 Consumer 的零拷贝数据传输流程详解
数据流转过程
磁盘数据首先通过 DMA(Direct Memory Access)技术直接拷贝到内核缓冲区(Kernel Buffer),完全绕过 CPU 处理
数据再从内核缓冲区通过 DMA 直接传输到网卡缓冲区(NIC Buffer/Socket Buffer)
整个过程中数据始终保持在操作系统内核空间,避免了用户空间和内核空间之间的多次拷贝
性能优化关键点
零拷贝(Zero-copy)技术实现:
传统方式需要 4 次拷贝:磁盘->内核 Buffer->用户 Buffer->Socket Buffer->网卡
零拷贝方式只需 2 次 DMA 拷贝,完全由硬件完成
上下文切换大幅减少:
传统方式需要 4 次上下文切换
sendfile 方式只需 2 次切换(用户态->内核态->用户态)
系统调用简化:整个读文件+网络发送流程合并为单个 sendfile 系统调用
Java NIO 实现细节
核心 API:
实现原理:
FileChannel 的 transferTo()底层通过 JNI 调用操作系统的 sendfile 系统调用
在 Linux 2.4+内核上支持完善的 sendfile 实现
传输的文件描述符和 socket 描述符都在内核空间处理
Kafka 中的具体实现
传输层架构:
TransportLayer 抽象层定义基本网络操作
PlaintextTransportLayer 实现明文传输
SslTransportLayer 实现 SSL 加密传输
零拷贝实现:
性能对比:
小文件:零拷贝提升约 30%吞吐量
大文件:零拷贝可提升 200%以上吞吐量
平均降低 60%的 CPU 使用率
典型应用场景
消息消费者从 Broker 拉取消息
Broker 之间副本同步
日志段(LogSegment)文件传输
消息批量压缩包的传输
限制条件
只适用于文件到 socket 的直接传输
需要操作系统支持 sendfile 系统调用
传输的文件内容在传输过程中不可修改
某些旧版本 Windows 系统支持有限

注:
transferTo 和 transferFrom 并不保证一定能使用零拷贝,需要操作性系统支持
Linux2.4+ 内核通过 sendfile 系统调用,提供了零拷贝
页缓存
页缓存是操作系统实现的一种主要磁盘缓存,以此来减少对磁盘的 IO 操作。具体来说,就是把磁盘中的数据存到闪存中,把对磁盘访问变为内存访问。Kafka 接收来自 SocketBuffer 的网络数据,应用进程不需要中间处理、直接进行持久化时。可以使用 mmap 内存文件映射。
Memory Mapped Files
mmap(Memory-mapped files)是一种高效的文件 I/O 操作机制,其核心功能是将磁盘文件直接映射到进程的虚拟地址空间,建立起文件内容与内存地址之间的直接对应关系。当用户程序通过内存指针访问这些映射区域时,实际上就是在操作对应的磁盘文件内容。
具体工作原理:
映射建立阶段:当调用 mmap()系统调用时,操作系统会在进程的虚拟地址空间中分配一段连续的地址范围,但此时并不立即加载文件内容到物理内存,而是仅建立虚拟内存区域(VMA)与磁盘文件的映射关系。
按需加载机制:当进程首次访问某个映射页面时,会触发缺页异常(page fault),此时操作系统才会将对应的文件内容从磁盘加载到物理内存的页帧(page frame)中,并更新页表建立映射。
同步机制:被修改的页面会被标记为"脏页"(dirty page),操作系统会根据特定的策略(如定期刷盘、内存压力或显式调用 msync())将这些修改写回磁盘。现代操作系统通常采用以下同步策略:
定期回写(默认每 30 秒)
内存压力时的 LRU 淘汰
显式调用 msync()强制同步
典型应用场景:
大文件处理:处理远大于物理内存的文件时,mmap 可以避免一次性加载整个文件
进程间通信:多个进程映射同一个文件实现共享内存通信
随机访问:需要频繁随机访问文件不同位置的场景
数据库系统:许多数据库引擎使用 mmap 实现缓存管理
性能优势:
减少数据拷贝:避免了 read()/write()系统调用中的内核缓冲区到用户缓冲区的拷贝
利用页缓存:自动利用 OS 的页缓存机制
延迟加载:按需加载的机制节省内存使用
零拷贝:某些情况下可以直接 DMA 传输到映射区域
注意事项:
需要处理 SIGSEGV 信号以防访问未映射区域
大文件映射需要考虑地址空间限制(32 位系统尤为明显)
同步时机由 OS 控制,重要数据应显式同步
不适合流式顺序访问(传统 I/O 可能更高效)

通过 mmap,进程读写硬盘一样读写内存(当然是虚拟机内存),使用这种方式可以获取很大的 IO 提升,省去了用户空间到内核空间复制的开销。mmap 也有一个很明显的缺陷:不可靠,写到 mmap 中的数据并没有真正的写入到磁盘中,操作系统会在程序主动调用 flush 的时候才会把数据真正写入到硬盘。
Kafka 提供了一个 producer.type 来控制是不是主动 flush。
如果 Kafka 写入到 mmap 之后就立即 flush 然后再返回 Product 叫同步(sync)
写入 mmap 之后立即返回 Producer 不调用 flush 叫做异步(async)。
JavaNIO 对文件映射支持
JavaNIO,提供了 MappedByteBuffer 类可以实现内存映射,MapperByteBuffer 只能通过调用 FileChannel 的 map()取得。再没有其他方式。FileChannel.map() 是抽象方法,具体实现是在 FileChannel.map()可自行查看 JDK 源码,其 map0()方法就是调用了 Linux 内核的 mmap 的 API。

使用 MappedByteBuffer 类要注意的是:
mmap 的文件映射,在 full gc 时才会进行释放。当 close 时,需要手动清除内存映射文件,可以反射调用 sun.misc.Cleaner 方法。
当一个进程准备读取磁盘上的文件内容时:
操作系统会先查看待读取的数据所在的页(page)是否在页缓存中(pagecache)中,如果存在(命中)则直接返回数据,从而避免了物理磁盘的 IO 操作。
如果没有命中,则操作系统会向磁盘发起读取请求并将读取的数据存入入页缓存,之后再将数据返回给进程。
如果一个进程需要将数据写入磁盘:
操作系统也会检测数据对应的页是否在页缓存中,如果不存在,则会先在页缓存中添加相应的页,最后将数据写入对应的页。
被修改过后的页也就变成了脏页,操作系统会在合适的时间把脏页中的数据写入磁盘,以保持数据的一致性。
对一个进程而言,它会在进程内部缓存处理所需的数据,然而这些数据有可能还缓存在操作系统的页缓存中,因此同一份数据可能被缓存了两次。并且,除非使用 DirectIO 的方式,否则页缓存很难被禁止。当使用页缓存的时候,即使 Kafka 服务重启,页缓存还是会保持有效,然而进程内的缓存却需要重建。这样也极大的简化了代码逻辑,因为维护页缓存和文件之间的一致性交由操作系统负责,这样会比进程内维护更加完全有效。Kafka 中有大量使用了页缓存,这是 Kafka 实现高吞吐的重要因素之一,消息先被写入页缓存,由操作系统负责刷盘任务。
顺序写入
操作系统可以针对线性读写做深层次的优化,比如预读(Read-ahead,提前将一个比较大的磁盘快读入内存)和后写(write-behind,将很多消的逻辑写操作合并起来组成一个大的物理操作)技术。

Kafka 在设计时采用了文件追加的方式来写入消息,即只能在日志文件的尾部追加新的消息,并且也不允许修改已写入的消息,这种方式属于典型的顺序写盘的操作,所以就算 Kafka 使用磁盘作为存储介质,也能承载非常大的吞吐量。
mmap 和 sendfile
Linux 内核提供,实现零拷贝的 API
sendfile 是将读到内核空间的数据,转到 SocketBuffer,进行网络发送。
mmap 将磁盘文件映射到内存,支持读和写,对内存的操作会反映在磁盘文件中。
RocketMQ 在消费消息时,使用了 mmap
Kafka 使用了 sendfile
Kafka 速度快是因为
partition 顺序读写,充分利用磁盘特性,这是基础。
producer 生产的数据持久化到 broker,采用 mmap 文件映射,实现顺序的快速写入
customer 从 broker 读取数据,采用 sendfile,将磁盘文件读到 OS 内核缓冲区中,直接转到 SocketBuffer 进行网络发送。
版权声明: 本文为 InfoQ 作者【武子康】的原创文章。
原文链接:【http://xie.infoq.cn/article/4e33ef2fab970e01884fb36b4】。文章转载请联系作者。
评论