在这一章里,我们来具体了解一下客户端与HDFS、Namenode和Datanode之间的数据流到底是什么样子的。我们分别从两个方面来了解数据流,一个是客户端从HDFS中读数据,一个是客户端向HDFS中写数据。
一、读流程解析
我们来结合一下代码和流程图来解析一下HDFS的读流程。
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();// 读取配置文件 core-default 和 core-site.xml
FileSystem fs = FileSystem.get(conf);// 获取DistributedFileSystem
// System.out.println(fs.getClass().getName());//确认类型
Path path = new Path("/hyxy/LICENSE.txt");// 创建Path对象,指定要读取的文件
FSDataInputStream fsinput = fs.open(path);// 向Namenode发送读取请求
IOUtils.copyBytes(fsinput, System.out, 4096, false);// 将输入流内内的数据复制到System.out输出流中
IOUtils.closeStream(fsinput);// 关闭流
}

1、获取分布式文件系统对象
在客户端中,我们在读取文件之前要做的工作,必然是通过FileSystem的get()方法,来加载相关配置文件,分析相关参数的值,获取DistributedFileSystem对象。DistributedFileSystem是FileSystem的子类,在加载配置文件时,会读取本地jar包内置的core-default.xml默认配置文件以及本地的core-site.xml,而本地的core-site.xml的fs.defaultFS的值为file:///,所以,还要将你在集群中etc/hadoop/core-site.xml复制一份到你的eclipse项目下的src目录里。这样get方法加载的就是你集群的配置了。
2、调用文件系统对象的open方法
客户端通过调用DistributedFileSystem的open方法(图中步骤1),向HDFS的Namenode发送请求,希望读取指定路径下的文件。这个时候,客户端的DistributedFileSystem是通过远程调用协议(rpc)来请求Namenode的,Namenode从内存中确定文件的第一个块,以及块副本的位置信息,也就是Datanode节点列表,发送回给客户端(图中步骤2)。并返回一个输入流对象FSDataInputStream,这个FSDataInputStream是javaIO流中的DataInputStream的子类。
3、调用read方法,读取数据块
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); //while循环中重复执行read方法
}
}
客户端获取输入流对象FSDataInputStream后,会调用该对象的read方法来读取数据块(图中步骤3)。FSDataInputStream是一个支持文件定位的输入流,内部封装了DFSInputStream对象,该对象管理着DataNode和Namenode的I/O操作。DFSInputStream会选择离客户端最近的存有块副本的Datanode节点上读取数据。通过上图copyBytes方法的源码,我们可以看到,会重复调用read()方法来数据块的数据(图中步骤4),当数据块被读完后,DFSInputStream关闭与该Datanode的连接。
4、请求读取下一个块数据
DFSInputStream读取完,并关闭与存有第一个数据块的Datanode的连接后,DFSInputStream就会继续从Namenode发送回来的下一个块的Datanode列表信息中寻找最佳的Datanode节点,继续读取(图中步骤五)。
5、调用close方法,结束读流程
当文件的最后一个数据块,被DFSInputStream读取完毕后,客户端就会调用FSDataInputStream的close方法,结束读流程任务。
6、读流程的错误处理
在读取数据过程中,如果DFSInputStream与Datanode的通信出现错误后,会尝试从离该块所在Datanode的最近的存储该块副本的Datanode上继续读取数据。同时记录这个故障Datanode,用于保证以后不会再从该节点上读取其他块信息。
另一个种情况,DFSInputStream也会校验和确认从Datanode上发来的数据是否完整(校验和)。如果发现有损坏,DFSInputStream会尝试从其他Datanode上读取该块的副本。当然也会将损坏的块通知给Namenode,由Namenode进行副本数恢复。
二、写流程解析
下面我们在从写操作API和流程图,来详细解析以下HDFS的写流程。
public static void main(String[] args) throws Exception {
//加载本地指定目录下的文件
InputStream is = new BufferedInputStream(
new FileInputStream("D:/source/App/ApacheSoft/hadoop-2.6.1.tar.gz"));
Configuration conf = new Configuration();
//获取分布式文件系统
FileSystem fs = FileSystem.get(conf);
//指定要上传到HDFS文件系统上的目录和文件名
Path des = new Path("/apps/apache/hadoop.tar.gz");
//获取输出流对象
FSDataOutputStream fsdos = fs.create(des);
//开始上传
IOUtils.copyBytes(is, fsdos, 4096, false);
//关闭流对象
IOUtils.closeStream(fsdos);
}

