零拷贝技术以及在kafka 中的应用

一、基础概念

Zero-Copy技术省去了将操作系统的read buffer拷贝到程序的buffer,以及从程序buffer拷贝到socket buffer的步骤,直接将read buffer拷贝到socket buffer. Java NIO中的FileChannal.transferTo()方法就是这样的实现,这个实现是依赖于操作系统底层的sendFile()实现的。

举例子:

吧磁盘上的数据发送给其他的用户的过程

1、一般流程

在这个过程中文件A的经历了4次copy的过程:

  1. 首先,调用read时,文件A拷贝到了kernel模式;

  2. 之后,CPU控制将kernel模式数据copy到user模式下;

  3. 调用write时,先将user模式下的内容copy到kernel模式下的socket的buffer中;

  4. 最后将kernel模式下的socket buffer的数据copy到网卡设备中传送;

从上面的过程可以看出,数据白白从kernel模式到user模式走了一圈,浪费了2次copy

 

传输数据时,操作系统首先会检查,是不是最近访问过此文件,文件内容是否缓存在内核缓冲区,如果是,操作系统则直接根据read系统调用提供的buf地址,将内核缓冲区的内容拷贝到buf所指定的用户空间缓冲区中去。如果不是,操作系统则首先将磁盘上的数据拷贝的内核缓冲区,这一步目前主要依靠DMA来传输,然后再把内核缓冲区上的内容拷贝到用户缓冲区中。 接下来,write系统调用再把用户缓冲区的内容拷贝到网络堆栈相关的内核缓冲区中,最后socket再把内核缓冲区的内容发送到网卡上。

 

read(file, tmp_buf, len);
write(socket, tmp_buf, len);

步骤一:系统调用read导致了从用户空间到内核空间的上下文切换。DMA模块从磁盘中读取文件内容,并将其存储在内核空间的缓冲区内,完成了第1次复制。

步骤二:数据从内核空间缓冲区复制到用户空间缓冲区,之后系统调用read返回,这导致了从内核空间向用户空间的上下文切换。此时,需要的数据已存放在指定的用户空间缓冲区内(参数tmp_buf),程序可以继续下面的操作。

步骤三:系统调用write导致从用户空间到内核空间的上下文切换。数据从用户空间缓冲区被再次复制到内核空间缓冲区,完成了第3次复制。不过,这次数据存放在内核空间中与使用的socket相关的特定缓冲区中,而不是步骤一中的缓冲区。

步骤四:系统调用返回,导致了第4次上下文切换。第4次复制在DMA模块将数据从内核空间缓冲区传递至协议引擎的时候发生,这与我们的代码的执行是独立且异步发生的。你可能会疑惑:“为何要说是独立、异步?难道不是在write系统调用返回前数据已经被传送了?write系统调用的返回,并不意味着传输成功——它甚至无法保证传输的开始。调用的返回,只是表明以太网驱动程序在其传输队列中有空位,并已经接受我们的数据用于传输。可能有众多的数据排在我们的数据之前。除非驱动程序或硬件采用优先级队列的方法,各组数据是依照FIFO的次序被传输的(图1中叉状的DMA copy表明这最后一次复制可以被延后)。

从上图中可以看出,共产生了四次数据拷贝,即使使用了DMA来处理了与硬件的通讯,CPU仍然需要处理两次数据拷贝,与此同时,在用户态与内核态也发生了多次上下文切换,无疑也加重了CPU负担。

 

二、常用零拷贝技术

Linux 中提供类似的系统调用主要有 mmap(),sendfile() 以及 splice()

 

2.1 mmap()

我们减少拷贝次数的一种方法是调用mmap()来代替read调用:

buf = mmap(diskfd, len);
write(sockfd, buf, len);

应用程序调用 mmap(),磁盘上的数据会通过 DMA被拷贝的内核缓冲区,接着操作系统会把这段内核缓冲区与应用程序共享,这样就不需要把内核缓冲区的内容往用户空间拷贝。应用程序再调用 write(),操作系统直接将内核缓冲区的内容拷贝到 socket缓冲区中,这一切都发生在内核态,最后, socket缓冲区再把数据发到网卡去。

