Fork me on GitHub

HDFS系列 -- HDFS预研

1 HDFS概述

由于传统集中式的物理服务器在存储容量和数据传输速度等方面都有限制,故而越来越不符合这些数据的实际存储需要。

在大数据时代,大数据处理需要解决的首要问题是:如何高效地存储所产生的规模庞大的数据?

所以为了实现对大数据的存储,就需要利用成百上千台甚至更多的分布式服务器节点(由多磁盘存储到多机器存储)。同时,为了对这些分布式服务器节点上存储的数据进行统一管理,必须要使用一种特殊的文件系统 --- 分布式文件系统。

HDFS(Hadoop Distributed File System)是一个可以运行在通用硬件上、提供流式数据访问和存储海量数据的分布式文件存储系统,最早是为开源的Apache Nutch项目的基础架构而开发的,是Apache Hadoop Core项目的一部分,目前HDFS已经发展为Hadoop应用体系的存储基石,在Hadoop应用体系所处的位置如图1-1所示。

HDFS不仅和现有的一些分布式文件系统有很多相似的地方,还具有不同于其他系统的独特优点,比如:高度容错、高吞吐量、容易扩展和高可靠性等特征。

图1-1 HDFS在Hadoop应用体系所处的位置

1.1 HDFS基本特性

  1. 大规模数据存储能力

    • HDFS凭借分布存储的方式和良好的可扩展性提供了大规模数据存储能力。
    • 典型文件大小为千兆字节到太字节;
    • 支持单个实例中的数千万个文件;
    • 支持扩展至10K+节点。
  2. 高并发访问能力

    • 移动计算而非数据;
    • 数据位置暴露给计算框架;
    • 数据访问的高吞吐量,支持多节点并发的方式进行数据集进行流式访问。
  3. 高度容错能力

    • 可以由数百或数千个服务器机器组成,每个服务器机器存储文件系统数据的一部分;
    • 数据自动保存多个副本;
    • 副本丢失后检测故障快速,自动恢复。
  4. 顺序式文件访问

在大数据批处理中的多数情况下,都是对大量的数据记录进行简单的顺序处理。针对这个特点,为了提高对大量数据记录的访问效率,HDFS对数据的顺序读操作进行了优化操作,确保了大量数据信息能够快速地顺序读出。

  1. 简单一致性模型

HDFS采用了简单的"一次写多次读"模式来对文件进行访问,能够很好地保证数据访问的高吞吐量。它仅支持大量数据的一次写入、多次读取,不支持对已写入数据执行更新操作,但允许在文件尾部"追加写"新的数据。

  1. 大粒度数据块存储模式

HDFS也采用数据块存储的模式对文件进行存储。但与传统的文件系统中大小为若干KB的数据块不同的是:它采用基于大粒度数据块的方式来存储文件,默认一个数据块的大小是64MB(HDFS2.X版本之后是128M)。这样:一方面可以减少元数据的数量,并允许将这些数据块通过随机的方式选择DataNode节点存放,最终把这些数据块分布存储在不同的地方;另一方面是为了减少寻址开销的时间,这是因为:在HDFS中,当Client发起数据访问请求时,NameNode会首先检索文件对应的数据块信息,找到存放数据块的DataNode;DataNode则会根据数据块信息,在自身的存储目录中寻找相应的文件,进而与Client应用程序之间交换数据。因为检索的过程都是单机运行,所以将单个数据块设置的大一些,能够减少系统访问数据的寻址频度和吋间开销。

1.2 HDFS不足之处

  1. 不适合低延迟的数据访问

HDFS设计更多的是批处理,而不是用户交互使用。

其设计之初的目的就是高吞吐量的数据访问需要而不是为了提供低时间延迟的数据访问的分布式文件系统,一般在秒级。

对于数据访问有低时间延迟需求的应用可以尝试采用 Hbase。

  1. 不适合海量小文件存取

存储小文件也占用一个block的空间,导致占用DataNode大量空间;寻道时间超过读取时间。且占用Namenode元数据内存空间。

  1. 无法编辑文件、并发写入

只支持对文件的追加操作而不支持任意位置的写入操作。 需要修改文件只能进行覆盖操作。因为如果要随机写,由于文件被切块,需要先找到内容在哪个块,然后读入内存,修改完成之后再更新所有备份,由于一个块并不小,这个效率会很低吧?
所以,为了不乱套,一个文件只能有一个Writer,不能并发写。

  1. block太大也会导致

数据平衡性不好,单个 block 上传时只调用了3个 DataNode 的存储资源,没有充分利用整个集群的存储上限 。写入带宽也不够大。

1.3 HDFS系统架构

HDFS采用经典的主从式结构,一个HDFS集群主要包括NameNode、DataNode和HDFSClient三个核心组成部分,其体系结构如图1-2所示。在HDFS系统中,通常包括一个NameNode节点、一组DataNode节点和多个HDFSClient。其中,每个节点分别运行在不同的服务器上,并通过网络进行通信。

图1-2 HDFS架构1

图1-3 HDFS架构2

1.4 HDFS基本组成

1.4.1 NameNode

Namenode起一个统领的作用,用户通过namenode来实现对其他数据的访问和操作,类似于root根目录的感觉。下面介绍NameNode主要功能。

  1. 管理元数据及数据块

在 HDFS 中,元数据虽然数量不多,但对文件系统至关重要,整个文件系统产生的元数据都由 NameNode 负责。这些元数据分为三种: 文件属性信息文件到数据块的映射 以及 数据块在 DataNode 上的具体分布映射信息 。此外,系统中对于数据块所有操作,如创建、复制、删除也由 NameNode 负责。

文件到数据块的映射:反映了文件的分块情况,和命名空间一起在NameNode 启动时主动加载到内存。

数据块在DataNode上映射信息:反映了每个数据块的分布位置并未持久化到本地,而是在NameNode 启动时由 DataNode 上报给 NameNode 节点。这是因为集群中 DataNode 会根据具体应用经常动态的加入或离开,DataNode 节点上数据块信息并不确定,系统随时依照具体情况对数据块做出处理,如复制失效 DataNode 节点上的数据块保证高可靠性,将各 DataNode上的数据块迁移保证负载均衡,回收集群中失效孤立的数据块保证空间不被占用等。

  1. NameSpace持久化

文件系统NameSpace概念

HDFS支持传统的层级文件组织形式。 用户或应用程序可以创建目录和存储文件在这些目录。 namespace即代表当前文件系统的各种状态,文件系统 namespace 层次结构类似于其他大多数现有的文件系统:

可以创建和删除文件,一个文件从一个目录移到另一个,或重命名一个文件;也支持 user quotas 和 access permissions ;但是不支持硬链接和软链接。

基于系统访问效率的考虑,HDFS 将NameSpace 以 事务日志(EditLog)和镜像文件(FSImage) 的形式持久化地保存在 NameNode 本地磁盘中。对于文件系统的任何操作,NameNode都会在 EditLog 中插入相应记录,例如当客户端向文件系统写入一个文件时,NameNode就会向 EditLog 中插入一条文件写入记录。

EditLog: 顾名思义,以日志形式保存了系统对于元数据的历史操作,即每当对命名空间做出修改或文件到数据块映射信息发生变化时,EditLog 中都会生成一条相关记录;

FSImage:一个定期对 EditLog 中的元数据信息进行备份的二进制镜像文件,保存了上次checkpoint 操作之前的 NameSpace、EditLog 和系统对NameSpace修改,保存在 NameNode 本地磁盘上。

依靠这两个文件,NameNode 能够快速将出错数据恢复,保证数据一致性。

  1. 监听并处理请求

NameNode 通过 DatanodeProtocol 接口和 ClientProtocol 接口,分别监听和处理来自DataNode 和客户端的请求。对 DataNode 的监听是为了实时获取整个集群的状态,涉及有心跳响应、数据块汇报和错误信息等。对客户端的监听一方面是为了确定客户端是否发起了文件或目录的创建、删除、移动、重命名等操作请求,另一方面是为了随时获取文件列表信息(HDFS的内部工作机制对客户端保持透明,客户端请求访问HDFS都是通过向NameNode申请来进行)。处理请求则是对上述两类监听事件做出相应响应。

  1. 接收心跳检测

为了及时检测集群中 DataNode 的健康状态,HDFS 设计了心跳检测机制。但该检测并不是由NameNode主动发起,而是其被动等待DataNode周期性上报自身负载情况。如果限定时间内始终未收到来自 DataNode 的信息,NameNode 就认为该 DataNode 节点已失效,对其做出相应的故障处理。

1.4.2 DataNode

DataNode作为真正存储数据的角色,有以下主要功能。

  1. 存储文件数据块

