一、背景与架构

1.前言

1.1背景

    自从hadoop2版本开始,社区引入了NameNode高可用方案。NameNode主从节点间需要同步操作日志来达到主从节点元数据一致。最初业界均通过NFS来实现日志同步,大家之所以选择NFS,一方面因为可以很方便地实现数据共享,另外一方面因为NFS已经发展20多年,已经相对稳定成熟。

虽然如此,NFS也有缺点不能满足HDFS的在线存储业务:网络单点及其存储节点单点。业界提供了数据共享的一些高可用解决方案,但均不能很好地满足目前HDFS的应用场景。

方案

网络单点

存储单点

备注

Mysql HA

数据有丢失风险

Drbd+heartbeat+NFS

脑裂;数据有丢失风险

Keepalive+NFS

数据有丢失风险

        为了满足共享日志的高可用性,社区引入QJM。QJM由cloudera开发,实现了读写高可用性,使HDFS达到真正的高可用性成为可能。

1.2.术语和定义

术语和定义

解释

Epoch

由主节点在启动及其切换为主的时候分配,每次操作JN节点均会检查该值,类似zookeeper中的zxid,此时主NameNode类似zookeeper中的leader,JN节点类似ZK中的Follower

JournalNode

QJM存储段进程,提供日志读写,存储,修复等服务

QJM

Qurom Journal Manager

startLogSegment

开始一个新的日志段,该日志段状态为接收写入日志的状态

finalizeLogSegment

将文件由正在写入日志的状态转化为不接收写日志的状态

recoverUnfinalizedSegments

主从切换等情况下,恢复没有转换为finalized状态的日志

journalId

日志ID,由配置指定,如qjournal://g42:8485;g35:8485;uhp9:8485/geminifs,则其中的geminifs即为journalId

 

2.设计方案

    QJM通过读写多个存储节点达到高可用性,同时为了恢复由于异常造成的多节点数据不一致性,引入了数据一致性算法。QJM逻辑图如下:


 QJM的基本原理就是用2N+1台JournalNode存储EditLog,每次写数据操作有大多数(>=N+1)返回成功时即认为该次写成功,保证数据高可用。当然这个算法所能容忍的是最多有N台机器挂掉,如果多于N台挂掉,这个算法就失效。

    QJM写原理示意图

 

2.1.写日志机制

写操作由主节点来完成,当主节点调用flush操作,会调用RPC同时向N个JN服务异步写日志,有N/2+1个节点返回成功,本次写操作才算成功。

 

主节点会标记返回失败的JN节点,下次写日志将不再写该节点,直到下次调用滚动日志操作,如果此时该JN节点恢复正常,之后主节点会向其写日志。虽然该节点丢失部分日志,由于主节点写入了多份,因此相应的日志并没有丢失。

为了保证写入每个日志文件txid的连续性,主节点保证分配的txid是连续的,同时JN节点在接受写日志的时候,首先会检查txid是否跟上次写连续,如果不连续会向主节点报错,连续则写入日志文件。

2.2读日志机制

相比写日志过程,读日志要相对简单一些。

读取日志示意图


当从节点触发读日志的时候,会经历如下几个步骤:

1、  选择日志文件,建立输入流

从节点遍历出所有还没有消化的日志文件,同时过滤inprocess状态的文件。对于每个JN节点上的日志文件,均按照txid从小到大进行排序放入一个集合。每个JN节点在从节点端均对应这样一个集合。再将每个JN节点间相同的日志文件进行归类为一组(组内日志会检查fisrtTxid是否相等,及其lastTxid是否相等);每个组之间再按照txid从小到大进行排序,这样方便从节点按照txid顺序消化日志;同时也会判断每个组之间txid是否连续。

2、  消化日志

准备好输入流以后,开始消化日志,从节点按照txid先后顺序从每个日志组里面消化日志。在每个日志组里面,首先会检查起始txid是否正确,如果正确,从节点先消化第一个日志文件,如果消化第一个日志文件失败则消化第二个日志文件,以此类推,如果日志组内文件遍历完还没有找到需要的日志,则该日志消化失败,消化每个日志的如果消化的上一个txid等于该日志文件的lastTxid,则该日志文件消化结束。

2.3.日志恢复

         在从节点切换为主节点的过程中,会进行最近的日志段状态检查,如果没有转换为finalized状态会将其转换为该状态,日志恢复就处于该过程当中。

恢复日志finalized状态图


2.3.1.触发条件

     跟其他一些数据恢复方案不同,QJM并非每次写日志文件出现异常均恢复,而是从切换为主的情况下进行最新的一个日志文件的数据一致性检查,然后决定是否触发数据修复流程,之所以这样实现我想有如下原因:

