本文部分转自 Hadoop 2.0 NameNode HA和Federation实践
本文部分转自 详细讲解hadoop2的automatic HA+Federation+Yarn配置的教程
Hadoop 2.0 中 HDFS HA 解决方案可阅读文章: “Hadoop 2.0 NameNode HA 和 Federation 实践“,目前 HDFS2 中提供了两种 HA 方案,一种是基于 NFS 共享存储的方案,一种基于 Paxos 算法的方案 Quorum Journal Manager(QJM),它的基本原理就是用 2N+1 台 JournalNode 存储 EditLog,每次写数据操作有大多数(>=N+1)返回成功时即认为该次写成功,数据不会丢失了。目前社区正尝试使用 Bookeeper 作为共享存储系统,具体可参考。
HDFS-1623 给出的 HDFS HA 架构图如下所示:
以前的 HDFS 是 share nothing but NN,现在 NN 又 share storage,这样其实是转移了单点故障的位置,但中高端的存储设备内部都有各种 RAID 以及冗余硬件包括电源以及网卡等,比服务器的可靠性还是略有提高。通过 NN 内部每次元数据变动后的 flush 操作,加上 NFS 的 close-to-open,数据的一致性得到了保证。社区现在也试图把元数据存储放到 BookKeeper上,以去除对共享存储的依赖,Cloudera 也提供了 Quorum Journal Manager 的实现和代码。
这是让Standby NN保持集群最新状态的必需步骤,不赘述。
显然,我们不能在 NN 进程内进行心跳等信息同步,最简单的原因,一次 FullGC 就可以让 NN 挂起十几分钟,所以,必须要有一个独立的短小精悍的 watchdog 来专门负责监控。这也是一个松耦合的设计,便于扩展或更改,目前版本里是用 ZooKeeper (以下简称 ZK )来做同步锁,但用户可以方便的把这个 ZooKeeper FailoverController (以下简称 ZKFC )替换为其他的 HA 方案或 leader 选举方案。
防止脑裂,就是保证在任何时候只有一个主 NN,包括三个方面:
多个 NN 共用一个集群里 DN 上的存储资源,每个 NN 都可以单独对外提供服务
每个 NN 都会定义一个存储池,有单独的 id,每个 DN 都为所有存储池提供存储
DN 会按照存储池 id 向其对应的 NN 汇报块信息,同时,DN 会向所有 NN 汇报本地存储可用资源情况
如果需要在客户端方便的访问若干个 NN 上的资源,可以使用客户端挂载表,把不同的目录映射到不同的 NN,但 NN 上必须存在相应的目录
1. 改动最小,向前兼容
2. 分离命名空间管理和块存储管理
3. 客户端挂载表
以上是 HA 和 Federation 的简介,对于已经比较熟悉 HDFS 的朋友,这些信息应该已经可以帮助你快速理解其架构和实现,如果还需要深入了解细节的话,可以去详细阅读设计文档或是代码。这篇文章的主要目的是总结我们的测试结果,所以现在才算是正文开始。
为了彻底搞清 HA 和 Federation 的配置,我们直接一步到位,选择了如下的测试场景,结合了 HA 和 Federation:
这张图里有个概念是前面没有说明的,就是 NameService。Hadoop 2.0 里对 NN 进行了一层抽象,提供服务的不再是 NN 本身,而是 NameService (以下简称 NS)。Federation 是由多个 NS 组成的,每个 NS 又是由一个或两个 (HA) NN 组成的。在接下里的测试配置里会有更直观的例子。
图中 DN-1 到 DN-6 是六个 DataNode,NN-1 到 NN-4 是四个 NameNode,分别组成两个 HA 的 NS,再通过 Federation 组合对外提供服务。Storage Pool 1 和 Storage Pool 2 分别对应这两个 NS。我们在客户端进行了挂载表的映射,把 /share
映射到 NS1,把 /user
映射到 NS2,这个映射其实不光是要指定 NS,还需要指定到其上的某个目录,稍后的配置中大家可以看到。
下面我们来看看配置文件里需要做哪些改动,为了便于理解,我们先把 HA 和 Federation 分别介绍,然后再介绍同时使用 HA 和 Federation 时的配置方式,首先我们来看 HA 的配置:
对于 HA 中的所有节点,包括 NN 和 DN 和 客户端,需要做如下更改:
1. HA,所有节点,hdfs-site.xml
<property>
<name>dfs.nameservices</name>
<value>ns1</value>
<description>提供服务的NS逻辑名称,与core-site.xml里的对应</description>
</property>
<property>
<name>dfs.ha.namenodes.${NS_ID}</name>
<value>nn1,nn3</value>
<description>列出该逻辑名称下的NameNode逻辑名称</description>
</property>
<property>
<name>dfs.namenode.rpc-address.${NS_ID}.${NN_ID}</name>
<value>host-nn1:9000</value>
<description>指定NameNode的RPC位置</description>
</property>
<property>
<name>dfs.namenode.http-address.${NS_ID}.${NN_ID}</name>
<value>host-nn1:50070</value>
<description>指定NameNode的Web Server位置</description>
</property>
以上的示例里,我们用了${}来表示变量值,其展开后的内容大致如下:
<property>
<name>dfs.ha.namenodes.ns1</name>
<value>nn1,nn3</value>
</property>
<property>
<name>dfs.namenode.rpc-address.ns1.nn1</name>
<value>host-nn1:9000</value>
</property>
<property>
<name>dfs.namenode.http-address.ns1.nn1</name>
<value>host-nn1:50070</value>
</property>
<property>
<name>dfs.namenode.rpc-address.ns1.nn3</name>
<value>host-nn3:9000</value>
</property>
<property>
<name>dfs.namenode.http-address.ns1.nn3</name>
<value>host-nn3:50070</value>
</property>
与此同时,在 HA 集群的 NameNode 或 客户端还需要做如下配置的改动:
2. HA,NameNode,hdfs-site.xml
<property>
<name>dfs.namenode.shared.edits.dir</name>
<value>file:///nfs/ha-edits</value>
<description>指定用于HA存放edits的共享存储,通常是NFS挂载点</description>
</property>
<property>
<name>ha.zookeeper.quorum</name>
<value>host-zk1:2181,host-zk2:2181,host-zk3:2181,</value>
<description>指定用于HA的ZooKeeper集群机器列表</description>
</property>
<property>
<name>ha.zookeeper.session-timeout.ms</name>
<value>5000</value>
<description>指定ZooKeeper超时间隔,单位毫秒</description>
</property>
<property>
<name>dfs.ha.fencing.methods</name>
<value>sshfence</value>
<description>指定HA做隔离的方法,缺省是ssh,可设为shell,稍后详述</description>
</property>
3. HA,客户端,hdfs-site.xml
<property>
<name>dfs.ha.automatic-failover.enabled</name>
<value>true</value>
<description>或者false</description>
</property>
<property>
<name>dfs.client.failover.proxy.provider.${NS_ID}</name>
<value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value>
<description>指定客户端用于HA切换的代理类,不同的NS可以用不同的代理类以上示例为Hadoop 2.0自带的缺省代理类</description>
</property>
最后,为了方便使用相对路径,而不是每次都使用hdfs://ns1作为文件路径的前缀,我们还需要在各角色节点上修改core-site.xml:
4. HA,所有节点,core-site.xml
<property>
<name>fs.defaultFS</name>
<value>hdfs://ns1</value>
<description>缺省文件服务的协议和NS逻辑名称,和hdfs-site里的对应此配置替代了1.0里的fs.default.name</description>
</property>
接下来我们看一下如果单独使用 Federation,应该如何配置,这里我们假设没有使用 HA,而是直接使用 nn1 和 nn2 组成了 Federation 集群,他们对应的 NS 的逻辑名称分别是 ns1 和 ns2。为了便于理解,我们从客户端使用的 core-site.xml 和 挂载表 入手:
1. Federation,所有节点,core-site.xml
<xi:include href=“cmt.xml"/>
<property>
<name>fs.defaultFS</name>
<value>viewfs://nsX</value>
<description>整个Federation集群对外提供服务的NS逻辑名称,注意,这里的协议不再是hdfs,而是新引入的viewfs 这个逻辑名称会在下面的挂载表中用到</description>
</property>
我们在上面的 core-site 中包含了一个 cmt.xml 文件,也就是 Client Mount Table,客户端挂载表,其内容就是虚拟路径到具体某个 NS 及其物理子目录的映射关系,譬如 /share
映射到 ns1 的 /real_share
,/user
映射到 ns2 的 /real_user
,示例如下:
2. Federation,所有节点,cmt.xml
<configuration>
<property>
<name>fs.viewfs.mounttable.nsX.link./share</name>
<value>hdfs://ns1/real_share</value>
</property>
<property>
<name>fs.viewfs.mounttable.nsX.link./user</name>
<value>hdfs://ns2/real_user</value>
</property>
</configuration>
注意,这里面的 nsX 与 core-site.xml 中的 nsX 对应。而且对每个 NS,你都可以建立多个虚拟路径,映射到不同的物理路径。与此同时,hdfs-site.xml 中需要给出每个 NS 的具体信息:
<property>
<name>dfs.nameservices</name>
<value>ns1,ns2</value>
<description>提供服务的NS逻辑名称,与core-site.xml或cmt.xml里的对应</description>
</property>
<property>
<name>dfs.namenode.rpc-address.ns1</name>
<value>host-nn1:9000</value>
</property>
<property>
<name>dfs.namenode.http-address.ns1</name>
<value>host-nn1:50070</value>
</property>
<property>
<name>dfs.namenode.rpc-address.ns2</name>
<value>host-nn2:9000</value>
</property>
<property>
<name>dfs.namenode.http-address.ns2</name>
<value>host-nn2:50070</value>
</property>
可以看到,在只有 Federation 且没有 HA 的情况下,配置的 name 里只需要直接给出 ${NS_ID}
,然后 value 就是实际的机器名和端口号,不需要再 .${NN_ID}
。
这里有一个情况,就是 NN 本身的配置。从上面的内容里大家可以知道,NN 上是需要事先建立好客户端挂载表映射的目标物理路径,譬如 /real_share
,之后才能通过以上的映射进行访问,可是,如果不指定全路径,而是通过 “映射+相对路径” 的话,客户端只能在挂载点的虚拟目录之下进行操作,从而无法创建映射目录本身的物理目录。所以,为了在 NN 上建立挂载点映射目录,我们就必须在命令行里使用 hdfs 协议和绝对路径:
hdfs dfs -mkdir hdfs://ns1/real_share
在 NN 上不要使用 viewfs://
来配置,而是使用 hdfs://
,那样是可以解决问题,但是是并不是最好的方案,也没有把问题的根本说清楚。
最后,我们来组合 HA 和 Federation,真正搭建出和本节开始处的测试环境示意图一样的实例。通过前面的描述,有经验的朋友应该已经猜到了,其实 HA+Federation 配置的关键,就是组合 hdfs-site.xml 里的dfs.nameservices
以及 dfs.ha.namenodes.${NS_ID}
,然后按照 ${NS_ID}
和 ${NN_ID}
来组合 name,列出所有 NN 的信息即可。其余配置一样。
1. HA + Federation,所有节点,hdfs-site.xml
<property>
<name>dfs.nameservices</name>
<value>ns1, ns2</value>
</property>
<property>
<name>dfs.ha.namenodes.ns1</name>
<value>nn1,nn3</value>
</property>
<property>
<name>dfs.ha.namenodes.ns2</name>
<value>nn2,nn4</value>
</property>
<property>
<name>dfs.namenode.rpc-address.ns1.nn1</name>
<value>host-nn1:9000</value>
</property>
<property>
<name>dfs.namenode.http-address.ns1.nn1</name>
<value>host-nn1:50070</value>
</property>
<property>
<name>dfs.namenode.rpc-address.ns1.nn3</name>
<value>host-nn3:9000</value>
</property>
<property>
<name>dfs.namenode.http-address.ns1.nn3</name>
<value>host-nn3:50070</value>
</property>
<property>
<name>dfs.namenode.rpc-address.ns2.nn2</name>
<value>host-nn2:9000</value>
</property>
<property>
<name>dfs.namenode.http-address.ns2.nn2</name>
<value>host-nn2:50070</value>
</property>
<property>
<name>dfs.namenode.rpc-address.ns2.nn4</name>
<value>host-nn4:9000</value>
</property>
<property>
<name>dfs.namenode.http-address.ns2.nn4</name>
<value>host-nn4:50070</value>
</property>
对于没有 .${NS_ID}
,也就是未区分 NS 的项目,需要在每台 NN 上分别使用不同的值单独配置,尤其是 NFS 位置(dfs.namenode.shared.edits.dir),因为不同 NS 必定要使用不同的 NFS 目录来做各自内部的 HA (除非 mount 到本地是相同的,只是在 NFS 服务器端是不同的,但这样是非常不好的实践);而像 ZK 位置和隔离方式等其实大可使用一样的配置。
除了配置以外,集群的初始化也有一些额外的步骤,譬如,创建 HA 环境的时候,需要先格式化一台 NN,然后同步其 name.dir 下面的数据到第二台,然后再启动集群 (我们没有测试从单台升级为 HA 的情况,但道理应该一样)。在创建 Federation 环境的时候,需要注意保持 ${CLUSTER_ID}
的值,以确保所有 NN 能共享同一个集群的存储资源,具体做法是在格式化第一台 NN 之后,取得其 ${CLUSTER_ID}
的值,然后用如下命令格式化其他NN:
hadoop namenode -format -clusterid ${CLUSTER_ID}
当然,你也可以从第一台开始就使用自己定义的 ${CLUSTER_ID}
值。
如果是 HA + Federation 的场景,则需要用 Federation 的格式化方式初始化两台,每个 HA 环境一台,保证 ${CLUSTER_ID}
一致,然后分别同步 name.dir 下的元数据到 HA 环境里的另一台上,再启动集群。
Hadoop 2.0 中的 HDFS 客户端和 API 也有些许更改,命令行引入了新的 hdfs 命令,hdfs dfs 就等同于以前的 hadoop fs 命令。API 里引入了新的 ViewFileSystem 类,可以通过它来获取挂载表的内容,如果你不需要读取挂载表内容,而只是使用文件系统的话,可以无视挂载表,直接通过路径来打开或创建文件。代码示例如下:
ViewFileSystem fsView = (ViewFileSystem) ViewFileSystem.get(conf);
MountPoint[] m = fsView.getMountPoints();
for (MountPoint m1 : m)
System.out.println( m1.getSrc() );
// 直接使用/share/test.txt创建文件
// 如果按照之前的配置,客户端会自动根据挂载表找到是ns1
// 然后再通过failover proxy类知道nn1是Active NN并与其通信
Path p = new Path("/share/test.txt");
FSDataOutputStream fos = fsView.create(p);
主机名 | 是 NameNode 吗? | 是 DataNode 吗? | 是 JournalNode 吗? | 是 ZooKeeper 吗? | 是 ZKFC 吗? |
---|---|---|---|---|---|
nn1 | 是,属集群 ns1 | 不是 | 不是 | 不是 | 是 |
nn2 | 是,属集群 ns2 | 不是 | 不是 | 不是 | 是 |
nn3 | 是,属集群 ns1 | 不是 | 不是 | 不是 | 是 |
nn4 | 是,属集群 ns2 | 不是 | 不是 | 不是 | 是 |
dn1 | 不是 | 是 | 是 | 是 | 不是 |
dn2 | 不是 | 是 | 是 | 是 | 不是 |
dn3 | 不是 | 是 | 是 | 是 | 不是 |
1. 在 dn1 、dn2 、dn3 上分别启动 ZooKeeper 集群
zkServer.sh start
2. 在 nn1 和 nn2 上分别执行格式化 ZooKeeper 集群命令
hdfs zkfc –formatZK
3. 在 dn1 、dn2 、dn3 上分别启动 JournalNode 集群
hadoop-daemon.sh start journalnode
4. 格式化集群 ns1 的任意一个 NameNode (暂选在 nn1 上执行)
hdfs namenode -format -clusterId ns1
5. 启动 ns1 中刚才格式化的 NameNode (在 nn1 上执行)
hadoop-daemon.sh start namenode
6. 在 nn3 上把 NameNode 的数据从 nn1 同步到 nn3 中
hdfs namenode –bootstrapStandby
7. 启动 ns1 中另一个 Namenode (在 nn3 上执行)
hadoop-daemon.sh start namenode
8. 格式化集群 ns2 的任意一个 NameNode (暂选在 nn2 上执行 )
/hdfs namenode -format -clusterId ns2
9. 启动 ns2 中刚才格式化的 NameNode (在 nn2 上执行)
hadoop-daemon.sh start namenode
10. 在 nn4 上把 NameNode 的数据从 nn2 同步到 nn4 中
hdfs namenode –bootstrapStandby
11. 启动 ns2 中另一个 Namenode (在 nn4 上执行)
hadoop-daemon.sh start namenode
12. 在 nn1 上启动所有的 DataNode
hadoop-daemons.sh start datanode
13. 在 nn1 上启动Yarn
start-yarn.sh
14. 在 nn1、nn2、nn3、nn4 上分别启动 ZooKeeperFailoverController
hadoop-daemon.sh start zkfc
在任意一个节点上执行以下命令(这里以 nn1 为例),把数据上传到HDFS集群中
cd $HADOOP_HOME/etc/hadoop
hadoop fs -put core-site.xml /
hadoop fs -ls /
进行查看Federation 的测试主要是功能性上的,能用就OK了,这里的测试方案只是针对 HA 而言。我们设计了两个维度的测试矩阵:系统失效方式,客户端连接模型
1. 终止 NameNode 进程
2. NN 机器掉电
1. 已连接的客户端 (持续拷贝 96M 的文件,1M 每块)
通过增加块的数目,我们希望客户端会不断的向 NN 去申请新的块;一般是在第一个文件快结束或第二个文件刚开始拷贝的时候使系统失效。
2. 新发起连接的客户端(持续拷贝 96M 的文件,100M 每块)
因为只有一个块,所以在实际拷贝过程中失效并不会立刻导致客户端或 DN 报错,但下一次新发起连接的客户端会一开始就没有 NN 可连;一般是在第一个文件快结束拷贝时使系统失效。
针对每一种组合,我们反复测试 10-30 次,每次拷贝 5 个文件进入 HDFS,因为时间不一定掐的很准,所以有时候也会是在第三或第四个文件的时候才使系统失效,不管如何,我们会在结束后从 HDFS 里取出所有文件,并挨个检查文件 MD5,以确保数据的完整性。
1. ZKFC 主动释放锁
2. ZK 锁超时
3. 可确保数据完整性
我们的结论是:Hadoop 2.0 里的 HDFS HA 基本可满足高可用性
我们另外还(试图)测试 Append 时候 NN 失效的情形,因为 Append 的代码逻辑非常复杂,所以期望可以有新的发现,但是由于复杂的那一段只是在补足最尾部块的时候,所以必须在测试程序一运行起来就关掉 NN,测了几次,没发现异常情况。另外我们还使用 HBase 进行了测试,由于 WAL 只是 append,而且 HFile 的 compaction 操作又并不频繁,所以也没有遇到问题。
1. ha.zookeeper.session-timeout.ms = 10000
2. dfs.ha.fencing.methods = shell(/path/to/the/script)
代码可在 org.apache.hadoop.io.retry.RetryPolicies.FailoverOnNetworkExceptionRetry 里找到。目前的客户端在遇到以下 Exception 时启动重试:
// 连接失败
ConnectException
NoRouteToHostException
UnKnownHostException
// 连到了Standby而不是Active
StandbyException
其重试时间间隔的计算公式为:
RAND(0.5~1.5) * min (2^retryies * baseMillis, maxMillis)
baseMillis = dfs.client.failover.sleep.base.millis,缺省500
maxMillis = dfs.client.failover.sleep.max.millis,缺省15000
最大重试次数:dfs.client.failover.max.attempts,缺省15
关于那 15% 失败的情况,我们从日志和代码分析,基本确认是 HA 里的问题,就是 Standby NN 在变为 Active NN 的过程中,会试图重置文件的 lease 的 owner,从而导致 LeaseExpiredException: Lease mismatch,客户端遇到这个异常不会重试,导致操作失败。这是一个非常容易重现的问题,相信作者也知道,可能是为了 lease 安全性也就是数据完整性做的一个取舍吧:宁可客户端失败千次,不可lease分配错一次,毕竟,客户端失败再重新创建文件是一个很廉价且安全的过程。另外,与 MapReduce 2.0 (YARN) 的整合测试我们也没来得及做,原因是我们觉得 YARN 本身各个组件的 HA还不完善,用它来测 HDFS 的 HA 有点本末倒置。