在 HDFS 中系统将每个文件分割成大小相同的 block (默认为 64M),每个block 设置副本系数(默认为 3),分布存储于不同 Data Node 中。因此读取文件时,必须通过NameNode 获取该文件的具体存储位置,然后去相应 DataNode 上以流的方式进行后续的交互通信。

  1. 向 NameNode 汇报状态

DataNode 需要依据配置文件中设定的时间阈值周期性向 NameNode 发送心跳包和文件块状态报告,默认时间分别为 3 秒和 1 小时。为了保证系统可靠,一旦确定某个 DataNode 失效,NameNode 向 DataNode 发起备份命令,将该失效节点上的所有数据块备份到其他 DataNode 上,保证每个数据块都满足系统设定的副本数量。

  1. 数据块的复制

基于文件容错性的考虑,HDFS 中每个文件在写入之初就被备份。当客户端有创建文件请求时,从 NameNode 处获取将要复制的数据块列表,列表中包含了每个数据块和备份放置的具体信息。然后将数据块内容写入列表中第一个节点,写入的同时第一个DataNode 上的数据以流的方式一段一段传输至第二个 DataNode 上,如此反复直到完成 所有数据块的备份。

1.4.3 Secondary NameNode

在非高可用的模式下,HDFS主要通过Secondary NameNode来处理元数据。

NameNode启动的时候,会从FSImage 中读取HDFS的状态,然后将 Edits log文件应用到FSImage中,得到最新的状态再将最新的HDFS状态写到FSImage,接着就开始正常的HDFS文件操作并写操作到一个空的Edits Log File。

因为NameNode只在启动的时候将 FSImage 和 Edits log files合并,所以 Edits log file可能会很大,导致合并花费很长时间。 NameNode本身的任务就非常重要,为了不再给NameNode压力,日志合并到fsimage 就引入了另一个角色SecondaryNamenode。(非高可用情况下)

SecondaryNameNode 并不是 Hadoop 第二个 NameNode,它虽然有 NameNode 元数据信息,但是不能更新元数据,不提供 NameNode 服务,所以不能作为NameNode使用。它仅仅是 NameNode 的一个工具,帮助 NameNode 管理 Metadata 数据。

Secondary NameNode负责按时间或者日志记录条数为间隔,将 NameNode 上积累的所有 EditLog 和一个最新的 FSImage 下载到本地,并加载到内存进行merge(这个过程称为checkpoint)(创建 namespace的 checkpoint) ,让 EditLog 大小保持在一定范围内。

checkpoint机制流程

  1)NameNode 向 Secondary NameNode 发送RPC请求,请求合并 EditLog 到FSImage,NameNode 停止写入 edits 文件,之后的 log 记录写入一个名为 edits.new 的文件。

  2)Secondary NameNode 收到请求后从 NameNode 上通过Http方式读取EditLog(多个,滚动日志文件)和 FSImager 文件。(考虑到网络传输,所以一般将 NameNode 和Secondary NameNode 放在相同的节点上,这样就无需走网络带宽了,以提高运行效率)

  3)Secondary NameNode 会根据拿到的 EditLog文件在本机进行合并(第一次hdfs初始化时才下载FSImage),创建出一个新的 FSImage 文件。(中间有很多步骤,把文件加载到内存,还原成元数据结构,合并,再生成文件,新生成的文件名为FSImage.checkpoint )。

  4)Secondary NameNode 通过http服务把 FSImage.checkpoint 文件上传到 NameNode,并且通过RPC调用把文件改名为FSImage。

5)NameNode 收到 Secondary NameNode 发回的 FSImage 后,就拿它覆盖掉原来的 FSImage 文件,并删除 edits 文件,把 edits.new 重命名为 edits。

通过这样一番操作,就避免了 NameNode 的 edits 日志的无限增长,加速 Namenode 的启动过程。具体 chcekpoint 流程如下图所示:

chcekpoint 流程图

checkpoint 操作的相关配置:

dfs.namenode.checkpoint.period=3600 // 两次checkpoint之间的时间间隔1小时,文件系统中达到这个操作数就进行强制 checkpointdfs.namenode.checkpoint.txns=1000000 // 两次checkpoint之间最大的操作记录,当文件系统中达到这个操作数就进行强制 checkpointdfs.namenode.checkpoint.check.period=60 // 检查触发条件是否满足的频率,60秒dfs.namenode.checkpoint.dir=file://${hadoop.tmp.dir}/dfs/namesecondarydfs.namenode.checkpoint.edits.dir=${dfs.namenode.checkpoint.dir}// 以上两个参数是做checkpoint操作时,Secondary NameNode 的本地工作目录dfs.namenode.checkpoint.max-retries=3 // 最大重试次数

EditLog 合并到 FSImage 文件存储在 $dfs.namenode.name.dir/current 目录下,这个目录可以在hdfs-site.xml中配置的。这个目录下的文件结构如下:

  

包括 edits 日志文件(滚动的多个文件),有一个是 edits_inprogress_* 是当前正在写的日志。fsimage 文件以及 md5 校检文件。seen_txid 是记录当前滚动序号, 代表 seen_txid 之前的日志都已经合并完成

  $dfs.namenode.name.dir/current/seen_txid 非常重要,是存放 transactionId 的文件,format之后是0,它代表的是 NameNode 里面的 edits_* 文件的尾数,NameNode 重启的时候,会按照seen_txid 的数字恢复。

所以当 HDFS 发生异常重启的时候,一定要比对 seen_txid 内的数字是不是 edits 最后的尾数,不然会发生重启 NameNode 时metaData的资料有缺少,导致误删 Datanode 上多余Block的信息。

NameNode 和 Secondary NameNode 的工作目录存储结构完全相同,所以,当 NameNode故障退出需要重新恢复时,可以从 Secondary NameNode 的工作目录中将 FSImage 拷贝到NameNode 的工作目录,以恢复 NameNode 的元数据。

2HDFS关键流程

2.1 HDFS运行原理

  1. NameNode和DataNode节点初始化完成后,通过RPC进行信息交换,采用的机制是心跳机制,即DataNode节点定时向NameNode反馈状态信息,反馈信息如:是否正常、磁盘空间大小、资源消耗情况等信息,以确保NameNode知道DataNode的情况;
  2. NameNode会将子节点的相关元数据信息缓存在内存中,对于文件与Block块的信息会通过fsImage 和 edits 文件方式持久化在磁盘上,以确保 NameNode 知道文件各个块的相关信息;

2.2 HDFS写数据流程

客户端要向 HDFS 写数据,首先要跟NameNode 通信,以确认可以写文件并获得接收文件 block的DataNode,然后,客户端按顺序将文件逐个 block 传递给相应DataNode,并由接收到 block 的DataNode 负责向其他 DataNode 复制block的副本。具体流程如下:

  1. HDFS Client 向 NameNode 发送一条写文件的请求。

  2. NameNode收到客户端提交的请求后,会先判断此客户端在此目录下是否有写权限,有的话NameNode 遍历查看文件系统,验证该文件为新文件(目标文件是否已存在,父目录是否存在),返回是否可以上传;

  3. HDFS Client 对文件做切分。比如 default block size 是 128M, 而上传文件是 300M,那么文件就会被分割成 3 个 block。

  4. HDFS Client 请求第一个block该传输到哪些 DataNode 服务器上。

  5. NameNode 通过分析集群情况,返回该 block 需要上传的 3个 DataNode地址(HDFS 的冗余策略是三副本)。

  6. Client 拿到数据存放节点位置信息后,会和对应的DataNode节点进行直接交互:通过建立pipeline,请求3台 DataNode 中的一台A上传数据(本质上是一个RPC调用,建立pipeline),A收到请求会继续调用B,然后B调用C,将整个pipeline建立完成,逐级返回客户端。

  7. Client 开始往A上传第一个block(先从磁盘读取数据放到一个本地内存缓存),以packet为单位,A收到一个packet就会传给B,B传给C;A每传一个packet会放入一个应答队列等待应答。

  8. 当一个block传输完成之后,client再次请求namenode上传第二个block的服务器。由此往复(重复4-7步骤),直到文件传输完毕。

  9. 随着所有副本写完后,客户端会收到数据节点反馈回来的一个成功状态,成功结束后,关闭与数据节点交互的通道,并反馈状态给NameNode,告诉NameNode文件已成功写入到对应的DataNode。

图2-1 HDFS写数据流程

比较重要的有以下几点

  1. 创建文件、上传 block 时需要先访问 NameNode。
  2. NameNode 上存放了文件对应的元数据、block 信息。维护一个 文件=》block文件=》datanode的映射。
  3. HDFS 客户端在上传、读取时直接与 DataNode 交互。

