写点什么

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

作者:武子康
  • 2025-08-18
    山东
  • 本文字数:4699 字

    阅读完需:约 15 分钟

大数据-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 流程中,数据通常需要在多个缓冲区之间来回拷贝:


  1. 磁盘/网卡缓冲区

  2. 内核空间缓冲区

  3. 用户空间缓冲区


这种多次拷贝会消耗宝贵的 CPU 资源和内存带宽,而零拷贝技术通过以下方式优化这个过程:

Kafka 中的零拷贝实现

Kafka 作为高性能消息系统,充分利用了零拷贝技术:


  1. 生产者到 Broker:使用sendfile系统调用,直接将消息从生产者网络缓冲区传输到磁盘文件

  2. Broker 到消费者:通过sendfile将日志段文件直接发送到网络套接字

  3. 内存映射文件:使用 mmap 将磁盘文件映射到内存地址空间,避免用户空间和内核空间的数据拷贝


具体实现中,Kafka 结合了 Java NIO 的FileChannel.transferTo()方法,该方法在底层会调用操作系统的零拷贝机制。

Nginx 中的零拷贝应用

Nginx 作为高性能 Web 服务器,同样采用了零拷贝技术:


  1. 静态文件传输:使用sendfile系统调用直接将文件内容发送到网络套接字

  2. 大文件处理:对于大文件采用异步 I/O 结合零拷贝的方式

  3. TCP 优化:通过设置TCP_NODELAYTCP_CORK等选项优化网络传输


Nginx 的零拷贝实现使其在处理静态内容时能够达到极高的吞吐量,特别是在高并发场景下优势明显。

操作系统支持

现代操作系统提供了多种零拷贝机制:


  1. sendfile:Linux 2.4+引入,支持文件到套接字的直接传输

  2. splice:Linux 2.6+引入,支持任意两个文件描述符之间的数据传输

  3. mmap:内存映射文件,减少用户空间和内核空间的数据拷贝

  4. Direct I/O:绕过页面缓存,直接操作存储设备

应用场景

零拷贝技术特别适用于以下场景:


  1. 大文件传输(如视频流媒体)

  2. 高吞吐量消息系统(如 Kafka)

  3. 静态 Web 内容服务(如 Nginx)

  4. 数据库系统(如 MySQL 的大查询结果传输)

  5. 网络代理和网关

性能对比

在实际测试中,使用零拷贝技术可以带来显著的性能提升:


  • 文件传输吞吐量可提高 30%-50%

  • CPU 利用率降低 20%-40%

  • 内存带宽压力显著减小


这些优化对于构建高性能分布式系统至关重要,特别是在处理海量数据时效果尤为明显。

传统 IO

比如:读取文件,Socket 发送传统实现方式:先读取、再发送、实际经过 1-4 次 Copy


buffer = File.readSocket.send(buffer)
复制代码


  • 第一次:将磁盘文件,读取到操作系统内核缓冲区

  • 第二次:将内核缓冲区的数据,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 的零拷贝数据传输流程详解


  1. 数据流转过程


  • 磁盘数据首先通过 DMA(Direct Memory Access)技术直接拷贝到内核缓冲区(Kernel Buffer),完全绕过 CPU 处理

  • 数据再从内核缓冲区通过 DMA 直接传输到网卡缓冲区(NIC Buffer/Socket Buffer)

  • 整个过程中数据始终保持在操作系统内核空间,避免了用户空间和内核空间之间的多次拷贝


  1. 性能优化关键点


  • 零拷贝(Zero-copy)技术实现:

  • 传统方式需要 4 次拷贝:磁盘->内核 Buffer->用户 Buffer->Socket Buffer->网卡

  • 零拷贝方式只需 2 次 DMA 拷贝,完全由硬件完成

  • 上下文切换大幅减少:

  • 传统方式需要 4 次上下文切换

  • sendfile 方式只需 2 次切换(用户态->内核态->用户态)

  • 系统调用简化:整个读文件+网络发送流程合并为单个 sendfile 系统调用


  1. Java NIO 实现细节


  • 核心 API:


  fileChannel.transferTo(position, count, socketChannel);  fileChannel.transferFrom(srcChannel, position, count);
