NameNode HA 采用方案

原文:http://yanbohappy.sinaapp.com/?p=50


Hadoop的设计初衷是服务于off-line的数据存储和处理应用。随着这个产品的不断成熟和发展,对于支持on-line应用的需求越来越强烈。例如HBase已经被Facebook和淘宝用到了在线存储应用中。所以Hadoop的on-line化也是一个趋势。目前制约Hadoop作为on-line存储和处理的瓶颈主要是系统的availability。衡量一个分布式系统的主要指标有:reliability, availability & scalability。Hadoop可以做到横向扩展,所以scalability非常好;而用户存在Hadoop里的数据几乎不会丢失,所以reliability也是非常不错的;目前的主要问题在availability,也就是用户向HDFS集群请求数据的时候集群是否能够保证100%提供服务,目前的主要问题体现在HDFS的SPOF(single point of failure),整个HDFS集群的启动/重启时间非常长,配置参数无法动态更改等。这些方面都是apache社区目前工作的重点,本文主要讨论HDFS NameNode的SPOF问题相关的HA机制。

Hadoop目前的trunk中的代码已经merge了原来的ha-branch,所以现在的trunk中的代码已经实现了基本的HA机制的功能。Hadoop PMC的人表示将会在后面的版本中发布这个功能。下面这张图是目前的HDFS HA的实现逻辑。

Right now the HA branch supports HOT-Failover, except that it is manual failover. We are now moving into a phase to implement automatic failover.

Significant enhancements were completed to make HOT Failover work:
- Configuration changes for HA
- Notion of active and standby states were added to the Namenode
- Client-side redirection
- Standby processing editlogs form Active
- Dual block reports to Active and Standby.

这是Hadoop mailing list中关于目前HA现状的阐述。下面首先简单介绍下这5个方面是怎么实现的,后面从源代码的角度分析具体的实现细节。

(1) Configuration changes for HA

在配置文件中会增加关于HA配置的参数,具体参数配置可以参考CDH4 Beta 2 High Availability Guide,这里介绍一些比较重要的参数。

例如dfs.ha.namenodes.[nameservice ID]这个参数表示在[nameservice ID]这个nameservice下的两台NameNode(分别作为Active和Standby模式运行)的主机名。然后针对每一台NN配置其对应的dfs.namenode.rpc-address.[nameservice ID].[name node ID]用来标示每一台NN。

由于目前的两台主机之间的HA机制是通过一个共享存储来存放editlog来实现的。所以需要配置参数dfs.namenode.shared.edits.dir表示共享存储的位置,一般是通过NFS挂载的形式,所以其实这个参数的值就是一个本地文件系统中的目录。

dfs.client.failover.proxy.provider.[nameservice ID]这个参数指定具体的failover proxy provider类,也就是在client端发现原来Active的NameNode变成了Standby模式时(在client发送RPC请求时返回了StandbyException时),该如何去连接当前Active的NameNode。目前的Hadoop里只有一个具体实现策略ConfiguredFailoverProxyProvider,实现方法就是如果client failover时,下次把RPC发送给另外一个NameNode的proxy。

另外就是dfs.ha.fencing.methods参数,指定在Active NameNode切换到Standby模式时,确保切换成功或者进程被杀死。
(2) Notion of active and standby states were added to the Namenode

有两种模式的NameNode,分别是Active和Standby模式。Active模式的NameNode接受client的RPC请求并处理,同时写自己的Editlog和共享存储上的Editlog,接收DataNode的Block report, block location updates和heartbeat;Standby模式的NameNode同样会接到来自DataNode的Block report, block location updates和heartbeat,同时会从共享存储的Editlog上读取并执行这些log操作,使得自己的NameNode中的元数据(Namespcae information + Block locations map)都是和Active NameNode中的元数据是同步的。所以说Standby模式的NameNode是一个热备(Hot Standby NameNode),一旦切换成Active模式,马上就可以提供NameNode服务。

(3) Client-side redirection