1、  在开始一个新的edits文件前,HDFS会确保之前最新的日志文件已经由inprocess状态转化为finalized状态,同时QJM每次操作有N/2+1个节点返回成功才算成功,因此除了最新日志文件前,之前老的日志文件是finalized状态,且是高可用的;仅最新的日志文件可能由于主节点服务异常或者QJM某个进程异常导致日志没有正常转换为finalized状态,因此在从切换为主的时候需要恢复处理;

2.3.2.恢复流程

在日志恢复的过程中,需要经历准备恢复(prepareRecovery),接受恢复(acceptRecovery)两个阶段。

2.3.2.1.prepareRecovery

该操作向JN端发送RPC请求,查询需要恢复的日志段文件是否存在,如果存在则判断日志段文件状态(inprocess或finalized),同时也会返回epoch编号,NameNode根据返回的查询信息通过修复算法选择修复的源节点,准备进行数据修复。

2.3.2.2.acceptRecovery

计算获得源节点后,NameNode会向JN端发送恢复操作,JN节点根据接收到的RPC恢复请求,判断当前节点是否需要进行日志修复,如果需要进行修复,则通过doGet方式到源节点下载需要恢复的目标日志文件。下载过程中,先将下载的文件放到临时目录(tmp)目录下,下载完成后进行md5校验,检查是否有数据丢失,数据检查通过再将下载的文件放置到工作目录(current)下,这样数据恢复完成。

在JN节点执行该方法中,有两个问题需要考虑:

1、如果在JN节点下载日志的文件时候,源节点连接不通,会抛异常,如果有多个JN节点可以作为源节点,在NameNode调用JN节点的acceptRecovery方法是,可以考虑返回URL数组而不是单个URL,这样一个URL不能连接还可以尝试连接另外的JN节点进行文件下载;

2、有可能某JN节点下载日志文件的时候,自己进程挂掉,在QJM中,有对该问题的处理方式;

开始接触的时候我担心是否可能有文件既在被从节点读取,又在恢复该日志文件,通过分析后发现不会有何种情况,因为从节点消化的日志均是finalized状态的文件而不是inprocess状态的文件。

2.3.3.恢复算法

2.3.2.1节中提到,查询到每个JN相关信息返回后,会使用修复算法选择源节点进行日志数据的修复,此处补上修复算法的策略:

1、  首先判断JN节点是否有指定的txid,如果某节点没有,则该节点不会作为源节点;

2、  如果JN节点存在指定的txid,然后判断该文件是否为finalized状态,如果不同的JN节点,txid所在的文件既有finalized状态的文件又有inprocess状态的文件,以finalized状态文件为候选源节点,当然finalized状态的文件之间还需要判断结束txid是否相等,然后返回其中任意一个节点作为源节点;

JN

segment

Last txid

JN1

edits_101-150

150

JN2

edits_101-150

150

JN3

Edits_inprogres_ 101

145

3、  如果节点间文件均是inprocess状态的文件,首先判断其epoch编号,如果epoch编号不一致,则以epoch编号大的作为候选源节点;如果epoch编号一致,则选择结束txid更大的作为源节点。

JN

segment

Last txid

lastWriterEpoch

JN1

Edits_inprogres_ 101

150

1

JN2

Edits_inprogres_ 101

150

1

JN3

Edits_inprogres_ 101

145

1



二、硬件资源

为了构建HA集群架构,你需要准备如下资源: 
1、Namenode机器:两台配置对等的物理机器,它们分别运行Active和Standby Node。 
2、JouralNode机器:运行JouralNodes的机器。JouralNode守护进程相当的轻量级,它们可以和hadoop的其他进程部署在一起,比如Namenodes、jobTracker、ResourceManager等。不过为了形成多数派(majority),至少需要3个JouralNodes,因为edits操作必须在多数派上写入成功。当然JNS的个数可以 > 3,且通常为奇数(3,5,7),这样可以更好的容错和形成多数派。如果你运行了N个JNS,那么它可以允许(N-1)/2个JNS进程失效并且不影响工作。

此外,在HA集群中,standby namenode还会对namespace进行checkpoint操作(继承Backup Namenode的特性),因此,就不需要在HA集群中运行SecondaryNamenode、CheckpointNode或者BackupNode。事实上,HA架构中运行上述节点,将会出错(不允许)。

三、部署

1)、配置