写入一致性

  1. 新建一个文件后,它能够在文件系统命名空间中立即可见。
  2. 写入文件的内容不保证立即可见(即逝数据流已经调用flush()方法刷新并存储)
  3. 当前正在写入的块对其他reader不可见。
  4. 通过hflush()方法后,数据被写入datanode的内存中。可保证对所有reader可见
  5. 通过hsync()方法后,数据被写入到磁盘上。
  6. 如果没有调用hflush或者hsync()方法。客户端在故障的情况下就会存在数据块丢失。

2.3 HDFS读数据流程

HDFS读数据流程如下:

  1. Client 提交读操作到 NameNode上,NameNode收到客户端提交的请求后,会先判断此客户端在此目录下是否有读权限
  2. Client 如果有权限,NameNode 获取文件的元信息(主要是block的存放位置信息)给Client 返回存放文件的 block 的节点信息,Client就可以到DataNode下去读取数据块。
  3. 对于每个block, Namenode都会返回有该 block 副本的 DataNode 地址;客户端Client会选取离客户端最近的DataNode来读取block;如果客户端本身就是DataNode,那么将从本地直接获取数据;
  4. Client 拿到块位置信息后,会去和相关的DataNode 直接构建读取通道,逐个获取文件的block,并在客户端本地进行数据追加合并从而获得整个文件。
  5. 当读完列表block后, 且文件读取还没有结束, 客户端会继续向Namenode获取下一批的block列表;
  6. 当所有数据块都读取完成后关闭通道,并给NameNode 返回状态信息,告诉NameNode已经读取完毕。

以上这些步骤对于客户端来说都是透明的。客户端只需通过 DistributedFileSystem 返回的 FSDataInputStream 读取数据即可。

如果客户端和所连接的 DataNode 在读取时出现故障,那么它就会去尝试连接存储这个块的下一个最近的DataNode,同时它会记录这个节点的故障,以免后面再次连接该节点。 客户端还会验证从 DataNode 传送过来的数据校验和。 如果发现一个损坏块,那么客户端将再尝试从别的DataNode读取数据块,并且会告诉NameNode 这个信息,NameNode也会更新保存的文件信息,进行数据修复。

图2-2 HDFS读数据流程

通过以上写入读出操作,可发现

  • 客户端写入、读取数据时直接与DataNode交互,能有效提高性能。
  • HDFS是设计成适应一次写入,多次读出的场景,且不支持文件的修改。需要频繁的RPC交互,写入性能不好。
  • HDFS 的中心思想是通过data locality 这一概念来实现的。Hadoop 在运行 Mapper 任务时,会尽量让计算任务落在更接近对应的数据节点,由此来减少数据在网络间的传输,达到很大的读取性能。就需要让 block 足够大(默认 128M),如果太小的话,那么 data locality 的效果就会大打折扣。

3 HDFS高可用

3.1. HA概述

相比于Hadoop1.0,Hadoop 2.0中的HDFS增加了两个重大特性,HA(HDFS-1623)和Federaion。HA用于解决NameNode单点故障问题,该特性通过热备的方式为Active NameNode提供一个备用者。Active NameNode对外提供服务,比如处理来自客户端的RPC请求,而Standby NameNode仅同步active namenode的状态,以便能够在它失败时快速进行切换。一旦主NameNode出现故障,可以迅速切换至备NameNode,从而实现不间断对外提供服务。Federation即为"联邦",该特性允许一个HDFS集群中存在多个NameNode同时对外提供服务,这些NameNode分管一部分目录(水平切分),彼此之间 相互隔离,但共享底层的DataNode存储资源。

在 Hadoop 1.0 时代,Hadoop 的两大核心组件 HDFS NameNode 和 JobTracker 都存在着单点问题,这其中以 NameNode 的单点问题尤为严重。因为 NameNode 保存了整个 HDFS 的元数据信息,一旦 NameNode 挂掉,整个 HDFS 就无法访问,同时 Hadoop 生态系统中依赖于 HDFS 的各个组件,包括 MapReduce、Hive、Pig 以及 HBase 等也都无法正常工作,并且重新启动 NameNode 和进行数据恢复的过程也会比较耗时。这些问题在给 Hadoop 的使用者带来困扰的同时,也极大地限制了 Hadoop 的使用场景,使得 Hadoop 在很长的时间内仅能用作离线存储和离线计算,无法应用到对可用性和数据一致性要求很高的在线应用场景中。

所幸的是,在 Hadoop2.0 中,HDFS NameNode 和 YARN ResourceManger(JobTracker 在 2.0 中已经被整合到 YARN ResourceManger 之中) 的单点问题都得到了解决,经过多个版本的迭代和发展,目前已经能用于生产环境。HDFS NameNode 和 YARN ResourceManger 的高可用方案基本类似,两者也复用了部分代码,但是由于 HDFS NameNode 对于数据存储和数据一致性的要求比 YARN ResourceManger 高得多,所以 HDFS NameNode 的高可用实现更为复杂一些,本文从内部实现的角度对 HDFS NameNode 的高可用机制进行详细的分析。

图3-1.HDFS NameNode 高可用整体架构

从上图中,我们可以看出 NameNode 的高可用架构主要分为下面几个部分:

  • Active NameNode 和 Standby NameNode:两台 NameNode 形成互备,一台处于 Active 状态,为主 NameNode,另外一台处于 Standby 状态,为备 NameNode,只有主 NameNode 才能对外提供读写服务。
  • 主备切换控制器 ZKFailoverController:ZKFailoverController 作为独立的进程运行, 对 NameNode 的主备切换进行总体控制。 ZKFailoverController 能及时检测到 NameNode 的健康状况,在主 NameNode 故障时借助 Zookeeper 实现自动的主备选举和切换,当然 NameNode 目前也支持不依赖于 Zookeeper 的手动主备切换。
  • Zookeeper 集群:为主备切换控制器提供主备选举支持。
  • 共享存储系统:共享存储系统是实现 NameNode 的高可用最为关键的部分,共享存储系统保存了 NameNode 在运行过程中所产生的 HDFS 的元数据。主 NameNode 和NameNode 通过共享存储系统实现元数据同步。在进行主备切换的时候,新的主 NameNode 在确认元数据完全同步之后才能继续对外提供服务。
  • DataNode 节点:除了通过共享存储系统共享 HDFS 的元数据信息之外,主 NameNode 和备 NameNode 还需要共享 HDFS 的数据块和 DataNode 之间的映射关系。DataNode 会同时向主 NameNode 和备 NameNode 上报数据块的位置信息。

下面分别介绍 NameNode 的主备切换实现和共享存储系统的实现。

3.2.NameNode 的主备切换实现

NameNode 主备切换主要由 ZKFailoverController、HealthMonitor 和 ActiveStandbyElector 这 3 个组件来协同实现:

ZKFailoverController 作为 NameNode 机器上一个独立的进程启动 (在 hdfs 启动脚本之中的进程名为 zkfc),启动的时候会创建 HealthMonitor 和 ActiveStandbyElector 这两个主要的内部组件,ZKFailoverController 在创建 HealthMonitor 和 ActiveStandbyElector 的同时,也会向 HealthMonitor 和 ActiveStandbyElector 注册相应的回调方法。

HealthMonitor 主要负责检测 NameNode 的健康状态,如果检测到 NameNode 的状态发生变化,会回调 ZKFailoverController 的相应方法进行自动的主备选举。

ActiveStandbyElector 主要负责完成自动的主备选举,内部封装了 Zookeeper 的处理逻辑,一旦 Zookeeper 主备选举完成,会回调 ZKFailoverController 的相应方法来进行 NameNode 的主备状态切换。

图3-2 NameNode 的主备切换流程

NameNode 实现主备切换的流程如图3-2所示,有以下几步:

  1. HealthMonitor 初始化完成之后会启动内部的线程来定时调用对应 NameNode 的 HAServiceProtocol RPC 接口的方法,对 NameNode 的健康状态进行检测。
  2. HealthMonitor 如果检测到 NameNode 的健康状态发生变化,会回调 ZKFailoverController 注册的相应方法进行处理。
  3. 如果 ZKFailoverController 判断需要进行主备切换,会首先使用 ActiveStandbyElector 来进行自动的主备选举。
  4. ActiveStandbyElector 与 Zookeeper 进行交互完成自动的主备选举。
  5. ActiveStandbyElector 在主备选举完成后,会回调 ZKFailoverController 的相应方法来通知当前的 NameNode 成为主 NameNode 或备 NameNode。
  6. ZKFailoverController 调用对应 NameNode 的 HAServiceProtocol RPC 接口的方法将 NameNode 转换为 Active 状态或 Standby 状态。

3.2.1. HealthMonitor 实现分析