Client的通过RPC的Proxy与NameNode交互。在client端会有两个代理同时存在,分别代表与Active和Standby的NameNode的连接。由于Client端有Retry机制,当与Active NameNode正常通信的client proxy收到RPC返回的StandbyException时,说明这个Active NameNode已经变成了Standby模式,所以触发dfs.client.failover.proxy.provider.[nameservice ID]这个参数指定的类来做failover,目前唯一的实现是ConfiguredFailoverProxyProvider,实现方法就是下次开始把RPC发向另外一个NameNode。此后的RPC都是发往另外一个NameNode,也就是NameNode发生了主从切换。

1
2
3
4
5
public synchronized void performFailover(T currentProxy) {
 
currentProxyIndex = (currentProxyIndex + 1 ) % proxies.size();
 
}

(4) Standby processing editlogs form Active

开启Standby模式后,Standby NameNode会通过EditLogTailerThread从共享存储中读取Active NameNode写到那里的Editlog,然后执行操作,从而保持自己的元数据是最新的,所以说是热备。

(5)Dual block reports to Active and Standby.

DataNode的Block report, block location updates和heartbeat等RPC操作会发向两个NameNode,从而使得两个NameNode的Block locations map都是最新的,这样可以做到切换主从后原来的从(新的主)不再需要block report的时间。

可以看出client与NameNode之间的RPC是只向一个NameNode发送的(收到StandbyException后才会重试另外一个);而DataNode与NameNode之间的RPC在任何时候都是同时向两个NameNode发送的。


(1)NameNode启动流程

对于HDFS HA机制来说,NameNode是核心,NameNode有Active和Standby两种状态。在NameNode的构造函数中,读取配置文件,如果配置文件配置了开启HA,那么NameNode进入STANDBY_STATE状态;反之则进入ACTIVE_STATE状态。

1
2
3
4
5
6
7
8
9
10
11
this .haEnabled = HAUtil.isHAEnabled(conf, nsId);
 
if (!haEnabled) {
 
state = ACTIVE_STATE;
 
} else {
 
state = STANDBY_STATE;
 
}

创建HA上下文,NameNodeHAContext类包含了NameNode的Active和Standby模式变换相关操作的函数实现。

1
this .haContext = createHAContext();

然后就是初始化操作,包括配置参数,RPC server,metrics,加载Namespace,然后进入当前的haContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
try {
 
initializeGenericKeys(conf, nsId, namenodeId);
 
initialize(conf);
 
state.prepareToEnterState(haContext);
 
state.enterState(haContext);
 
} catch (IOException e) {
 
this .stop();
 
throw e;
 
} catch (HadoopIllegalArgumentException e) {
 
this .stop();
 
throw e;
 
}

(2)管理员执行HA管理命令流程

当两个NameNode都已经启动并进入Standby模式之后,就可以通过bin/hdfs脚本执行HDFS管理功能,执行如下命令:

1
bin /hdfs haadmin

就会调用DFSHAAdmin.java这个类来执行用户指定的功能,例如:

1
2
3
4
5
bin /hdfs haadmin -transitionToActive serviceId
 
bin /hdfs haadmin -transitionToStandby serviceId
 
bin /hdfs haadmin -failover serviceId serviceId

以-failover为例,就会调用HAAdmin.java类中的failover方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
private int failover( final String[] argv)
 
