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

零拷贝,零开销

本文仅是中文版本,原文由 Sathish Palaniappan, Pramod Nagaraja 发布于 2008年09月2号。文章适合初次接触零拷贝技术并想进一步学习的读者,零拷贝本身是一种思想,不与任何编程语言绑定,不懂Java的读者可以跳过零拷贝技术在Java中实现的具体细节。


许多Web应用提供大量的静态内容,主要就是从磁盘读取数据然后将数据写回套接字,中间不涉及数据的变换。这种操作对CPU的使用相对较少,但是效率很低:首先,内核从文件读取数据,然后将数据从内核空间拷贝到用户进程空间,最后应用程序将数据拷贝回内核空间并通过套接字发送。实际上,在整个流程中应用程序仅充当一个将数据从磁盘拷贝到套接字的低效中间层。

每次数据跨越用户态和内核态的边界,数据都需要拷贝,拷贝操作消耗CPU和内存带宽。幸运的是通过一种称为“零拷贝”的技术可消除这些不必要的拷贝。使用零拷贝的应用要求内核将磁盘数据直接拷贝到套接字而不再经过应用。零拷贝可以极大的提高应用的性能并减少上下文在内核态和用户态之间的切换次数。

在 Linux 和 Unix 系统中 Java 类库通过java.nio.channels.FileChanneltransgerTo方法支持零拷贝。可以使用transgerTo方法在两个通道之间直接传递数据,而不要求数据经过应用程序。为了更好的理解零拷贝技术对性能的提升,首先通过传统复制语义实现一个简单文件传输功能,然后通过零拷贝技术实现同样功能,并比较两种实现在性能上的差异。

数据传输: 传统语义

考虑这样的场景:从文件读取数据并通过网络将数据传递给其他程序(这是很多应用的行为,包括提供静态内容的Web应用,FTP 服务器,邮件服务器等)。两个核心的操作如代码1所示:

代码 1. 从文件拷贝数据到套接字
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);

虽然代码1非常的简单,但是在代码内部实现,拷贝操作需要上下文在用户态和内核态切换四次,在操作完成前数据需要拷贝四次。图1展示了数据如何从文件转移到套接字:

数据拷贝路径
图 1. 传统数据拷贝方法

图 2 展示了上下文切换:

上下文切换
图 2. 传统方法下的上下文切换

涉及的步骤包括:

  1. read()调用导致上下文从用户态切换到内核态。内核通过sys_read()(或等价的方法)从文件读取数据。DMA引擎执行第一次拷贝:从文件读取数据并存储到内核空间的缓冲区。

  2. 请求的数据从内核的读缓冲区拷贝到用户缓冲区,然后read()方法返回。read()方法返回导致上下文从内核态切换到用户态。现在待读取的数据已经存储在用户空间内的缓冲区。

  3. send()调用导致上下文从用户态切换到内核态。第三次拷贝数据从用户空间重新拷贝到内核空间缓冲区。但是,这一次,数据被写入一个不同的缓冲区,一个与目标套接字相关联的缓冲区。

  4. send()系统调用返回导致第四次上下文切换。当DMA引擎将数据从内核缓冲区传输到协议引擎缓冲区时,第四次拷贝是独立且异步的。

使用中间内核缓冲区(而不是将数据直接发送到用户缓冲区)似乎非常低效。但是,进程引入中间内核缓冲区可以提高性能。在读取端使用中间内核缓冲区,在应用请求的数据没有超出内核缓冲区的数据时,内核缓冲区可以担当“预读缓存”的角色。在写端,中间内核缓冲区使写操作完全异步化。

不幸的是,当请求的数据大于内核缓冲区大小时这种方法往往会成为性能瓶颈。数据在最终被发送之前,在磁盘,内核缓冲区和用户缓冲区之间发生多次拷贝。零拷贝通过减少不必要的数据拷贝以提供性能。

数据传输: 零拷贝方式

如果你回想使用传统语义传递数据的场景,你会发现第二次和第三次数据拷贝并不是真的需要。应用程序除了缓存数据然后将数据传回套接字缓冲区外没有做任何事情。数据可以直接从内核的读缓冲区传输到套接字缓冲区。transferTo方法允许你实现这样的流程。transferTo方法的签名如代码 2所示:

代码 2. The transferTo() method
public void transferTo(long position, long count, WritableByteChannel target);