从代码中,我们可以看到,我们的目的是将D盘上的ApacheSoft目录下的Hadoop-2.6.1.tar.gz压缩包文件复制到HDFS文件系统的/apps/apache目录下,文件名为hadoop.tar.gz。我们来详细解析一下步骤:
1、获取分布式文件系统对象
和读取流程一样,在客户端要使用Configuration类来加载配置文件信息,然后再调用FileSystem的get()方法,获取一个分布式文件系统对象DistributedFileSystem(看代码片段)。
2、客户端请求Namenode
客户端调用DistributedFileSystem的create(Path p)方法,向Namenode发送请求新建指定路径下的文件(步骤1)。客户端采用远程调用协议(RPC)与Namenode进行通信,这个时候Namenode要经过各种不同的检查,如命名空间里该路径文件是否已存在,客户端是否有相应权限。如果没有通过检查,返回IOException。如果检查通过,Namenode就会在命名空间下新建该文件,记录元数据(步骤2,注意,此时新文件为0字节,还没有数据块的信息),并返回一个FSDataOutputStream输出流对象。FSDataOutputStream封装了一个DFSDataOutputStream对象,由该对象负责处理datanode和namenode之间的通信。
这个时候,无需考虑父目录是否存在,create方法会帮助创建多级目录。
3、客户端加载数据
客户端调用DFSOutputStream的create方法,开始执行写入操作。DFSOutputStream先将数据封装成一个个的数据包Packet,依次写入一个缓存队列中,这个缓存队列,我们称之为dataqueue。然后由线程DataStreamer,向Namenode请求一组合适的Datanode列表,这一组Datanode构成一个管线(Pipeline of datanodes)。
4、向Datanode中写数据
当Datanode管线确认好之后,DataStreamer开始从dataqueue中依次取出Packet数据包,向管线中的datanode写入。第一个Packet写入管线中的第一个Datanode的内存中,然后第一个Datanode再将已经写好的Packet,发送给管线中的第二个Datanode,第二个Datanode将Packet写入内存后,再将Packet发送给第三个Datanode,然后写到内存中(图中步骤4)。
DataStreamer在将Packet写入管线中时,同时也会将该Packet存储到另外一个由ResponseProcessor线程管理的缓存队列ackqueue中,这个队列我们称之为“确认队列”。ResponseProcessor线程会等待Datanode列表写好的确认信息。当收到所有的Datanode的确认信息后(图中步骤5),该线程再将ackqueue里的packet删除。
5、调用close方法
当写入一个块大小的n个packet数据包后,客户端调用FSDataOutputStream的close()方法(图中步骤6)。在调用close方法前,Datanode列表将内存中的数据写入本地磁盘。然后,DataStreamer继续向Namenode请求下一个块的Datanode列表,开始下一个块的写入。直到写完整个文件的最后一个块数据,然后客户端通知Namenode,整个文件已经写完(图中步骤7)。
-----------------------------如有疑问,敬请留言------------------------
本文深入解析HDFS的读写流程。读流程包括获取分布式文件系统对象、打开文件、读取数据块、请求下一个块及错误处理。写流程涵盖创建文件、请求Namenode、加载数据、向Datanode写数据及关闭操作。详细阐述了客户端与HDFS、Namenode和Datanode之间的交互过程。
HDFS读写流程的详解&spm=1001.2101.3001.5002&articleId=86499972&d=1&t=3&u=53e1f1e264ee48e4aed54e7fbdd075ec)
1081

被折叠的 条评论
为什么被折叠?



