HDFS的读写流程
HDFS的读取过程
HDFS的读取流程大致有以下几个步骤:
(1)客户端通过调用FileSystem对象的open()来打开希望读取的文件。对于HDFS平台来说,这个对象是DistributedFileSystem类的是一个实例,所以实际调用的是DistributedFileSystem对象的open方法。(DistributedFileSystem类是FileSystem类的一个子类)
public class DistributedFileSystem extends FileSystem{
…………
}
(2)DistributedFileSystem实例通过调用RPC接口ClientProtocol.getBlockLocations()方法向名称节点NameNode获取该HDFS文件起始块的位置。对于每一个Block,NameNode返回存有该Block副本的DataNode地址,同一Block按照副本数会返回多个位置,这些位置按照Hadoop集群拓扑结构排序,距离客户端近的排在前面。
其中DistributedFileSystem的open方法会返回一个FSDataInputStream对象给Client以便读取数据块,FSDataInputStream是一个DFSInputStream的装饰类,真正进行数据块读取操作的是DFSInputStream对象。
FSDataInputStream对象是一个支持文件定位的输入流。DFSInputStream对象管理着DataNode和NameNode的I/O。
(3)然后客户端通过DFSInputStream对象反复调用read()方法从最优的(与客户端节点距离最近)DataNode节点上读取数据块,数据块会以数据包(packet)为单位从数据节点通过流式接口传递到客户端,当一个数据块读取完毕时,其会再次调用ClientProtocol.getBlockLocations()获取文件的下一个数据块位置信息,并建立和这个新的数据块的最优DataNode之间的连接,然后HDFS客户端就会继续读取该数据块了。如果该Client本身就是一个存有该块副本的DataNode,便从本地DataNode中读取。
(4)一旦客户端完成读取,就对FSDataInputStream调用close()方法关闭文件读取的输入流。
在读取数据的时候,如果DFSInputStream在于DataNode通信的时候遇到错误,会尝试从这个块的另外一个最邻近DataNode读取数据,同时它也会记住那个故障DataNode,以保证以后不会反复读取该节点上后续的块。DFSInputStream也会通过校验和确认从DataNode发来的数据是否完整。如果发现有损坏的块,DFSInputStream会试图从其他的DFSInputStream读取其副本,也会将损坏的块通知给NameNode。
HDFS的写流程
HDFS的数据写流程大致有以下几个步骤:
(1)客户端通过对DistributedFileSystem对象调用create()方法来新建文件。
(2)DistributedFileSystem对NameNode创建一个RPC调用,在文件系统的命名空间创建一个新的文件,此时该文件还没有与之关联的数据块。Namenode执行各种不同的检查以确保这个文件不存在以及客户端有新建该文件的权限。如果这些检查全部通过,Namenode就会生成一个新的文件记录;否则,文件创建失败并向client抛出一个IOException异常。
DistributedFileSystem向客户端返回一个FSDataOutputStream对象,由此客户端可以开始写入数据。就像读取事件一样,FSDataOutputStream封装了一个DFSOutputStream对象,该对象负责处理与DataNode和Namenode之间的通信。
(3)在Client写入数据时,DFSOutputStream将它分成一个个的数据包,写入内部的队列,称为数据队列。其中DataStreamer处理数据队列,它的责任是挑选出适合存储数据副本的一组DataNode,并据此要求Namenode来分配新的数据块。这一组DataNode构成一个传输管道-----我们假设副本数为3,那么此时传输管道中有三个节点。
DataStreamer将数据包流式传输到管道中的第一个DataNode,该DataNode存储数据包并将它发送到传输管道中的第二个DataNode。同样,第二个DataNode存储该数据包并且发送到传输管道中的第三个DataNode。
(4)DFSOutputStream也维护着一个内部数据队列来等待DataNode的收到确认回执,这个队列被称为“确认队列(ack queue)”。只有收到管道中所有DataNode的确认回执后,该数据包才会从数据队列删除。
(5)如果任何DataNode在数据写入期间发生故障,则执行以下操作:
- 首先关闭传输管道,确认把队列中的所有数据包都添加回数据队列的最前端,以确保故障节点下游的DataNode不会漏掉任意一个包。
- 为存储在另一正常DataNode的当前数据块制定一个新的标识,并将该标识传给Namenode,以便故障节点DataNode在恢复后可以删除存储的部分数据块。
- 从传输管道中删除故障数据节点,基于正常的DataNode构建一条新的传输管道。把余下的数据块写入传输管道中正常的DataNode。Namenode注意到块副本量不足时,会在另一个节点上创建一个新的副本。后续的数据块继续正常接收处理。
- 只要写入了dfs.replication.min参数设置的副本(默认是1)数量,写操作就是成功的。并且这个块会在集群中被异步复制,直到其满足目标副本数(dfs.replication 默认值为3)。
(6)client完成数据的写入后,就会在数据输出流中调用close()方法。该操作将剩余的所有数据包写入DataNode传输管道中,然后等待确认。
(7)收到所有的等待确认以后,客户端会告知NameNode文件已经写入完成,此时由于Namenode节点已经知道文件由哪些块组成(因为DataStreamer之前已经请求分配了数据块),所以它只需在返回成功前等待块进行最小量的复制。
副本布局策略
Hadoop的默认布局策略是在运行客户端的节点上放第1个副本(如果客户端运行在集群之外,就随机选择一个节点,不过系统会避免挑选那些存储太满或太忙的节点。)
第2个副本放在与第1个副本不同且随机另外选择的机架的节点上(离架)。第3个副本与第2个副本放在相同的机架,且随机选择另一个节点。其他副本放在集群中随机的节点上,不过系统会尽量避免相同的机架放太多副本。
之所以这么布局,是为了实现很好的负载均衡以及提供一个更好的稳定性,包括以下几点:
- 写入带宽:写入操作只需要遍历一个交换机。
- 读取性能:可以从两个机架上选择读取。
- 集群中块的均匀分布:客户端只在本地机架上写入一个块。
参考:《Hadoop权威指南》