transferTo 方法将数据从文件通道传输到给定的可写字节通道。transferTo 内部实现依赖底层操作系统对零拷贝的支持:在UNIX和各 Linux 版本中,transgerTo方法调用最终会调用sendfile()方法,代码如 List 3 所示,sendfile将数据从一个文件描述符传输到另一个:

代码 3. The sendfile() system call
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

代码 1 中的file.read()socket.send()两个方法调用可以替换为一个transferTo()方法调用,如代码 4所示:

代码 4. 使用 transferTo() 从磁盘拷贝数据到套接字
transferTo(position, count, writableChannel);

图 3 展示了使用 transferTo() 方法时,数据的流向:

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

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

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

使用transgerTo()方法时涉及的步骤包括以下两步:

  1. transgerTo方法调用触发DMA引擎将文件上下文信息拷贝到内核读缓冲区,接着内核将数据从内核缓冲区拷贝到与外出套接字相关联的缓冲区。

  2. DMA引擎将数据从内核套接字缓冲区传输到协议引擎(第三次数据拷贝)。

这是一个改进:上下文切换的次数从4次减少到2次,数据拷贝的次数从4次减少到3次(仅有一次数据拷贝消耗CPU资源)。然而,这并没有实现零拷贝的目标,如果底层网卡支持gather operations,可以进一步减少内核拷贝数据的次数。Linux 内核 从2.4 版本开始修改了套接字缓冲区描述符以满足这个要求。这种方法不仅减少了多个上下文切换,还消除了消耗CPU的重复数据拷贝。用户使用的方法没有任何变化,依然通过transferTo方法,但是方法的内部实现

发生了变化:

  1. transferTo方法调用触发 DMA 引擎将文件上下文信息拷贝到内核缓冲区。

  2. 数据不会被拷贝到套接字缓冲区,只有数据的描述符(包括数据位置和长度)被拷贝到套接字缓冲区。DMA 引擎直接将数据从内核缓冲区拷贝到协议引擎,这样减少了最后一次需要消耗CPU的拷贝操作。

图 5 展示了在有gather option条件下使用transferTo时数据拷贝情况:

gather opreation 数据拷贝
图 5. 使用 transferTo() and gather operations 时的数据拷贝

性能测试

现在让我们在一个需要在客户端和服务器之间传输文件的程序中应用零拷贝。 TraditionalClient.javaTraditionalServer.java 基于传统复制语义,使用 File.read()Socket.send()方法读取和发送数据. TraditionalServer.java 是一个监听在5230端口等待客户端连接的服务器应用,每次从套接字中读取4kb的数据。 TraditionalClient.java 连接到服务器, 使用File.read() 方法每次从文件读取4kb数据,然后调用方法socket.send()) 将数据通过套接字发送给服务器.

类似的, TransferToServer.javaTransferToClient.java 通过使用transferTo()(使用sendfile()系统调用发送数据)实现一样的将数据从客户端发送到服务器的功能。

性能比较

在Linux 内核2.6版本上,以毫秒统计使用传统方法和使用transferTo方法传输不同大小的文件的耗时。表1展示了测试结果:

表 1. 性能标胶: 传统方法 vs. 零拷贝
File size Normal file transfer (ms) transferTo (ms)
7MB 156 45
21MB 337 128
63MB 843 387
98MB 1320 617
200MB 2124 1150
350MB 3631 1762
700MB 13498 4422
1GB 18399 8537

从测试结果来看使用transgerTo的API和传统方法相比可以降低65%的传输时间。这可以有效的提高在不同I/O通道之间大量拷贝数据应用的性能。

总结

我们已经证明了在从一个通道读取数据并将相同的数据写入另一个通道的场景下使用transferTo带来的巨大性能优势。内部缓冲区的拷贝,尽管这些拷贝隐藏在内核里,但是也是可观的消耗。对于需要处理在通道之间拷贝大量数据的应用,零拷贝技术可以显著的提升性能。性能测试使用的用例可以从Github免费下载。

扩展阅读

在Java编程领域,Netty是一个非常流行的基于事件驱动的异步网络应用框架,Netty的核心框架之一就是拥有丰富的支持零拷贝的字节缓冲区,想进一步了解零拷贝技术的朋友可以深入研究Netty中零拷贝技术的实现。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,271评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,725评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,252评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,634评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,549评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,985评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,471评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,128评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,257评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,233评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,235评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,940评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,528评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,623评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,858评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,245评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,790评论 2 339

推荐阅读更多精彩内容