和HDFS Federation类似,HA配置向后兼容,运行只有一个Namenode运行而无需做任何修改。新的配置中,要求集群中所有的Nodes都有相同的配置文件,而不是根据不同的Node设定不同的配置文件。

和HDFS Federation一样,HA集群重用了“nameservice ID”来标识一个HDFS 实例(事实上它可能包含多个HA Namenods);此外,“NameNode ID”概念被添加到HA中,集群中每个Namenode都有一个不同的ID;为了能够让一个配置文件支持所有的Namenodes(适用与Federation环境),那么相关的配置参数都以“nameservice ID”或“Namenode ID”作为后缀。

修改hdfs-site.xml,增加如下几个配置参数,其参数的顺序无关。 
1、dfs.nameservices:nameservice的逻辑名称。可以为任意可读字符串;如果在Federation中使用,那么还应该包含其他的nameservices,以”,”分割。

<property>
  <name>dfs.nameservices</name>
  <value>hadoop-ha,hadoop-federation</value>
</property>

2、dfs.ha.namenodes.[nameservice ID]:

<property>
  <name>dfs.ha.namenodes.hadoop-ha</name>
  <value>nn1,nn2</value>
</property>

3、dfs.namenode.rpc-address.[nameservice ID].[namenode ID]

<property>
  <name>dfs.namenode.rpc-address.hadoop-ha.nn1</name>
  <value>machine1.example.com:8020</value>
</property>
<property>
  <name>dfs.namenode.rpc-address.hadoop-ha.nn2</name>
  <value>machine2.example.com:8020</value>
</property>

其中nameservice ID需要和1)匹配,namenode ID需要和2) 匹配。配置项的值为相应namenode的hostname以及通讯端口号(Client与namenode RPC通讯端口),它和non-ha模式下“dfs.namenode.rpc-address”作用一样。每个namenode ID都需要单独配置。

你可以根据需要,配置“dfs.namenode.servicerpc-address”,格式和上述一致。(SNN,backup节点与Namenode通讯地址) 
4、dfs.namenode.http-address.[nameservice ID].[namenode ID]

<property>
  <name>dfs.namenode.http-address.hadoop-ha.nn1</name>
  <value>machine1.example.com:50070</value>
</property>
<property>
  <name>dfs.namenode.http-address.hadoop-ha.nn2</name>
  <value>machine2.example.com:50070</value>
</property>

各个namenode的HTTP地址。它和non-ha下的”dfs.namenode.http-address”配置作用一样。

5、dfs.namenode.shared.edits.dir:

<property>
  <name>dfs.namenode.shared.edits.dir</name>
  <value>qjournal://node1.example.com:8485;node2.example.com:8485;node3.example.com:8485/hadoop-ha</value>
</property>

配置JNS组的url地址,Namenodes将会从JNS组中读写edits。这是一个共享存储区,Active Namenode写入,Standby Node读取,每个Namenodeservice必须配置足够多的JNS地址(>=3,多数派),每条的格式为:
“qjournal://host1:port1;host2:port2;host3:port3/journalId” 
其中journalId需要和上述配置中的“nameserviceID”匹配。

<property>
  <name>dfs.journalnode.rpc-address</name>
  <value>0.0.0.0:8485</value>
</property>
<property>
  <name>dfs.journalnode.http-address</name>
  <value>0.0.0.0:8480</value>
</property>

此外,我们还需要在相应的JournalNodes上增加上述配置。

6、dfs.client.failover.proxy.provider.[nameservice ID]: 
HDFS Client链接Namenode所使用的类,Client可以通过此类来判定哪个Namenode为Alive,并与它保持通信。目前hadoop中唯一的实现类为”ConfiguaredFailoverProxyProvider”。

<property>
  <name>dfs.client.failover.proxy.provider.hadoop-ha</name>
  <value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value>
</property>

7、dfs.ha.fencing.methods:在failover期间用来隔离Active Namenode的脚本或者java 类列表。 
虽然JNS可以确保集群中只有一个Active Node写入edits,这对保护edits一致性很重要,但是在failover期间,有可能Acitive Node仍然存活,Client可能还与其保持连接提供旧的数据服务,我们可以通过此配置,指定shell脚本或者java程序,SSH到Active NameNode然后Kill Namenode进程。它有两种可选值(具体参见官方文档): 
1) sshfence:SSH登录到Active Namenode,并Kill此进程。首先当前机器能够使用SSH登录到远端,前提是已经授权(rsa)。 
2) shell:运行shell指令隔离Active Namenode。

<property>
  <name>dfs.ha.fencing.methods</name>
  <value>shell(/path/to/my/script.sh arg1 arg2 ...)</value>