throws IOException, ServiceFailedException {
 
boolean forceFence = false ;
 
boolean forceActive = false ;
 
Options failoverOpts = new Options();
 
failoverOpts.addOption(“failover”, false , “failover”);
 
failoverOpts.addOption(FORCEFENCE, false , “force fencing”);
 
failoverOpts.addOption(FORCEACTIVE, false , “force failover”);
 
CommandLineParser parser = new GnuParser();
 
CommandLine cmd;
 
try {
 
cmd = parser.parse(failoverOpts, argv);
 
forceFence = cmd.hasOption(FORCEFENCE);
 
forceActive = cmd.hasOption(FORCEACTIVE);
 
} catch (ParseException pe) {
 
errOut.println(“failover: incorrect arguments”);
 
printUsage(errOut, “-failover”);
 
return - 1 ;
 
}
 
int numOpts = cmd.getOptions() == null ? 0 : cmd.getOptions().length;
 
final String[] args = cmd.getArgs();
 
if (numOpts > 2 || args.length != 2 ) {
 
errOut.println(“failover: incorrect arguments”);
 
printUsage(errOut, “-failover”);
 
return - 1 ;
 
}
 
HAServiceTarget fromNode = resolveTarget(args[ 0 ]);
 
HAServiceTarget toNode = resolveTarget(args[ 1 ]);
 
FailoverController fc = new FailoverController(getConf());
 
try {
 
fc.failover(fromNode, toNode, forceFence, forceActive);
 
out.println(“Failover from “+args[ 0 ]+” to “+args[ 1 ]+” successful”);
 
} catch (FailoverFailedException ffe) {
 
errOut.println(“Failover failed: ” + ffe.getLocalizedMessage());
 
return - 1 ;
 
}
 
return 0 ;
 
}

在这个函数中首先解析参数,然后会生成两个HAServiceTarget,分别表示发生主从切换的两个NameNode。由于这个DFSHAAdmin命令可以在任何一台可以连接到集群中的机器上运行,所以HAServiceTarget实际上是发生主从切换的两个NameNode的代理的封装。这个代理与两个NameNode通信的RPC协议时HAServiceProtocol。目前的Hadoop的RPC已经默认了Protocol Buffer作为RPC的实现。

然后生成FailoverController对象,这个类就是用于控制主从切换的。然后执行这个类中的failover方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
public void failover(HAServiceTarget fromSvc,
 
HAServiceTarget toSvc,
 
boolean forceFence,
 
boolean forceActive)
 
throws FailoverFailedException {
 
Preconditions.checkArgument(fromSvc.getFencer() != null ,
 
“failover requires a fencer”); //强制需要一种fencing方法
 
// Failover前的检查,例如fromSvc和toSvc是不是同一NameNode,toSvc是不是已经处于Active状态等
 
preFailoverChecks(fromSvc, toSvc, forceActive);
 
// 第一步是先把fromSvc转换成standby模式
 
boolean tryFence = true ;
 
// 通过向fromSvc的发送HAServiceProtocol的方式使得fromSvc transition to Standby mode
 
// 如果这个RPC返回的结果是ServiceFailedException或者IOException,
 
// 那么说明transition fail,从而是否要tryFence就是true,就必须fencing了。
 
if (tryGracefulFence(fromSvc)) {
 
tryFence = forceFence;
 
}
 
// Fence fromSvc if it’s required or forced by the user
 
if (tryFence) {
 
if (!fromSvc.getFencer().fence(fromSvc)) {
 
throw new FailoverFailedException(“Unable to fence ” +
 
fromSvc + “. Fencing failed.”);
 
}
 
}
 
// 第二步就是让toSvc转换成active模式,操作方法和上面类似,
 
// 通过RPC给toSvc发送transitionToActive命令。
 
boolean failed = false ;
 
Throwable cause = null ;
 
try {
 
HAServiceProtocolHelper.transitionToActive(
 
toSvc.getProxy(conf, rpcTimeoutToNewActive));
 
} catch (ServiceFailedException sfe) {
 
LOG.error(“Unable to make ” + toSvc + ” active (” +
 
sfe.getMessage() + “). Failing back.”);
 
failed = true ;
 
cause = sfe;
 
} catch (IOException ioe) {
 
LOG.error(“Unable to make ” + toSvc +
 
” active (unable to connect). Failing back.”, ioe);
 
failed = true ;
 
cause = ioe;
 
}
 
// 如果我们在第二步的时候,把toSvc转换成Active模式失败,需要考虑回滚。
 
// 如果我们没有强制fencing原来的fromSvc,那么就回滚。
 
// 如果我们强制fencing掉原来的fromSvc,那么只能抛异常了。
 
if (failed) {
 
String msg = “Unable to failover to ” + toSvc;
 
// Only try to failback if we didn’t fence fromSvc
 
if (!tryFence) {
 
try {
 
// Unconditionally fence toSvc in case it is still trying to
 
// become active, eg we timed out waiting for its response.
 
// Unconditionally force fromSvc to become active since it
 
// was previously active when we initiated failover.
 
failover(toSvc, fromSvc, true , true );
 
} catch (FailoverFailedException ffe) {
 
msg += “. Failback to ” + fromSvc +
 
” failed (” + ffe.getMessage() + “)”;
 
LOG.fatal(msg);
 
}
 
}
 
throw new FailoverFailedException(msg, cause);
 
}
 
}

