kafka通过零拷贝实现高效的数据传输

许多Web应用程序都提供了大量的静态内容,这相当于从磁盘读取数据并将完全相同的数据写回到响应socket这个活动可能似乎只需要相对较少的CPU活动,但是效率有些低下:内核从磁盘读取数据,并将其从内核用户边界推送到应用程序,然后应用程序将其推回到内核用户边界写出来的socket。实际上,应用程序作为一个低效的媒介,从磁盘文件获取数据到socket

每次数据遍历用户内核边界时,都必须进行复制,这会消耗CPU周期和内存带宽。幸运的是,您可以通过一种称为“适当地 - 零拷贝”的技术来消除这些副本内核使用零拷贝的应用程序要求内核直接将数据从磁盘文件复制到套接字,而不通过应用程序。零拷贝大大提高了应用程序的性能,减少了内核和用户模式之间的上下文切换次数。

Java类库通过transferTo()in方法在 Linux和UNIX系统上支持零拷贝java.nio.channels.FileChannel您可以使用该transferTo()方法将字节从其调用的通道直接传输到另一个可写字节通道,而不需要数据流经应用程序。本文首先演示了通过传统的复制语义进行简单文件传输所带来的开销,然后展示了如何使用零复制技术 transferTo()获得更好的性能。

日期转移:传统的方法

考虑从文件读取并通过网络将数据传输到另一个程序的情况。(本场景描述了许多服务器应用程序的行为,包括提供静态内容的Web应用程序,FTP服务器,邮件服务器等等)。操作的核心是清单1中的两个调用(参见下载链接完整的示例代码):

清单1.将文件中的字节复制到套接字
1
2
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);

虽然清单1在概念上很简单,但在内部,复制操作需要在用户模式和内核模式之间进行四次上下文切换,并且在操作完成之前将数据复制四次。图1显示了数据如何从文件内部移动到套接字:

图1.传统的数据复制方法

图2显示了上下文切换:

图2.传统的上下文切换

涉及的步骤是:

  1. read()调用导致从用户模式到内核模式的上下文切换(参见图2)。内部sys_read()发行一个(或等同的)来从文件中读取数据。第一个副本(见图1)由直接内存访问(DMA)引擎执行,该引擎从磁盘读取文件内容并将其存储到内核地址空间缓冲区中。
  2. 所请求的数据量从读取缓冲区复制到用户缓冲区,然后read()调用返回。调用返回导致另一个上下文从内核切换回用户模式。现在数据存储在用户地址空间缓冲区中。
  3. send()插座调用导致从用户模式到内核模式的上下文切换。执行第三个拷贝将数据再次放入内核地址空间缓冲区。但是这一次,数据被放入不同的缓冲区,一个与目标socket相关的缓冲区。
  4. send()系统调用返回,创造了第四上下文切换。独立地和异步地,当DMA引擎将数据从内核缓冲区传递到协议引擎时发生第四个副本。

使用中间内核缓冲区(而不是将数据直接传输到用户缓冲区)可能看起来效率低下。但是中间内核缓冲区被引入进程来提高性能。在读取端使用中间缓冲区允许内核缓冲区充当“预读缓存”,当应用程序没有要求与内核缓冲区一样多的数据时。当请求的数据量小于内核缓冲区大小时,这会显着提高性能。写入侧的中间缓冲区允许写入异步完成。

不幸的是,如果所请求数据的大小远远大于内核缓冲区的大小,这种方法本身可能会成为性能瓶颈。在磁盘,内核缓冲区和用户缓冲区最终传递到应用程序之前,数据被复制多次。

零拷贝通过消除这些冗余数据副本来提高性能。

数据传输:零拷贝方法

如果您重新检查传统方案,则会注意到第二个和第三个数据副本实际上不是必需的。除了缓存数据并将其传回到socket缓冲区之外,应用程序只做其他事情。相反,数据可以直接从读取缓冲区传输到socket缓冲区。transferTo() 方法可以让你做到这一点。清单2显示了以下方法的签名 transferTo()

