2013 年,InfoQ 将会密切关注大数据领域的最新进展,并在“大数据专栏“中向读者展示优秀的开发实践,欢迎大家向InfoQ 投稿和广大的开发者分享在大数据的开发经验和心得,投稿地址: editors@cn.infoq.com 。
一、背景
天云趋势在 2012 年下半年开始为某大型国有银行的历史交易数据备份及查询提供基于 Hadoop 的技术解决方案,由于行业的特殊性,客户对服务的可用性有着非常高的要求,而 HDFS 长久以来都被单点故障的问题所困扰,直到 Apache Hadoop 在 2012 年 5 月发布了 2.0 的 alpha 版本,其中 MRv2 还很不成熟,可 HDFS 的新功能已经基本可用,尤其是其中的的 High Availability(以下简称 HA) 和 Federation。Cloudera 也于 7 月制作了 CDH4.0.1,包含了 Hadoop 2.0 的诸多新功能和组件,于是我们就基于 CDH4.0.1 进行了 HA 和 Federation 的测试。
此工作由我和同事张军、钱兴会共同完成。
二、为什么需要 HA 和 Federation
1. 单点故障
在 Hadoop 2.0 之前,也有若干技术试图解决单点故障的问题,我们在这里做个简短的总结
- Secondary NameNode。它不是 HA,它只是阶段性的合并 edits 和 fsimage,以缩短集群启动的时间。当 NameNode(以下简称 NN) 失效的时候,Secondary NN 并无法立刻提供服务,Secondary NN 甚至无法保证数据完整性:如果 NN 数据丢失的话,在上一次合并后的文件系统的改动会丢失。
- Backup NameNode ( HADOOP-4539 )。它在内存中复制了 NN 的当前状态,算是 Warm Standby,可也就仅限于此,并没有 failover 等。它同样是阶段性的做 checkpoint,也无法保证数据完整性。
- 手动把 name.dir 指向 NFS。这是安全的 Cold Standby,可以保证元数据不丢失,但集群的恢复则完全靠手动。
- Facebook AvatarNode 。Facebook 有强大的运维做后盾,所以 Avatarnode 只是 Hot Standby,并没有自动切换,当主 NN 失效的时候,需要管理员确认,然后手动把对外提供服务的虚拟 IP 映射到 Standby NN,这样做的好处是确保不会发生脑裂的场景。其某些设计思想和 Hadoop 2.0 里的 HA 非常相似,从时间上来看,Hadoop 2.0 应该是借鉴了 Facebook 的做法。
- 还有若干解决方案,基本都是依赖外部的 HA 机制,譬如 DRBD , Linux HA , VMware 的 FT 等等。
2. 集群容量和集群性能
单 NN 的架构使得 HDFS 在集群扩展性和性能上都有潜在的问题,当集群大到一定程度后,NN 进程使用的内存可能会达到上百 G,常用的估算公式为 1G 对应 1 百万个块,按缺省块大小计算的话,大概是 64T (这个估算比例是有比较大的富裕的,其实,即使是每个文件只有一个块,所有元数据信息也不会有 1KB/block)。同时,所有的元数据信息的读取和操作都需要与 NN 进行通信,譬如客户端的 addBlock、getBlockLocations,还有 DataNode 的 blockRecieved、sendHeartbeat、blockReport,在集群规模变大后,NN 成为了性能的瓶颈。Hadoop 2.0 里的 HDFS Federation 就是为了解决这两个问题而开发的。
三、Hadoop 2.0 里 HA 的实现方式
图片来源: HDFS-1623 设计文档
图片作者: Sanjay Radia, Suresh Srinivas
在这个图里,我们可以看出 HA 的大致架构,其设计上的考虑包括:
-
利用共享存储来在两个 NN 间同步 edits 信息。
以前的 HDFS 是 share nothing but NN,现在 NN 又 share storage,这样其实是转移了单点故障的位置,但中高端的存储设备内部都有各种 RAID 以及冗余硬件包括电源以及网卡等,比服务器的可靠性还是略有提高。通过 NN 内部每次元数据变动后的 flush 操作,加上 NFS 的 close-to-open,数据的一致性得到了保证。社区现在也试图把元数据存储放到 BookKeeper 上,以去除对共享存储的依赖,Cloudera 也提供了 Quorum Journal Manager 的实现和代码,这篇中文的 blog 有详尽分析:基于 QJM/Qurom Journal Manager/Paxos 的 HDFS HA 原理及代码分析 -
DataNode(以下简称 DN) 同时向两个 NN 汇报块信息。
这是让 Standby NN 保持集群最新状态的必需步骤,不赘述。 -
用于监视和控制 NN 进程的 FailoverController 进程
显然,我们不能在 NN 进程内进行心跳等信息同步,最简单的原因,一次 FullGC 就可以让 NN 挂起十几分钟,所以,必须要有一个独立的短小精悍的 watchdog 来专门负责监控。这也是一个松耦合的设计,便于扩展或更改,目前版本里是用 ZooKeeper(以下简称 ZK) 来做同步锁,但用户可以方便的把这个 ZooKeeper FailoverController(以下简称 ZKFC) 替换为其他的 HA 方案或 leader 选举方案。 -
隔离 (Fencing) ,防止脑裂,就是保证在任何时候只有一个主 NN,包括三个方面:
- 共享存储 fencing,确保只有一个 NN 可以写入 edits。
- 客户端 fencing,确保只有一个 NN 可以响应客户端的请求。
- DataNode fencing,确保只有一个 NN 可以向 DN 下发命令,譬如删除块,复制块,等等。
四、Hadoop 2.0 里 Federation 的实现方式
图片来源: HDFS-1052 设计文档
图片作者: Sanjay Radia, Suresh Srinivas
这个图过于简明,许多设计上的考虑并不那么直观,我们稍微总结一下
- 多个 NN 共用一个集群里 DN 上的存储资源,每个 NN 都可以单独对外提供服务
- 每个 NN 都会定义一个存储池,有单独的 id,每个 DN 都为所有存储池提供存储
- DN 会按照存储池 id 向其对应的 NN 汇报块信息,同时,DN 会向所有 NN 汇报本地存储可用资源情况
- 如果需要在客户端方便的访问若干个 NN 上的资源,可以使用客户端挂载表,把不同的目录映射到不同的 NN,但 NN 上必须存在相应的目录
这样设计的好处大致有:
- 改动最小,向前兼容
- 现有的 NN 无需任何配置改动.
- 如果现有的客户端只连某台 NN 的话,代码和配置也无需改动。
- 分离命名空间管理和块存储管理
- 提供良好扩展性的同时允许其他文件系统或应用直接使用块存储池
- 统一的块存储管理保证了资源利用率
- 可以只通过防火墙配置达到一定的文件访问隔离,而无需使用复杂的 Kerberos 认证
- 客户端挂载表
- 通过路径自动对应 NN
- 使 Federation 的配置改动对应用透明
五、测试环境
以上是 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 和客户端,需要做如下更改:
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>
以上的示例里,我们用了 ${}来表示变量值,其展开后的内容大致如下:
与此同时,在 HA 集群的 NameNode 或客户端还需要做如下配置的改动:
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> 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:
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 和挂载表入手:
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,示例如下:
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 的具体信息:
Federation,所有节点,hdfs-site.xml
<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
上面这个问题,我在 EasyHadoop 的聚会上没有讲清楚,只是简单的说在 NN 上不要使用 viewfs:// 来配置,而是使用 hdfs://,那样是可以解决问题,但是是并不是最好的方案,也没有把问题的根本说清楚。
最后,我们来组合 HA 和 Federation,真正搭建出和本节开始处的测试环境示意图一样的实例。通过前面的描述,有经验的朋友应该已经猜到了,其实 HA+Federation 配置的关键,就是组合 hdfs-site.xml 里的 dfs.nameservices 以及 dfs.ha.namenodes.${NS_ID},然后按照 ${NS_ID}和 ${NN_ID}来组合 name,列出所有 NN 的信息即可。其余配置一样。
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);
六、HA 测试方案和结果
Federation 的测试主要是功能性上的,能用就 OK 了,这里的测试方案只是针对 HA 而言。我们设计了两个维度的测试矩阵:系统失效方式,客户端连接模型
系统失效有两种:
- 终止 NameNode 进程:ZKFC 主动释放锁
模拟机器 OOM、死锁、硬件性能骤降等故障 - NN 机器掉电:ZK 锁超时
模拟网络和交换机故障、以及掉电本身
客户端连接也是两种:
- 已连接的客户端 (持续拷贝 96M 的文件,1M 每块)
通过增加块的数目,我们希望客户端会不断的向 NN 去申请新的块;一般是在第一个文件快结束或第二个文件刚开始拷贝的时候使系统失效。 - 新发起连接的客户端 (持续拷贝 96M 的文件,100M 每块)
因为只有一个块,所以在实际拷贝过程中失效并不会立刻导致客户端或 DN 报错,但下一次新发起连接的客户端会一开始就没有 NN 可连;一般是在第一个文件快结束拷贝时使系统失效。
针对每一种组合,我们反复测试 10-30 次,每次拷贝 5 个文件进入 HDFS,因为时间不一定掐的很准,所以有时候也会是在第三或第四个文件的时候才使系统失效,不管如何,我们会在结束后从 HDFS 里取出所有文件,并挨个检查文件 MD5,以确保数据的完整性。
测试结果如下:
- ZKFC 主动释放锁
- 5-8 秒切换 (需同步 edits)
- 客户端偶尔会有重试 (~10%)
- 但从未失败
- ZK 锁超时
- 15-20s 切换 (超时设置为 10s)
- 客户端重试几率变大 (~75%)
- 且偶有失败 (~15%),但仅见于已连接客户端
- 可确保数据完整性
- MD5 校验从未出错 + 失败时客户端有 Exception
我们的结论是:Hadoop 2.0 里的 HDFS HA 基本可满足高可用性
扩展测试
我们另外还 (试图) 测试 Append 时候 NN 失效的情形,因为 Append 的代码逻辑非常复杂,所以期望可以有新的发现,但是由于复杂的那一段只是在补足最尾部块的时候,所以必须在测试程序一运行起来就关掉 NN,测了几次,没发现异常情况。另外我们还使用 HBase 进行了测试,由于 WAL 只是 append,而且 HFile 的 compaction 操作又并不频繁,所以也没有遇到问题。
七、HA 推荐配置及其他
HA 推荐配置
- ha.zookeeper.session-timeout.ms = 10000
- ZK 心跳是 2000
- 缺省的 5000 很容易因为网络拥塞或 NN GC 等导致误判
- 为避免电源闪断,不要把 start-dfs.sh 放在 init.d 里
- dfs.ha.fencing.methods = shell(/path/to/the/script)
- STONITH (Shoot The Other Node In The Head) 不一定可行,当没有网络或掉电的时候,是没法 shoot 的
- 缺省的隔离手段是 sshfence,在掉电情况下就无法成功完成,从而切换失败
- 唯一能保证不发生脑裂的方案就是确保原 Active 无法访问 NFS
- 通过 script 修改 NFS 上的 iptables,禁止另一台 NN 访问
- 管理员及时介入,恢复原 Active,使其成为 Standby。恢复 iptables
客户端重试机制
代码可在 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 有点本末倒置。
关于天云趋势
天云趋势由天云科技和趋势科技共同投资成立于 2010 年 3 月,趋势科技是 Hadoop 的重度使用者:2006 年开始使用, 用于处理 Web Reputation 和 Email Reputation 等、五个数据中心,近 1000 个节点、日均处理 3.6T 日志数据、亚洲最早的主要代码贡献者、HBase 0.92 新功能的主要开发者 (coprocessors, security);
天云趋势的 BigData 业务:TCloud BigData Platform; 集部署、监控、配置优化、数据处理以及扩展 SDK 库于一体的集群管理和开发平台;丰富的垂直行业开发经验, 包括电信、电力、银行等、为多家大型传统软件开发厂商提供咨询服务; Hadoop 管理员、开发者培训;也可为企业内部进行定制化培训,详情请访问: http://www.tcloudcomputing.com.cn
关于作者
孙振南,资深系统架构师。天云趋势技术总监。曾就职趋势科技 10 年。参与了云端网页评级、云端垃圾邮件过滤等核心业务系统的构建。作者博客。
评论