(3)NameNode端的HA状态切换执行的操作代码

以上是DFSHAAdmin的操作,那么当它把对应的RPC命令发送到NameNode时,NameNode端的逻辑是怎么处理的呢?新的机遇Protocol Buffer实现的RPC请求在NameNode端会调用NameNodeRpcServer.java类中的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Override // HAServiceProtocol
 
public synchronized void transitionToActive()
 
throws ServiceFailedException, AccessControlException {
 
nn.transitionToActive();
 
}
 
@Override // HAServiceProtocol
 
public synchronized void transitionToStandby()
 
throws ServiceFailedException, AccessControlException {
 
nn.transitionToStandby();
 
}
 
@Override // HAServiceProtocol
 
public synchronized HAServiceStatus getServiceStatus()
 
throws AccessControlException, ServiceFailedException {
 
return nn.getServiceStatus();
 
}

然后调用NameNode.java类中对应的方法,把NameNode的HAState置为Active或者Standby。NameNode处于这两种不同的State会有不同的代码执行逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
synchronized void transitionToActive()
 
throws ServiceFailedException, AccessControlException {
 
namesystem.checkSuperuserPrivilege();
 
if (!haEnabled) {
 
throw new ServiceFailedException(“HA for namenode is not enabled”);
 
}
 
state.setState(haContext, ACTIVE_STATE);
 
}

无论当前的state是哪种,state.setState()函数都会调用setStateInternal()函数,执行状态切换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
protected final void setStateInternal( final HAContext context, final HAState s)
 
throws ServiceFailedException {
 
prepareToExitState(context);
 
s.prepareToEnterState(context);
 
context.writeLock();
 
try {
 
exitState(context);
 
context.setState(s);
 
s.enterState(context);
 
} finally {
 
context.writeUnlock();
 
}
 
}

prepareToExitState(context)函数目前只有当前处于StandbyState的实现,用于取消Standby NameNode正在进行的checkpoint操作(StandbyCheckpointer类),不需要锁住NameNodeHAContext。下面是prepareToExitState()函数的调用关系。

s.prepareToEnterState(context)函数目前没有执行任何操作。

Exit和Enter的准备工作做好之后,就要真正进行状态切换了。在进行状态切换的时候Namespace是要加锁的,不允许同时还有其他的状态切换。下面以Standby模式向Active模式切换为例说明。

exitState(context)调用NameNodeHAContext.stopStandbyService()函数,然后调用FSNamesystem.stopStandbyService()函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void stopStandbyServices() throws IOException {
 
LOG.info(“Stopping services started for standby state”);
 
//关闭standbyCheckpointer线程,这个线程负责定期把共享存储上的Editlog合并成FsImage,
 
//保存在本地存储并且发送到Active NameNode
 
if (standbyCheckpointer != null ) {
 
standbyCheckpointer.stop();
 
}
 
//由于要切换到Active模式,不需要EditLogTailer线程,所以关闭这个线程。
 
if (editLogTailer != null ) {
 
editLogTailer.stop();
 
}
 
//由于要切换到Active模式,需要开启新的EditLog文件,所以关闭旧的文件输出流
 
if (dir != null && dir.fsImage != null && dir.fsImage.editLog != null ) {
 
dir.fsImage.editLog.close();
 
}
 
}