</property>

“()”之间为shell脚本的路径,以及参数列表。

8、fs.defaultFS(core-site.xml): 
在non-ha下,这个参数值为namenode的地址:“hdfs://namenode:8020”;不过在HA架构下,将使用namenservice名称替代[回答了第三个问题]

<property>
  <name>fs.defaultFS</name>
  <value>hdfs://hadoop-ha</value>
</property>

9、dfs.journalnode.edits.dir: 
指定journalNode存储edits文件的本地路径。

最终,上述配置信息,需要在server和Client端同时配置才能有效的适应HA与failover特性。

2)、部署

上述配置调整完毕后,我们就可以启动journalNodes守护进程,默认的”sbin/start-dfs.sh”脚本会根据”dfs.namenode.shared.edits.dir”配置,在相应的Datanode上启动journalNodes。当然我们可以使用::”bin/hdfs start journalnode”分别在相应的机器上启动。 
一旦JournalNodes启动成功,它们将会从Namenode上同步metadata。 
1、如果你的HDFS集群是新建的,那么需要在每个Namenode上执行”hdfs namenode -format”指令。 
2、如果你的namenodes已经format了,或者是将non-ha转换成ha架构,你应该在将其中一个namenode上的metadata复制到另一台上(dfs.namenode.name.dir目录下的数据),然后在那个没有format的新加入的namenode上执行”hdfs namenode -bootstrapStandby”。运行这个指令需要确保JournalNodes中持有足够多的edits。 
3、如果你将一个non-ha的Namenode(比如backup,其已经formated)切换成HA,你需要首先运行”hdfs -initializeSharedEdits”,这个指令将本地Namenode中的edits初始化Journalnodes。

此后,你就可以启动HA Namenodes。可以通过配置指定的HTTP地址(dfs.namenode.https-address)来查看各个Namenode的状态,Active or Standby。

3)、管理员指令

HA集群启动后,我们可以通过一些指令来管理HDFS集群。“bin/hdfs haadmin -DFSHAAdmin”指令,其可选参数:

  1. -transitionToActive 与-transitionToStandbyl :将指定的namenode ID切换为Active或者standby。这个指令并不会触发“fencing method”,所以不常用,我们通常使用”hdfs haadmin -failover”来切换Namenode状态。
  2. -failover [–forcefence] [–foreactive] :在两个Namenode之间failover。这个指令会触发将first节点failover到second节点。如果first处于standby,那么只是简单的将second提升为Active。如果first为Active,那么将会友好的将其切换为standby,如果失败,那么fencing methods将会触发直到成功,此后second将会提升为Active。如果fencing method失败,那么second将不会被提升为Active。 
    例如:”hdfs haadmin -DFSHAAdmin -failover nn1 nn2”
  3. -getServiceState :获取serviceId的状态,Active还是Standby。链接到指定的namenode上,并获取其当前的状态,打印出“standby”或者“active”。我可以在crontab中使用此命令,用来监测各个Namenode的状况。
  4. -checkHealth :检测指定的namenode的健康状况。

四、自动Failover

上述介绍了如何配置手动failover,在这种模式下,系统不会自动触发failover,即不会将Standby提升为Active,即使Active已经失效。接下来介绍如何实现自动failover。

1)、组件

Automatic Failover中,增加了2个新的组件:zookeeper集群,ZKFailoverController进程(简称为ZKFC)。 
Zookeeper是一个高可用的调度服务,可以保存一系列调度数据,当这些数据变更(notify)时可以通知Client,以及监控(montitor)Clients失效,自动failover的实现将依赖于Zookeeper的几个特性: 
1、Failure delection:失效检测,每个Namenode将会和zookeeper建立一个持久session,如果Namenode失效,那么次session将会过期失效,此后Zookeeper将会通知另一个Namenode,然后触发Failover。 
2、Active Namenode election:zookeeper提供了简单的机制来实现Acitve Node选举,如果当前Active失效,Standby将会获取一个特定的排他锁(lock),那么获取(持有)锁的Node接下来将会成为Active。

