Hadoop分布式文件系统(HDFS)详解
当数据集的大小超过一台独立物理计算机的存储能力时,就有必要对它进行分区 (partition)并存储到若干台单独的计算机上。管理网络中跨多台计算机存储的文件系统成为分布式文件系统 (Distributed filesystem)。该系统架构于网络之上,势必会引入网络编程的复杂性,因此分布式文件系统比普通磁盘文件系统更为复杂。
HDFS是基于流数据模式访问和处理超大文件的需求而开发的,它可以运行于廉价的商用服务器上。总的来说,可以将 HDFS的主要特点概括为以下几点:
(1 )处理超大文件
这里的超大文件通常是指数百 MB、甚至数百TB 大小的文件。目前在实际应用中, HDFS已经能用来存储管理PB(PeteBytes)级的数据了。在 Yahoo!,Hadoop 集群也已经扩展到了 4000个节点。
(2 )流式地访问数据
HDFS的设计建立在更多地响应“一次写入,多次读取”任务的基础之上。这意味着一个数据集一旦由数据源生成,就会被复制分发到不同的存储节点中,然后响应各种各样的数据分析任务请求。在多数情况下,分析任务都会涉及数据集中的大部分数据,也就是说,对HDFS 来说,请求读取整个数据集要比读取一条记录更加高效。
(3 )运行于廉价的商用机器集群上
Hadoop设计对硬件需求比较低,只须运行在廉价的商用硬件集群上,而无须昂贵的高可用性机器上。廉价的商用机也就意味着大型集群中出现节点故障情况的概率非常高。这就要求在设计 HDFS时要充分考虑数据的可靠性、安全性及高可用性。
正是由于以上的种种考虑,我们会发现现在的 HDFS在处理一些特定问题时不但没有优势,而且有一定的局限性,主要表现在以下几个方面。
(1 )不适合低延迟数据访问
如果要处理一些用户要求时间比较短的低延迟应用请求,则 HDFS不适合。HDFS 是为了处理大型数据集分析任务的,主要是为达到高的数据吞吐量而设计的,这就可能要求以高延迟作为代价。目前有一些补充的方案,比如使用HBase,通过上层数据管理项目来尽可能地弥补这个不足。
(2 )无法高效存储大量小文件
在Hadoop 中需要用 NameNode来管理文件系统的元数据,以响应客户端请求返回文件位置等,因此文件数量大小的限制要由 NameNode来决定。例如,每个文件、索引目录及块大约占 100字节,如果有100 万个文件,每个文件占一个块,那么至少要消耗 200MB内存,这似乎还可以接受。但如果有更多文件,那么 NameNode的工作压力更大,检索处理元数据的时间就不可接受了。
(3 )不支持多用户写入及任意修改文件
在HDFS 的一个文件中只有一个写入者,而且写操作只能在文件末尾完成,即只能执行追加操作。目前 HDFS还不支持多个用户对同一文件的写操作,以及在文件任意位置进行修改。
HDFS的相关概念:
1、数据块( block)
我们知道,在操作系统中都有一个文件块的概念,文件以块的形式存储在磁盘中,此处块的大小代表系统读 /写可操作的最小文件大小。也就是说,文件系统每次只能操作磁盘块大小的整数倍数据。通常来说,一个文件系统块为几千字节,而磁盘块大小为 512字节。文件的操作都由系统完成,这些对用户来说都是透明的。
这里,我们所要介绍的HDFS中的块是一个抽象的概念,它比上面操作系统中所说的块要大得多。在配置Hadoop系统时会看到,它的默认块为 64MB。和单机上的文件系统相同, HDFS分布式文件系统中的文件也被分成块进行存储,它是文件存储处理的逻辑单元。
为何HDFS 中的块如此之大? HDFS的块比磁盘块大,其目的是为了最小化寻址开销。如果块设置得足够大,从磁盘传输数据的时间可以明显大于定位这个块开始位置所需的时间。这样,传输一个由多个块组成的文件的时间取决于磁盘传输速率。我们来做一个速算,如果寻址时间为 10ms左右,而传输速率为100MB/s,为了使寻址时间仅占传输时间的 1%,我们需要设置块大小为 100MB左右。而默认的块大小实际为 64MB,但是很多情况下HDFS使用128MB的块设置。以后随着新一代磁盘驱动器传输速率的提升,块的大小将被设置得更大。但是该参数也不会设置得过大。 MapReduce中的map 任务通常一次处理一个块中的数据,因此如果任务数太少 (少于集群中的节点数量),作业的运行速度就会比较慢。
HDFS作为一个分布式文件系统,是设计用来处理大文件的,使用抽象的块会带来很多好处。一个好处是可以存储任意大的文件,而又不会受到网络中任一单个节点磁盘大小的限制。可以想象一下,单个节点磁盘 100TB的数据是不可能的,但是由于逻辑块的设计, HDFS可以将这个超大的文件分成众多块,分别存储在集群的各台机器上。另外一个好处是使用抽象块作为操作的单元可简化存储子系统。这里之所以提到简化,是因为这是所有系统的追求。而对故障出现频繁和种类繁多的分布式系统来说,简化就显得尤为重要。在 HDFS中块的大小固定,这样它就简化了存储系统的管理,特别是元数据信息可以和文件块内容分开存储。不仅如此,块更有利于分布式文件系统中复制容错的实现。在 HDFS中为了处理节点故障,默认将文件块副本数设定为 3份,分别存储在集群的不同节点上。当一个块损坏时,系统会通过 NameNode获取元数据信息,在另外的机器上读取一个副本并进行存储,这个过程对用户来说都是透明的。当然,这里的文件块副本冗余量可以通过文件进行配置,比如在有些应用中,可能会为操作频率较高的文件块设置较高的副本数量以提高集群的吞吐量。
2、 NameNode和DataNode
HDFS采用 master/slave架构对文件系统进行管理。一个 HDFS集群是由一个NameNode 和一定数目的DataNode组成的。这两类节点分别承担 Master和Worker 的任务。 NameNode就是Master 管理集群中的执行调度,是一个中心服务器, DataNode就是Worker 具体任务的执行节点。 NameNode管理文件系统的命名空间,维护整个文件系统的文件目录树及这些文件的所以目录。这些信息以两种形式存储在本地文件系统中,一种是命名空间镜像(Namespace image);一种是编辑日志(Edit log)。从 NameNode中你可以获得每个文件的每个块所在 DataNode。有一点需要注意的是,这些信息不是永久保存的, NameNode会在每次启动系统时动态地重建这些信息。当运行任务时,客户端通过 NameNode获取元数据信息,和DataNode进行交互以访问整个文件系统。系统会提供一个类似于POSIX的文件接口,这样用户在编程时无需考虑 NameNode和DataNode 的具体功能。
客户端通过调用FileSystem对象中的 open()方法来打开希望读取的文件。 FileSystem是HDFS 中的DistributedFileSystem的一个实例( 步骤1:open)。 DistributedFileSystem会通过RPC 协议调用 NameNode来确定请求文件起始块所在的位置( 步骤2:get block locations)。这里需要注意的是, NameNode只会返回所调用文件中开始的几个块而不是全部返回。对于每个返回的块,都包含块所在的 DataNode地址。随后,这些返回的DatanNode会按照Hadoop 定义的集群拓扑结构得出客户端的距离,然后再进行排序。如果客户端本身就是一个DataNode,那么它将从本地读取文件。
DistributedFileSystem会向客户端返回一个支持文件定位的输入流对象 FSDataInputStream,用于给客户端读取数据。 FSDataInputStream包含一个DFSInputStream 对象,这个对象用来管理 DataNode和NameNode 之间的I/O。
当以上步骤完成时,客户端便会在这个输入流之上调用 read()函数(步骤3:read)。DFSInputStream 对象中包含文件开始部分的数据块所在的 DataNode地址,首先它会连接包含文件第一个块最近的 DataNode。随后,在数据流中重复调用 read()函数,直到这个块全部读完为止( 步骤4:read)。当最后一个块读取完毕时, DFSInputStream会关闭连接,并查找存储下一个数据块距离客户端最近的 DataNode(步骤5:read)。以上这些步骤对客户端来说都是透明的。(这里传输的是整个数据块,应该不涉及输入格式的问题。)
客户端按照DFSInputStream 打开和DataNode连接返回的数据流的顺序读取该块,它也会调用 NameNode来检索下一组块所在的DataNode的位置信息。当客户端完成所有文件的读取时,则会在 FSDataInputStream中调用close()函数(步骤6:close)。
如果客户端和所连接的DataNode在读取时出现故障,那么它就会去尝试连接存储这个块的下一个最近的DataNode,同时它会记录这个节点的故障,这样它就不会再去尝试连接和读取块。客户端还会验证从 DataNode传送过来的数据校验和。如果发现一个损坏的块,那么客户端将会再尝试从别的 DataNode读取数据块,向NameNode报告这个信息, NameNode也会更新保存的文件信息。
客户端创建文件夹是通过命令行执行的,例如: bin/hadoop dfs -mkdir input
客户端写入文件也是通过命令行执行的,例如 :
bin/hadoop dfs -copyFromLocal conf/* input。上传的同时通过FileInputFormat类来对大文件进行分割。
客户端通过调用DistributedFileSystem对象中的 create()方法来创建一个文件(步骤1:create)。DistributedFileSystem通过RPC 调用在NameNode的文件系统命名空间中创建一个新文件,此时还没有相关的DataNode与之关联(步骤2:create)。 NameNode会通过多种验证保证新的文件不存在于文件系统中,并且确保请求客户端拥有创建文件的权限。当所有验证通过时, NameNode会创建一个新文件的记录,如果创建失败,则抛出一个 IOException异常;如果创建成功,则 DistributedFileSystem返回一个FSDataOutputStream 给客户端用来写入数据( 步骤3:write)。这里 FSDataOutputStream和读取数据时的FSDataInputStream一样都包含一个数据流对象DFSOutputStream,客户端将使用它来处理与 DataNode和NameNode 之间的通信。
当客户端写入数据时,DFSOutputStream会将文件分割成包(这里利用 FileInputFormat类来分割大文件,FileInputFormat是所有使用文件作为其数据源的 InputFormat实现的基类。它提供了两个功能:一个定义哪些文件包含在一个作业的输入中;一个为输入文件生成分片的实现),然后放入一个内部队列,我们称为“数据队列”。DataStreamer会将这些小的文件包放入数据流中, DataStreamer的作用是请求NameNode 为新的文件包分配合适的DataNode存放副本。返回的DataNode列表形成一个“管道”,假设这里的副本数是 3,那么这个管道中就会有3个DataNode 。DataStreamer将文件包以流的方式传送给管道中的第一个 DataNode,第一个DataNode 会存储这个包,然后将它推送到第二个 DataNode中,随后照这样进行,直到管道中的最后一个 DataNode(步骤4:write packet )文件的元数据存储在 NameNode的内存中。
DFSOutputStream同时也会保存一个包的内部队列,用来等待管道中的 DataNode返回确认信息,这个队列被称为确认队列( ack queue)。只有当所有管道中的 DataNode都返回了写入成功的返回信息文件包,才会从确认队列中删除( 步骤5:ack packet)。
当数据写入节点失败时, HDFS会做出以下反应:首先管道会被关闭,任何在确认通知队列中的文件包都会被添加到数据队列的前端,这样管道中故障节点下游的 DataNode都不会丢失任何一个数据包。当前这个文件中的其他存放在正常工作的 DataNode之上的文件块会被赋予一个新的身份,并且和 NameNode进行关联,这样,如果失败的DataNode过段时间后会从故障中恢复出来,其中的部分数据块就会被删除。然后,管道会把失败的 DataNode删除,文件会继续被写到管道中的另外两个 DataNode中。最后,NameNode 会注意到现在的文件块副本数没有达到配置属性要求,会在另外的 DataNode上重新安排创建一个副本,随后的文件会正常执行写入操作。
客户端成功完成数据写入的操作后,会对数据流调用 close()方法(步骤6:close )。该操作将剩余的所有数据包写入 DataNode管线中,并在联系NameNode且发送文件写入完成信号之前,等待确认( 步骤7:complete)。NameNode已经知道文件由哪些块组成(通过 DataStreamer询问数据块的分配),所以它在返回成功前只需要等待数据块进行最小量的复制。