一. 前言
Datanode最重要的功能之一就是读取数据块,如果高效的完成数据的读取是影响效率的关键.
二. 操作系统层面读取数据
步骤一 : Datanode会首先将数据块从磁盘存储(也可能是SSD、 内存等异构存储) 读入操作系统的内核缓冲区
步骤二 : 将数据跨内核推到Datanode进程
步骤三 : Datanode会再次跨内核将数据推回内核中的套接字缓冲区
步骤四 : 最后将数据写入网卡缓冲区
Datanode对数据进行了两次多余的数据拷贝操作(步骤二和步骤三) , Datanode只是起到缓存数据并将其传回套接字的作用而以, 别无他用。 这里需要注意的是, 步骤一和步骤四的拷贝发生在外设(例如磁盘和网卡) 和内存之间, 由DMA(Direct MemoryAccess, 直接内存存取) 引擎执行, 而步骤二和步骤三的拷贝则发生在内存中, 由CPU执行。
上述读取方式除了会造成多次数据拷贝操作外, 还会增加内核态与用户态之间的上下文切换。
切换1 : Datanode通过read()系统调用将数据块从磁盘(或者其他异构存储) 读取到内核缓冲区时, 会造成第一次用户态到内核态的上下
切换2 : 在系统调用read()返回时, 会触发内核态到用户态的上下文切换
切换3 Datanode成功读入数据后, 会调用系统调用send()发送数据到socket, 也就是在数据块第三次拷贝时,会再次触发用户态到内核态的上下文切换 。
切换4 当系统调用send()返回时, 内核态又会重新切换回用户态。
三. 零拷贝
使用零拷贝的应用程序可以要求内核直接将数据从磁盘文件拷贝到网卡缓冲区, 而无须通过应用程序周转, 从而大大提高了应用程序的性能.
Java类库定义了**java.nio.channels.FileChannel.transferTo()**方法, 用于在Linux(UNIX)系统上支持零拷贝.
步骤1 : Datanode调用transferTo()方法引发DMA引擎将文件内容拷贝到内核缓冲区
步骤2 : DMA引擎直接把数据从内核缓冲区传输到网卡缓冲区
使用零拷贝模式的数据块读取, 数据拷贝的次数从4次降低到了2次.
transferTo()方法读取文件通道(FileChannel) 中position参数指定位置处开始的count个字节的数据, 然后将这些数据直接写入目标通道target中。 HDFS的SocketOutputStream对象的transferToFully()方法封装了FileChannel.transferTo()方法, 对Datanode提供支持零拷贝的数据读取功能。
/**
* Transfers data from FileChannel using
* {@link FileChannel#transferTo(long, long, WritableByteChannel)}.
* Updates <code>waitForWritableTime</code> and <code>transferToTime</code>
* with the time spent blocked on the network and the time spent transferring
* data from disk to network respectively.
*
* Similar to readFully(), this waits till requested amount of
* data is transfered.
*
* @param fileCh FileChannel to transfer data from.
* @param position position within the channel where the transfer begins
* @param count number of bytes to transfer.
* @param waitForWritableTime nanoseconds spent waiting for the socket
* to become writable
* @param transferTime nanoseconds spent transferring data
*
* @throws EOFException
* If end of input file is reached before requested number of
* bytes are transfered.
*
* @throws SocketTimeoutException
* If this channel blocks transfer longer than timeout for
* this stream.
*
* @throws IOException Includes any exception thrown by
* {@link FileChannel#transferTo(long, long, WritableByteChannel)}.
*/
public void transferToFully(FileChannel fileCh, long position, int count,
LongWritable waitForWritableTime,
LongWritable transferToTime) throws IOException {
long waitTime = 0;
long transferTime = 0;
while (count > 0) {
/*
* Ideally we should wait after transferTo returns 0. But because of
* a bug in JRE on Linux (http://bugs.sun.com/view_bug.do?bug_id=5103988),
* which throws an exception instead of returning 0, we wait for the
* channel to be writable before writing to it. If you ever see
* IOException with message "Resource temporarily unavailable"
* thrown here, please let us know.
*
* Once we move to JAVA SE 7, wait should be moved to correct place.
*/
long start = System.nanoTime();
waitForWritable();
long wait = System.nanoTime();
int nTransfered = (int) fileCh.transferTo(position, count, getChannel());
if (nTransfered == 0) {
//check if end of file is reached.
if (position >= fileCh.size()) {
throw new EOFException("EOF Reached. file size is " + fileCh.size() +
" and " + count + " more bytes left to be " +
"transfered.");
}
//otherwise assume the socket is full.
//waitForWritable(); // see comment above.
} else if (nTransfered < 0) {
throw new IOException("Unexpected return of " + nTransfered +
" from transferTo()");
} else {
position += nTransfered;
count -= nTransfered;
}
long transfer = System.nanoTime();
waitTime += wait - start;
transferTime += transfer - wait;
}
if (waitForWritableTime != null) {
waitForWritableTime.set(waitTime);
}
if (transferToTime != null) {
transferToTime.set(transferTime);
}
}
使用零拷贝模式除了降低数据拷贝的次数外, 上下文切换次数也从4次降低到了2次。
当Datanode调用transferTo()方法时会发生用户态到内核态的切换,transferTo()方法执行完毕返回时内核态又会切换回用户态。
BlockSender使用SoucketoutputStream.transferToFully()封装的零拷贝模式发送数据块到客户端, 由于数据不再经过Datnaode中转, 而是直接在内核中完成了数据的读取与发送,所以大大地提高了读取效率。 由于数据不经过Datanode的内存,所以Datanode失去了在客户端读取数据块过程中对数据校验的能力。 为了解决这个问题,HDFS将数据块读取操作中的数据校验工作放在客户端执行, 客户端完成校验工作后, 会将校验结果发送回Datanode。
在DataXceiver.readBlock()的清理动作中, 数据节点会接收客户端的响应码, 以获取客户端的校验结果。
参考:
Hadoop 2.X HDFS源码剖析 – 徐鹏