本文已同步至个人微信公众号【不能止步】,链接为通过零拷贝高效传输数据,欢迎订阅。
许多Web应用程序提供大量的静态内容,当客户端请求数据时,服务端需要从磁盘读取数据并将完全相同的数据写回响应套接字。此活动可能看起来需要相对较少的CPU活动,但它在某种程度上是低效的:内核从磁盘读取数据并将其跨内核-用户边界推给应用程序,然后应用程序将其跨内核-用户边界推回以写入套接字。实际上,应用程序充当了一个低效的中介,将数据从磁盘文件传输到套接字。
每次数据在通过用户-内核边界时,都必须复制数据,这会消耗CPU周期和内存带宽。幸运的是,您可以通过一种称为零拷贝的技术来消除这些数据复制。使用零拷贝的应用程序请求内核直接将数据从磁盘文件复制到套接字,而不需要经过应用程序。零拷贝极大地提高了应用程序性能,并减少了内核模式和用户模式之间的上下文切换次数。
Java类库通过java.nio.channels.filechannel
中的transferTo()
方法在Linux和Unix系统上支持零拷贝。您可以使用transferTo()
方法将字节从调用它的通道直接传输到另一个可写字节通道,而不需要数据流经应用程序。本文首先演示了通过传统复制语义完成的简单文件传输所带来的开销,然后展示了使用transferTo()的零拷贝技术如何获得更好的性能。
传统方法的数据传输
考虑从文件中读取数据并通过网络将数据传输到另一个程序的场景。(此场景描述了许多服务器应用程序的行为,包括提供静态内容的Web应用程序、FTP服务器、邮件服务器等。)该操作的核心在代码清单1中的两个调用中(完整的示例代码请参考附录1):
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);
虽然代码清单1在概念上很简单,但是在内部,复制操作需要在用户模式和内核模式之间进行四次上下文切换(如图2),并且在操作完成之前复制数据四次。图1显示了数据如何在内部从文件复制到套接字:
传统数据复制方法包括四次上下文切换,如下图所示。
read()
调用导致上下文从用户模式切换到内核模式(参见图2)。内部会发出sys_read()
(或等效函数)来从文件中读取数据。第一次复制(参见图1)由直接内存访问(DMA)引擎执行,它从磁盘读取文件内容并将其存储到内核地址空间缓冲区中。- 请求的数据量从读取缓冲区复制到用户缓冲区,然后
read()
调用返回。read()
调用的返回导致第二次上下文切换,从内核模式切换回用户模式。现在数据存储在用户地址空间缓冲区中。 send()
套接字调用导致上下文再次从用户模式切换到内核模式。执行第三次数据复制以再次将数据放入内核地址空间缓冲区。但这一次,数据被放入与目标套接字相关联的另一个缓冲区中。send()
系统调用返回,导致第四个上下文切换。当DMA引擎将数据从内核缓冲区传递到协议引擎时,会独立地、异步地进行第四次数据复制。
使用中间内核缓冲区(而不是直接将数据传输到用户缓冲区)似乎效率很低。但是在数据传输过程中引入内核缓冲去是为了提高性能。当应用程序请求的数据无法填充满内核缓冲区时,在读端使用中间缓冲区允许内核缓冲区充当“预读缓存”的角色。当请求的数据量小于内核缓冲区大小时,这将显著提高性能。写端的中间缓冲区允许写操作异步完成。
不幸的是,如果请求的数据的大小远远大于内核缓冲区的大小,这种方法本身就会成为性能瓶颈。在数据最终推送给应用程序之前,数据在磁盘、内核缓冲区和用户缓冲区之间被复制多次。
零拷贝通过消除这些冗余的数据拷贝来提高性能。
基于零拷贝的数据传输
如果您重新检查传统场景,您会注意到实际上并不需要第二次和第三次数据复制。在这两次数据复制中,应用程序只是缓存数据并将其传输回套接字缓冲区。相反,数据可以直接从读缓冲区传输到套接字缓冲区。transferTo()
方法让您可以做到这一点。代码清单2显示了transferTo()
的方法签名。
public void transferTo(long position, long count, WritableByteChannel target);
transferTo()
方法将数据从文件通道传输到给定的可写字节通道。在内部,它取决于底层操作系统对零拷贝的支持。在UNIX和各种类型的Linux中,这个调用被路由到sendfile()
系统调用,如代码清单3所示,它将数据从一个文件描述符传输到另一个文件描述符。
#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所示。
transferTo(position, count, writableChannel);
图3显示了使用transferTo()
方法的数据复制路径。
图4显示了使用transferTo()
方法时的上下文切换。
transferTo()
方法使文件内容首先被DMA引擎复制到读缓冲区中。然后内核再将数据复制到与输出套接字关联的内核缓冲区中。- 第三次复制发生在DMA引擎将数据从内核套接字缓冲区传递到协议引擎时。
这是一个改进:我们将上下文切换的数量从4个减少到2个,并将数据复制的次数从4个减少到3个(其中只有一个涉及CPU)。但这还没有让我们达到零拷贝的目标。如果底层网络接口卡支持收集操作(gather operation),我们可以进一步减少内核完成的数据复制次数。在Linux内核2.4及更高版本中修改了套接字缓冲区描述符以适应这一要求。这种方法不仅减少了多个上下文切换,而且消除了需要CPU参与的重复的数据复制。用户侧的用法没有发生改变,但本质却发生了变化。
transferTo()
方法使文件内容被DMA引擎复制到内核缓冲区中。- 数据没有被复制到套接字缓冲区。相反,只有包含有关数据位置和长度信息的描述符会被添加到套接字缓冲区中。DMA引擎将数据直接从内核缓冲区传递到协议引擎,从而消除了CPU拷贝。
图5显示了在支持数据收集操作时使用transferTo()
的数据复制过程。
构建文件服务器
现在让我们将零拷贝付诸实践,使用在客户端和服务器之间传输文件的相同示例;完整的示例代码请参考附录1和附录2。TraditionalClient.java
和TraditionalServer.java
基于传统的复制语义,使用File.read()
和Socket.send()
。TraditionalServer.java
是一个服务器程序,它侦听客户端连接的特定端口,然后每次从套接字读取4K字节的数据。TraditionalClient.java
连接到服务器,每次从文件中读取(使用File.read()
) 4K字节的数据,并通过套件字将内容发送(使用socket.send()
)到服务器。
类似地,TransferToServer.java
和TransferToClient.java
完成相同的功能,但使用transferTo()
方法(内部使用sendfile()
系统调用)将文件从服务器传输到客户端。
性能对比
我们在运行2.6内核的Linux系统上执行示例程序,并测量了传统方法和transferTo()
方法在不同文件大小下的运行时间(以毫秒为单位)。结果如表1所示:
文件大小(MB) | 普通的文件传输(ms) | transferTo(ms) |
---|---|---|
7 | 156 | 45 |
21 | 337 | 128 |
63 | 843 | 387 |
98 | 1320 | 617 |
200 | 2124 | 1150 |
350 | 3631 | 1762 |
700 | 13498 | 4422 |
1024 | 18399 | 8537 |
如您所见,与传统方法相比,`transferTo()` API缩短了大约65%的时间。对于需要将大量数据从一个I/O通道复制到另一个I/O通道的应用程序(例如Web服务器),这有可能显著提高性能。
总结
我们已经演示了与从一个通道读取数据并将相同的数据写入另一个通道,使用transferTo()
的性能优势。中间缓冲区拷贝(即使这些是隐藏在内核中的)可能会增加数据复制的时间。如果应用需要在不同通道之间进行大量的数据复制,零拷贝技术可以显著提高性能。
附录1:传统方法的数据传输
import java.io.*;
import java.net.*;
public class TraditionalClient {
public static void main(String[] args) {
int port = 2000;
String server = "localhost";
Socket socket = null;
String lineToBeSent;
DataOutputStream output = null;
FileInputStream inputStream = null;
int ERROR = 1;
// connect to server
try {
socket = new Socket(server, port);
System.out.println("Connected with server " +
socket.getInetAddress() +
":" + socket.getPort());
} catch (UnknownHostException e) {
System.out.println(e);
System.exit(ERROR);
} catch (IOException e) {
System.out.println(e);
System.exit(ERROR);
}
try {
String fname = "sendfile/NetworkInterfaces.c";
inputStream = new FileInputStream(fname);
output = new DataOutputStream(socket.getOutputStream());
long start = System.currentTimeMillis();
byte[] b = new byte[4096];
long read = 0, total = 0;
while ((read = inputStream.read(b)) >= 0) {
total = total + read;
output.write(b);
}
System.out.println("bytes send--" + total + " and totaltime--" + (System.currentTimeMillis() - start));
} catch (IOException e) {
System.out.println(e);
} finally {
try {
output.close();
socket.close();
inputStream.close();
} catch (IOException e) {
System.out.println(e);
}
}
}
}
import java.net.*;
import java.io.*;
public class TraditionalServer {
public static void main(String args[]) {
int port = 2000;
ServerSocket server_socket;
DataInputStream input;
try {
server_socket = new ServerSocket(port);
System.out.println("Server waiting for client on port " +
server_socket.getLocalPort());
// server infinite loop
while (true) {
Socket socket = server_socket.accept();
System.out.println("New connection accepted " +
socket.getInetAddress() +
":" + socket.getPort());
input = new DataInputStream(socket.getInputStream());
// print received data
try {
byte[] byteArray = new byte[4096];
while (true) {
int nread = input.read(byteArray, 0, 4096);
if (0 == nread)
break;
}
} catch (IOException e) {
System.out.println(e);
} finally {
try {
socket.close();
System.out.println("Connection closed by client");
} catch (IOException e) {
System.out.println(e);
}
}
}
} catch (IOException e) {
System.out.println(e);
}
}
}
附录2:基于零拷贝的数据传输
package sendfile;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
public class TransferToClient {
public static void main(String[] args) throws IOException {
TransferToClient sfc = new TransferToClient();
sfc.testSendfile();
}
public void testSendfile() throws IOException {
String host = "localhost";
int port = 9026;
SocketAddress sad = new InetSocketAddress(host, port);
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(sad);
socketChannel.configureBlocking(true);
String fname = "sendfile/NetworkInterfaces.c";
long fsize = 183678375L, sendzise = 4094;
// FileProposerExample.stuffFile(fname, fsize);
FileChannel fileChannel = new FileInputStream(fname).getChannel();
long start = System.currentTimeMillis();
long nsent = 0, curnset = 0;
curnset = fileChannel.transferTo(0, fsize, socketChannel);
System.out.println("total bytes transferred--" + curnset + " and time taken in MS--" + (System.currentTimeMillis() - start));
//fc.close();
}
}
package sendfile;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class TransferToServer {
ServerSocketChannel listener = null;
protected void mySetup() {
InetSocketAddress listenAddr = new InetSocketAddress(9026);
try {
listener = ServerSocketChannel.open();
ServerSocket ss = listener.socket();
ss.setReuseAddress(true);
ss.bind(listenAddr);
System.out.println("Listening on port : " + listenAddr.toString());
} catch (IOException e) {
System.out.println("Failed to bind, is port : " + listenAddr.toString()
+ " already in use ? Error Msg : " + e.getMessage());
e.printStackTrace();
}
}
public static void main(String[] args) {
TransferToServer transferToServer = new TransferToServer();
transferToServer.mySetup();
transferToServer.readData();
}
private void readData() {
ByteBuffer byteBuffer = ByteBuffer.allocate(4096);
try {
while (true) {
SocketChannel conn = listener.accept();
System.out.println("Accepted : " + conn);
conn.configureBlocking(true);
int nread = 0;
while (nread != -1) {
try {
nread = conn.read(byteBuffer);
} catch (IOException e) {
e.printStackTrace();
nread = -1;
}
byteBuffer.rewind();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}