Hadoop分布式文件系统(HDFS)是指被设计成适合运行在通用硬件(commodity hardware)上的分布式
文件系统(Distributed File System)。它和现有的分布式文件系统有很多共同点。但同时,它和其他的分布式文件系统的区别也是很明显的。HDFS是一个高度
容错性的系统,适合部署在廉价的机器上。HDFS能提供高
吞吐量的数据访问,非常适合大规模
数据集上的应用。HDFS放宽了一部分
POSIX约束,来实现流式读取文件系统数据的目的。HDFS在最开始是作为
Apache Nutch搜索引擎项目的基础架构而开发的。HDFS是
Apache Hadoop Core项目的一部分。
体系结构
HDFS采用了主从(Master/Slave)
结构模型,一个HDFS集群是由一个
NameNode和若干个DataNode组成的。其中NameNode作为主服务器,管理文件系统的命名空间和客户端对
文件的访问操作;集群中的DataNode管理存储的数据。
特点和目标
硬件故障
硬件故障是常态,而不是异常。整个HDFS系统将由数百或数千个存储着文件数据片段的服务器组成。实际上它里面有非常巨大的组成部分,每一个组成部分都很可能出现故障,这就意味着HDFS里的总是有一些部件是失效的,因此,故障的检测和自动快速恢复是HDFS一个很核心的
设计目标。
数据访问
运行在HDFS之上的
应用程序必须流式地访问它们的
数据集,它不是运行在普通文件系统之上的
普通程序。HDFS被设计成适合批量处理的,而不是用户交互式的。重点是在数据
吞吐量,而不是数据访问的
反应时间,POSIX的很多硬性需求对于HDFS应用都是非必须的,去掉
POSIX一小部分关键语义可以获得更好的
数据吞吐率。
大数据集
运行在HDFS之上的程序有很大量的数据集。典型的HDFS
文件大小是GB到
TB的级别。所以,HDFS被调整成支持大文件。它应该提供很高的
聚合数据带宽,一个集群中支持数百个节点,一个集群中还应该支持千万级别的文件。
简单一致性模型
大部分的HDFS程序对文件操作需要的是一次写多次读取的
操作模式。一个文件一旦创建、写入、关闭之后就不需要修改了。这个假定简单化了数据一致的问题,并使高吞吐量的数据访问变得可能。一个Map-Reduce程序或者
网络爬虫程序都可以完美地适合这个模型。
在靠近计算数据所存储的位置来进行计算是最理想的状态,尤其是在数据集特别巨大的时候。这样消除了网络的拥堵,提高了系统的整体吞吐量。一个假定就是迁移计算到离数据更近的位置比将数据移动到程序运行更近的位置要更好。HDFS提供了接口,来让程序将自己移动到离
数据存储更近的位置。
HDFS被设计成可以简便地实现平台间的迁移,这将推动需要大数据集的应用更广泛地采用HDFS作为平台。
名字节点和数据节点
HDFS是一个主从结构,一个HDFS集群是由一个名字节点,它是一个
管理文件命名空间和调节客户端访问文件的主服务器,当然还有一些数据节点,通常是一个节点一个机器,它来管理对应节点的存储。HDFS对外开放文件命名空间并允许用户数据以文件形式存储。
内部机制是将一个
文件分割成一个或多个块,这些块被存储在一组数据节点中。名字节点用来操作文件命名空间的文件或目录操作,如打开,关闭,
重命名等等。它同时确定块与数据节点的映射。数据节点负责来自文件系统客户的读写请求。数据节点同时还要执行块的创建,删除,和来自名字节点的块复制指令。
名字节点和数据节点都是运行在普通的机器之上的软件,机器典型的都是
GNU/Linux,HDFS是用java编写的,任何支持java的机器都可以运行名字节点或数据节点,利用java语言的超轻便性,很容易将HDFS部署到大范围的机器上。典型的部署是由一个专门的机器来运行名字节点软件,集群中的其他每台机器运行一个数据节点实例。
体系结构不排斥在一个机器上运行多个数据节点的实例,但是实际的部署不会有这种情况。
集群中只有一个名字节点极大地简单化了系统的体系结构。名字节点是仲裁者和所有HDFS元数据的仓库,用户的实际数据不经过名字节点。
文件命名空间
HDFS支持传统的继承式的文件
组织结构。一个用户或一个程序可以创建目录,
存储文件到很多目录之中。文件系统的名字空间层次和其他的文件系统相似。可以创建、移动文件,将文件从一个目录移动到另外一个,或重命名。HDFS还没有实现用户的配额和
访问控制。HDFS还不支持
硬链接和
软链接。然而,HDFS结构不排斥在将来实现这些功能。
名字节点维护文件系统的命名空间,任何文件命名空间的改变和或属性都被名字节点记录。应用程序可以指定文件的副本数,文件的副本数被称作文件的复制因子,这些信息由命名空间来负责存储。
数据复制
HDFS设计成能可靠地在集群中大量机器之间存储大量的文件,它以块序列的形式存储文件。文件中除了最后一个块,其他块都有相同的大小。属于文件的块为了故障容错而被复制。块的大小和复制数是以文件为单位进行配置的,应用可以在
文件创建时或者之后修改复制因子。HDFS中的文件是一次写的,并且任何时候都只有一个写操作。
名字节点负责处理所有的块复制相关的决策。它周期性地接受集群
中数据节点的心跳和块报告。一个心跳的到达表示这个数据节点是正常的。一个块报告包括该数据节点上所有块的列表。
副本位置:第一小步
块副本存放位置的选择严重影响HDFS的可靠性和性能。副本存放位置的优化是HDFS区分于其他
分布式文件系统的的特征,这需要精心的调节和大量的经验。机架敏感的副本存放策略是为了提高数据的可靠性,
可用性和
网络带宽的
利用率。副本存放策略的实现是这个方向上比较原始的方式。短期的实现目标是要把这个策略放在
生产环境下验证,了解更多它的行为,为以后测试研究更精致的策略打好基础。
HDFS运行在跨越大量机架的集群之上。两个不同机架上的节点是通过交换机实现通信的,在大多数情况下,相同机架上机器间的
网络带宽优于在不同机架上的机器。
在开始的时候,每一个数据节点自检它所属的机架id,然后在向名字节点注册的时候告知它的机架id。HDFS提供接口以便很容易地挂载检测机架标示的模块。一个简单但不是最优的方式就是将副本放置在不同的机架上,这就防止了机架故障时数据的丢失,并且在读数据的时候可以充分利用不同机架的带宽。这个方式均匀地将复制分散在集群中,这就简单地实现了组建故障时的
负载均衡。然而这种方式增加了写的成本,因为写的时候需要跨越多个机架传输文件块。
默认的HDFS block放置策略在最小化
写开销和最大化数据可靠性、可用性以及总体读取带宽之间进行了一些折中。一般情况下复制因子为3,HDFS的副本放置策略是将第一个副本放在本地节点,将第二个副本放到本地机架上的另外一个节点而将第三个副本放到不同机架上的节点。这种方式减少了机架间的写流量,从而提高了写的性能。机架故障的几率远小于节点故障。这种方式并不影响数据可靠性和可用性的限制,并且它确实减少了读操作的网络
聚合带宽,因为文件块仅存在两个不同的机架, 而不是三个。文件的副本不是均匀地分布在机架当中,1/3在同一个节点上,1/3副本在同一个机架上,另外1/3均匀地分布在其他机架上。这种方式提高了写的性能,并且不影响数据的可靠性和读性能。
副本的选择
为了尽量减小全局的带宽消耗读延迟,HDFS尝试返回给一个读操作离它最近的副本。假如在读节点的同一个机架上就有这个副本,就直接读这个,如果HDFS集群是跨越多个数据中心,那么本地数据中心的副本优先于远程的副本。
安全模式
在启动的时候,名字节点进入一个叫做
安全模式的特殊状态。安全模式中不允许发生文件块的复制。名字节点接受来自数据节点的心跳和块报告。一个块报告包含数据节点所拥有的
数据块的列表。
每一个块有一个特定的最小复制数。当名字节点检查这个块已经大于最小的复制数就被认为是安全地复制了,当达到配置的块
安全复制比例时(加上额外的30秒),名字节点就退出安全模式。它将检测数据块的列表,将小于特定复制数的块复制到其他的数据节点。
文件系统的元数据的持久化
HDFS的命名空间是由名字节点来存储的。名字节点使用叫做EditLog的
事务日志来持久记录每一个对文件系统元数据的改变,如在HDFS中创建一个新的文件,名字节点将会在EditLog中插入一条记录来记录这个改变。类似地,改变文件的复制因子也会向EditLog中插入一条记录。名字节点在本地文件系统中用一个文件来存储这个EditLog。整个文件系统命名空间,包括文件块的映射表和文件系统的配置都存在一个叫FsImage的文件中,FsImage也存放在名字节点的本地文件系统中。
名字节点在内存中保留一个完整的文件系统命名空间和文件块的映射表的镜像。这个元数据被设计成紧凑的,这样4GB内存的名字节点就足以处理非常大的文件数和目录。名字节点启动时,它将从磁盘中读取FsImage和EditLog,将EditLog中的所有事务应用到FsImage的仿
内存空间,然后将新的FsImage刷新到
本地磁盘中,因为事务已经被处理并已经持久化的FsImage中,然后就可以截去旧的EditLog。这个过程叫做
检查点。当前实现中,检查点仅在名字节点启动的时候发生,正在支持周期性的检查点。
数据节点将HDFS数据存储到本地的文件系统中。数据节点并不知道HDFS文件的存在,它在本地文件系统中以单独的文件存储每一个HDFS文件的数据块。数据节点不会将所有的数据块文件存放到同一个目录中,而是
启发式的检测每一个目录的最优文件数,并在适当的时候创建
子目录。在本地同一个目录下创建所有的数据块文件不是最优的,因为本地文件系统可能不支持单个目录下巨额文件的高效操作。当数据节点启动的时候,它将
扫描它的本地文件系统,根据本地的文件产生一个所有HDFS
数据块的列表并报告给名字节点,这个报告称作块报告。
通信协议
所有的
通信协议都是在
TCP/IP协议之上构建的。一个客户端和指定
TCP配置端口的名字节点建立连接之后,它和名字节点之间通信的协议是Client Protocol。数据节点和名字节点之间通过Datanode Protocol通信。
RPC(Remote Procedure Call)抽象地封装了Client Protocol和DataNode Protocol协议。按照设计,名字节点不会主动发起一个RPC,它只是被动地对数据节点和客户端发起的RPC作出反馈。
异常处理
可靠性
HDFS的主要目标就是在存在故障的情况下也能可靠地
存储数据。三个最常见的故障是名字节点故障,数据节点故障和网络断开。
重新复制
一个数据节点周期性发送一个
心跳包到名字节点。网络断开会造成一组数据节点子集和名字节点失去联系。名字节点根据缺失的心跳信息判断故障情况。名字节点将这些数据节点标记为死亡状态,不再将新的IO请求转发到这些数据节点上,这些数据节点上的数据将对HDFS不再可用,可能会导致一些块的复制因子降低到指定的值。
名字节点检查所有的需要复制的块,并开始复制他们到其他的数据节点上。重新复制在有些情况下是不可或缺的,例如:数据节点失效,副本损坏,数据节点磁盘损坏或者文件的复制因子增大。
数据正确性
从数据节点上取一个文件块有可能是坏块,坏块的出现可能是
存储设备错误,网络错误或者软件的漏洞。HDFS客户端实现了HDFS文件内容的校验。当一个客户端创建一个HDFS文件时,它会为每一个文件块计算一个
校验码并将校验码存储在同一个HDFS命名空间下一个单独的
隐藏文件中。当客户端访问这个文件时,它根据对应的校验文件来验证从数据节点接收到的数据。如果校验失败,客户端可以选择从其他拥有该块副本的数据节点获取这个块。
元数据失效
FsImage和Editlog是HDFS的核心
数据结构。这些文件的损坏会导致整个集群的失效。因此,名字节点可以配置成支持多个FsImage和EditLog的副本。任何FsImage和EditLog的更新都会同步到每一份副本中。
同步更新多个EditLog副本会降低名字节点的命名空间事务交易速率。但是这种降低是可以接受的,因为HDFS程序中产生大量的数据请求,而不是元数据请求。名字节点重新启动时,选择最新一致的FsImage和EditLog。
名字节点对于一个HDFS集群是
单点失效的。假如名字节点失效,就需要人工的干预。还不支持自动重启和到其它名字节点的切换。
数据特点
快照
快照支持在一个特定时间存储一个数据拷贝,快照可以将失效的集群回滚到之前一个正常的
时间点上。HDFS已经支持
元数据快照。
数据组织
HDFS的设计是用于支持大文件的。运行在HDFS上的程序也是用于处理大
数据集的。这些程序仅写一次数据,一次或多次读数据请求,并且这些
读操作要求满足流式
传输速度。HDFS支持文件的一次写多次读操作。HDFS中典型的块大小是64MB,一个HDFS文件可以被切分成多个64MB大小的块,如果需要,每一个块可以分布在不同的数据节点上。
阶段状态
一个客户端创建一个文件的请求并不会立即转发到名字节点。实际上,一开始HDFS客户端将文件
数据缓存在本地的临时文件中。应用程序的写操作被透明地重定向到这个临时本地文件。当本地文件堆积到一个HDFS块大小的时候,客户端才会通知名字节点。名字节点将文件名插入到
文件系统层次中,然后为它分配一个
数据块。名字节点构造包括数据节点ID(可能是多个,副本数据块存放的节点也有)和目标数据块标识的报文,用它回复客户端的请求。客户端收到后将本地的
临时文件刷新到指定的数据节点数据块中。
当文件关闭时,本地临时文件中未上传的
残留数据就会被转送到数据节点。然后客户端就可以通知名字节点文件已经关闭。此时,名字节点将文件的创建操作添加到到持久化存储中。假如名字节点在文件关闭之前死掉,文件就丢掉了。
上述流程是在认真考虑了运行在HDFS上的
目标程序之后被采用。这些应用程序需要流式地写文件。如果客户端对远程文件系统进行直接写入而没有任何本地的缓存,这就会对
网速和
网络吞吐量产生很大的影响。这方面早有前车之鉴,早期的
分布式文件系统如AFS,也用客户端缓冲来提高性能,
POSIX接口的限制也被放宽以达到更高的数据
上传速率。
流水式复制
当客户端写数据到HDFS文件中时,如上所述,数据首先被写入本地文件中,假设HDFS文件的
复制因子是3,当本地文件堆积到一块大小的数据,客户端从名字节点获得一个数据节点的列表。这个列表也包含存放数据块副本的数据节点。当客户端刷新数据块到第一个数据节点。第一个数据节点开始以4kb为单元
接收数据,将每一小块都写到本地库中,同时将每一小块都传送到列表中的第二个数据节点。同理,第二个数据节点将小块数据写入本地库中同时传给第三个数据节点,第三个数据节点直接写到本地库中。一个数据节点在接前一个节点数据的同时,还可以将数据流水式传递给下一个节点,所以,数据是流水式地从一个数据节点传递到下一个。
可访问性
HDFS提供多种方式由应用程序访问,自然地,HDFS提供为程序提供
java api,为
c语言包装的java api也是可用的,还有一个HTTP浏览器可以浏览HDFS中的文件,通过
WebDAV协议访问HDFS库的方式也正在构建中。
DFSShell
H
DFS允许用户
数据组织成
文件和文件夹的方式,它提供一个叫DFSShell的接口,使用户可以和HDFS中的数据交互。命令集的语法跟其他用户熟悉的shells(bash,csh)相似。以下是一些例子:
DFSAdmin
DFSAdmin命令集是用于管理dfs集群的,这些命令只由HDFS管理员使用。示例:
浏览器接口
典型的HDFS初始化配置了一个web 服务,通过一个可配的
TCP端口可以访问HDFS的
命名空间。这就使得用户可以通过web浏览器去查看HDFS命名空间的内容。
存储空间回收
当一个文件被用户或程序删除时,它并没有立即从
HDFS中删除。HDFS将它重新命名
后转存到/trash目录下,这个文件只要还在/trash目录下保留就可以重新快速恢复。文件在/trash中存放的时间是可配置的。存储时间超时后,名字节点就将
目标文件从
名字空间中删除,同时此
文件关联的所有文件块都将被释放。注意,用户
删除文件的时间和HDF系统回收空闲存储之间的
时间间隔是可以估计的。
删除一个文件之后,只要它还在/trash目录下,用户就可以
恢复删除一个文件。如果一个用户希望恢复删除他已经删除的文件,可以查找/trash目录获得这个文件。/trash目录仅保存最新版本的删除文件。/trash目录也像其他目录一样,只有一个特殊的功能,HDFS采用一个特定的策略去自动地删除这个目录里的文件,当前默认的策略是删除在此目录存放超过6小时的文件。以后这个策略将由一个定义好的接口来配置。
当文件的复制因子减少了,名字节点选择删除多余的副本,下一次的
心跳包的回复就会将此
信息传递给数据节点。然后,数据节点移除相应的块,对应的空闲空间将回归到集群中,需要注意的就是,在setReplication函数调用后和集群空闲空间更新之间会有一段
时间延迟。
文件读取解析
文件内容读取的代码可以分为三个大步骤。
1、获取文件系统
2、通过文件系统打开文件
3、将文件内容输出
接下来,我们来看一下每个步骤的详细过程
获取文件系统对象
要从HDFS上读取文件,必须先得到一个
FileSystem。HDFS本身就是一个文件系统,所以,我们得到一个文件系统后就可以对HDFS进行相关操作。获取文件系统的步骤可以分为以下2步。
2、获取文件系统。
读取配置文件:Configuration类有三个
构造器,无参数的构造器表示直接加载默认资源,也可以指定一个boolean参数来关闭加载
默认值,或直接使用另外一个Configuration对象来初始化。
打开文件
打开文件其实就是创建一个文件
输入流,跟踪文件系统的open方法,可以找到源码
在返回结果的时候,创建了一个FileSystemLinkResolver对象,并实现了此类的两个抽象方法。doCall方法和next方法都在resolve方法里用到了,而next方法只是在resolve方法异常捕获时才调用。
跟踪doCall方法,doCall方法里的open()方法有3个参数,src表示要打开的文件路径,buffersize表示缓冲大小,verifyChecksum表示是否
校验和,的
源代码如下。
checkOpen方法表示检查文件系统是否已经打开,如果没有打开,则
抛出异常(FileSystemclosed)。
然后返回一个
分布式文件系统输入流(
DFSInputStream),此处调用的
构造方法源代码如下。
这个方法先是做了一些准备工作,然后调用openInfo()方法,openInfo()方法是一个线程安全的方法,作用是从namenode获取已打开的文件信息。其源代码如下。
此方法有调用fetchLocatedBlocksAndGetLastBlockLength()方法获取块的
位置信息。
getLocatedBlocks方法可以获取块的位置信息。LocatedBlocks类是许多块的位置信息的集合。因为从此类的源码可以发现有这个一个私有属性:
通过文件名,FSDataInputStream类可以获取相文件内容,也可以充当name
node与datanode桥梁。
将文件内容在标准输出显示
因为之前已经获得了一个FS
DataInputStream,所以,我们可以调用方法copyBytes将FSDataInputStream拷贝到标准
输出流System.out显示。
此方法里又调用了另外一个copyBytes方法,作用同样是从一个流拷贝到另外一个流。
先从输入流
中读取buffSize大小的数据到缓冲里面,然后将缓冲里的数据写入到输出流out里。一直循环,直到从输入流中读到缓冲里的字节长度为0,表示输入流里的数据已经读取完毕。