Hadoop文件读取和写入剖析
文件读取剖析
客户端通过调用FileSystem对象的open()方法来打开希望读取的文件,对于HDFS来说,这个对象是DistributedFileSystem
的一个实例,DistributedFileSystem
通过使用远程过程调用(RPC)来调用namenode
,以确定文件起始块的位置(步骤2),对于每一个块,namenode
返回存有该块副本的datanode
地址,此外,这些datanode
根据他们与客户端的距离来排序,如果客户端本身就是一个datanode
,那么该客户端将会从保存有相应数据块副本的本地datanode
读取数据
DistributedFileSystem
类返回一个FSDataInputStream
对象(一个支持文件定位的输入流)给客户端以便读取数据,FSDataInputStream
类转而封装DFSInputStream
对象,该对象管理着namenode
和datanode
的I/O
接着,客户端对这个输入流调用read()
方法(步骤3),存储着文件起始几个块的datanode
地址的DFSInputStream
随即连接距离最近的文件中第一个块所在的datanode
。通过对数据流反复调用read()
方法,可以将数据从datanode
传输到客户端(步骤4),到达块的末端时,DFSInputStream
关闭与该datanode
的连接,然后寻找下一个块的最佳datanode
(步骤5)。
客户端从流中读取数据时,块是按照打开DFSInputStream
与datanode
新建连接的顺序读取的,他也会根据需要询问namenode
来检索下一批数据块的datanode
的位置,一旦客户端完成读取,就对FSDataInputStream
调用close()
方法(步骤6)
在读取数据的时候,如果DFSInputStream
在与datanode
通信时遇到错误,会尝试从这个块的最邻近datanode
读取数据,它也记住那个故障datanode
,以保证以后不会反复读取该节点上后续的块。DFSInputStream
也会通过校验和确认从datanode
发来的数据是否完整。如果发现有损坏的块,DFSInputStream
会试图从其他datanode
读取副本,也会将损坏的块通知给namenode
这个设计的一个重点是,客户端可以直接连接到namenode
检索数据,且namenode
告知客户端每个块所在的最佳datanode
。由于数据流分散在集群中的所有datanode
,所以这种设计能使HDFS扩展到大量的并发客户端。同时,namenode
只需要响应块位置的请求(这些信息存储在内存中,因为非常高效),无需响应数据请求,否则随着客户端数量的增长,namenode
会很快成为瓶颈
文件写入剖析
客户端通过对DistributedFileSystem
对象调用create()
方法来新建文件(步骤1),DistributedFileSystem
对namenode
创建一个RPC调用,在文件系统的命名空间中新建一个文件,此时该文件中还没有响应的数据块(步骤2),namenode
执行各种不同的检查以确保这个文件不存在以及客户端有新建该文件的权限。如果这些检查均通过,namenode
就会为创建新文件记录一条记录;否则,文件创建失败并向客户端抛出一个异常IOException
。DistributedFileSystem
向客户端返回一个FSDataOutputStream
对象,由此客户端可以开始写入数据,就像读取事件一样,FSDataOutputStream
封装一个DFSOutputStream
对象,该对象负责处理datanode
和namenode
之间的通信。
在客户端写入数据时(步骤3),DFSOutputStream
将它分成一个个的数据包,并写入内部队列,称为“数据队列”(data queue),DataStreamer处理数据队列,它的责任是挑选出适合存储数据副本的一组datanode
,并据此来要求namenode
分配新的数据块。这一组datanode
构成一个管线-----我们假设副本数为3,所以管线中有三个节点。DataStreamer
将数据包流式传输到管线中的第一个datanode
,该datanode
存储数据包并将它发送到管线中的第二个datanode
,以此类推。
DFSOutputStream
也维护着一个内部数据包队列来等待datanode
的收到确认回执,称为“确认队列”(ack queue),收到管道中所有datanode
确认消息后,该数据包才会从确认队列中删除(步骤5)
如果任何datanode
在数据写入时发生故障,则执行以下操作,首先关闭管线,确认把队列中的所有数据包都添加回数据队列的最前端,以确保故障节点下游的datanode
不会漏掉任何一个包。为存储在另一正常datanode
的当前数据块指定一个新的标识,并将该标识传递给namenode
,以便故障datanode
在恢复后可以删除存储的部分数据块,从管线中删除故障datanode
,基于两个正常datanode
构建一条新管线,余下的数据块写入管线中正常的datanode。namenode
注意到块副本量不足时,会在另一个节点上创建一个新的副本,后续的数据块继续正常接受处理。
在一个块被写入期间可能会有多个datanode
同时发生故障,但非常少见,只要写入了dfs.namenode.replication.min
的副本数,写操作就会成功,并且这个块可以在集群中异步复制,直到达到其目标副本数。
客户端完成数据的写入后,对数据流调用close()
方法,该操作将剩余的所有数据包写入datanode
管线,并在联系到namenode
告知其文件写入完成之前,等待确认(步骤7),namenode
已经知道文件有哪些块组成(因为DataStreamer
请求分配数据块),所以他在返回成功前只需要等待数据块进行最小量的复制
参考《Hadoop权威指南》