复制代码


  • 实现原理:

  • FileChannel 的 transferTo()底层通过 JNI 调用操作系统的 sendfile 系统调用

  • 在 Linux 2.4+内核上支持完善的 sendfile 实现

  • 传输的文件描述符和 socket 描述符都在内核空间处理


  1. Kafka 中的具体实现


  • 传输层架构:

  • TransportLayer 抽象层定义基本网络操作

  • PlaintextTransportLayer 实现明文传输

  • SslTransportLayer 实现 SSL 加密传输

  • 零拷贝实现:


  // Kafka实际调用的核心代码片段  long transferred = fileChannel.transferTo(position, length, socketChannel);
复制代码


  • 性能对比:

  • 小文件:零拷贝提升约 30%吞吐量

  • 大文件:零拷贝可提升 200%以上吞吐量

  • 平均降低 60%的 CPU 使用率


  1. 典型应用场景


  • 消息消费者从 Broker 拉取消息

  • Broker 之间副本同步

  • 日志段(LogSegment)文件传输

  • 消息批量压缩包的传输


  1. 限制条件


  • 只适用于文件到 socket 的直接传输

  • 需要操作系统支持 sendfile 系统调用

  • 传输的文件内容在传输过程中不可修改

  • 某些旧版本 Windows 系统支持有限



注:


  • transferTo 和 transferFrom 并不保证一定能使用零拷贝,需要操作性系统支持

  • Linux2.4+ 内核通过 sendfile 系统调用,提供了零拷贝

页缓存

页缓存是操作系统实现的一种主要磁盘缓存,以此来减少对磁盘的 IO 操作。具体来说,就是把磁盘中的数据存到闪存中,把对磁盘访问变为内存访问。Kafka 接收来自 SocketBuffer 的网络数据,应用进程不需要中间处理、直接进行持久化时。可以使用 mmap 内存文件映射。

Memory Mapped Files

mmap(Memory-mapped files)是一种高效的文件 I/O 操作机制,其核心功能是将磁盘文件直接映射到进程的虚拟地址空间,建立起文件内容与内存地址之间的直接对应关系。当用户程序通过内存指针访问这些映射区域时,实际上就是在操作对应的磁盘文件内容。


具体工作原理:


  1. 映射建立阶段:当调用 mmap()系统调用时,操作系统会在进程的虚拟地址空间中分配一段连续的地址范围,但此时并不立即加载文件内容到物理内存,而是仅建立虚拟内存区域(VMA)与磁盘文件的映射关系。

  2. 按需加载机制:当进程首次访问某个映射页面时,会触发缺页异常(page fault),此时操作系统才会将对应的文件内容从磁盘加载到物理内存的页帧(page frame)中,并更新页表建立映射。

  3. 同步机制:被修改的页面会被标记为"脏页"(dirty page),操作系统会根据特定的策略(如定期刷盘、内存压力或显式调用 msync())将这些修改写回磁盘。现代操作系统通常采用以下同步策略:

  4. 定期回写(默认每 30 秒)

  5. 内存压力时的 LRU 淘汰

  6. 显式调用 msync()强制同步


典型应用场景:


  1. 大文件处理:处理远大于物理内存的文件时,mmap 可以避免一次性加载整个文件

  2. 进程间通信:多个进程映射同一个文件实现共享内存通信

  3. 随机访问:需要频繁随机访问文件不同位置的场景

  4. 数据库系统:许多数据库引擎使用 mmap 实现缓存管理


性能优势:


  1. 减少数据拷贝:避免了 read()/write()系统调用中的内核缓冲区到用户缓冲区的拷贝

  2. 利用页缓存:自动利用 OS 的页缓存机制

  3. 延迟加载:按需加载的机制节省内存使用

  4. 零拷贝:某些情况下可以直接 DMA 传输到映射区域


注意事项:


  1. 需要处理 SIGSEGV 信号以防访问未映射区域

  2. 大文件映射需要考虑地址空间限制(32 位系统尤为明显)

  3. 同步时机由 OS 控制,重要数据应显式同步

  4. 不适合流式顺序访问(传统 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 进行网络发送。

发布于: 刚刚阅读数: 2
用户头像

武子康

关注

永远好奇 无限进步 2019-04-14 加入

Hi, I'm Zikang,好奇心驱动的探索者 | INTJ / INFJ 我热爱探索一切值得深究的事物。对技术、成长、效率、认知、人生有着持续的好奇心和行动力。 坚信「飞轮效应」,相信每一次微小的积累,终将带来深远的改变。

评论

发布
暂无评论
大数据-71 Kafka 从 sendfile 到 mmap:高性能背后的 I/O 技术全解析_Java_武子康_InfoQ写作社区