2. HDFS数据流
2.1 文件读取流程剖析(重要)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1a48nDo0-1586279469073)(picture\read.jpg)]
public static void getFileFromHDFS() throws IOException, InterruptedException, URISyntaxException {
// 1 获取文件系统
Configuration configuration = new Configuration();
FileSystem fs = FileSystem.get(new URI("hdfs://hadoop001:9000"), configuration, "root");
// 2 获取输入流
FSDataInputStream fis = fs.open(new Path("/in.txt"));
// 3 获取输出流
FileOutputStream fos = new FileOutputStream(new File("in.txt"));
// 4 流的对拷
IOUtils.copyBytes(fis, fos, configuration);
// 5 关闭资源
IOUtils.closeStream(fos);
IOUtils.closeStream(fis);
fs.close();
}
-
客户端实例化一个FileSystem对象(其实就是HDFS的DistributedFileSystem实例,DistributedFileSystem继承于FileSystem),调用DistributedFileSystem.open()方法通过RPC向NameNode请求下载文件,NameNode通过查询元数据,找到文件开头部分块(第一个块)所有副本所在的DataNode地址并传给客户端,并且每个块的副本都基于距离远近排序。
@Override public FSDataInputStream open(Path f, final int bufferSize) throws IOException { statistics.incrementReadOps(1); Path absF = fixRelativePart(f); return new FileSystemLinkResolver<FSDataInputStream>() { @Override public FSDataInputStream doCall(final Path p) throws IOException, UnresolvedLinkException { final DFSInputStream dfsis = dfs.open(getPathName(p), bufferSize, verifyChecksum); return dfs.createWrappedInputStream(dfsis); } @Override public FSDataInputStream next(final FileSystem fs, final Path p) throws IOException { return fs.open(p, bufferSize); } }.resolve(this, absF); }
-
open()方法返回的是一个FSDataInputStream对象(支持文件定位的输入流)给客户端读取数据。这个FSDataInputStream其实是一个被包装的DFSInputStream对象。
-
客户端调用这个输入流的read()方法。文件开头部分的块的数据节点地址的DFSInputStream随即与这些块最近的数据节点相连接,数据从数据节点返回客户端(以Packet为单位来做校验,先在本地缓存,默认buffer为4096位,然后写入目标文件)。当到达块的末端时,DFSInputStream会关闭与数据节点之间的联系,然后为下一个块找到最佳的数据节点。客户端只需要读取一个连续的流,这些对于客户端都是透明的。
/** * Copies from one stream to another. * * @param in InputStrem to read from * @param out OutputStream to write to * @param buffSize the size of the buffer */ public static void copyBytes(InputStream in, OutputStream out, int buffSize) throws IOException { PrintStream ps = out instanceof PrintStream ? (PrintStream)out : null; byte buf[] = new byte[buffSize]; int bytesRead = in.read(buf); while (bytesRead >= 0) { out.write(buf, 0, bytesRead); if ((ps != null) && ps.checkError()) { throw new IOException("Unable to write to output stream."); } bytesRead = in.read(buf); } }
-
客户端从流中读取数据时,块是按DFSInputStream打开与数据节点的新连接的顺序读取的。它也会调用NameNode检索下一组需要的块的位置。一旦完成读取,就会对FSDataInputStream调用close()。
2.2 文件写入流程剖析(重要)
public void putFileToHDFS() throws IOException, InterruptedException, URISyntaxException {
// 1 获取文件系统
Configuration configuration = new Configuration();
FileSystem fs = FileSystem.get(new URI("hdfs://hadoop001:9000"), configuration, "root");
// 2 创建输入流
FileInputStream fis = new FileInputStream(new File("in.txt"));
// 3 获取输出流
FSDataOutputStream fos = fs.create(new Path("/in.txt"));
// 4 流对拷
IOUtils.copyBytes(fis, fos, configuration);
// 5 关闭资源
IOUtils.closeStream(fos);
IOUtils.closeStream(fis);
fs.close();
}
-
客户端通过DistributedFileSystem对象调用create()向NameNode请求上传文件,NameNode检查目标文件是否已存在,父目录是否存在,以及客户端是否有权限创建文件,如果检查通过NameNode就会生成一个新的文件记录**“file.copying”,并返回一个文件系统输出流**,否则会向客户端返回一个IOException异常。
-
客户端将所要上传的文件转换为输出流FSDataOutputStream;
-
当客户端开始写入文件的时候,客户端会将文件切分成多个 packets( 默认64kB ),并在内部以数据队列“data queue(数据队列)”的形式管理这些 packets,并向 namenode 申请 blocks,获取用来存储 replicas 的合适的 datanode 列表,列表的大小根据 namenode 中 replication 的设定而定,并按距离远近排序;
-
开始以 pipeline(管道)的形式将 packet 写入所有的 replicas 中。客户端把 packet 以流的方式写入第一个 datanode,该 datanode 把该 packet 存储之后,再将其传递给在此 pipeline 中的下一个 datanode,直到最后一个 datanode,这种写数据的方式呈流水线的形式;
客户端会根据返回的三个节点和第一个节点建立一个socket连接(只会和第一个节点建立),第一个节点又会和第二个节点建立socket连接,由第二个节点又会和第三个节点建立一个socket连接,这种连接的方式叫Pipeline
-
DataNode完成接收block块后,block的metadata(MD5校验码)通过一个心跳将信息汇报给NameNode
-
最后一个 datanode 成功存储之后会返回一个ack packet(确认包),在 pipeline 里传递至客户端,在客户端的开发库内部维护着**“ack queue”**,成功收到 datanode 返回的ack packet 后会从"data queue"移除相应的 packet;
-
如果传输过程中,有某个 datanode 出现了故障,那么当前的 pipeline 会被关闭,出现故障的 datanode 会从当前的 pipeline 中移除,剩余的 block 会继续剩下的 datanode 中继续 以 pipeline 的形式传输,同时 namenode通过心跳检测机制会发现副本数不足,并分配一个新的datanode,保持replicas设定的数量;
-
客户端完成数据的写入后,会对数据流调用 close()方法,关闭数据流。并且NameNode若收到所有DataNode的汇报,NameNode会将元数据后的.copying去掉成为正式文件。
2.3 副本节点的选择——机架感知
第一个副本选择在Client所在节点上,或者随机选一个;
第二个副本选择在第一个副本同机架的任一节点;
第三个副本选择在同集群另一个机架中的任意节点。