ZKFailoverController 在初始化的时候会创建 HealthMonitor,HealthMonitor 在内部会启动一个线程来循环调用 NameNode 的 HAServiceProtocol RPC 接口的方法来检测 NameNode 的状态,并将状态的变化通过回调的方式来通知 ZKFailoverController。

HealthMonitor 主要检测 NameNode 的两类状态,分别是 HealthMonitor.State 和 HAServiceStatus。HealthMonitor.State 是通过 HAServiceProtocol RPC 接口的 monitorHealth 方法来获取的,反映了 NameNode 节点的健康状况,主要是磁盘存储资源是否充足。HealthMonitor.State 包括下面几种状态:

  • INITIALIZING:HealthMonitor 在初始化过程中,还没有开始进行健康状况检测
  • SERVICE_HEALTHY:NameNode 状态正常;
  • SERVICE_NOT_RESPONDING:调用 NameNode 的 monitorHealth 方法调用无响应或响应超时;
  • SERVICE_UNHEALTHY:NameNode 还在运行,但是 monitorHealth 方法返回状态不正常,磁盘存储资源不足;
  • HEALTH_MONITOR_FAILED:HealthMonitor 自己在运行过程中发生了异常,不能继续检测 NameNode 的健康状况,会导致 ZKFailoverController 进程退出;

HealthMonitor.State 在状态检测之中起主要的作用,在 HealthMonitor.State 发生变化的时候,HealthMonitor 会回调 ZKFailoverController 的相应方法来进行处理,具体处理见后文 ZKFailoverController 部分所述。

而 HAServiceStatus 则是通过 HAServiceProtocol RPC 接口的 getServiceStatus 方法来获取的,主要反映的是 NameNode 的 HA 状态,包括:

  • INITIALIZING:NameNode 在初始化过程中;
  • ACTIVE:当前 NameNode 为主 NameNode;
  • STANDBY:当前 NameNode 为备 NameNode;
  • STOPPING:当前 NameNode 已停止;

HAServiceStatus 在状态检测之中只是起辅助的作用,在 HAServiceStatus 发生变化时,HealthMonitor 也会回调 ZKFailoverController 的相应方法来进行处理,具体处理见后文 ZKFailoverController 部分所述。

3.2.2. ActiveStandbyElector 实现分析

Namenode(包括 YARN ResourceManager) 的主备选举是通过 ActiveStandbyElector 来完成的,ActiveStandbyElector 主要是利用了 Zookeeper 的写一致性和临时节点机制,具体的主备选举实现如下:

创建锁节点

如果 HealthMonitor 检测到对应的 NameNode 的状态正常,那么表示这个 NameNode 有资格参加 Zookeeper 的主备选举。如果目前还没有进行过主备选举的话,那么相应的 ActiveStandbyElector 就会发起一次主备选举,尝试在 Zookeeper 上创建一个路径为/hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock 的临时节点 (${dfs.nameservices} 为 Hadoop 的配置参数 dfs.nameservices 的值,下同),Zookeeper 的写一致性会保证最终只会有一个 ActiveStandbyElector 创建成功,那么创建成功的 ActiveStandbyElector 对应的 NameNode 就会成为主 NameNode,ActiveStandbyElector 会回调 ZKFailoverController 的方法进一步将对应的 NameNode 切换为 Active 状态。而创建失败的 ActiveStandbyElector 对应的 NameNode 成为备 NameNode,ActiveStandbyElector 会回调 ZKFailoverController 的方法进一步将对应的 NameNode 切换为 Standby 状态。

注册 Watcher 监听

不管创建/hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock 节点是否成功,ActiveStandbyElector 随后都会向 Zookeeper 注册一个 Watcher 来监听这个节点的状态变化事件,ActiveStandbyElector 主要关注这个节点的 NodeDeleted 事件。

自动触发主备选举

如果 Active NameNode 对应的 HealthMonitor 检测到 NameNode 的状态异常时, ZKFailoverController 会主动删除当前在 Zookeeper 上建立的临时节点/hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock,这样处于 Standby 状态的 NameNode 的 ActiveStandbyElector 注册的监听器就会收到这个节点的 NodeDeleted 事件。收到这个事件之后,会马上再次进入到创建/hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock 节点的流程,如果创建成功,这个本来处于 Standby 状态的 NameNode 就选举为主 NameNode 并随后开始切换为 Active 状态。

当然,如果是 Active 状态的 NameNode 所在的机器整个宕掉的话,那么根据 Zookeeper 的临时节点特性,/hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock 节点会自动被删除,从而也会自动进行一次主备切换。

防止脑裂

Zookeeper 在工程实践的过程中经常会发生的一个现象就是 Zookeeper 客户端"假死",所谓的"假死"是指如果 Zookeeper 客户端机器负载过高或者正在进行 JVM Full GC,那么可能会导致 Zookeeper 客户端到 Zookeeper 服务端的心跳不能正常发出,一旦这个时间持续较长,超过了配置的 Zookeeper Session Timeout 参数的话,Zookeeper 服务端就会认为客户端的 session 已经过期从而将客户端的 Session 关闭。"假死"有可能引起分布式系统常说的双主或脑裂 (brain-split) 现象。具体到本文所述的 NameNode,假设 NameNode1 当前为 Active 状态,NameNode2 当前为 Standby 状态。如果某一时刻 NameNode1 对应的 ZKFailoverController 进程发生了"假死"现象,那么 Zookeeper 服务端会认为 NameNode1 挂掉了,根据前面的主备切换逻辑,NameNode2 会替代 NameNode1 进入 Active 状态。但是此时 NameNode1 可能仍然处于 Active 状态正常运行,即使随后 NameNode1 对应的 ZKFailoverController 因为负载下降或者 Full GC 结束而恢复了正常,感知到自己和 Zookeeper 的 Session 已经关闭,但是由于网络的延迟以及 CPU 线程调度的不确定性,仍然有可能会在接下来的一段时间窗口内 NameNode1 认为自己还是处于 Active 状态。这样 NameNode1 和 NameNode2 都处于 Active 状态,都可以对外提供服务。这种情况对于 NameNode 这类对数据一致性要求非常高的系统来说是灾难性的,数据会发生错乱且无法恢复。Zookeeper 社区对这种问题的解决方法叫做 fencing,中文翻译为隔离,也就是想办法把旧的 Active NameNode 隔离起来,使它不能正常对外提供服务。

ActiveStandbyElector 为了实现 fencing,会在成功创建 Zookeeper 节点 hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock 从而成为 Active NameNode 之后,创建另外一个路径为/hadoop-ha/${dfs.nameservices}/ActiveBreadCrumb 的持久节点,这个节点里面保存了这个 Active NameNode 的地址信息。Active NameNode 的 ActiveStandbyElector 在正常的状态下关闭 Zookeeper Session 的时候 (注意由于/hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock 是临时节点,也会随之删除),会一起删除节点/hadoop-ha/${dfs.nameservices}/ActiveBreadCrumb。但是如果 ActiveStandbyElector 在异常的状态下 Zookeeper Session 关闭 (比如前述的 Zookeeper 假死),那么由于/hadoop-ha/${dfs.nameservices}/ActiveBreadCrumb 是持久节点,会一直保留下来。后面当另一个 NameNode 选主成功之后,会注意到上一个 Active NameNode 遗留下来的这个节点,从而会回调 ZKFailoverController 的方法对旧的 Active NameNode 进行 fencing,具体处理见后文 ZKFailoverController 部分所述。

3.2.3. ZKFailoverController 实现分析

ZKFailoverController 在创建 HealthMonitor 和 ActiveStandbyElector 的同时,会向 HealthMonitor 和 ActiveStandbyElector 注册相应的回调函数,ZKFailoverController 的处理逻辑主要靠 HealthMonitor 和 ActiveStandbyElector 的回调函数来驱动。

对 HealthMonitor 状态变化的处理

如前所述,HealthMonitor 会检测 NameNode 的两类状态,HealthMonitor.State 在状态检测之中起主要的作用,ZKFailoverController 注册到 HealthMonitor 上的处理 HealthMonitor.State 状态变化的回调函数主要关注 SERVICE_HEALTHY、SERVICE_NOT_RESPONDING 和 SERVICE_UNHEALTHY 这 3 种状态:

如果检测到状态为 SERVICE_HEALTHY,表示当前的 NameNode 有资格参加 Zookeeper 的主备选举,如果目前还没有进行过主备选举的话,ZKFailoverController 会调用 ActiveStandbyElector 的 joinElection 方法发起一次主备选举。

