这里参考了
客户端通过调用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() {
@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对数据的安全性做了很多工作,只要不是集群毁坏的严重问题,基本可以保证数据的安全性.