HDFS读取文件的流程

这里参考了<Hadoop 2.X HDFS源码剖析>(徐鹏),<大数据处理系统:Hadoop源代码情景分析>(毛德操).主要是在<权威指南>的基础上扩展一些.

客户端通过调用FileSystem对象的open()来打开希望读取的文件,对HDFS来说,这个对象是DistributedFileSystem.

接下来我们来看看这个FileSystem.open()里面发生了什么:

 public abstract FSDataInputStream open(Path f, int bufferSize)
    throws IOException;

这是个抽象方法,我们可以来看看它的具体实现,我们进入DistributedFileSystem.open():

  public FSDataInputStream open(Path f, final int bufferSize)
      throws IOException {
    return new FileSystemLinkResolver<FSDataInputStream>() {
      @Override
      public FSDataInputStream doCall(final Path p)
        ......
      }
      @Override
      public FSDataInputStream next(final FileSystem fs, final Path p)
        ......
      }
    }.resolve(this, absF);
  }

返回的是FSDataInputStream对象,其中的FileSystemLinkResolver是一个抽象类,这里使用了匿名内部类,需要实现doCall()与next().这两个方法不去管它,来看看最后的这个resolve(),只摘取了比较关键的代码.

FileSystemLinkResolver.resolve() :
    Path p = path;
    FileSystem fs = filesys;
    for (boolean isLink = true; isLink;) {
        //注意这个for循环,用的是boolean变量.
        //isLink为true就继续循环,为false就跳出循环.
        try {
            in = doCall(p);
                ==>doCall(){
                         dfs.open(getPathName(p), bufferSize, verifyChecksum);
                   }
            isLink = false;
        } catch (UnresolvedLinkException e) {
             p = FSLinkResolver.qualifySymlinkTarget(fs.getUri(), p,
                filesys.resolveLink(p));
            fs = FileSystem.getFSofPath(p, filesys.getConf());
            if (!fs.equals(filesys)) {
              return next(fs, p);
            }
            return in;
        }
    }
}
    

doCall()代表对文件系统中符号链接的解析,返回一个FSDataInputStream对象,它随后会封装一个DFSInputStream对象,这是实际用来读取文件的对象,如果成功返回FSDataInputStream对象,就成功完成操作,退出这个for循环.而doCall()内部有DFSClient.open(),这是实际打开文件的操作.

若没有成功,就代表符号链接解析失败,这里暂且跳过,先来看看DFSClient.open():

public DFSInputStream open(String src, int buffersize, boolean verifyChecksum)
      throws IOException, UnresolvedLinkException {
    checkOpen();//检查文件是否正在运行
    TraceScope scope = getPathTraceScope("newDFSInputStream", src);
    try {
      return new DFSInputStream(this, src, verifyChecksum);
    } finally {
      scope.close();
    }
  }

返回了一个DFSInputStream对象,下面来看看new DFSInputStream的时候发生了什么: 

 DFSInputStream(DFSClient dfsClient, String src, boolean verifyChecksum
                 ) throws IOException, UnresolvedLinkException {
    this.dfsClient = dfsClient;
    this.verifyChecksum = verifyChecksum;
    this.src = src;
    synchronized (infoLock) {
      this.cachingStrategy = dfsClient.getDefaultReadCachingStrategy();
    }
    openInfo();
  }

上面是一系列初始化操作,到最后有一个openInfo(),这个方法里面从fetchLocatedBlocksAndGetLastBlockLength()开始层层调用,最后一步FSNamesystem.getBlockLocations(),最终目的是:向namenode获取文件各数据块的存储地点和文件长度,也就是返回一个LocatedBlocks对象,LocatedBlocks代表多个LocatedBlock对象,也就是我们要读取的块的信息.

这也就代表了<权威指南>上的第2步:

DistributedFileSystem通过远程调用(RPC)来调用namenode,以确定文件起始块的位置.对于每一个块,namenode返回存有该块副本的datanode地址.此外,这些datanode根据它们与客户端的距离来排序.如果该客户端本身就是一个datanode,那么该客户端将会从保存有相应的数据块复本的本地datanode读取数据.

RPC调用namenode,是通过NameNodeRpcServer类实现的.

下一步就是开始读取数据:

DistributedFileSystem返回一个FSDataInputStream(一个支持文件定位的输入流)对象给客户端以便读取数据,该对象转而封装DFSInputStream对象,该对象管理着namenode与datanode的IO.

接着,客户端对这个输入流调用read(),存储着文件起始几个块的datanode地址的DFSInputStream随即连接距离最近的文件中的第一个块所在的datanode.通过对数据流反复调用read(),可以将数据从datanode传输到客户端.

下面就来看看DFSInputStream.read()的实现:

public synchronized int read(final byte buf[], int off, int len) throws IOException {
    ReaderStrategy byteArrayReader = new ByteArrayStrategy(buf);
    TraceScope scope =
        dfsClient.getPathTraceScope("DFSInputStream#byteArrayRead", src);
    try {
      return readWithStrategy(byteArrayReader, off, len);
    } finally {
      scope.close();
    }
  }

可以看到读取数据的是readWithStrategy(),其中有一个readBuffer(),readBuffer()内有一个reader.doRead(),最终会调用一个private内部类ByteBufferStrategy中的doRead(),而ByteBufferStrategy.doRead()中有一个blockReader.read(),指向的是接口ByteBufferReadable.read(),这里要走的是RemoteBlockReader2.read(),其内部有一个readNextPacket(),这是最终的读取方法:以packet为单位.

到达块末端时,DFSInoutStream关闭与该datanode的连接,然后寻找下一个块的最佳datanode...客户端从流中读取数据时,块是按照打开DFSInputStream与datanode新建连接的顺序读取的.它也会根据需要向namenode询问来检索下一数据块的datanode的位置.一旦客户端读取完成,就对FSDataInputStream调用close().

到此一个文件的读取也就结束了,但是前面说过,Hadoop集群的故障率还是很高的,那么假如在读取的过程中一个datanode发生了故障,Hadoop又该如何处理?

如果DFSInputStream与datanode通信时遇到错误,它会尝试从这个块的另一个最近的datanode读取数据.它也会记住那个故障datanode,以保证以后不会反复读取该节点上后续的块.DFSInputStream也会通过校验和来确认datanode发来的数据是否完整.如果发现有损坏的块,DFSInputStream会试图从其他datanode读取复本,也会将损坏的块通知给namenode.

可以看出,Hadoop对数据的安全性做了很多工作,只要不是集群毁坏的严重问题,基本可以保证数据的安全性.

猜你喜欢

转载自blog.csdn.net/bujiujie8/article/details/86562171
今日推荐