如果检测到状态为 SERVICE_NOT_RESPONDING 或者是 SERVICE_UNHEALTHY,就表示当前的 NameNode 出现问题了,ZKFailoverController 会调用 ActiveStandbyElector 的 quitElection 方法删除当前已经在 Zookeeper 上建立的临时节点退出主备选举,这样其它的 NameNode 就有机会成为主 NameNode。

而 HAServiceStatus 在状态检测之中仅起辅助的作用,在 HAServiceStatus 发生变化时,ZKFailoverController 注册到 HealthMonitor 上的处理 HAServiceStatus 状态变化的回调函数会判断 NameNode 返回的 HAServiceStatus 和 ZKFailoverController 所期望的是否一致,如果不一致的话,ZKFailoverController 也会调用 ActiveStandbyElector 的 quitElection 方法删除当前已经在 Zookeeper 上建立的临时节点退出主备选举。

对 ActiveStandbyElector 主备选举状态变化的处理

在 ActiveStandbyElector 的主备选举状态发生变化时,会回调 ZKFailoverController 注册的回调函数来进行相应的处理:

如果 ActiveStandbyElector 选主成功,那么 ActiveStandbyElector 对应的 NameNode 成为主 NameNode,ActiveStandbyElector 会回调 ZKFailoverController 的 becomeActive 方法,这个方法通过调用对应的 NameNode 的 HAServiceProtocol RPC 接口的 transitionToActive 方法,将 NameNode 转换为 Active 状态。

如果 ActiveStandbyElector 选主失败,那么 ActiveStandbyElector 对应的 NameNode 成为备 NameNode,ActiveStandbyElector 会回调 ZKFailoverController 的 becomeStandby 方法,这个方法通过调用对应的 NameNode 的 HAServiceProtocol RPC 接口的 transitionToStandby 方法,将 NameNode 转换为 Standby 状态。

如果 ActiveStandbyElector 选主成功之后,发现了上一个 Active NameNode 遗留下来的/hadoop-ha/${dfs.nameservices}/ActiveBreadCrumb 节点 (见"ActiveStandbyElector 实现分析"一节"防止脑裂"部分所述),那么 ActiveStandbyElector 会首先回调 ZKFailoverController 注册的 fenceOldActive 方法,尝试对旧的 Active NameNode 进行 fencing,在进行 fencing 的时候,会执行以下的操作:

首先尝试调用这个旧 Active NameNode 的 HAServiceProtocol RPC 接口的 transitionToStandby 方法,看能不能把它转换为 Standby 状态。

如果 transitionToStandby 方法调用失败,那么就执行 Hadoop 配置文件之中预定义的隔离措施,Hadoop 目前主要提供两种隔离措施,通常会选择 sshfence:

sshfence:通过 SSH 登录到目标机器上,执行命令 fuser 将对应的进程杀死;

shellfence:执行一个用户自定义的 shell 脚本来将对应的进程隔离;

只有在成功地执行完成 fencing 之后,选主成功的 ActiveStandbyElector 才会回调 ZKFailoverController 的 becomeActive 方法将对应的 NameNode 转换为 Active 状态,开始对外提供服务。

3.3.NameNode 的共享存储实现

过去几年中 Hadoop 社区涌现过很多的 NameNode 共享存储方案,比如 shared NAS+NFS、BookKeeper、BackupNode 和 QJM(Quorum Journal Manager) 等等。目前社区已经把由 Clouderea 公司实现的基于 QJM 的方案合并到 HDFS 的 trunk 之中并且作为默认的共享存储实现,本部分只针对基于 QJM 的共享存储方案的内部实现原理进行分析。为了理解 QJM 的设计和实现,首先要对 NameNode 的元数据存储结构有所了解。

3.3.1. NameNode 的元数据存储概述

一个典型的 NameNode 的元数据存储目录结构如下图所示,这里主要关注其中的 EditLog 文件和 FSImage 文件:

图3-3 NameNode 的元数据存储目录结构

NameNode 在执行 HDFS 客户端提交的创建文件或者移动文件这样的写操作的时候,会首先把这些操作记录在 EditLog 文件之中,然后再更新内存中的文件系统镜像。

内存中的文件系统镜像用于 NameNode 向客户端提供读服务,而 EditLog 仅仅只是在数据恢复的时候起作用。

记录在 EditLog 之中的每一个操作又称为一个事务,每个事务有一个整数形式的事务 id 作为编号。EditLog 会被切割为很多段,每一段称为一个 Segment。正在写入的 EditLog Segment 处于 in-progress 状态,其文件名形如 edits_inprogress_${start_txid},其中${start_txid} 表示这个 segment 的起始事务 id,例如上图中的 edits_inprogress_0000000000000000020。而已经写入完成的 EditLog Segment 处于 finalized 状态,其文件名形如 edits_${start_txid}-${end_txid},其中${start_txid} 表示这个 segment 的起始事务 id,${end_txid} 表示这个 segment 的结束事务 id,例如上图中的 edits_0000000000000000001-0000000000000000019。

NameNode 会定期对内存中的文件系统镜像进行 checkpoint 操作,在磁盘上生成 FSImage 文件,FSImage 文件的文件名形如 fsimage_${end_txid},其中${end_txid} 表示这个 fsimage 文件的结束事务 id,例如上图中的 fsimage_0000000000000000020。在 NameNode 启动的时候会进行数据恢复,首先把 FSImage 文件加载到内存中形成文件系统镜像,然后再把 EditLog 之中 FsImage 的结束事务 id 之后的 EditLog 回放到这个文件系统镜像上。

3.3.2. 基于 QJM 的共享存储系统的总体架构

到内存中形成文件系统镜像,然后再把 EditLog 之中 FsImage 的结束事务 id 之后的 EditLog 回放到这个文基于 QJM 的共享存储系统主要用于保存 EditLog,并不保存 FSImage 文件。FSImage 文件还是在 NameNode 的本地磁盘上。QJM 共享存储的基本思想来自于 Paxos 算法,采用多个称为 JournalNode 的节点组成的 JournalNode 集群来存储 EditLog。每个 JournalNode 保存同样的 EditLog 副本。

每次 NameNode 写 EditLog 的时候,除了向本地磁盘写入 EditLog 之外,也会并行地向 JournalNode 集群之中的每一个 JournalNode 发送写请求,只要大多数 (majority) 的 JournalNode 节点返回成功就认为向 JournalNode 集群写入 EditLog 成功。如果有 2N+1 台 JournalNode,那么根据大多数的原则,最多可以容忍有 N 台 JournalNode 节点挂掉。

基于 QJM 的共享存储系统的内部实现架构图如下图所示,主要包含下面几个主要的组件:

 图3-4 QJM 的共享存储系统的内部实现架构图

  • FSEditLog:这个类封装了对 EditLog 的所有操作,是 NameNode 对 EditLog 的所有操作的入口。
  • JournalSet: 这个类封装了对本地磁盘和 JournalNode 集群上的 EditLog 的操作,内部包含了两类 JournalManager,一类为 FileJournalManager,用于实现对本地磁盘上 EditLog 的操作。一类为 QuorumJournalManager,用于实现对 JournalNode 集群上共享目录的 EditLog 的操作。FSEditLog 只会调用 JournalSet 的相关方法,而不会直接使用 FileJournalManager 和 QuorumJournalManager。
  • FileJournalManager:封装了对本地磁盘上的 EditLog 文件的操作,不仅 NameNode 在向本地磁盘上写入 EditLog 的时候使用 FileJournalManager,JournalNode 在向本地磁盘写入 EditLog 的时候也复用了 FileJournalManager 的代码和逻辑。
  • QuorumJournalManager:封装了对 JournalNode 集群上的 EditLog 的操作,它会根据 JournalNode 集群的 URI 创建负责与 JournalNode 集群通信的类 AsyncLoggerSet, QuorumJournalManager 通过 AsyncLoggerSet 来实现对 JournalNode 集群上的 EditLog 的写操作,对于读操作,QuorumJournalManager 则是通过 Http 接口从 JournalNode 上的 JournalNodeHttpServer 读取 EditLog 的数据。
  • AsyncLoggerSet:内部包含了与 JournalNode 集群进行通信的 AsyncLogger 列表,每一个 AsyncLogger 对应于一个 JournalNode 节点,另外 AsyncLoggerSet 也包含了用于等待大多数 JournalNode 返回结果的工具类方法给 QuorumJournalManager 使用。
  • AsyncLogger:具体的实现类是 IPCLoggerChannel,IPCLoggerChannel 在执行方法调用的时候,会把调用提交到一个单线程的线程池之中,由线程池线程来负责向对应的 JournalNode 的 JournalNodeRpcServer 发送 RPC 请求。
  • JournalNodeRpcServer:运行在 JournalNode 节点进程中的 RPC 服务,接收 NameNode 端的 AsyncLogger 的 RPC 请求。
  • JournalNodeHttpServer:运行在 JournalNode 节点进程中的 Http 服务,用于接收处于 Standby 状态的 NameNode 和其它 JournalNode 的同步 EditLog 文件流的请求。

