点一下关注吧!!!非常感谢!!持续更新!!!
🚀 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
buffer = File.read
Socket.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的零拷贝数据传输流程详解
- 数据流转过程
- 磁盘数据首先通过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(position, count, socketChannel); fileChannel.transferFrom(srcChannel, position, count); - 实现原理:
- FileChannel的transferTo()底层通过JNI调用操作系统的sendfile系统调用
- 在Linux 2.4+内核上支持完善的sendfile实现
- 传输的文件描述符和socket描述符都在内核空间处理
- Kafka中的具体实现
- 传输层架构:
- TransportLayer抽象层定义基本网络操作
- PlaintextTransportLayer实现明文传输
- SslTransportLayer实现SSL加密传输
- 零拷贝实现:
// Kafka实际调用的核心代码片段 long transferred = fileChannel.transferTo(position, length, socketChannel); - 性能对比:
- 小文件:零拷贝提升约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进行网络发送。