1.概述
从HDFS的应用层面来看,我们可以非常容易的使用其API来操作HDFS,实现目录的创建、删除,文件的上传下载、删除、追加(Hadoop2.x版本以后开始支持)等功能。然而仅仅局限与代码层面是不够的,了解其实现的具体细节和过程是很有必要的,本文笔者给大家从以下几个方面进行剖析:
Create
Delete
Read
Write
Heartbeat
下面开始今天的内容分享。
2.Create
在HDFS上实现文件的创建,该过程并不复杂,Client到NameNode的相关操作,如:修改文件名,创建文件目录或是子目录等,而这些操作只涉及Client和NameNode的交互,过程如下图所示:
我们很熟悉,在我们使用Java 的API去操作HDFS时,我们会在Client端通过调用HDFS的FileSystem实例,去完成相应的功能,这里我们不讨论FileSystem和DistributedFileSystem和关系,留在以后在阅读这部分源码的时候在去细说。而我们知道,在使用API实现创建这一功能时是非常方便的,代码如下所示:
public static void mkdir(String remotePath) throws IOException { FileSystem fs = FileSystem.get(conf); Path path = new Path(remotePath); fs.create(path); fs.close(); }
在代码层面上,我们只需要获取操作HDFS的实例即可,调用其创建方法去实现目录的创建。但是,其中的实现细节和相关步骤,我们是需要清楚的。在我们使用HDFS的FileSystem实例时,DistributedFileSystem对象会通过IPC协议调用NameNode上的mkdir()方法,让NameNode执行具体的创建操作,在其指定的位置上创建新的目录,同时记录该操作并持久化操作记录到日志当中,待方法执行成功后,mkdir()会返回true表示创建成功来结束创建过程。在此期间,Client和NameNode不需要和DataNode进行数据交互。
3.Delete
在执行创建的过程当中不涉及DataNode的数据交互,而在执行一些较为复杂的操作时,如删除,读、写操作时,需要DataNode来配合完成对应的工作。下面以删除HDFS的文件为突破口,来给大家展开介绍。删除流程图如下所示:
在使用API操作删除功能时,我使用以下代码,输入要删除的目录地址,然后就发现HDFS上我们所指定的删除目录就被删除了,而然其中的删除细节和过程却并不一定清楚,删除代码如下所示:
public static void rmr(String remotePath) throws IOException { FileSystem fs = FileSystem.get(conf); Path path = new Path(remotePath); boolean status = fs.delete(path, true); LOG.info("Del status is [" + status + "]"); fs.close(); }
通过阅读这部分删除的API实现代码,代码很简单,调用删除的方法即可完成删除功能。但它是如何完成删除的,下面就为大家这剖析一下这部分内容。
在NameNode执行delete()方法时,它只是标记即将要删除的Block(操作删除的相关记录是被记录并持久化到日志当中的,后续的相关HDFS操作都会有此记录,便不再提醒),NameNode是被动服务的,它不会主动去联系保存这些数据的Block的DataNode来立即执行删除。而我们可以从上图中发现,在DataNode向NameNode发送心跳时,在心跳的响应中,NameNode会通过DataNodeCommand来命令DataNode执行删除操作,去删除对应的Block。而在删除时,需要注意,整个过程当中,NameNode不会主动去向DataNode发送IPC调用,DataNode需要完成数据删除,都是通过DataNode发送心跳得到NameNode的响应,获取DataNodeCommand的执行命令。
4.Read
在读取HDFS上的文件时,Client、NameNode以及DataNode都会相互关联。按照一定的顺序来实现读取这一过程,读取过程如下图所示:
通过上图,读取HDFS上的文件的流程可以清晰的知道,Client通过实例打开文件,找到HDFS集群的具体信息(我们需要操作的是ClusterA,还是ClusterB,需要让Client端知道),这里会创建一个输入流,这个输入流是连接DataNode的桥梁,相关数据的读取Client都是使用这个输入流来完成的,而在输入流创建时,其构造函数中会通过一个方法来获取NameNode中DataNode的ID和Block的位置信息。Client在拿到DataNode的ID和Block位置信息后,通过输入流去读取数据,读取规则按照“就近原则”,即:和最近的DataNode建立联系,Client反复调用read方法,并将读取的数据返回到Client端,在达到Block的末端时,输入流会关闭和该DataNode的连接,通过向NameNode获取下一个DataNode的ID和Block的位置信息(若对象中为缓存Block的位置信息,会触发此步骤,否则略过)。然后拿到DataNode的ID和Block的位置信息后,在此连接最佳的DataNode,通过此DataNode的读数据接口,来获取数据。
另外,每次通过向NameNode回去Block信息并非一次性获取所有的Block信息,需得多次通过输入流向NameNode请求,来获取下一组Block得位置信息。然而这一过程对于Client端来说是透明的,它并不关系是一次获取还是多次获取Block的位置信息,Client端在完成数据的读取任务后,会通过输入流的close()方法来关闭输入流。
在读取的过程当中,有可能发生异常,如:节点掉电、网络异常等。出现这种情况,Client会尝试读取下一个Block的位置,同时,会标记该异常的DataNode节点,放弃对该异常节点的读取。另外,在读取数据的时候会校验数据的完整性,若出现校验错误,说明该数据的Block已损坏,已损坏的信息会上报给NameNode,同时,会从其他的DataNode节点读取相应的副本内容来完成数据的读取。Client端直接联系NameNode,由NameNode分配DataNode的读取ID和Block信息位置,NameNode不提供数据,它只处理Block的定位请求。这样,防止由于Client的并发数据量的迅速增加,导致NameNode成为系统“瓶颈”(磁盘IO问题)。
5.Write
HDFS的写文件过程较于创建、删除、读取等,它是比较复杂的一个过程。下面,笔者通过一个流程图来为大家剖析其中的细节,如下图所示:
Client端通过实例的create方法创建文件,同时实例创建输出流对象,并通过远程调用,通知NameNode执行创建命令,创建一个新文件,执行此命令需要进行各种校验,如NameNode是否处理Active状态,被创建的文件是否存在,Client创建目录的权限等,待这些校验都通过后,NameNode会创建一个新文件,完成整个此过程后,会通过实例将输出流返回给Client。
这里,我们需要明白,在向DataNode写数据的时候,Client需要知道它需要知道自身的数据要写往何处,在茫茫Cluster中,DataNode成百上千,写到DataNode的那个Block块下,是Client需要清楚的。在通过create创建一个空文件时,输出流会向NameNode申请Block的位置信息,在拿到新的Block位置信息和版本号后,输出流就可以联系DataNode节点,通过写数据流建立数据流管道,输出流中的数据被分成一个个文件包,并最终打包成数据包发往数据流管道,流经管道上的各个DataNode节点,并持久化。
Client在写数据的文件副本默认是3份,换言之,在HDFS集群上,共有3个DataNode节点会保存这份数据的3个副本,客户端在发送数据时,不是同时发往3个DataNode节点上写数据,而是将数据先发送到第一个DateNode节点,然后,第一个DataNode节点在本地保存数据,同时推送数据到第二个DataNode节点,依此类推,直到管道的最后一个DataNode节点,数据确认包由最后一个DataNode产生,并逆向回送给Client端,在沿途的DataNode节点在确认本地写入成功后,才会往自己的上游传递应答信息包。这样做的好处总结如下:
分摊写数据的流量:由每个DataNode节点分摊写数据过程的网络流量。
降低功耗:减小Client同时发送多份数据到DataNode节点造成的网络冲击。
另外,在写完一个Block后,DataNode节点会通过心跳上报自己的Block信息,并提交Block信息到NameNode保存。当Client端完成数据的写入之后,会调用close()方法关闭输出流,在关闭之后,Client端不会在往流中写数据,因而,在输出流都收到应答包后,就可以通知NameNode节点关闭文件,完成一次正常的写入流程。
在写数据的过程当中,也是有可能出现节点异常。然而这些异常信息对于Client端来说是透明的,Client端不会关心写数据失败后DataNode会采取哪些措施,但是,我们需要清楚它的处理细节。首先,在发生写数据异常后,数据流管道会被关闭,在已经发送到管道中的数据,但是还没有收到确认应答包文件,该部分数据被重新添加到数据流,这样保证了无论数据流管道的哪个节点发生异常,都不会造成数据丢失。而当前正常工作的DateNode节点会被赋予新的版本号,并通知NameNode。即使,在故障节点恢复后,上面只有部分数据的Block会因为Blcok的版本号与NameNode保存的版本号不一致而被删除。之后,在重新建立新的管道,并继续写数据到正常工作的DataNode节点,在文件关闭后,NameNode节点会检测Block的副本数是否达标,在未达标的情况下,会选择一个新的DataNode节点并复制其中的Block,创建新的副本。这里需要注意的是,DataNode节点出现异常,只会影响一个Block的写操作,后续的Block写入不会收到影响。
6.Heartbeat
前面说过,NameNode和DataNode之间数据交互,是通过DataNode节点向NameNode节点发送心跳来获取NameNode的操作指令。心跳发送之前,DataNode需要完成一些步骤之后,才能发送心跳,流程图如下所示:
从上图来看,首先需要向NameNode节点发送校验请求,检测是否NameNode节点和DataNode节点的HDFS版本是否一致(有可能NameNode的版本为2.6,DataNode的版本为2.7,所以第一步需要校验版本)。在版本校验结束后,需要向NameNode节点注册,这部分的作用是检测该DataNode节点是否属于NameNode节点管理的成员之一,换言之,ClusterA的DataNode节点不能直接注册到ClusterB的NameNode节点上,这样保证了整个系统的数据一致性。在完成注册后,DataNode节点会上报自己所管理的所有的Block信息到NameNode节点,帮助NameNode节点建立HDFS文件Block到DataNode节点映射关系(即保存Meta),在完成此流程之后,才会进入到心跳上报流程。
另外,如果NameNode节点长时间接收不到DataNode节点到心跳,它会认为该DataNode节点的状态处理Dead状态。如果NameNode有些命令需要DataNode配置操作(如:前面的删除指令),则会通过心跳后的DataNodeCommand这个返回值,让DataNode去执行相关指令。
7.总结
简而言之,关于HDFS的创建、删除、读取以及写入等流程,可以一言以蔽之,内容如下:
Create:Client直接与NameNode交互,不涉及DataNode
Delete:Client将删除指令存于NameNode,DataNode通过心跳获取NameNode的操作指令
Read:Client通过NameNode获取读取数据的位置,找到DataNode节点对应的Block位置读取数据
Write:Client通过NameNode后区写数据的位置,找到DataNode节点对应的Block位置进行写入