@
前言:浅谈Hadoop
Hadoop作为大数据入门的基石内容,其中HDFS更是所有生态的地基,so,我们有必要更深入去理解HDFS,以及HDFS在高可用的演变过程。如果有小可爱说hadoop和HDFS有啥区别的。の。。。,那容我之后在做背书来说明,暖男行为的先提一下:目前我们所说的Hadoop更多是指Hadoop的生态,包括hadoop本身及其他组件,如flume、kafka、hive、Hbase等等,如下图所示:
其中hadoop本身的结构(以Hadoop2.x为例,3.x亦复如是)如下:
Hadoop的发展历程
1.1 Hadoop产生背景
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顶级项目,迎来了它的快速发展期。
1.引入HDFS设计
HDFS作为Hadoop的旗舰级文件系统(也就是说除了HDFS,Hadoop也可以与其他文件存储系统集成,所以不能简单认为HDFS和Hadoop是有等价关系的哦。),被定义为以流式数据访问模式来存储超大文件,运行于廉价的商用硬件集群上。
我们解读一下其定义:
- 超大文件 。即超过几百MB或者GB甚至几百TB大小的文件来充分发挥其优势;
- 流式数据访问:也是HDFS的构建思想:一次写入、多次读取是最高效的访问模式。数据集通常由数据源生成或者从数据源复制而来,此后长时间在此数据集上进行各种分析,因此其读取数据的时间延迟不满足即席查询。HDFS是为高数据吞吐量应用优化的,对于低时间延迟的数据访问是不适合的,这是其优点也在某种程度看是缺点。
1.1 HDFS主要特性
- 支持超大文件。超大文件这里指的是几百MB、几百GB甚至几TB大小的文件,一般来说,一个Hadoop文件系统会存储T(1T=1024GB)、P(1P=1024TB)级别的数据。Hadoop需要能够支持这种级别的大文件;
- 检测和快速应对硬件故障:在大量通用硬件平台上构建集群时,故障,特别是硬件故障是常见的问题。一般的HDFS系统是由数百台甚至上千台存储者数据文件的服务器组成,这么多的服务器意味着高故障率。因此,过账检测和自动恢复是HDFS的一个设计目标;
- 流式数据访问:HDFS处理的数据规模都比较大,应用一般需要访问大量的数据。同时,这些应用一般是批量处理,而不是用户交互式处理。HDFS使应用程序能够以流的形式访问数据集,注重的是数据的吞吐量,而不是数据访问的速度;
- 简化的一致性模型:大部分的HDFS程序操作文件是需要一次写入,多次读取。在HDFS中,一个文件一旦经过创建、写入、关闭后,一半就不需要修改了。这样简单的一致性模型,有利于提供高吞吐量的数据访问模型。
2.HDFS体系结构
在一个全配置的集群上,“运行HDFS”意味着在网络分布的不同服务器上运行一些守护进程(daemon),这些进程有各自的特殊角色,并相互配合,一起形成一个分布式文件系统。
HDFS采用了主从(Master/Slaver)体系结构,名字节点 NameNode、数据节点DataNode和客户端Client是HDFS的三个重要的角色。
- NameNode 管理文件系统的命名空间,维护者文件系统及整棵树内所有的文件和目录。这些信息以两个文件形式永久保存在本地磁盘中:命名空间镜像文件(Fsimagexxx)和编辑日志文件(editxxx)。NameNode 也记录着每个文件中各个块所在的数据节点信息,但是并不会永久保存块的位置信息,而是在系统启动的时候根据数据节点信息重建(在系统启动的时候,各个DataNode会向NameNode进行注册,同时汇报数据节点的信息)。
- SecondaryNameNode,也称SNN或2NN,是用于定期合并命名空间镜像和镜像编辑日志的辅助守护进程。其与NameNode的区别是,2NN不接受或者说不记录HDFS的任何实时变化,而只是根据集群配置的时间间隔,不停的获取HDFS某一个时间点的命名空间镜像和镜像编辑日志,合并得到一个新的命名空间镜像。该新镜像会上传到NameNode,替换原有的命名空间镜像,并清空上述日志。2NN配合NameNode,为NameNode提供了一个简单的检查点(checkpoint)机制,并避免出现编辑日志过大,导致NameNode启动时间过长的问题。
- DataNode 是文件系统的工作节点,再其内部都会驻留一个数据节点的守护进程,来执行分布式文件系统中最忙碌的部分:将HDFS数据块写到Linux本地文件系统的事件文件中,或者从这些实际文件读取数据块。它们根据需要存储并检索数据块(受客户端和NameNode调度),并定期(心跳为每三秒一次,当NameNode10分钟没有接受到DataNode的心跳就认为节点不可用,发送信息为每一小时一次)向NameNode发送他们所存储的块的列表。
通常DataNode从磁盘读取数据块,但是对于访问频繁的文件,其对应的块可能被显式的缓存在DataNode的内存中,以堆外快缓存的形式存在。默认情况下一个块仅缓存一个DataNode的内存中。作业调度器(用于MapReduce、Spark和其他框架的)通过在缓存块的DataNode上运行任务,可以利用块缓存的优势提高读操作的性能。 - 客户端:用户与HDFS进行交互的手段。HDFS提供了各种各样的客户端,包括命令行接口、JavaAPI、用户文件系统等等。
此外,由于NameNode将文件系统的元数据存储在内存中,因此一个HDFS文件系统所能存储的文件总数受限于NameNode的内存容量(一般一个文件、目录和数据块的存储信息即元数据大约占150字节)。因此当有一百万个文件,且每个文件占一个数据块,至少需要300M的内存。
有必要说明一下:分布式文件系统中的块抽象带来的好处:
1.一个文件的大小可以大于网络中任意一个磁盘的容量,(也就是说我们需要的文件有2T,但是单个节点的存储只有1T,这也是Hadoop应用的得意之处)。文件的块并不需要存储在同一个磁盘上,我们将大文件按块划分为多个文件,并将这些块存储在任意的节点磁盘进行储存。
2.使用抽象块而不是整个文件作为存储单元,大大简化了存储子系统的设计。将存储子系统的处理独享设置为块,可简化存储管理,由于块的大小是固定,因此计算单个磁盘可以存储多少个块就相对简单。同事也消除了对元数据的顾虑,块只是来存储数据,并不存储文件的元数据,就可以将数据和元数据分离,单独管理元数据。
3.块的抽象非常适合数据备份,将每个块复制到几个物理上互相独立的节点(默认为3),可以确保在块、磁盘或者机器发生故障后数据不会丢失。如果发现一个块不可用,系统会从其他地方读取另一个副本,而这个过程对用户是透明的。且当一个损坏或机器故障而丢失的块可以从其他存储的节点复制到另一台正常运行的机器上,保证复本数量是满足设定的。进而提供了系统数据容错能力和可用性。
HDFS工作流程机制
当我们向HDFS上传数据的时候是怎么进行,HDFS是如何工作的呢?基于NameNode和DataNode分别都做了那些工作呢?我们一一道来。
1.各个节点是如何互通有无的?
分布式系统,如浏览器和服务器端的通信,需要在不同的实体中显示交换信息,处理诸如消息的编解码、发送和接收等具体任务。Hadoop中各个实体间的交互通过远程过程调用(RPC),让用户可以像调用本地方法一样调用另外一个应用程序提供的服务,而不必关注具体的实现。从而提升了可操作性和交互性。那我们先来了解一下什么是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对象,(DFSOutputStream是对应的实例对象),此对象处理DataNode和NameNode之间的通信。
用更通俗的话来说,这一步可以细分为两个步骤,Distributed FileSystem 一下简写DFS。- I 客户端通过DFS对象对NameNode发送请求创建一个和上传文件同名的空文件,此时NameNode会检测是否有同名文件以及请求方是否有创建文件的权限。这个时候会根据检查结果返回不同的响应结果。
- II 若检查通过,DFS对象会给客户端一个FSDataOutputStream对象,就像是给客户端一个写入数据的入口,客户端不必关心具体是向那些DataNode发送数据的,这些工作都交给FSDataOutputStream中的DFSOutputStream实例对象来完成,客户端就向此对象写入数据流即可。
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之间的数据流,我们参考上图解释一下读取文件是发生的事件顺序。
- 1)客户端通过调用FileSystem(Java API )对象的open()方法来打开希望读取的文件,对于HDFS来说,该对象是DistributedFileSystem的一个实例;
- 2)DFS(DistributedFileSystem)通过使用远程过程调用(RPC)来调用NameNode,以确定文件起始块的位置,对于每一个块,NameNode返回存有该副本的DataNode地址。DataNode的选择依旧是依据集群的网络拓扑排序。(如果该客户端本身就是一个DataNode【比如:在一个MapReduce任务中】,那么客户端就会从保存有相应数据块复本的本地DataNode读取数据);
- 3)DFS类返回一个FSDataInputStream对象(一个支持文件的输入流)给客户端以便读取数据。FSDataInputStream 类内封装DFSInputStream对象,由该对象管理DataNode和NameNode的I/O。接着客户端对这个输入流调用read()方法。
- 4)存储文件起始几个块的DataNode地址的DFSInputStream随机连接距离最近的文件中第一个块所在的DataNode。通过对数据流反复调用read()方法,将数据从DataNode传输到客户端。到达块的末端时,DFSInputStream关闭与该DataNode的连接,然后寻找下一个块的最佳DataNode。所有操作对于客户端都是透明的,对客户端而言都是读取一个连续的流。
- 5)客户端从流中读取数据时,块是按照打开DFSInputStream与DataNode新建连接的顺序读取的。会根据需要询问NameNode来检索下一批数据块的DataNode位置。一旦客户端完成读取,就对FSDataInputStream调用close()方法。
在整个读取过程中,如果DFSInputStream与DataNode通信时遇到错误,会尝试从这个快的另外一个最近邻DataNode读取数据,也会记住故障DataNode,保证以后不会反复读取该节点上后续的块。DFSInputStream也会通过校验和确认从DataNode发来的数据是否完整。如果发现有损坏的块,DFSInputStream会试图从其他DataNode读取其复本,同时会将损坏的块通知NameNode。
结论
HDFS是怎么保证运行的?
NameNode 容错机制
因此,对于NameNode实现容错非常重要,Hadoop为此提供了两种机制。
- 1.第一种机制是备份那些组成文件系统元数据持久状态的文件。Hadoop可以通过配置使NameNode在多个文件系统上保存元数据的持久状态。这些写操作是实时同步且是原子性的。当时NameNode的处理效率会变低,实时去持久化是不被接受的。一般的配置:将持久状态写入本地磁盘的同时,写入一个远程挂在的网络文件系统(NFS)。
- 2运行一个辅助NameNode,但不能被用作NameNode。这个辅助NameNode的重要作用是 定期合并编辑日志与命名空间镜像,以防止编辑日志过大。这个辅助NameNode一般在另一台单独的物理计算机上运行,他需要占用大量CPU时间,并且需要和NameNode一样多的内存来执行合并操作。他会保存合并后的命名空间镜像的副本,并在NameNode发生故障时启动。但是辅助NameNode保存的状态总是滞后于主节点,所以在主节点全部失效时,难免会丢失部分数据。这种情况下,一般是将存储在NFS的NameNode元数据复制到辅助NameNode并作为新的主NameNode运行。
方式二的部分也正是hadoop2.x就有的方式,配置NameNode(NN)和secondNameNode(2NN)。且2NN可以将NameNode的镜像文件和编辑日志文件合并过程接手处理,减少NameNode的额外开销。具体的NN和2NN之间的处理关系会稍后详细在讲。
如何NN突破内存限制?联邦HDFS设计思想
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需要满足以下情况才能响应服务:
- 1)将命名空间的映像导入内存中
- 2) 重演编辑日志;
- 3) 接受到足够多的来自DataNode的数据块报告并退出安全模式。
对于一个大型并拥有大量文件和数据块的集群,NameNode的冷启动需要30分钟,或者更长时间。系统恢复时间太长,也会影响到日常维护。
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启动
- (1)第一次启动NameNode格式化后,创建Fsimage和Edits文件。如果不是第一次启动,直接加载编辑日志和镜像文件到内存。
- (2)客户端对元数据进行增删改的请求。
- (3)NameNode记录操作日志,更新滚动日志。
- (4)NameNode在内存中对元数据进行增删改。
Secondary NameNode工作
- (1)Secondary NameNode询问NameNode是否需要CheckPoint(checkpoint在2NN默认每隔一小时执行一次checkpoint检测,查看是否需要execute checkpoint,并每一分钟进行一次操作数检测,当操作数达100万时,2NN执行checkpoint)。直接带回NameNode是否检查结果。
- (2)Secondary NameNode请求执行CheckPoint。
- (3)NameNode滚动正在写的Edits日志。
- (4)将滚动前的编辑日志和镜像文件拷贝到Secondary NameNode。
- (5)Secondary NameNode加载编辑日志和镜像文件到内存,并合并。
- (6)生成新的镜像文件fsimage.chkpoint。
- (7)拷贝fsimage.chkpoint到NameNode。
- (8)NameNode将fsimage.chkpoint重新命名成fsimage。