@
目录
Hadoop作为大数据入门的基石内容,其中HDFS更是所有生态的地基,so,我们有必要更深入去理解HDFS,以及HDFS在高可用的演变过程。如果有小可爱说hadoop和HDFS有啥区别的。の。。。,那容我之后在做背书来说明,暖男行为的先提一下:目前我们所说的Hadoop更多是指Hadoop的生态,包括hadoop本身及其他组件,如flume、kafka、hive、Hbase等等,如下图所示:
其中hadoop本身的结构(以Hadoop2.x为例,3.x亦复如是)如下:
Hadoop最早起源于Nutch 。Nutch是一个开源的网络搜索引擎,由Doug Cutting于2002年创建。Nutch的设计目标是构建一个大型的全网搜索引擎,包括网 页抓取、索引、查询等功能,但随着抓取网页数量的增加,遇到了严重的可扩展性问 题,即不能解决数十亿网页的存储和索引问题。之后,谷歌发表的两篇论文为该问题 提供了可行的解决方案。一篇是2003年发表的关于谷歌分布式文件系统(GFS)的论 文。该论文描述了谷歌搜索引擎网页相关数据的存储架构,该架构可解决Nutch遇 到的网页抓取和索引过程中产生的超大文件存储需求的问题。但由于谷歌仅开源了思 想而未开源代码,Nutch项目组便根据论文完成了一个开源实现,即Nutch的分布式 文件系统(NDFS)。另一篇是2004年发表的关于谷歌分布式计算框架MapReduce 的论文。该论文描述了谷歌内部最重要的分布式计算框架MapReduce的设计艺术, 该框架可用于处理海量网页的索引问题。同样,由于谷歌未开源代码,Nutch的开发 人员完成了一个开源实现。由于NDFS和MapReduce不仅适用于搜索领域,2006年 年初,开发人员便将其移出Nutch,成为Lucene [4]的一个子项目,称为Hadoop。大 约同一时间,Doug Cutting加入雅虎公司,且公司同意组织一个专门的团队继续发 展Hadoop。同年2月,Apache Hadoop项目正式启动以支持MapReduce和HDFS 的独立发展。2008年1月,Hadoop成为Apache顶级项目,迎来了它的快速发展期。
文字必定是比较枯燥的,贴心为大家准备一个时间树:
HDFS作为Hadoop的旗舰级文件系统(也就是说除了HDFS,Hadoop也可以与其他文件存储系统集成,所以不能简单认为HDFS和Hadoop是有等价关系的哦。),被定义为以流式数据访问模式来存储超大文件,运行于廉价的商用硬件集群上。
我们解读一下其定义:
在一个全配置的集群上,“运行HDFS”意味着在网络分布的不同服务器上运行一些守护进程(daemon),这些进程有各自的特殊角色,并相互配合,一起形成一个分布式文件系统。
HDFS采用了主从(Master/Slaver)体系结构,名字节点 NameNode、数据节点DataNode和客户端Client是HDFS的三个重要的角色。
此外,由于NameNode将文件系统的元数据存储在内存中,因此一个HDFS文件系统所能存储的文件总数受限于NameNode的内存容量(一般一个文件、目录和数据块的存储信息即元数据大约占150字节)。因此当有一百万个文件,且每个文件占一个数据块,至少需要300M的内存。注意一点:Hadoop的数据是分块在物理存储的(hadoop2.x及之后版本,块默认其大小为128M,块的划分大小只和磁盘的传输速率强正相关),但是在DataNode节点实际占用的空间大小和文件真实大小一致,而不是占据整个块的空间(当一个1MB的文件存在一个一个128MB的块中时,文件只是用1MB的磁盘空间,而不是128MB)。
简单的速算,当寻址时间为10ms,磁盘传输速率为100M/S,一般经验认为寻址时间占传输时间的1%为佳,so,传输时间即为1s,故大小为100M,存储单位都是二进制,故设置为128MB,如果磁盘传输速率为500M时,那么相应的块大小可以设置为512MB,可以通过配置参数dfs.blocksize 来设置。
有必要说明一下:分布式文件系统中的块抽象带来的好处:
1.一个文件的大小可以大于网络中任意一个磁盘的容量,(也就是说我们需要的文件有2T,但是单个节点的存储只有1T,这也是Hadoop应用的得意之处)。文件的块并不需要存储在同一个磁盘上,我们将大文件按块划分为多个文件,并将这些块存储在任意的节点磁盘进行储存。
2.使用抽象块而不是整个文件作为存储单元,大大简化了存储子系统的设计。将存储子系统的处理独享设置为块,可简化存储管理,由于块的大小是固定,因此计算单个磁盘可以存储多少个块就相对简单。同事也消除了对元数据的顾虑,块只是来存储数据,并不存储文件的元数据,就可以将数据和元数据分离,单独管理元数据。
3.块的抽象非常适合数据备份,将每个块复制到几个物理上互相独立的节点(默认为3),可以确保在块、磁盘或者机器发生故障后数据不会丢失。如果发现一个块不可用,系统会从其他地方读取另一个副本,而这个过程对用户是透明的。且当一个损坏或机器故障而丢失的块可以从其他存储的节点复制到另一台正常运行的机器上,保证复本数量是满足设定的。进而提供了系统数据容错能力和可用性。
当我们向HDFS上传数据的时候是怎么进行,HDFS是如何工作的呢?基于NameNode和DataNode分别都做了那些工作呢?我们一一道来。
分布式系统,如浏览器和服务器端的通信,需要在不同的实体中显示交换信息,处理诸如消息的编解码、发送和接收等具体任务。Hadoop中各个实体间的交互通过远程过程调用(RPC),让用户可以像调用本地方法一样调用另外一个应用程序提供的服务,而不必关注具体的实现。从而提升了可操作性和交互性。那我们先来了解一下什么是RPC。
简要地说,RPC就是允许程序调用位于其他机器上的过程(也可以是同一台机器上的不同的进程)。当机器A上的进程调用机器B上的进程时,A上的调用进程被挂起,而B上的被调用进程开始执行。调用方使用参数将信息传送给到被调用方,然后通过传回的结果得到信息。在这个过程中,A是RPC客户端,B是RPC服务端。同时我们不用关注任何消息的传递,就像在一个过程到另一个过程的调用一样,如同方法的调用。
class ProgressDemo{
public static void main(String[] args){
...
func(a1,a2,...,an);
...
}
public static int func(int p1,int p2,... ,int pn){
...
}
}
上边是一个简单的常规过程调用
RPC调用示例
RPC的Server运行时会阻塞在接受消息的调用上,当接到客户端的请求后,它会解包以获取请求参数,类似于传统过程调用,被调用函数从栈中接受参数,然后确定调用过程的名字并调用相应过程。调用结束后,返回值通过主程序打包并发送回客户端,通知客户端调用结束。
我们对于RPC先有一个大致映象,帮助我们理解后续的一些内容,具体的RPC实现可以参考分布式系统相关内容。
我们从客户端到NameNode有大量的元数据操作,比如修改文件名,创建子目录等。这些操作只涉及到客户端和NameNode的交互。
剖析文件写入HDFS
1 (步骤1)客户端通过对Distributed FileSystem对象调用create() 来新建文件
2(步骤2、3).Distributed FileSystem 对NameNode 创建一个RPC(Remote Procedure Call Protocol 远程过程调用协议)调用(关于RPC调用,可看这里什么是RPC调用),让NameNode执行同名方法,在文件系统中的命名空间中新建一个文件,此时该文件还没有相应的数据块。NameNode创建新文件时,执行各种不同的检查以确保这个文件不存在以及客户端有新建该文件的权限等等。如果这些检查通过,NameNode就会构建一个新文件,并记录创建操作到编辑日志edits中;否则,当用户没有权限或者默认情况下没有说明覆盖文件的情况下,会发生文件创建失败并向客户端抛出一个IOException异常。通过检查之后,DistributedFileSystem向客户端返回一个FSDataOutputStream对象,FSDataOutputStream封装一个DFSOutputStream对象(DFSOutputStream是对应的实例对象),此对象处理DataNode和NameNode之间的通信。
用更通俗的话来说,这一步可以细分为两个步骤,Distributed FileSystem 一下简写DFS。
3(步骤4、5) 在客户端写入数据时,DFSOutputStream将它分成一个个数据包,并写入内部队列,称为“数据队列”。DataStreamer处理数据队列,其责任是挑选出适合存储数据复本的一组DataNode(此处挑选DataNode的原则要素为复本数量和节点就近距离(拓扑网络)),并要求NameNode分配新的数据块,因为上步我们只是创建了一个空文件,所以DFSOutputStream实例首先向NameNode节点申请数据块,申请成功之后(内部调用addBlock()方法,返回是否成功),就获得对应的数据块对象(此对象包含数据块的数据块表示和版本号)。这一组DataNode就构成了一个管线,复本数量决定了管线中节点的数量,默认复本数为3,则节点有三台。DataStreamer将数据包流式传输到管线中第一个DataNode,该DataNode存储数据包并将它发送到管线中的第2个DataNode。同样,第2个DataNode存储该数据包并且发送给第3个(也就是最后一个)DataNode。
4(步骤6)DFSOutputStream也维护着一个内部数据包队列来等待DataNode的收到确认回执,成为“确认队列(ack queue)”。收到管道中所有DataNode确认信息后,该数据包才会从确认队列删除。
如果任何DataNode在数据写入期间发生故障,则执行以下操作(对客户端是透明的)。首先关闭管线,确认把队列中的所有数据包都添加回数据队列的最前端,以确保故障节点下游的DataNode不会漏掉任何一个数据包。为存储在另一正常DataNode的当前数据块指定一个新的标识,并将该标识传送给NameNode,以便故障DataNode在恢复后可以删除存储的部分数据块。从管线中删除故障DataNode,基于正常的DataNode重新构建一条新管线。余下的数据块写入管线中正常的DataNode。NameNode注意到块复本量不足时,会在另一个节点上创建一个新的复本。
5(步骤7、8)客户端完成数据写入后,对数据流调用close()方法,意味着客户单不会再向六中写入数据。该操作将剩余的所有数据包写入DataNode管线,并在联系到NameNode告知其文件写入完成之前,等待确认(步骤8)。NameNode已经知道文件有哪些块组成(因为DataStreamer请求分配数据块),所以他在返回成功前只需要等待数据块进行最小量的复制。
剖析HDFS文件读取
为了了解客户端及与之交互的HDFS、NameNode和DataNode之间的数据流,我们参考上图解释一下读取文件是发生的事件顺序。
在整个读取过程中,如果DFSInputStream与DataNode通信时遇到错误,会尝试从这个快的另外一个最近邻DataNode读取数据,也会记住故障DataNode,保证以后不会反复读取该节点上后续的块。DFSInputStream也会通过校验和确认从DataNode发来的数据是否完整。如果发现有损坏的块,DFSInputStream会试图从其他DataNode读取其复本,同时会将损坏的块通知NameNode。
因而,没有NameNode,整个HDFS将无法使用。事实上,如果运行NameNode服务的及其损坏,文件系统上所有的文件将会丢失,因为我们不知道如何根据DataNode的块重建文件。
HDFS是怎么保证运行的?因此,对于NameNode实现容错非常重要,Hadoop为此提供了两种机制。
方式二的部分也正是hadoop2.x就有的方式,配置NameNode(NN)和secondNameNode(2NN)。且2NN可以将NameNode的镜像文件和编辑日志文件合并过程接手处理,减少NameNode的额外开销。具体的NN和2NN之间的处理关系会稍后详细在讲。
NameNode在内存中保存文件系统中每个文件和每个数据块的引用关系。意味着对于一个拥有大量文件的超级集群而言,单台NameNode内存限制了系统横向扩展的瓶颈。
且根据NameNode的架构局限性:
1)Namespace(命名空间)的限制
由于NameNode在内存中存储所有的元数据(metadata),因此单个NameNode所能存储的对象(文件+块)数目受到NameNode所在JVM的heap size的限制。50G的heap能够存储20亿(200million)个对象,这20亿个对象支持4000个DataNode,12PB的存储(假设文件平均大小为40MB)。随着数据的飞速增长,存储的需求也随之增长。单个DataNode从4T增长到36T,集群的尺寸增长到8000个DataNode。存储的需求从12PB增长到大于100PB。
2)隔离问题
由于HDFS仅有一个NameNode,无法隔离各个程序,因此HDFS上的一个实验程序就很有可能影响整个HDFS上运行的程序。
3)性能的瓶颈
由于是单个NameNode的HDFS架构,因此整个HDFS文件系统的吞吐量受限于单个NameNode的吞吐量。
在2.x的发行版本引入了 HDFS Federation(联邦HDFS),该方案允许系统通过添加NameNode实现扩展,其中每个NameNode管理文件系统命名空间中的一部分。例如,一个NameNode可能管理/user目录下的所有文件,而另一个NameNode管理/share 目录下的所有文件。
通过联合使用在多个文件系统中备份NameNode的元数据和通过备用NameNode创建检测点能防止数据丢失,但是依旧无法实现文件系统的高可用性。NameNode依旧存在单点失效(SPOF,single point of failure )的问题。如果NameNode失效了,那么所有的客户端包括MapReduce作业,均无法工作,以为内NameNode是唯一存储元数据与文件到数据块映射的地方。
想要一个失效的NameNode恢复,系统管理员的启动一个拥有文件系统元数据副本的新的NameNode,并配置DataNode和客户端使用这个新的NameNode。新的NameNode需要满足以下情况才能响应服务:
Hadoop2 针对以上问题增加了对HDFS的高可用性(HA)的支持。通过配置一对活动-备用 NameNode。当活动NameNode失效, 备用NameNode就会接管他的任务并开始服务与来自客户端的请求,不会有任何明显的中断。也就是NN和2NN之间的处理逻辑。HA会在后边再开一篇来讨论它的实现以及好处。
我们先来了解一下NN和2NN的工作机制。
我们在上边已经说了NameNode的元数据存储是通过FsImage和Edits日志文件来完成的。为什么会有这样的设计?
我们不妨假设,如果元数据存储在NameNode节点的磁盘中,因为经常需要进行随机访问,还有响应客户请求,必然是效率过低。因此,元数据需要存放在内存中。
但如果只存在内存中,一旦断电,元数据丢失,整个集群就无法工作了。因此产生在磁盘中备份元数据的FsImage。这样又会带来新的问题,当在内存中的元数据更新时,如果同时更新FsImage,就会导致效率过低,但如果不更新,就会发生一致性问题,一旦NameNode节点断电,就会产生数据丢失。
因此,引入Edits文件(只进行追加操作,效率很高)。每当元数据有更新或者添加元数据时,修改内存中的元数据并追加到Edits中。这样,一旦NameNode节点断电,可以通过FsImage和Edits的合并,合成元数据。
但是,如果长时间添加数据到Edits中,会导致该文件数据过大,效率降低,而且一旦断电,恢复元数据需要的时间过长。因此,需要定期进行FsImage和Edits的合并,如果这个操作由NameNode节点完成,又会效率过低。因此,引入一个新的节点SecondaryNamenode,专门用于FsImage和Edits的合并。
NameNode启动
Secondary NameNode工作