ZKFailoverControllor(ZKFC)是一个zookeeper客户端,它主要用来监测和管理Namenodes的状态,每个Namenode机器上都会运行一个ZKFC程序,它的职责为: 
1、Health monitoring:ZKFC间歇性的使用health-check指令ping本地的Namenode,Namenode也会及时的反馈自己的health status。如果Namenode失效,或者unhealthy,或者无响应,那么ZKFS将会标记其为“unhealthy”。 
2、Zookeeper session manangement:当本地Nanenode运行良好时,ZKFC将会持有一个zookeeper session,如果本地Namenode为Active,它同时也持有一个“排他锁”(znode);这个lock在zookeeper中为“ephemeral” znode(临时节点),如果session过期,那么次lock所对应的znode也将被删除。(参见zookeeper特性) 
3、Zookeeper-based election:如果本地Namenode运行良好,并且ZKFS没有发现其他的的Namenode持有lock(比如Active失效后,释放了lock),它将尝试获取锁,如果获取成功,即“赢得了选举”,那么此后将会把本地Namenode标记为Active,然后触发Failover:首先,调用fencing method,然后提升本地Namenode 为Active。

具体Failover过程和详细内容,请参见 HDFS-2185 。

2)、配置

在Automatic Failover中,需要把一个重要的配置项添加到hdfs-site.xml中。

<property>
    <name>dfs.ha.automatic-failover.enabled</name>
    <value>true</value>
</property>

此外还需要在core-site.xml中,增加如下配置:

<property>
    <name>ha.zookeeper.quorum</name>
    <value>zk1.example.com:2181,zk2.example.com:2181,zk3.example.com:2181</value>
</property>

上述zookeeper集群为即备,尽可能选择相对平稳的zk集群。

其中”dfs.ha.automatic-failover.enabled”可以为每个nameservice ID分别配置:dfs.ha.automatic-failover.enabled.[nameservice ID]。此外在core-site.xml中还可以配置Zookeeper Client的相关参数,比如sessionTimeout,这些配置项以”ha.zookeeper”开头,其中”dfs.ha.”开头的部分配置项可以用来设定fencing method的相关控制。

3)、初始化HA状态

上述准备工作结束后,我们还需要在zookeeper中初始化HA的状态,通过执行“hdfs zkfc -formatZK”,此命令将会在zookeeker中创建一个znode,用来保存HA或failover的数据。

4)、启动集群

可以使用”start-dfs.sh”这个便捷的指令,它启动了hdfs所需要的所有守护进程,当然包括ZKFC。也可以使用”hadoop-daemon.sh start zkfc”手动启动ZKFC客户端。

5)、检验Failover

一旦Automatic Failover集群启动之后,我们需要检测Failover是否符合预期。首先,我们需要通过命令(getServiceState)或者在Namenode的Web UI上查看各个Namenode的状态,确认两个Namenode是否分别处于Active和Standby;此后,你可以手动关闭Active Namenode,比如使用kill -9 ,在确定Acitve Node失效后,再次检测原来的Standby是否已经提升为Active;不过因为zookeeper session过期判定需要达到sessionTimeout(可配置,ha.zookeeper.session-timeout),这个failover过程可能需要滞后数秒,默认为5秒。

如果没有按照预期failover,那么你需要检测配置文件是否正确,zk服务是否正确。此外,我们还可以使用上述DFSHAAadmin指令多次尝试。

五、FAQ

  1. ZKFC和Namenodes守护进程的启动顺序是否重要? 
    No,对于指定的Namenode,你可以在其之前或者之后启动ZKFC均可以,ZKFC只是调度Namenode的存活状态,如果不启动ZKFC,此Namenode将无法参与自动failover过程。
  2. 是否需要额外的monitoring? 
    你需要在Namenode机器上,添加额外的monitor用来监控ZKFC是否运行。在某些情况下,zookeeper集群的故障可能导致ZKFC意外中断,你需要适时的重启ZKFC。此外,还需要监控Zookeeper集群的运行状况,如果Zookeeper集群失效,那么HA集群将无法failover。
  3. 如果Zookeeper失效,将会怎么样? 
    如果zookeeper集群故障,那么Automatic Failover将不会触发,即使Namenode失效,这也意味着ZKFC无法正常运行。不过,如果Namenodes正常(即使有一个失效),那么HDFS系统将不会受到影响。因为HDFS Client并没有基于zookeeper做任何事情,当zookeeper集群仍需要尽快的恢复以避免当前Active失效而造成的“split-brain”等问题。
  4. 是否可以在Namenodes之间指定优先级? 
    NO,这是不能支持的。首先启动的Namenode将作为Active,我们只能认为控制Namenode启动的顺序来做到“优先级”。
  5. 在Automatic Failover中,手动Failover怎么做? 
    和普通的Failover一样,我们总是可以通过”hdfs haadmin -DFSHAAdmin -failover”来实现手动Failover。

文章出处:http://blog.csdn.net/lnho2015/article/details/51423619

   http://blog.csdn.net/lifuxiangcaohui/article/details/25338985


Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