下面对基于 QJM 的共享存储系统的两个关键性问题同步数据和恢复数据进行详细分析。

3.3.3. 基于 QJM 的共享存储系统的数据同步机制分析

Active NameNode 和 StandbyNameNode 使用 JouranlNode 集群来进行数据同步的过程,如下图所示。Active NameNode 首先把 EditLog 提交到 JournalNode 集群,然后 Standby NameNode 再从 JournalNode 集群定时同步 EditLog:

图3-5 基于 QJM 的共享存储的数据同步机制

Active NameNode 提交 EditLog 到 JournalNode 集群

当处于 Active 状态的 NameNode 调用 FSEditLog 类的 logSync 方法来提交 EditLog 的时候,会通过 JouranlSet 同时向本地磁盘目录和 JournalNode 集群上的共享存储目录写入 EditLog。

写入 JournalNode 集群是通过并行调用每一个 JournalNode 的 QJournalProtocol RPC 接口的 journal 方法实现的,如果对大多数 JournalNode 的 journal 方法调用成功,那么就认为提交 EditLog 成功,否则 NameNode 就会认为这次提交 EditLog 失败。提交 EditLog 失败会导致 Active NameNode 关闭 JournalSet 之后退出进程,留待处于 Standby 状态的 NameNode 接管之后进行数据恢复。

从上面的叙述可以看出, Active NameNode 提交 EditLog 到 JournalNode 集群的过程实际上是同步阻塞的 ,但是并不需要所有的 JournalNode 都调用成功,只要大多数 JournalNode 调用成功就可以了。 如果无法形成大多数,那么就认为提交 EditLog 失败,NameNode 停止服务退出进程。

如果对应到分布式系统的 CAP 理论的话,虽然采用了 Paxos 的"大多数"思想对 C(consistency,一致性) 和 A(availability,可用性) 进行了折衷,但还是可以认为 NameNode 选择了 C 而放弃了 A,这也符合 NameNode 对数据一致性的要求。

Standby NameNode 从 JournalNode 集群同步 EditLog

当 NameNode 进入 Standby 状态之后,会启动一个 EditLogTailer 线程。这个线程会定期调用 EditLogTailer 类的 doTailEdits 方法从 JournalNode 集群上同步 EditLog,然后把同步的 EditLog 回放到内存之中的文件系统镜像上 (并不会同时把 EditLog 写入到本地磁盘上)。

这里需要关注的是:从 JournalNode 集群上同步的 EditLog 都是处于 finalized 状态的 EditLog Segment。

Active NameNode 在完成一个 EditLog Segment 的写入之后,就会向 JournalNode 集群发送 finalizeLogSegment RPC 请求,将完成写入的 EditLog Segment finalized,然后开始下一个新的 EditLog Segment。

一旦 finalizeLogSegment 方法在大多数的 JournalNode 上调用成功,表明这个 EditLog Segment 已经在大多数的 JournalNode 上达成一致。一个 EditLog Segment 处于 finalized 状态之后,可以保证它再也不会变化。

从上面描述的过程可以看出,虽然 Active NameNode 向 JournalNode 集群提交 EditLog 是同步的,但 Standby NameNode 采用的是定时从 JournalNode 集群上同步 EditLog 的方式,那么 Standby NameNode 内存中文件系统镜像有很大的可能是落后于 Active NameNode 的,所以 Standby NameNode 在转换为 Active NameNode 的时候需要把落后的 EditLog 补上来。

3.3.4. 基于 QJM 的共享存储系统的数据恢复机制分析

处于 Standby 状态的 NameNode 转换为 Active 状态的时候,有可能上一个 Active NameNode 发生了异常退出,那么 JournalNode 集群中各个 JournalNode 上的 EditLog 就可能会处于不一致的状态,所以 首先要做的事情就是让 JournalNode 集群中各个节点上的 EditLog 恢复为一致。

另外如前所述,当前处于 Standby 状态的 NameNode 的内存中的文件系统镜像有很大的可能是落后于旧的 Active NameNode 的,所以在 JournalNode 集群中各个节点上的 EditLog 达成一致之后, 接下来要做的事情就是从 JournalNode 集群上补齐落后的 EditLog。

只有在这两步完成之后,当前新的 Active NameNode 才能安全地对外提供服务。

补齐落后的 EditLog 的过程复用了前面描述的 Standby NameNode 从 JournalNode 集群同步 EditLog 的逻辑和代码,最终调用 EditLogTailer 类的 doTailEdits 方法来完成 EditLog 的补齐。

使 JournalNode 集群上的 EditLog 达成一致的过程是一致性算法 Paxos 的典型应用场景,QJM 对这部分的处理可以看做是 Single Instance Paxos算法的一个实现,在达成一致的过程中,Active NameNode 和 JournalNode 集群之间的交互流程如下图所示,具体描述如下:

图3-6 Active NameNode 和 JournalNode 集群的交互流程图

交互过程做的事情如下:

生成一个新的 Epoch

Epoch 是一个单调递增的整数,用来标识每一次 Active NameNode 的生命周期,每发生一次 NameNode 的主备切换,Epoch 就会加 1。这实际上是一种 fencing 机制,为什么需要 fencing 已经在前面"ActiveStandbyElector 实现分析"一节的"防止脑裂"部分进行了说明。产生新 Epoch 的流程与 Zookeeper 的 ZAB(Zookeeper Atomic Broadcast) 协议在进行数据恢复之前产生新 Epoch 的过程完全类似:

  1. Active NameNode 首先向 JournalNode 集群发送 getJournalState RPC 请求,每个 JournalNode 会返回自己保存的最近的那个 Epoch(代码中叫 lastPromisedEpoch)。

  2. NameNode 收到大多数的 JournalNode 返回的 Epoch 之后,在其中选择最大的一个加 1 作为当前的新 Epoch,然后向各个 JournalNode 发送 newEpoch RPC 请求,把这个新的 Epoch 发给各个 JournalNode。

  3. 每一个 JournalNode 在收到新的 Epoch 之后,首先检查这个新的 Epoch 是否比它本地保存的 lastPromisedEpoch 大,如果大的话就把 lastPromisedEpoch 更新为这个新的 Epoch,并且向 NameNode 返回它自己的本地磁盘上最新的一个 EditLogSegment 的起始事务 id,为后面的数据恢复过程做好准备。如果小于或等于的话就向 NameNode 返回错误。

  4. NameNode 收到大多数 JournalNode 对 newEpoch 的成功响应之后,就会认为生成新的 Epoch 成功。

在生成新的 Epoch 之后,每次 NameNode 在向 JournalNode 集群提交 EditLog 的时候,都会把这个 Epoch 作为参数传递过去。每个 JournalNode 会比较传过来的 Epoch 和它自己保存的 lastPromisedEpoch 的大小,如果传过来的 epoch 的值比它自己保存的 lastPromisedEpoch 小的话,那么这次写相关操作会被拒绝。

一旦大多数 JournalNode 都拒绝了这次写操作,那么这次写操作就失败了。如果原来的 Active NameNode 恢复正常之后再向 JournalNode 写 EditLog,那么因为它的 Epoch 肯定比新生成的 Epoch 小,并且大多数的 JournalNode 都接受了这个新生成的 Epoch,所以拒绝写入的 JournalNode 数目至少是大多数,这样原来的 Active NameNode 写 EditLog 就肯定会失败,失败之后这个 NameNode 进程会直接退出,这样就实现了对原来的 Active NameNode 的隔离了。

选择需要数据恢复的 EditLog Segment 的 id

需要恢复的 Edit Log 只可能是各个 JournalNode 上的最后一个 Edit Log Segment,如前所述,JournalNode 在处理完 newEpoch RPC 请求之后,会向 NameNode 返回它自己的本地磁盘上最新的一个 EditLog Segment 的起始事务 id,这个起始事务 id 实际上也作为这个 EditLog Segment 的 id。

NameNode 会在所有这些 id 之中选择一个最大的 id 作为要进行数据恢复的 EditLog Segment 的 id。

向 JournalNode 集群发送 prepareRecovery RPC 请求

NameNode 接下来向 JournalNode 集群发送 prepareRecovery RPC 请求,请求的参数就是选出的 EditLog Segment 的 id。JournalNode 收到请求后返回本地磁盘上这个 Segment 的起始事务 id、结束事务 id 和状态 (in-progress 或 finalized)。

这一步对应于 Paxos 算法的 Phase 1a 和 Phase 1b两步。

