写流程解析
public static void main(String[] args) throws Exception {
//加载本地指定目录下的文件
InputStream is = new BufferedInputStream(new FileInputStream("D:/123/word.txt"));
//获取配置文件
Configuration conf = new Configuration();
//获取分布式文件系统
FileSystem fs = FileSystem.get(conf);
//指定HDFS文件系统上的目标路径
Path des = new Path("/123/word.txt");
//获取输出流对象
FSDataOutputStream fsdos = fs.create(des);
//开始上传
IOUtils.copyBytes(is, fsdos, 4096, false);
//关闭流对象
IOUtils.closeStream(fsdos);
}
- 获取分布式文件系统对象
在客户端要使用Configuration类来加载配置文件信息,然后再调用FileSystem的get()方法,获取一个分布式文件系统对象DistributedFileSystem。
- 客户端请求Namenode
客户端调用DistributedFileSystem的create()方法,向Namenode发送请求新建指定路径下的文件。客户端采用远程调用协议(RPC)与Namenode进行通信,这个时候Namenode要经过各种不同的检查,如命名空间里该路径文件是否已存在,客户端是否有相应权限。如果没有通过检查,返回IOException。如果检查通过,Namenode就会在命名空间下新建该文件,记录元数据(注意,此时新文件为0字节,还没有数据块的信息
),并返回一个FSDataOutputStream输出流对象(dfsClient.namenode.create()在命名空间中创建一个新文件(HDFS文件),再调用DFSOutputStream的构造方法创建一个输出流,并开启DFSOutputStream的streamr线程。
)FSDataOutputStream封装了一个DFSDataOutputStream对象,由该对象负责处理datanode和namenode之间的通信。(DistributedFileSystem.create()会调用DFSClient.create()方法创建DFSOutputStream输出流并构造一个HdfsDataOutputStream来包装DFSOutputStream
)
- 客户端加载数据
客户端调用DFSOutputStream的create方法,开始执行写入操作。当写入字节流数据达到一个数据包的长度时,DFSOutputStream就会构造一个Packet对象保存这个数据包(如果当前数据块中的数据包都发送完毕,DFSOutputStream就会发送一个空的数据包,用来标识数据块发送完毕
),新构造的Packet会放到DFSOutputStream.dataQueue(数据队列)中,由DataStreamer(DFSOutputStream的内部线程类)处理,向Namenode请求一组合适的Datanode列表,这一组Datanode构成一个管线。(首先调用私有的构造方法初始化一些属性,并对shouldSyncBlock属性赋值(是否强制关闭块到磁盘。)然后调用computePacketChunkSize()方法设置数据包大小,同时确定一个数据包中包含校验块的个数,然后创建里一个DataStreamer对象(负责建立流管道并发送数据包到数据流管道的第一个Datanode,DFSOutputStream的内部类线程)最后设置了favoredNodes字段,确认客户端想要写入数据块的Datanode
)
使用Packet类封装数据包,每个数据包中都有若干个校验块及对应的校验和
完整的数据包的数据分成三部分:
1.数据包包头:记录了数据包的概要属性信息
2.校验和数据
3.校验块数据
(DFSOutputStream.write()方法可以将指定大小的数据写入数据流内部的一个缓冲区中,写入的数据会被切分成多个数据包,每个数据包由一组校验块和对应的校验和组成,默认的数据包大小为65536字节(即64k,实际大小不到64k),校验块的大小为512字节,每个校验和都是校验块512字节数据对应的校验值,数据包、校验块的大小在computePacketChunkSize()中定义。)
- 向Datanode中写数据
当Datanode管线确认好之后,DataStreamer开始从dataqueue中依次取出Packet数据包,向管线中的datanode写入。第一个Packet写入管线中的第一个Datanode的内存中,然后第一个Datanode再将已经写好的Packet,发送给管线中的第二个Datanode,第二个Datanode将Packet写入内存后,再将Packet发送给第三个Datanode,然后写到内存中。(DataStreamer在将packet发送到Datanode之前,首先在Namenode的命名空间中分配一个空的数据块(block),建立数据块的数据流管道。当数据流管道将当前数据块写满后,会发送一个空的packet标识,然后DataStreamer会申请新的数据块
)
DataStreamer在将Packet写入管线中时,同时也会将该Packet存储到另外一个由ResponseProcessor线程管理的缓存队列ackqueue中,这个队列我们称之为“确认队列”。ResponseProcessor线程会等待Datanode列表写好的确认信息。当收到所有的Datanode的确认信息后,该线程再将ackqueue里的packet删除。若写入期间发生故障,则执行以下操作:首先关闭管线,把确认队列的所有packet都放回dataqueue的最前端,以确保故障节点后的节点不会漏掉任一packet。同时,会标识正常的Datanode,方便在故障节点恢复后,删除存储的部分数据块。然后从管线中删除故障节点,基于两个Datanode构建一个新的管线
- 调用close方法
当写入一个块大小的n个packet数据包后,客户端调用FSDataOutputStream的close()方法。在调用close方法前,Datanode列表将内存中的数据写入本地磁盘。然后,DataStreamer继续向Namenode请求下一个块的Datanode列表,开始下一个块的写入。直到写完整个文件的最后一个块数据,然后客户端通知Namenode,整个文件已经写完。