s.enterState(context)的调用流程类似,最终会走到FSNamesystem.startActiveServices()方法中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
void startActiveServices() throws IOException {
 
LOG.info(“Starting services required for active state”);
 
writeLock();
 
try {
 
// 开启新的EditLog输出流,读取最后的TxId
 
FSEditLog editLog = dir.fsImage.getEditLog();
 
if (!editLog.isOpenForWrite()) {
 
// During startup, we’re already open for write during initialization.
 
editLog.initJournalsForWrite();
 
// May need to recover
 
editLog.recoverUnclosedStreams();
 
LOG.info(“Catching up to latest edits from old active before ” +
 
“taking over writer role in edits logs.”);
 
editLogTailer.catchupDuringFailover();
 
LOG.info(“Reprocessing replication and invalidation queues…”);
 
blockManager.getDatanodeManager().markAllDatanodesStale();
 
blockManager.clearQueues();
 
blockManager.processAllPendingDNMessages();
 
blockManager.processMisReplicatedBlocks();
 
if (LOG.isDebugEnabled()) {
 
LOG.debug(“NameNode metadata after re-processing ” +
 
“replication and invalidation queues during failover:n” +
 
metaSaveAsString());
 
}
 
long nextTxId = dir.fsImage.getLastAppliedTxId() + 1 ;
 
LOG.info(“Will take over writing edit logs at txnid ” +
 
nextTxId);
 
editLog.setNextTxId(nextTxId);
 
dir.fsImage.editLog.openForWrite();
 
}
 
if (haEnabled) {
 
// Renew all of the leases before becoming active.
 
// This is because, while we were in standby mode,
 
// the leases weren’t getting renewed on this NN.
 
// Give them all a fresh start here.
 
leaseManager.renewAllLeases();
 
}
 
leaseManager.startMonitor();
 
startSecretManagerIfNecessary();
 
} finally {
 
writeUnlock();
 
}
 
}

(4)NameNode对RPC操作的处理流程代码

到此为止NameNode的状态切换就完成了。我们前面提到过Active模式的NameNode会处理所有的RPC请求,而Standby模式的NameNode只会处理一部分请求(Standby模式的NameNode元数据更新是通过执行共享存储中存放的EditLog来实现的)。那么NameNode在执行RPC时是怎么区分的呢?前面已经提到过了,RPC到达NameNode后,都是调用NameNodeRpcServer.java类里相应的函数来执行的,而这些操作又会调用FSNamesystem.java类里对应的方法,然后在这些方法里检查当前处于的模式,时候继续执行下去还是抛出异常。

以一个典型的文件系统操作命令create为例说明:

NameNodeRpcServer.create()函数调用FSNamesystem.startFile()函数,在这个函数里调用checkOperation(OperationCategory.WRITE)函数检查。如果当前NameNode处于Active模式,这个函数返回true;如果处于Standby模式,这个函数抛出StandbyException,结束执行,然后通过RPC返回给client一个StandbyException。Client就知道刚才的那个RPC发错了,然后向另外一个NameNode发送。

在client发给NameNode的RPC操作中,读命令是两种模式的NameNode都可以执行的,写命令只有Active NameNode才能执行。后面HDFS也考虑向MySQL那样“主可读写,从可读”的主从HA机制。

说完client-NameNode的相关RPC协议之后,再来说说DataNode-NameNode的RPC相关协议。由于Active和Standby NameNode都保存有Block locations map信息,所以DataNode向两个NameNode发送的RPC都会被处理,不会有StandbyException。这样能够保证Standby的NameNode是热备,一旦切换直接可用。

至此NameNode端的HA机制相关代码告一段落,下一篇主要讨论client端是如何做fail over的。


参考文献:

http://www.cloudera.com/blog/2012/03/high-availability-for-the-hadoop-distributed-file-system-hdfs/

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值