清单2. transferTo() 方法
1
public void transferTo(long position, long count, WritableByteChannel target);

transferTo()方法将数据从文件通道传输到给定的可写字节通道。在内部,它取决于底层操作系统对零拷贝的支持; 在UNIX和各种Linux中,这个调用被路由到sendfile() 系统调用,如清单3所示,它将数据从一个文件描述符传输到另一个:

清单3. sendfile()系统调用
1
2
#include < sys /socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

清单1中的file.read()socket.send() 调用的操作可以被一个调用所取代 ,如清单4所示:transferTo()

清单4.使用 transferTo()将数据从磁盘文件复制到socket
1
transferTo(position, count, writableChannel);

图3显示了transferTo()使用方法时的数据路径

图3.使用数据拷贝 transferTo()

图4显示了transferTo() 使用方法时的上下文切换

图4.使用的上下文切换 transferTo()

transferTo()清单4使用时所采取的步骤如下

  1. transferTo()方法使得文件内容被DMA引擎复制到读缓冲器中。然后,数据被内核复制到与输出套接字关联的内核缓冲区中。
  2. 第三个副本是在DMA引擎将数据从内核socket缓冲区socket传递给协议引擎时发生的。

这是一个改进:我们已经将上下文切换次数从四次减少到两次,并将数据副本的数量从四个减少到三个(其中只有一个涉及CPU)。但是这还没有使我们达到零拷贝的目标。如果底层网络接口卡支持收集操作,我们可以进一步减少内核的数据重复在Linux内核2.4和更高版本中,套接字缓冲区描述符被修改以适应这个要求。这种方法不仅减少了多个上下文切换,还消除了需要CPU参与的重复数据副本。用户端的使用情况仍然保持不变,但内在因素已经改变:

  1. transferTo()方法使得文件内容被DMA引擎复制到内核缓冲区中。
  2. 没有数据被复制到socket缓冲区中。相反,只有带有关于数据的位置和长度的信息的描述符被附加到socket缓冲区。DMA引擎直接将数据从内核缓冲区传递到协议引擎,从而消除了剩余的最终CPU副本。

图5显示了使用transferTo()收集操作的数据副本

图5. transferTo()使用和收集操作时的数据拷贝

建立一个文件服务器

现在,让我们将零拷贝付诸实践,使用在客户机和服务器之间传输文件的相同示例(请参阅下载以获取示例代码)。TraditionalClient.java并 TraditionalServer.java基于传统的复制语义,使用File.read()Socket.send()TraditionalServer.java是一个服务器程序,它侦听特定的端口以供客户端连接,然后从套接字一次读取4K字节的数据。TraditionalClient.java连接到服务器,File.read()从文件读取(使用)4K字节的数据,并socket.send()通过socket将内容发送(使用)到服务器。

类似地,TransferToServer.java和 TransferToClient.java执行相同的功能,而是使用transferTo()方法(以及反过来的sendfile()系统调用)将文件从服务器传送到客户端。

性能比较

我们在运行2.6内核的Linux系统上执行示例程序,并测量传统方法和transferTo()不同尺寸方法的运行时间(以毫秒为单位)表1显示了结果:

表1.性能比较:传统方法与零拷贝

正如您所看到的,transferTo()与传统方法相比,该API将时间缩短了约65%。这对于从一个I / O通道向另一个I / O通道复制大量数据的应用程序(如Web服务器)具有显着的提高性能的潜力。

概要

我们已经展示了使用transferTo()相比于从一个通道读取并将相同数据写入另一个通道的性能优点 中间缓冲区副本 - 即使是那些隐藏在内核中的副本 - 也会有可测量的成本。在通道之间大量复制数据的应用程序中,零复制技术可以显着提高性能。

你可能感兴趣的:(kafka通过零拷贝实现高效的数据传输)