详解步骤如下:

步骤一:mmap系统调用导致文件的内容通过DMA模块被复制到内核缓冲区中,该缓冲区之后与用户进程共享,这样就内核缓冲区与用户缓冲区之间的复制就不会发生。

步骤二:write系统调用导致内核将数据从内核缓冲区复制到与socket相关联的内核缓冲区中。

步骤三:DMA模块将数据由socket的缓冲区传递给协议引擎时,第3次复制发生。
 

图解如下:

使用mmap替代read很明显减少了一次拷贝,当拷贝数据量很大时,无疑提升了效率。但是使用 mmap是有代价的。当你使用 mmap时,你可能会遇到一些隐藏的陷阱。例如,当你的程序 map了一个文件,但是当这个文件被另一个进程截断(truncate)时, write系统调用会因为访问非法地址而被 SIGBUS信号终止。 SIGBUS信号默认会杀死你的进程并产生一个 coredump,如果你的服务器这样被中止了,那会产生一笔损失。

通常我们使用以下解决方案避免这种问题:

  1. 为SIGBUS信号建立信号处理程序 当遇到 SIGBUS信号时,信号处理程序简单地返回, write系统调用在被中断之前会返回已经写入的字节数,并且 errno会被设置成success,但是这是一种糟糕的处理办法,因为你并没有解决问题的实质核心。

  2. 使用文件租借锁 通常我们使用这种方法,在文件描述符上使用租借锁,我们为文件向内核申请一个租借锁,当其它进程想要截断这个文件时,内核会向我们发送一个实时的 RT_SIGNAL_LEASE信号,告诉我们内核正在破坏你加持在文件上的读写锁。这样在程序访问非法内存并且被 SIGBUS杀死之前,你的 write系统调用会被中断。 write会返回已经写入的字节数,并且置 errno为success。 我们应该在 mmap文件之前加锁,并且在操作完文件后解锁:

 

2.2 sendFile()

方法调用:

sendfile(socket, file, len);

 

2.3 

详解步骤:

步骤一:sendfile系统调用导致文件内容通过DMA模块被复制到内核缓冲区中。
步骤二:数据并未被复制到socket关联的缓冲区内。取而代之的是,只有记录数据位置和长度的描述符被加入到socket缓冲区中。DMA模块将数据直接从内核缓冲区传递给协议引擎,从而消除了遗留的最后一次复制。
由于数据实际上仍然由磁盘复制到内存,再由内存复制到发送设备,有人可能会声称这并不是真正的"零拷贝"。然而,从操作系统的角度来看,这就是"零拷贝",因为内核空间内不存在冗余数据。应用"零拷贝"特性,出了避免复制之外,还能获得其他性能优势,例如更少的上下文切换,更少的CPU cache污染以及没有CPU必要计算校验和。

图解如下:

 

二、在kafka 的运用

1、kafka server 端处理fetch 请求的时候使用了sendFile 技术

2、kafka 在维持索引的时候则使用到了mmap 技术

 

 

三、展望

Linux中“零拷贝”的实现还远未结束,并很可能在不久的未来发生变化。更多的功能将会被添加,例如,现在的sendfile不支持向量化传输,而诸如Samba和Apache这样的服务器不得不是用TCP_COKR标志来执行多个sendfile调用。该标志告知系统还有数据要在下一个sendfile调用中到达。TCP_CORK和TCP_NODELAY不兼容,后者在我们希望为数据添加头部时使用。这也正是一个完美的例子,用于说明支持向量化的sendfile将在那些情况下,消除目前实现所强制产生的多个sendfile调用和延迟。

当前sendfile一个相当令人不愉快的限制是它无法用户传输大于2GB的文件。如此尺寸大小的文件,在今天并非十分罕见,不得不复制数据是十分令人失望的。由于这种情况下sendfile和mmap都是不可用的,在未来内核版本中提供sendfile64,将会提供很大的帮助。

 

 

