HDFS读过程分析

为了了解客户端和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中源代码逻辑还是比较复杂的,笔者也是糊里糊涂的只是看个大概,当中有描述不清的地方希望看到的童鞋,再去查阅其他资料求证,以免造成误导


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值