最近自己动手把一套HDFS集群从单NameNode升级成了真正的高可用,顺带把Zookeeper也集成进去。Hadoop高可用这个事儿,光看官方文档很容易绕晕,因为不只是改两个配置文件那么简单,它牵扯到元数据共享、自动故障转移、隔离机制、JournalNode 和 Zookeeper 的联动。这篇博文就基于我这套完整的 Linux 软件安装过程,把从节点规划到配置、格式化、启动、切换测试的每个环节拆开来讲,再附上我实际踩过的几个坑。如果你正准备搭一套能自动切换的 HDFS 高可用集群,照着我这套思路和顺序走,能省掉不少折腾时间。
1. 为什么Hadoop要做高可用,Zookeeper在这里扮演什么角色
1.1 单NameNode的痛点
NameNode 是 HDFS 的大脑,整个文件系统的目录树、文件列表、数据块到 DataNode 的映射关系全在内存里。客户端读写文件之前,第一步就是找 NameNode 要元数据。它一旦宕机,整个集群就只能干瞪眼,客户端连目录列表都拿不到,更别说读写了。
最关键的是,NameNode 恢复不是重启一下就完事的。它启动时要加载 fsimage(元数据快照),再把 edits(操作日志)合并进去,文件量大、日志多的时候这个过程可能要持续几分钟甚至更久。更麻烦的是,非正常宕机时可能有一部分最新的 edits 还没来得及落盘,这部分元数据就直接丢了。所以早期大家以为 SecondaryNameNode 能做热备,实际上是误解,它只是定期帮 NameNode 做 checkpoint,并不能在故障时接管服务。
我当时升级这套环境,最直接的需求就是:主节点挂了,备份节点能顶上去,客户端不用改任何配置。
1.2 主流HA方案的取舍
Hadoop 官方支持的 HA 方案主要有两类:一类是共享存储,比如把 NameNode 的 edits 存到 NFS 上,两个 NameNode 共享同一份元数据日志;另一类是基于 Quorum Journal Manager,也就是常说的 QJM。
NFS 方案配置简单,但 NFS 本身就是单点,服务器挂了 HDFS 也跟着遭殃,等于把单点故障从 NameNode 挪到了 NFS,治标不治本。QJM 方案则是把 edits 同时写到多个 JournalNode 节点上,多数派写成功才算成功,至少需要 3 个 JournalNode,能容忍 1 台挂掉。这个思路和很多分布式系统中的共识机制有点像,但它自己已经把多数派逻辑实现了,不需要我们再写额外组件。
至于 Zookeeper,它在这里不是用来存 HDFS 元数据的,而是负责自动故障转移的选主和脑裂防护。具体来说,两个 NameNode 上各跑一个 ZKFC(ZookeeperFailoverController)进程,这个进程会去 Zookeeper 里抢一个临时节点。Active 节点持有这个节点,Standby 节点监听这个节点;Active 所在机器失联后,临时节点自动消失,Standby 的 ZKFC 抢到锁,把 Standby 升级为 Active。这套机制保证了失败切换是自动完成的,不需要人工干预。
1.3 本次安装的总体方案
我这次搭建用的是 3 台 Linux 服务器,角色分配如下:node1 和 node2 部署 NameNode,同时 node1、node2、node3 都部署 DataNode、JournalNode 和 Zookeeper。也就是说 ZK 和 JournalNode 是和 NameNode 混布的,没有单独拆机器。
这样做适合中小规模的集群。如果节点资源充足,也可以把 JournalNode 或 Zookeeper 拆到独立机器上,但 3 台混布是最省机器、也最容易演示 HA 效果的方案。版本方面,我用的是社区版 Hadoop 3.3.x,Zookeeper 3.7.x,JDK 必须 8 以上。Hadoop 2.x 的老教程里很多参数和端口都不一样,看的时候要留意。
2. 基础环境与节点规划
2.1 节点角色分配
先明确机器和角色,不然后面配置容易乱。我的规划是这样的:
| 节点 | IP(示例) | 角色 |
|---|---|---|
| node1 | 192.168.100.101 | NameNode、ZKFC、JournalNode、DataNode、Zookeeper |
| node2 | 192.168.100.102 | NameNode、ZKFC、JournalNode、DataNode、Zookeeper |
| node3 | 192.168.100.103 | JournalNode、DataNode、Zookeeper |
每个节点都要配 /etc/hosts,把三个节点的 hostname 和 IP 对应关系写进去,并且所有节点保持一致。这一步千万别跳过,后面 Zookeeper、JournalNode、NameNode 之间都是通过主机名互相访问的,如果某台机器解析不了对方,启动时就会出现各种奇葩连接失败。
2.2 系统层面要做的事
防火墙这里,如果只是测试环境,建议直接关闭,不然就要放行一堆端口。Zookeeper 需要 2181(客户端)、2888(Leader 和 Follower 通信)、3888(选举),HDFS 需要 8020(RPC)、9870(NameNode Web UI,Hadoop 2.x 是 50070)、8485(JournalNode)、9864(DataNode Web UI)。开发环境下关防火墙最省心,生产环境按最小权限原则放行。
时间同步也很重要,Zookeeper 的会话机制对时钟偏移非常敏感。时钟差得太多,ZK 会误判 session 超时,导致 ZKFC 频繁切换。我是用了 chrony 做时间同步,三条节点都指向同一个时间源,node1 做本地时间服务器也行。这个坑比较隐蔽,如果后面发现 ZK 连接时断时续,可以先检查时间。
文件描述符和进程数限制建议调高一些,DataNode 节点尤其明显,文件句柄不够时会出现莫名其妙的读写失败。在 /etc/security/limits.conf 里给运行 Hadoop 的用户配置 nofile 和 nproc 到 65535 以上。
2.3 SSH免密与目录规划
SSH 免密至少要配置 node1 到 node2、node3 的免密,最好三台之间互相都配。这一步有两处用途:一是 start-dfs.sh 这类脚本会通过 SSH 远程拉起其他节点的进程,二是因为后面 HA 的隔离机制默认用的 sshfence,需要从当前 NameNode SSH 到故障节点执行隔离命令,没有免密的话隔离会失败,可能导致双 Active 的脑裂。
配置方式就是经典的 ssh-keygen 和 ssh-copy-id,生成密钥后把公钥分发到其他两台节点。我记得初次配置完一定要用 ssh nodeX date 这种方式验证,很多问题都是密钥没分发完整,等启动时才暴露。
数据目录规划我建议单独用大磁盘分区,不要默认放 /tmp,也不要堆在系统盘根分区。我这边数据目录全部放在 /data 下,比如 /data/zookeeper、/data/hadoop-hdfs/journalnode、/data/hadoop-hdfs/nn、/data/hadoop-hdfs/dn。给每个目录确认属主是运行 Hadoop 的用户,权限不对会导致进程启动后立刻退出。我把这些目录写在文档开头,后面配置文件的路径都对着它填,避免改了这头忘了那头。
3. Zookeeper集群搭建
3.1 安装包准备与myid
Zookeeper 安装包下载解压后,我会做一个软链,方便切换版本。目录结构大致是 /usr/local/soft/zookeeper,然后在 conf 目录里复制一份 zoo_sample.cfg 改成 zoo.cfg。
Zookeeper 集群启动时靠一个叫 myid 的文件区分身份。这个文件放在数据目录里,内容就一个数字,和 zoo.cfg 里的 server.N 编号对应。例如 node1 的 myid 写 1,node2 写 2,node3 写 3。这一步特别容易被忽略,我第一次搭的时候三台节点的 myid 全写了 1,结果启动后大家都不认对方,日志里全是连接失败的报错。记住:myid 的数字必须唯一,并且和 server 编号严格对应。
3.2 zoo.cfg配置需要注意的细节
这是我的 zoo.cfg 核心片段:
tickTime=2000 initLimit=10 syncLimit=5 dataDir=/data/zookeeper clientPort=2181 admin.serverPort=8888 server.1=node1:2888:3888 server.2=node2:2888:3888 server.3=node3:2888:3888tickTime 是 Zookeeper 的最小时间单位,毫秒值;initLimit 是 Follower 启动时能容忍的同步初始化时间,是 tickTime 的倍数;syncLimit 是 Leader 和 Follower 之间心跳的超时倍数。一般默认即可。
不过有一点要特意说明:Zookeeper 3.5 以上版本默认启动了 Admin Server,监听 8080 端口。这个端口很容易被其他应用占用,如果启动时发现 8080 冲突,就在 zoo.cfg 里设置 admin.serverPort 改成别的端口,比如 8888。我当时就是和集群里一个 Web 服务撞了端口,排查了好一会儿才发现是 Zookeeper 的 Admin Server,不是 Hadoop 的问题。
3.3 启动Zookeeper并验证集群状态
三台节点依次执行:
bin/zkServer.sh start启动后我看一下进程,jps 里能看到 QuorumPeerMain 进程。再用命令检查状态:
bin/zkServer.sh status正常情况下一台显示 leader,另外两台显示 follower,这就说明 ZK 集群已经选主成功。如果显示 standalone,那多半是 zoo.cfg 里的 server 列表没写对,或者 myid 有问题,Zookeeper 没组成集群,而是各自单机运行。
如果启动过程报错,可以直接在前台运行:
bin/zkServer.sh start-foreground这样日志会直接打在前台,比翻日志方便很多。我习惯先用 start-foreground 确认没问题,再正常后台启动。
4. Hadoop核心配置,HA参数逐个拆
4.1 配置文件同步的大原则
Hadoop 的 HA 配置分布在 core-site.xml、hdfs-site.xml 里。最容易出问题的不是写错某个参数,而是两台 NameNode 上的配置不一致。比如 node1 改了 hdfs-site.xml 忘了同步到 node2,结果 node1 能识别 nn1 和 nn2,node2 只认识 nn1,ZKFC 一启动就找不到对应节点。
我的做法是:所有节点的 core-site.xml、hdfs-site.xml 内容完全一致,改完一处,用脚本同步到另外两台,再重启相关服务。手工去节点上改是最容易漏的。
4.2 core-site.xml里的关键配置
core-site.xml 里有几个参数决定了客户端和 NameNode 的访问关系:
<property> <name>fs.defaultFS</name> <value>hdfs://mycluster</value> </property> <property> <name>ha.zookeeper.quorum</name> <value>node1:2181,node2:2181,node3:2181</value> </property>fs.defaultFS 里的 mycluster 是 nameservice 的逻辑名称,它必须在 hdfs-site.xml 里对应定义。客户端访问 HDFS 时,会通过这个逻辑名称去找到底该连哪个 NameNode。ha.zookeeper.quorum 是 ZKFC 和故障转移模块要连接的 Zookeeper 地址,这里写全三台,只要多数派活着就能工作。
4.3 hdfs-site.xml:HA参数详解
这是整个部署中最重要的配置文件。我直接给出关键参数表,再逐个说明:
| 参数 | 值 | 说明 |
|---|---|---|
| dfs.nameservices | mycluster | 逻辑名称,必须和 fs.defaultFS 一致 |
| dfs.ha.namenodes.mycluster | nn1,nn2 | 两个 NameNode 的逻辑编号,不是主机名 |
| dfs.namenode.rpc-address.mycluster.nn1 | node1:8020 | nn1 的 RPC 地址 |
| dfs.namenode.rpc-address.mycluster.nn2 | node2:8020 | nn2 的 RPC 地址 |
| dfs.namenode.http-address.mycluster.nn1 | node1:9870 | nn1 的 Web UI 地址 |
| dfs.namenode.http-address.mycluster.nn2 | node2:9870 | nn2 的 Web UI 地址 |
| dfs.namenode.shared.edits.dir | qjournal://node1:8485;node2:8485;node3:8485/mycluster | 共享 edits 日志位置 |
| dfs.journalnode.edits.dir | /data/hadoop-hdfs/journalnode | JournalNode 本地数据目录 |
| dfs.client.failover.proxy.provider.mycluster | org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider | 客户端自动故障转移代理 |
| dfs.ha.automatic-failover.enabled | true | 开启自动故障转移 |
| dfs.ha.fencing.methods | sshfence | 隔离方式 |
| dfs.ha.fencing.ssh.private-key-files | /home/hadoop/.ssh/id_rsa | sshfence 使用的私钥 |
| dfs.namenode.name.dir | /data/hadoop-hdfs/nn | NameNode 元数据目录 |
| dfs.datanode.data.dir | /data/hadoop-hdfs/dn | DataNode 数据目录 |
值得多说两句的地方有两个。一个是 dfs.namenode.shared.edits.dir,它用的是 qjournal 协议,多个 JournalNode 之间用分号分隔,最后跟一个 nameservice 名称。这个写法很固定,漏了哪个分号或者最后的 /mycluster 不匹配,都会导致同步失败。
另一个是隔离机制。active 和 standby 两个 NameNode 同时认为自己是 active 的情况叫做脑裂,后果非常严重。sshfence 的意思是,当前节点接管之前,先通过 SSH 登录到老的主节点,执行 kill 或 fuser 把它的 NameNode 进程干掉,确保它真正退出。所以前面说的 SSH 免密在这里兑现价值了。如果免密没配好,fence 失败,就可能出现两个 active,这个要特别重视。
4.4 其他辅助配置
hadoop-env.sh 里需要设置 JAVA_HOME,否则 hdfs 脚本找不到 JDK。workers 文件(旧版本叫 slaves)里写上三台节点,这样 DataNode 能在所有节点上启动。同时把 Hadoop 的 bin 和 sbin 目录加到 PATH 环境变量里,方便直接敲 hdfs 命令。
这里要提醒一点,Hadoop 3.x 的 Web UI 端口是 9870,Hadoop 2.x 是 50070。网上很多教程是老的,如果按 50070 去访问 3.x 集群会访问不到。配置 http-address 时如果不确定,先看一下你用的版本对应的默认端口。
5. 初始化与启动:顺序决定成败
5.1 为什么顺序这么重要
Hadoop HA 初始化最坑的地方在于顺序。网上教程说法很多,有的让你先格式化,有的让你先启动 JournalNode,顺序错了后面全是连环坑。
我总结的顺序是:Zookeeper 先起,JournalNode 再起,然后才在主节点上格式化并启动 NameNode,接着在备节点上做 bootstrapStandby,最后才是格式化和启动 ZKFC。这个顺序背后的逻辑是:NameNode 格式化时要和 JournalNode 建立连接;Standby NameNode 同步元数据时需要主 NameNode 已经在运行;ZKFC 运行时需要 ZK 和 NameNode 都在线。倒过来任何一个环节都会失败。
5.2 启动JournalNode与格式化主NameNode
三台节点都执行:
hdfs --daemon start journalnode确认进程存在后,在 node1 上执行:
hdfs namenode -format注意这里只在 node1 执行,不能再在 node2 上执行,否则两个 NameNode 的 namespaceID 会不一样。格式化完成会生成 clusterID、namespaceID 和 blockpoolID,这些是 HDFS 集群的身份信息。之后在 node1 启动主 NameNode:
hdfs --daemon start namenode启动后先访问 node1:9870,能看到 NameNode 的 Web 页面,但状态可能是 Active,也可能还在安全模式。第一次启动处于安全模式是正常的,DataNode 还没上报足够的数据块信息,等 DataNode 陆续注册后会自动退出安全模式。
5.3 备NameNode的bootstrapStandby
在 node2 上执行元数据同步:
hdfs namenode -bootstrapStandby这个命令会从 node1 的主 NameNode 拷贝 fsimage 和 edits 到 node2 的本地 name.dir 目录,相当于把主节点的元数据快照复制一份。执行完成后,node2 本地就有了和 node1 一致的元数据。
这里必须提醒:不要在 node2 上执行 hdfs namenode -format。一旦误格式化,node2 会生成一套新的 namespaceID,和 node1 完全不一致,再启动备节点时日志里会报 Inconsistent namespaceID,原因是备节点已经启动,但 active 节点的 blockpool 对不上。
如果真误操作了,解决办法是:停止 node2 的 NameNode,删除 node2 上的 /data/hadoop-hdfs/nn 目录,重新执行一次 bootstrapStandby。千万不要去改 namespaceID 之类的元数据,那不是正道。
同步完成后在 node2 启动备 NameNode:
hdfs --daemon start namenode这时候再访问 node2:9870,页面顶部应该显示 Standby 状态。如果显示不了状态,多半是 hdfs-site.xml 里的 ha 相关参数没配全。
5.4 ZKFC初始化与启动
ZKFC 的初始化只在其中一台执行,通常是 node1:
hdfs zkfc -formatZK这条命令会在 Zookeeper 中创建 HA 需要的 znode 路径,默认在 /hadoop-ha/mycluster 下。然后分别在 node1 和 node2 启动 ZKFC:
hdfs --daemon start zkfc启动后可以用 Zookeeper 客户端看一眼:
bin/zkCli.sh -server node1:2181 ls /hadoop-ha/mycluster正常情况下能看到 ActiveBreadCrumb 和 ActiveStandbyElectorLock 之类的节点。ActiveStandbyElectorLock 就是 active NameNode 持有的锁,Standby 节点在等这个锁失效。
这里我想特别强调一点:ZKFC 必须等 NameNode 都启动后再启动,因为 ZKFC 启动时会检查本机 NameNode 的状态。如果你顺序反了,先把 ZKFC 拉起来再启动 NameNode,它可能认为本节点不可用,反复尝试选举,日志刷得飞快。
5.5 启动DataNode与整体验证
三台节点分别执行:
hdfs --daemon start datanode或者你配置没问题的话,在 node1 上执行 start-dfs.sh,脚本会自动去各个节点拉起 DataNode、JournalNode 等。不过既然前面已经分步手动启动了,我建议继续分步,避免脚本重复启动导致端口占用。
全部启动后做一个整体状态检查:
hdfs haadmin -getAllServiceState输出应该类似:
node1:8020 active node2:8020 standby同时看 DataNode 是否都注册上来了。我那次启动后 node3 的 DataNode 一直没出现在页面里,排查半天发现是 node3 的 hdfs-site.xml 里 dfs.datanode.data.dir 路径的目录权限不对,日志里报 Permission denied。
6. 故障切换测试与常见问题
6.1 手动切换和自动切换测试
高可用搭好之后一定要测,不测等于没搭。先测自动切换,在 node1 上找到 NameNode 进程:
ps -ef | grep NameNode然后模拟宕机,把主节点的 NameNode 进程干掉:
kill -9 <pid>等待几秒,然后在 node2 上执行:
hdfs haadmin -getServiceState nn2正常情况下 node2 会从 standby 变成 active。ZK 的临时节点超时机制会在几十秒内感知到主节点失联,ZKFC 完成接管。我这里实测从 kill 到 node2 变 active,大概十几秒,具体时长跟 session 超时配置有关。
测试完把 node1 的 NameNode 进程重新启动,它会自动作为 standby 加回来,不需要重新初始化。手动切换也有命令:
hdfs haadmin -transitionToActive nn2不过手动切换时要小心,这条命令在某些版本里需要加 -forcemanual 选项,否则会被拒绝,这是为了防止误操作引发脑裂。
6.2 常见问题速查表
我把实际遇到过的问题整理成了一张表:
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 备节点启动报 Inconsistent namespaceID | node2 执行过 format,namespaceID 不一致 | 删除备节点 name.dir,重新 bootstrapStandby |
| ZKFC 日志提示连接 Zookeeper 失败 | ZK 没启动或 quorum 地址写错 | 先确认三台 ZK 状态正常,检查 ha.zookeeper.quorum |
| active 和 standby 之外又多了一个 active | 脑裂,可能是 fencing 失败或 SSH 免密失效 | 检查 sshfence 配置,确认私钥路径正确;修复免密后再启动 |
| JournalNode 收不到 active 节点的日志 | 8485 端口不通、目录权限不对、磁盘满了 | 检查防火墙、目录属主和 df -h 磁盘空间 |
| NameNode 一直处于安全模式 | 刚启动正常现象;如果长期不退出,检查副本数 | 稍等片刻,等待 DataNode 上报;必要时执行 hdfs dfsadmin -safemode leave |
| ZK 启动报 8080 端口被占用 | Admin Server 默认端口冲突 | 在 zoo.cfg 里设置 admin.serverPort 为其他端口 |
| 2.x 教程配置的 50070 端口访问不了 | Hadoop 3.x 默认 Web 端口已变更 | 使用 9870,或检查 dfs.namenode.http-address |
表格里每一类问题我都遇到不止一次。特别是第二个,有时候不是 ZK 没启动,而是 node1 上 ZK 起来了,node2、node3 的 ZK 因为 myid 不对悄悄退出了,quorum 不满足多数派,客户端连谁都会失败。所以遇到 ZK 连接问题,第一件事是去三台节点都执行一下 zkServer.sh status,别只看一台。
6.3 排查日志的入口
Hadoop 的问题排查,日志目录是最重要的现场。默认在 Hadoop 安装目录的 logs 目录下,常见的有:
logs/hadoop-hdfs-namenode-node1.log logs/hadoop-hdfs-journalnode-node1.log logs/hadoop-hdfs-zkfc-node1.log logs/hadoop-hdfs-datanode-node1.log看日志也有门道。NameNode 日志记录了元数据加载、安全模式、block 上报;JournalNode 日志记录了 edits 的接收和同步;ZKFC 日志记录了选举和故障转移;DataNode 日志记录数据块操作。一个问题不要只看一个日志,比如备节点变不了 active,先看 ZKFC 日志,再看 NameNode 日志,最后看 JournalNode 有没有收到新的 edits。
我遇到过一种情况,active 节点上的写操作报错,但页面看着是正常的,最后是 JournalNode 的磁盘分区满了,edits 写不进去。JournalNode 日志里一直在刷 IOException,磁盘清理之后自动恢复。这类问题如果不看日志,光从页面状态是很难定位的。
7. 生产环境还要注意什么
7.1 生产级部署的几个方向
如果这套方案要上生产,有几点是必须加强的。一是 ZooKeeper 和 JournalNode 的节点数建议扩到 5 台,容忍度更高。3 台虽然能容忍 1 台故障,但故障后没有冗余,Zookeeper 和 JournalNode 都处在临界状态。二是所有元数据目录和数据目录要放到独立磁盘,最好做 RAID,不要和系统盘挤在一起。三是给 NameNode 的 heap 足够大,元数据都在内存里,堆小了容易 Full GC 频繁,影响整个集群的稳定性。四是建议引入监控,至少对 Zookeeper 的 leader 数量、JournalNode 的磁盘占用、NameNode 的 active/standby 状态做告警。
另外,YARN 的 ResourceManager 也有类似的高可用机制,可以和 HDFS HA 配合部署,配置上并不冲突。这块我没有在本文展开,但思路是类似的,也依赖 Zookeeper 做选主。
7.2 最后再分享一个小技巧
格式化之前,最好先做一次配置一致性校验。我每次改完 hdfs-site.xml 或 core-site.xml,都会用 diff 把 node1 的文件和 node2、node3 的对比一遍。这个动作看着简单,但能避免大量问题。很多时候故障转移失败,根本不是参数写错,而是某个节点的配置文件漏改了或者同步错了。把配置文件一致性当成部署的一部分,纳入检查项,能省下无数排查时间。
另外一个感受比较深的点是:不要在安装环境里图省事,所有目录和路径都要有清晰的规划。前期规划越好,后面排查越轻松。我这次搭建虽然中间踩了不少坑,但把这些坑记录下来之后,下次再搭一套高可用集群,实际上就是个机械操作。希望这篇 Linux 软件安装记录能帮你把顺序理清。