参考文档:

1、https://mp.weixin.qq.com/s?__biz=MzU0MzQ5MDA0Mw==&mid=2247483913&idx=1&sn=2da53737b8e8908cf3efdae9621c9698&chksm=fb0be89dcc7c618b0d5a1ba8ac654295454cfc2fa81fbae5a6de49bf0a91a305ca707e9864fc&scene=21#wechat_redirect

2、https://mp.weixin.qq.com/s/5SKgdkC0kaHN495psLd3Tg?scene=25#wechat_redirect

3、https://www.cnblogs.com/zlcxbb/p/6411568.html

 

Kafka 中,零拷贝技术被广泛应用以提高消息传输的效率,特别是在从磁盘读取消息并发送到网络时。Kafka 的高性能和高吞吐量在很大程度上依赖于对操作系统底层特性的利用,其中包括 Linux 的 `sendfile()` 系统调用,它正是实现零拷贝的核心机制。 --- ### 🧩 Kafka零拷贝应用场景 Kafka 的核心功能是将数据持久化到磁盘,并通过网络将数据高效地传输给消费者。在这个过程中,**当消费者从 Kafka broker 读取数据时**,Kafka 利用了零拷贝技术来减少不必要的内存拷贝和上下文切换。 --- ### 🔍 零拷贝Kafka 中的工作流程(基于 Linux) 传统方式(非零拷贝): 1. 数据从磁盘加载到内核空间缓冲区。 2. 内核将数据复制到用户空间缓冲区(JVM 堆内存)。 3. 数据再次复制回内核空间,通过 socket 发送到网络。 4. 总共发生 **两次内存拷贝** 和 **两次上下文切换**。 零拷贝方式(Kafka 使用): 1. 数据从磁盘加载到内核空间缓冲区。 2. 使用 `sendfile()` 系统调用直接将数据从内核缓冲区发送到 socket。 3. 数据不经过用户空间,仅一次上下文切换和一次内存拷贝。 --- ### 📦 Kafka 如何使用 Java NIO 实现零拷贝Kafka 在底层使用了 Java NIO 的 `FileChannel.transferTo()` 方法,该方法在底层会尽可能调用操作系统的 `sendfile()`: ```java FileChannel fileChannel = new RandomAccessFile("data.log", "r").getChannel(); SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("consumer", 9092)); // 零拷贝传输:数据从文件通道直接传输到网络通道 fileChannel.transferTo(position, size, socketChannel); ``` - Kafka 的日志段(LogSegment)管理中,每个分区的消息都存储为一组按偏移量分段的日志文件。 - 当消费者请求特定偏移量范围内的消息时,Kafka 只需定位到对应的日志文件,然后通过 `transferTo()` 直接发送到网络通道。 --- ### ⚙️ Kafka 零拷贝带来的性能优势 | 优势 | 描述 | |------|------| | 减少 CPU 使用率 | 避免了多次数据复制,降低 CPU 负载 | | 提高吞吐量 | 更快地处理大量并发消费者的请求 | | 降低延迟 | 减少了上下文切换和内存拷贝的时间开销 | --- ### ✅ Kafka 架构设计如何配合零拷贝 - **顺序写入磁盘**:Kafka 将消息追加写入磁盘,充分利用磁盘的顺序访问速度。 - **页缓存(Page Cache)**:Linux 操作系统自动将频繁访问的文件缓存在内存中,Kafka 利用这一点避免每次都访问磁盘。 - **批量发送与压缩**:虽然零拷贝不适合加密或压缩,但 Kafka 支持在应用层进行批量压缩后再传输。 --- ### 🚫 注意事项 - 零拷贝适用于大块数据传输,不适合需要中间处理(如加密、修改内容)的场景。 - Windows 上的 Java 并不完全支持 `transferTo()` 的零拷贝特性,因此 Kafka 推荐部署在 Linux 环境中以获得最佳性能。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值