为了了解客户端和HDFS以及NameNode和DataNode之间的数据流是什么样,我们先来分析一下,HDFS的读过程是什么样的。了解Hadoop的童鞋对下面的图一定不陌生
这是一张反映Client对HDFS上的数据进行读取的图,从图中我们可以看到,HDFS的读过程大概可以分为以下几个步骤:
1.客户端通过调用FileSystem确切的说是它的子类DistributedFileSystem对象的open方法来打开想要读取的文件
2.DistributedFileSystem通过RPC的方式调用NameNode来获取文件的数据块存放信息,从中确定文件起始块的位置。同时DistributedFileSystem将返回一个FSDataInputStream对象给到客户端。
3.客户端拿到FSDataInputStream然后调用read方法
4.DFSInputStream连接保存此文件第一个数据块的最近的数据节点对象并调用read方法将数据从DataNode读到客户端
5.当DFSInputStream读完一个DataNode上的数据时,它将自动去寻找下一个DataNode进行数据读取
6.当客户端读取完数据后,将会调用FSDataInputStream的close方法来关闭数据流
在数据的读取过程中,如果DFSInputStream在DataNode通信时遇到错误,它便会尝试从这个块的另一个最邻近的DataNode中读取数据,同时这个失败的节点也将会被记录以后不会再连接,DFSInputStream也会对从DataNode读出的数据进行校验以确认数据的完整性。
下面我们再从源码的角度了解一下这整个过程:
由于源码当中逻辑比较复杂,笔者也没有完全搞清楚搞明白,所以这里只是将HDFS的读过程,从源码的角度查看一下,串成一个逻辑线条,这里为了使线条清晰一些,将部分代码做省略
我们这里分为两个部分:一是文件的打开,而是文件的读取
一:文件的打开
客户端:
HDFS打开一个文件,需要在客户端通过DistributedFileSystem的对象调用open方法,最终获取到一个FSDataInputStream对象用于文件的读取
DistributedFileSystem这个类在org.apache.hadoop.hdfs包下面,下面是相关代码片段:
public FSDataInputStream open(Path f, int bufferSize) throws IOException {
statistics.incrementReadOps(1);
return new DFSClient.DFSDataInputStream(
dfs.open(getPathName(f), bufferSize, verifyChecksum, statistics));
}
从这个方法中可以看到它的返回值实际上是一个DFSDataInputStream对象它是FSDataInputStream子类,构造DFSDataInputStream对象需要一个DFSInputStream对象作为参数,所以这里又调用了dfs.open方法它将返回一个DFSInputStream对象,这里的dfs是DistributedFileSystem的成员变量,其类型是DFSClient。
这里的DFSInputStream类其实是DFSClient类的一个内部类,这里我们梳理一下这三个输入流:FSDataInputStream,DFSDataInputStream,DFSInputStream它们之间的关系
DFSDataInputStream是FSDataInputStream的子类,DFSDataInputStream封装了DFSInputStream,至于这里面的继承关系这里就不多说了。所以事实上读取文件调用的是DFSInputStream对象的read方法。
DFSClient类在 org.apache.hadoop.hdfs包中,以下是相关的代码片段:
public DFSInputStream open(String src, int buffersize, boolean verifyChecksum,FileSystem.Statistics stats) throws IOException {
return new DFSInputStream(src, buffersize, verifyChecksum);
}
在这个open方法的DFSInputStream的构造函数中调用了另一个方法openInfo这个方法调用了一个比较重要的方法fetchLocatedBlocks通过这个方法可以从NameNode中获取要打开文件的blocks信息,相关代码如下:
这里需要说明的是fetchLocatedBlocks方法属于DFSClient内部类DFSInputStream的,而其又调用的callGetBlockLocations则是DFSClient中的方法
private boolean fetchLocatedBlocks() throws IOException, FileNotFoundException {
.....
LocatedBlocks newInfo = callGetBlockLocations(namenode, src, 0,prefetchSize);
return isBlkInfoUpdated;
}
static LocatedBlocks callGetBlockLocations(ClientProtocol namenode,
......
String src, long start, long length) throws IOException {
return namenode.getBlockLocations(src, start, length);
}
在callGetBlockLocations方法中通过RPC方式调用到NameNode的getBlockLocations方法,从而获取到要打开文件的blocks信息。
NameNode:
DFSClient通过RPC方式调用到NameNode的getBlockLocations,在NameNode的相关代码,如下:
NameNode类在 org.apache.hadoop.hdfs.server.namenode包中
public LocatedBlocks getBlockLocations(String src,long offset,long length) throws IOException {
myMetrics.incrNumGetBlockLocations();
return namesystem.getBlockLocations(getClientMachine(),src, offset, length);
}
这里的namesystem是NameNode的成员变量,其类型为FSNamesystem。它保存的是NameNode的namespace,其中有一个重要的成员变量dir类型是FSDirectory。在这个类中通过一层层的调用最终到了getBlockLocationsInternal方法,在这个方法里有一个重要变量inode,它是INodeFile类型的通过dir.getFileINode(src)获取,相关代码如下:
private synchronized LocatedBlocks getBlockLocationsInternal(String src,long offset, long length, int nrBlocksToReturn,
boolean doAccessTime, boolean needBlockToken)throws IOException {
......
INodeFile inode = dir.getFileINode(src);
......
return inode.createLocatedBlocks(results);
}
自此获取到了要打开文件块的blocks信息,它封装在DFSInputStream类的成员变量locatedBlocks,最终
DFSDataInputStream又封装了DFSInputStream对象以FSDataInputStream对象的形式返回到客户端。
二:文件的读过程
客户端:
客户端获取到FSDataInputStream对象后调用它的read方法进行文件的读取,通过前面文件打开过程的分析可以知道,它实际调用的是DFSClient内部类DFSInputStream
public int read(long position, byte[] buffer, int offset, int length)
throws IOException {
......
List<LocatedBlock> blockRange = getBlockRange(position, realLen);
......
fetchBlockByteRange(blk, targetStart,
targetStart + bytesToRead - 1, buffer, offset);
......
return realLen;
}
private void fetchBlockByteRange(LocatedBlock block, long start,
long end, byte[] buf, int offset) throws IOException {
......
block = getBlockAt(block.getStartOffset(), false);
// 选择DataNode
DNAddrPair retval = chooseDataNode(block);
DatanodeInfo chosenNode = retval.info;
InetSocketAddress targetAddr = retval.addr;
BlockReader reader = null;
.....
// 建立Scoket连接
dn = socketFactory.createSocket();
// 连接DataNode
NetUtils.connect(dn, targetAddr, getRandomLocalInterfaceAddr(),
socketTimeout);
dn.setSoTimeout(socketTimeout);
// 利用Socket连接创建reader用来从DataNode中读取数据
reader = RemoteBlockReader.newBlockReader(dn, src,
block.getBlock().getBlockId(), accessToken,
block.getBlock().getGenerationStamp(), start, len, buffersize,
verifyChecksum, clientName);
// 读取数据
int nread = reader.readAll(buf, offset, len);
......
// 标记读取失败的节点
addToDeadNodes(chosenNode);
}
通过以上代码可以看出,DFSClient在从DataNode中读取数据时,采用的是Socket方式,所以自然也就容易想到,在DataNode上肯定是维护了一个ServerSocket,用来监控客户端的连接。
DataNode:
通过上面的分析,在DataNode上应该存在一个能够监听客户端连接的ServerSocket,其实在DataNode在构建时即启动时就会调用一个startDataNode的方法,在这个方法中,相关的代码如下:
void startDataNode(Configuration conf,
AbstractList<File> dataDirs, SecureResources resources
) throws IOException {
......
ServerSocket ss;
if(secureResources == null) {
ss = (socketWriteTimeout > 0) ?
ServerSocketChannel.open().socket() : new ServerSocket();
Server.bind(ss, socAddr, 0);
} else {
ss = resources.getStreamingSocket();
}
ss.setReceiveBufferSize(DEFAULT_DATA_SOCKET_SIZE);
// adjust machine name with the actual port
tmpPort = ss.getLocalPort();
selfAddr = new InetSocketAddress(ss.getInetAddress().getHostAddress(),
tmpPort);
this.dnRegistration.setName(machineName + ":" + tmpPort);
LOG.info("Opened data transfer server at " + tmpPort);
this.threadGroup = new ThreadGroup("dataXceiverServer");
this.dataXceiverServer = new Daemon(threadGroup,
new DataXceiverServer(ss, conf, this));
this.threadGroup.setDaemon(true); // auto destroy when empty
......
}
这里面在建立ServerSocket时生成了DataXceiverServer对象,它是一个实现了Runnable接口的线程类,在其run方法中又建立一个DataXceiver对象,它同样是一个实现了Runnable接口的线程类,在它的run方法中可以接收客户端所发送的指令,然后按照指令进行相应的操作,相关的代码如下:
我们只看一下DataXceiver的run方法
public void run() {
DataInputStream in=null;
// 建立输入流对象
in = new DataInputStream(
new BufferedInputStream(NetUtils.getInputStream(s), SMALL_BUFFER_SIZE));
// 获取操作指令
byte op = in.readByte();
// 进行对应的操作
switch ( op ) {
case DataTransferProtocol.OP_READ_BLOCK:
readBlock( in );
break;
case DataTransferProtocol.OP_WRITE_BLOCK:
writeBlock( in );
break;
.......
}
}
自此HDFS文件读取源码过程基本结束。
注:Hadoop中源代码逻辑还是比较复杂的,笔者也是糊里糊涂的只是看个大概,当中有描述不清的地方希望看到的童鞋,再去查阅其他资料求证,以免造成误导