Paxos 算法的 Phase1 是 prepare 阶段,这也与方法名 prepareRecovery 相对应。并且这里以前面产生的新的 Epoch 作为 Paxos 算法中的提案编号 (proposal number)。只要大多数的 JournalNode 的 prepareRecovery RPC 调用成功返回,NameNode 就认为成功。

选择进行同步的基准数据源,向 JournalNode 集群发送 acceptRecovery RPC 请求 NameNode 根据 prepareRecovery 的返回结果,选择一个 JournalNode 上的 EditLog Segment 作为同步的基准数据源。选择基准数据源的原则大致是:在 in-progress 状态和 finalized 状态的 Segment 之间优先选择 finalized 状态的 Segment。如果都是 in-progress 状态的话,那么优先选择 Epoch 比较高的 Segment(也就是优先选择更新的),如果 Epoch 也一样,那么优先选择包含的事务数更多的 Segment。

在选定了同步的基准数据源之后,NameNode 向 JournalNode 集群发送 acceptRecovery RPC 请求,将选定的基准数据源作为参数。JournalNode 接收到 acceptRecovery RPC 请求之后,从基准数据源 JournalNode 的 JournalNodeHttpServer 上下载 EditLog Segment,将本地的 EditLog Segment 替换为下载的 EditLog Segment。

这一步对应于 Paxos 算法的 Phase 2a 和 Phase 2b两步。

Paxos 算法的 Phase2 是 accept 阶段,这也与方法名 acceptRecovery 相对应。只要大多数 JournalNode 的 acceptRecovery RPC 调用成功返回,NameNode 就认为成功。

向 JournalNode 集群发送 finalizeLogSegment RPC 请求,数据恢复完成

上一步执行完成之后,NameNode 确认大多数 JournalNode 上的 EditLog Segment 已经从基准数据源进行了同步。接下来,NameNode 向 JournalNode 集群发送 finalizeLogSegment RPC 请求,JournalNode 接收到请求之后,将对应的 EditLog Segment 从 in-progress 状态转换为 finalized 状态,实际上就是将文件名从 edits_inprogress_${startTxid} 重命名为 edits_${startTxid}-${endTxid},见"NameNode 的元数据存储概述"一节的描述。

只要大多数 JournalNode 的 finalizeLogSegment RPC 调用成功返回,NameNode 就认为成功。此时可以保证 JournalNode 集群的大多数节点上的 EditLog 已经处于一致的状态,这样 NameNode 才能安全地从 JournalNode 集群上补齐落后的 EditLog 数据。

需要注意的是,尽管基于 QJM 的共享存储方案看起来理论完备,设计精巧,但是仍然无法保证数据的绝对强一致,下面选取参考文献 [2] 中的一个例子来说明:

假设有 3 个 JournalNode:JN1、JN2 和 JN3,Active NameNode 发送了事务 id 为 151、152 和 153 的 3 个事务到 JournalNode 集群,这 3 个事务成功地写入了 JN2,但是在还没能写入 JN1 和 JN3 之前,Active NameNode 就宕机了。同时,JN3 在整个写入的过程中延迟较大,落后于 JN1 和 JN2。最终成功写入 JN1 的事务 id 为 150,成功写入 JN2 的事务 id 为 153,而写入到 JN3 的事务 id 仅为 125,如图 7 所示 (图片来源于参考文献 [2])。按照前面描述的只有成功地写入了大多数的 JournalNode 才认为写入成功的原则,显然事务 id 为 151、152 和 153 的这 3 个事务只能算作写入失败。在进行数据恢复的过程中,会发生下面两种情况:

图 3-7.JournalNode 集群写入的事务 id 情况

  • 如果随后的 Active NameNode 进行数据恢复时在 prepareRecovery 阶段收到了 JN2 的回复,那么肯定会以 JN2 对应的 EditLog Segment 为基准来进行数据恢复,这样最后在多数 JournalNode 上的 EditLog Segment 会恢复到事务 153。从恢复的结果来看,实际上可以认为前面宕机的 Active NameNode 对事务 id 为 151、152 和 153 的这 3 个事务的写入成功了。但是如果从 NameNode 自身的角度来看,这显然就发生了数据不一致的情况。
  • 如果随后的 Active NameNode 进行数据恢复时在 prepareRecovery 阶段没有收到 JN2 的回复,那么肯定会以 JN1 对应的 EditLog Segment 为基准来进行数据恢复,这样最后在多数 JournalNode 上的 EditLog Segment 会恢复到事务 150。在这种情况下,如果从 NameNode 自身的角度来看的话,数据就是一致的了。

事实上不光本文描述的基于 QJM 的共享存储方案无法保证数据的绝对一致,大家通常认为的一致性程度非常高的 Zookeeper 也会发生类似的情况,这也从侧面说明了要实现一个数据绝对一致的分布式存储系统的确非常困难。

3.3.5. NameNode 在进行状态转换时对共享存储的处理

下面对 NameNode 在HA模式下进行状态转换的过程中对共享存储的处理进行描述,使得大家对基于 QJM 的共享存储方案有一个完整的了解。

NameNode 初始化启动,进入 Standby 状态

在 NameNode 以 HA 模式启动的时候,NameNode 会认为自己处于 Standby 模式,在 NameNode 的构造函数中会加载 FSImage 文件和 EditLog Segment 文件来恢复自己的内存文件系统镜像。在加载 EditLog Segment 的时候,调用 FSEditLog 类的 initSharedJournalsForRead 方法来创建只包含了在 JournalNode 集群上的共享目录的 JournalSet,也就是说,这个时候 只会从 JournalNode 集群之中加载 EditLog,而不会加载本地磁盘上的 EditLog。

另外值得注意的是,加载的 EditLog Segment 只是处于 finalized 状态的 EditLog Segment,而处于 in-progress 状态的 Segment 需要后续在切换为 Active 状态的时候,进行一次数据恢复过程,将 in-progress 状态的 Segment 转换为 finalized 状态的 Segment 之后再进行读取。

加载完 FSImage 文件和共享目录上的 EditLog Segment 文件之后,NameNode 会启动 EditLogTailer 线程和 StandbyCheckpointer 线程,正式进入 Standby 模式。

如前所述,EditLogTailer 线程的作用是定时从 JournalNode 集群上同步 EditLog。而 StandbyCheckpointer 线程的作用其实是为了替代 Hadoop 1.x 版本之中的 Secondary NameNode 的功能,StandbyCheckpointer 线程会在 Standby NameNode 节点上定期进行 Checkpoint,将 Checkpoint 之后的 FSImage 文件上传到 Active NameNode 节点。

NameNode 从 Standby 状态切换为 Active 状态

当 NameNode 从 Standby 状态切换为 Active 状态的时候,首先需要做的就是停止它在 Standby 状态的时候启动的线程和相关的服务,包括上面提到的 EditLogTailer 线程和 StandbyCheckpointer 线程,然后关闭用于读取 JournalNode 集群的共享目录上的 EditLog 的 JournalSet,接下来会调用 FSEditLog 的 initJournalSetForWrite 方法重新打开 JournalSet。不同的是,这个 JournalSet 内部同时包含了本地磁盘目录和 JournalNode 集群上的共享目录。这些工作完成之后,就开始执行"基于 QJM 的共享存储系统的数据恢复机制分析"一节所描述的流程,调用 FSEditLog 类的 recoverUnclosedStreams 方法让 JournalNode 集群中各个节点上的 EditLog 达成一致。然后调用 EditLogTailer 类的 catchupDuringFailover 方法从 JournalNode 集群上补齐落后的 EditLog。最后打开一个新的 EditLog Segment 用于新写入数据,同时启动 Active NameNode 所需要的线程和服务。

NameNode 从 Active 状态切换为 Standby 状态

当 NameNode 从 Active 状态切换为 Standby 状态的时候,首先需要做的就是停止它在 Active 状态的时候启动的线程和服务,然后关闭用于读取本地磁盘目录和 JournalNode 集群上的共享目录的 EditLog 的 JournalSet。

接下来会调用 FSEditLog 的 initSharedJournalsForRead 方法重新打开用于读取 JournalNode 集群上的共享目录的 JournalSet。这些工作完成之后,就会启动 EditLogTailer 线程和 StandbyCheckpointer 线程,EditLogTailer 线程会定时从 JournalNode 集群上同步 Edit Log。

4 HDFS运维管理

4.1HDFS集群搭建

测试环境搭建hadoop hdfs环境,记录如下:

服务器规划:

主机名 ip地址 子网掩码 角色

node1 172.16.100.239 255.255.255.0 Namenode

node2 172.16.100.128 255.255.255.0 Datanode

node3 172.16.100.112 255.255.255.0 Datanode/SecondaryNamenode

操作系统版本为CentOS 7.4

基础环境配置

  1. 配置服务器

配置ssh互信

同步hosts文件

时间同步

安装jdk

  1. 修改HDFS配置
# tar zxf hadoop-2.8.5.tar.gz -C /opt/

# mv /opt/hadoop-2.8.5 /opt/hadoop

# chown -R root.root /etc/hadoop
  1. 配置core-site.xml (配置文件目录在/opt/ hadoop-2.8.5/etc/hadoop/)
<configuration>
    <property>
        <name>fs.defaultFS</name>
        <value>hdfs://node1:9000</value>
    </property>
    <property>
        <name>hadoop.tmp.dir</name>
        <value>/opt/hadoop/tmp</value>
    </property>
    <property>
      <name>dfs.namenode.name.dir</name>
      <value>file://${hadoop.tmp.dir}/dfs/name</value>
    </property>
    <property>
      <name>dfs.datanode.data.dir</name>
      <value>file://${hadoop.tmp.dir}/dfs/data</value>
    </property>
</configuration>
  1. 配置hdfs-site.xml
<configuration>
    <property>
     <name>dfs.namenode.secondary.http-address</name>
     <value>node3:50090</value>
    </property>
     <property>
         <name>dfs.replication</name>
         <value>2</value>
     </property>
    <property>
    <name>dfs.hosts.exclude</name>
    <value>/opt/hadoop/etc/hadoop/excludes</value>
    </property>
</configuration>
  1. 在slaves文件中添加datanode节点主机名

  2. 创建excludes文件,在这个文件中的主机,不会添加到集群中。

# touch excludes
  1. 将配置文件拷贝到其他datanode节点上面。

  2. node1格式化hdfs

# bin/hdfs namenode -format
  1. node1启动hdfs
cd /opt/hadoop/sbinvm2

./start-dfs.sh
  1. 测试
cd /opt/hadoop/bin

./hdfs dfs -put /etc/fstab /

./hdfs dfs -ls  /

4.2运维经验

待添加

5 HDFS新特性及相关issue

HDFS-2.8.5(拟使用)

Apache Hadoop 2.8.5是Apache Hadoop 2.x.y 发行线上的一个关键发行版, 基于前一个稳定的发行版2.8.4构建,于2018年09月10日发布。选用该版本基于以下的考虑:

  1. 1HBase的相关依赖文档中指出Hadoop2.8.3+对HBase2.x能提供良好的支持,参考HBase Required
  2. 22.8.x版本不仅包含了2.7.x的机架感知机制、内存存储DataNode数据等特性,还包括了以下的新特性。
  3. 1HDFS支持异步调用重试(Async Call Retry)和容错转移(Failover),来降低DFS文件系统重连的门槛。
  4. 2构建方面,2.8则用Yetus取代了wrapper版本发布方式,还新增了Docker软体包的发布方式,更容易打包建置环境来测试或交换,有利于进行容器化改造。HADOOP-12651HADOOP-11843
  5. 3HDFS套件则是强化了WebHDFS对伪造跨站请求(Cross-site Request Forgery)的检查来提高安全性,也支持OAuth2验证方式。HDFS-8155
  6. 4HDFS还新增了多层式巢状加密区机制,不再只能指定单一个目录加密,也能对目录底下的不同目录,分别建立加密区来强化控管。
  7. 5新的DataNode协议(一种备选的DataNode状态上报的协议),配置以后DataNode不是用心跳的方式报告NameNode自己的存活状态,可以避免在超负荷集群中NameNode因为心跳的延迟而导致不正确地处理DataNodes的状态。其与旧协议的区别主要有三点,1)处理lifeline消息开销较低;2)其不会和block报告和用户操作争用名字空间的锁;3)其使用了一批独立的RPC处理线程,使其与繁重的用户操作线程隔离。通过使用这种协议,理论上可消除集群繁忙时心跳处理的延迟和状态的误报。个人觉得效果值得期待。 HDFS-9239
  8. 6添加了DataNode的阻塞信号控制,相信很多人都遇到过当个别DataNode非常繁忙的时候会无法上报心跳,导致各种误告警和HDFS不正常的行为。2.7已经实现对IO过高的DataNode发出拥塞信号,现在可对CPU负载超过该节点总核数150%的情况发出拥塞信号。HDFS-8009
  9. 7添加一种block放置策略使可达到最好的rack失败容忍度。目前的block放置策略在3副本下是LN(localnode)+RR_1(remote rack)+RR_2,这种策略只能容忍1个rack失效,如果LN所在的rack和remote rack都挂掉块就无法访问。这一新特性可将块放到尽可能多的rack上使得失败容忍度提高。当然,异机架通信的成本肯定比同机架大,因此这种策略肯定只是作为可选参数提供,不会替换默认值的。HDFS-7891
  10. 8添加了一个HDFS命令来查看当前文件系统中正在被写入的文件。HDFS-10480
  11. 9提供了Kerberos的一系列检查,包括JRE,网络,kinit,指定的tgt,hadoop安全参数等。HADOOP-12426
  12. 32.8.x版本修复了之前2.7.3版本前的严重bug以及有相关的提升性特性。

HDFS-2.7.6

Apache Hadoop 2.7.6于2018年04月17日发布,是Hadoop2.7.x版本线最新的发行版,基于前一个稳定的发行版 2.7.5构建。

特性一:机架感知机制

大型Hadoop集群会分布在很多机架上。在这种情况下,

  • 希望不同节点之间的通信能够尽量发生在同一个机架之内,而不是跨机架。
  • 为了提高容错能力,NameNode会尽可能把数据块的副本放到多个机架上。

综合考虑这两点的基础上Hadoop设计了机架感知功能。

低版本的Hadoop机架感知策略:

  • 第一个副本在client所处的节点上。如果客户端在集群外,随机选一个。
  • 第二个副本在和第一个副本不同机架的随机节点上。
  • 第三个副本和第二个副本相同机架,节点随机。

Hadoop2.7.X机架感知策略:

  • 第一个副本在client所处的节点上。如果客户端在集群外,随机选一个。
  • 第二个副本和第一个副本在相同的机架。
  • 第三个副本位于不同机架。

另外,还可以通过实现Hadoop提供的java类自定义机架感知的策略。

特性二:DataNode空间感知的block块位置确定机制

默认的 bolck块存放策略会随机选择datanode来存放blocks,当集群扩容时会造成磁盘空间使用百分比不平衡。比较老的datanode的磁盘使用百分比将高于新的datanode。

尽管可以通过外部工具来平衡各datanode的磁盘空间使用率,但是会消耗网络IO并且难以控制平衡过程的速度。通过实现一个平衡block块存放策略接口,选择磁盘使用百分比低datanode来存放新的block,这样集群内的datanode磁盘使用率就将一直趋向平衡。

相关issues:https://issues.apache.org/jira/browse/HDFS-8131

特性三:支持内存存储

HDFS可以支持通过DataNode节点来进行管理将数据写入到堆外内存中,能大大降低对HDFS客户端的响应时间。由于读写磁盘IO消耗巨大并且要从性能良好的存储路径中计算校验和,DataNode节点会异步地将内存中的数据往磁盘中flush。由此将这种写数据的方式称为懒持久写入(Lazy Persist Writes)。HDFS为这种写入方式提供了最有效的持久化保证。在节点重启的时候由于数据副本已经写入到磁盘中了将几乎没有数据丢失。应用能够选择使用Lazy Persist Writes

来有效地减少延迟。相关介绍请参考:MemoryStorage

HDFS-3.x

特性一:纠删码(Erasure Coding(EC))

纠删码技术能对数据进行分块,然后计算出一些冗余的校验块。当一部分数据块丢失时,可以通过剩余的数据块和校验块计算出丢失的数据块;如果在读的过程中发现数据丢失,能够立即解码出丢失的数据从而不影响读操作。将纠删码技术融入到HDFS中,可以保证在同等(或者更高)可靠性的前提下,将存储利用率提高了一倍。同样的集群用户可以存储两倍的数据,这将大大减少用户硬件方面的开销。参考:EC介绍HDFS-7285

特性二:多个NameNode备机

该特性支持增加多个备用的NameNode节点,极大提高了容错性。HDFS-6440

6 参考文档

Hadoop官网

https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/HdfsUserGuide.html

HADOOP相关博客

https://www.iteblog.com/archives/category/hadoop/

https://data-flair.training/blogs/hadoop-hdfs-tutorial/

https://www.ibm.com/developerworks/cn/opensource/os-cn-hadoop-name-node/index.html

posted @ 2019-01-25 20:38  stillcoolme  阅读(848)  评论(0编辑  收藏  举报