ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

ZooKeeper在Hadoop生态中的角色:主备切换、状态存储与排障

ZooKeeper在Hadoop生态中的角色:主备切换、状态存储与排障

凌晨两点半被电话叫起来,说某套集群的 NameNode 主备在十几分钟里来回切了三次,上层写入大面积超时。爬起来先翻 NameNode 自己的日志,干干净净,没有任何异常堆栈;再去看 ZKFC 的日志,满屏的ConnectionLossException和SessionExpiredException。问题根本不在 HDFS,是 ZooKeeper 那边有一台机器在做长时间的 Full GC,会话超时了,ZKFC 判定"我这边的 NameNode 不行了",把备机提了上来。这件事让我对"ZooKeeper 在 Hadoop 里到底扮演什么角色"有了完全不一样的认识——它不是可选的辅助组件,在很多链路里它就是那个决定"谁能当主、谁还活着"的裁判,裁判一抖,全场跟着乱。

这篇文章就把 ZooKeeper 在 Hadoop 生态里的实际用法掰开讲清楚:HDFS 的 NameNode 主备切换靠它做了什么、YARN 的 ResourceManager 除了选举还拿它存了什么、HBase 为什么对它的依赖比 Hadoop 本身还重、Hive 这类组件又是怎么轻量地用一下。顺带把配置参数、znode 路径、我踩过的坑和排查链路都摊开讲。不管你是刚接手一套 HA 集群,还是已经被"主备乱切"折腾过几轮,下面这些内容应该都能对上你的场景。

1. 先搞清楚 Hadoop 把哪些"命门"交给了 ZooKeeper

1.1 没有 ZooKeeper 的年代,HDFS 的高可用是半残的

早期的 HDFS 是单 NameNode 架构,SecondaryNameNode 这个名字骗了不少人。它压根不是备胎,只负责定期把 fsimage 和 edits 合并,缩短重启时的元数据加载时间,既不接管读也不接管写。真出故障,运维得手工把 SecondaryNameNode 上的元数据目录拷到一台新机器,改配置、改指向、重启进程,一套流程顺利的话半小时,不顺利的话半天,中间整个集群不可写。那时候所谓的"高可用"其实是"高可人工恢复"。

后来有了 NameNode HA,两台 NameNode 一主一备,共享 edits 目录(一般是 JournalNode 集群)。但马上就撞上一个绕不开的问题:**"谁是主"这件事,谁来裁决?**两台 NameNode 自己商量是商量不出来的,网络一断,A 不知道 B 是死了还是只是暂时联系不上,两边都觉得自己该当主,就成了双主脑裂,两份元数据同时写,数据直接废掉。必须引入一个双方都信任、能看到全局状态的第三方来做裁判。ZooKeeper 就是干这个的,而且它做的远不止"投票"这一件事。

1.2 ZooKeeper 在 Hadoop 体系里其实只干三件事

别看各种组件文档里提到 ZooKeeper 的地方很多,剥开看,用法就三类:

  • 选主:靠临时节点(Ephemeral Znode)的唯一性。谁先创建成功谁就是主,创建失败的进入待命状态并挂一个 Watch。会话一断,临时节点自动消失,其他人立刻收到通知重新抢。
  • 注册与心跳:还是临时节点,只不过每个实例建一个自己的节点,父节点下ls一下就知道当前有多少活着的成员。进程挂了节点自动消失,不需要额外的健康检查协议。
  • 状态存储:用持久节点存一些不能丢的小状态,比如 YARN 里跑了哪些应用、HBase 的 meta 表在哪台机器上。配合节点的 version 做乐观锁,保证并发写入不打架。

打个比方,ZooKeeper 就是公司门口那块公告栏。谁想当组长,就上去贴一张"我是组长"的便利贴,但这张便利贴是用绳子拴在手腕上的,人一走(会话过期)便利贴自动掉下来。其他人守在公告栏边上(Watch),一看到掉了就冲上去贴自己的。这套机制简单粗暴,但极其有效,因为"人还在不在"这件事不需要任何人去主动上报,是 ZooKeeper 会话机制天然保证的。

1.3 各组件到底在 ZooKeeper 上挂了什么

光说概念虚,直接把真实环境里ls出来的东西列出来更直观。下面这张表是我在一套跑了 HDFS + YARN + HBase + Hive 的集群上整理出来的,路径都是默认配置下的实际值:

组件用途关键 znode 路径节点类型
HDFS NameNode主备选举/hadoop-ha/<nameservice>/ActiveStandbyElectorLock临时
HDFS NameNode记录上次主节点,供 fencing 用/hadoop-ha/<nameservice>/ActiveBreadCrumb持久
YARN RM主备选举/yarn-leader-election/<rmId>/ActiveStandbyElectorLock临时
YARN RM应用/容器状态恢复/rmstore/ZKRMStateRoot/RMAppRoot/...持久
HBaseMaster 选举/hbase/master临时
HBaseRegionServer 注册/hbase/rs/<hostname,port,startcode>临时
HBasemeta 表位置寻址/hbase/meta-region-server持久
Hive表级锁/hive_zookeeper_namespace/...临时/顺序
HiveHiveServer2 实例注册/hiveserver2/...临时

看到这张表你应该能反应过来一件事:这些 znode 一旦丢了或者出现异常,对应的组件就会失去"自我认知"。NameNode 不知道自己该不该是主,HBase 的客户端不知道 meta 表在哪台机器上,新来的 RegionServer 注册不进去。所以 ZooKeeper 集群的稳定性,直接决定了上面这堆组件的稳定性,它不是"辅助",是"地基"。

2. NameNode 主备切换:ZKFC 与 ActiveStandbyElector 的真实分工

2.1 ZKFC 是个独立 JVM 进程,不是 NameNode 里的一个线程

很多人以为自动故障转移是 NameNode 自己干的,其实不是。HDFS 里有个叫ZKFC(ZKFailoverController)的独立进程,跟 NameNode 是分开启动、分开管理的。你在机器上jps一下,能看到ZKFC这个名字,它默认监听 8019 端口。启动命令是hdfs --daemon start zkfc,停机是hdfs --daemon stop zkfc。这个设计很关键:ZKFC 挂了不会拖死 NameNode,NameNode 挂了 ZKFC 还在,才能做出正确判断。

ZKFC 内部有两个核心模块在跑:

  • HealthMonitor:周期性通过 RPC 调本地 NameNode 的getServiceStatus,判断它是不是健康(比如是否处于安全模式卡死、磁盘是否写满导致 edits 写不进去)。一旦判定不健康,它会主动触发一次"退出选举"。
  • ActiveStandbyElector:真正跟 ZooKeeper 打交道的部分,负责抢锁、挂 Watch、在状态变化时回调 NameNode 的becomeActive/becomeStandby。

这两个模块对应两条完全不同的切换路径,搞清楚它们的区别,排查问题时能省下大量时间。HealthMonitor 触发的切换是"我知道我不行,我主动让位",日志里能看到明确的健康检查失败原因;而 ZooKeeper 会话超时触发的切换是"别人认为你不行了",本地 NameNode 可能一点毛病没有,纯粹是网络或者 ZK 那边的问题。

2.2 一次自动切换的完整链路

把这条链路拆开看,每一步都能对应到日志和 znode 状态:

  1. 两个 ZKFC 同时连上 ZooKeeper 集群,各自尝试创建/hadoop-ha/<nameservice>/ActiveStandbyElectorLock这个临时节点。
  2. 谁先创建成功,谁的 ActiveStandbyElector 就回调本地 NameNode 的becomeActive(),同时往ActiveBreadCrumb这个持久节点里写一份自己的信息(主机名、端口、znode 版本号)。
  3. 创建失败的这边,通过exists()挂上 Watch,进入 Standby,NameNode 保持备用状态,持续同步 edits。
  4. 主节点那边如果 NameNode 进程挂了、机器宕机、或者网络断开导致 ZK 会话超时,临时节点被 ZooKeeper 删除。
  5. 备节点上的 Watch 被触发,重新发起创建请求,成功之后走一遍fencing流程,确认老的主节点确实不可能再写了,然后才becomeActive()。
  6. 如果 fencing 失败,切换中止,备节点继续保持 Standby,宁可切不过去也不能出现双写。

第 5 步的 fencing 是最容易被忽略、也是出事时最要命的一环。

2.3 关键配置怎么写,以及超时参数的联动陷阱

一份典型的生产配置大概长这样:

<configuration> <property> <name>dfs.nameservices</name> <value>mycluster</value> </property> <property> <name>dfs.ha.automatic-failover.enabled</name> <value>true</value> </property> <property> <name>ha.zookeeper.quorum</name> <value>zk1:2181,zk2:2181,zk3:2181</value> </property> <property> <name>ha.zookeeper.session-timeout.ms</name> <value>10000</value> </property> <property> <name>ha.zookeeper.parent-znode</name> <value>/hadoop-ha</value> </property> <property> <name>ha.zookeeper.num.retries</name> <value>3</value> </property> <property> <name>ha.zookeeper.retry-interval.ms</name> <value>1000</value> </property> <property> <name>dfs.ha.fencing.methods</name> <value>sshfence</value> </property> <property> <name>dfs.ha.fencing.ssh.private-key-files</name> <value>/home/hadoop/.ssh/id_rsa</value> </property> </configuration>

这里有个极其典型的坑:ha.zookeeper.session-timeout.ms写多少,并不一定生效多少。ZooKeeper 服务端有两个参数minSessionTimeout(默认2 * tickTime)和maxSessionTimeout(默认20 * tickTime),客户端请求的超时值如果超出这个区间,服务端会直接把它夹到边界值上。

按tickTime=2000算,默认区间是 4000ms 到 40000ms。你配了 10 秒,落在区间内,没问题;但如果你为了"切换更灵敏"配了 3000ms,实际生效的是 4000ms;反过来,如果你配了 60000ms 想"避免误切",实际生效的是 40000ms——你以为设了 60 秒,实际只有 40 秒。我见过有团队排查了整整两天"为什么配了 60 秒还是频繁切",最后发现问题就出在这儿。所以配置之前,先把zoo.cfg里的tickTime和maxSessionTimeout拿出来对一遍,两边必须能对上。

2.4 fencing 为什么不能省

fencing 的意思是"隔离",在把备机提成主之前,必须先确认老的主机不可能再对外提供服务了。Hadoop 内置两种方式:

  • sshfence:通过 SSH 登录到老的主节点,用fuser -k杀掉占用 NameNode RPC 端口的进程。SSH 不通就视为失败。
  • shell(/path/to/script.sh):自己写脚本,通过 IPMI 断电、交换机端口关闭、存储隔离等方式干掉老节点。

如果不配dfs.ha.fencing.methods,Hadoop 启动时只给一条警告,运行时退化成"什么都不做"。这在网络分区场景下是灾难性的:主节点其实活着,只是跟 ZooKeeper 断了,它手里还握着活跃的写入链路,备节点这时候被提上来,两个 NameNode 同时对外写,元数据直接分叉。等网络恢复,两份 edits 已经没法合并了。

我的建议是至少配sshfence,并且提前把 SSH 免密和私钥路径调通。另外sshfence默认的超时时间比较短,跨机房或者机器负载高的时候容易误判失败,可以搭配dfs.ha.fencing.ssh.connect-timeout适当调大。

3. ResourceManager 那一侧:选举之外还多了一层状态存储

3.1 RM 的 Active/Standby 用的是同一套选举器

YARN 的 ResourceManager HA 在选举机制上跟 HDFS 几乎一模一样,复用的就是 Hadoop 的ActiveStandbyElector,znode 路径换成了/yarn-leader-election/<rmId>/。配置在yarn-site.xml里:

<configuration> <property> <name>yarn.resourcemanager.ha.enabled</name> <value>true</value> </property> <property> <name>yarn.resourcemanager.ha.rm-ids</name> <value>rm1,rm2</value> </property> <property> <name>yarn.resourcemanager.zk-address</name> <value>zk1:2181,zk2:2181,zk3:2181</value> </property> <property> <name>yarn.resourcemanager.store.class</name> <value>org.apache.hadoop.yarn.server.resourcemanager.recovery.ZKRMStateStore</value> </property> <property> <name>yarn.resourcemanager.zk-timeout-ms</name> <value>10000</value> </property> <property> <name>yarn.resourcemanager.zk-num-retries</name> <value>1000</value> </property> </configuration>

注意这里有两个容易混的地址参数:yarn.resourcemanager.zk-address是给选举用的,yarn.resourcemanager.zk-state-store.address是给状态存储用的。大多数部署里两者指向同一个集群,但如果你想让选举和状态存储走不同的 ZooKeeper 集群(比如担心状态存储的写入压力影响选举灵敏度),是可以分开配的。不少人改配置的时候只改了一个,另一处还指着老地址,切过去之后状态恢复失败,应用全丢。

3.2 ZKRMStateStore 存的到底是什么

这是 YARN 跟 HDFS 最大的不同点。HDFS 在 ZooKeeper 上只做选举,元数据本身存在 JournalNode 和 NameNode 本地磁盘里;而 YARN 除了选举,还把运行时状态直接存进 ZooKeeper 的持久节点。具体包括:

  • 所有正在运行的 Application 记录(RMAppRoot)
  • 每个 Application 的 Attempt 记录(AppAttemptRoot)
  • 已分配的 Container 信息(RMContainerRoot)
  • 各种 Token:ApplicationToken、AMRMToken、DelegationToken

这套状态的意义在于:主 RM 突然挂掉,备 RM 切上来之后,能从 ZooKeeper 里把正在跑的应用和容器全部恢复出来,不需要把整个集群上跑的作业重新提交一遍。想象一下一个 YARN 集群上有几百个长跑作业,如果切换一次全丢,代价是不可接受的。

除了ZKRMStateStore,Hadoop 还提供了FileSystemRMStateStore(把状态写到 HDFS 上)和MemoryRMStateStore(只存内存,重启即丢,仅用于测试)。生产上首选 ZooKeeper,原因很直接:如果用 HDFS 存状态,而 HDFS 本身在故障恢复期间不可用,那就陷入了"要恢复 YARN 得先有 HDFS,要恢复 HDFS 得先有 YARN"的死循环。ZooKeeper 是独立部署的,不受这套依赖关系影响,天然是更稳的落脚点。

3.3 状态存储带来的副作用:znode 数量膨胀

这套机制的代价是 znode 数量会随着集群规模线性增长。每提交一个应用、每分配一个容器,都会在/rmstore下面产生若干个节点。一个中等规模的集群跑上几天,几万个 znode 是很常见的。

znode 多了会带来一连串问题:ZooKeeper 的快照文件变大、重启时加载快照变慢、stat命令的响应时间变长。我见过一个集群的/rmstore下面堆了几十万个 znode,ZooKeeper 重启一次要十几分钟,这段时间里 YARN 完全无法提交新作业。

对策有这么几条:一是在 ZooKeeper 3.5 及以上版本开启autopurge.purgeInterval和autopurge.snapRetainCount,定期清理旧快照;二是在 YARN 侧配置yarn.resourcemanager.state-store.max-completed-applications,控制保留的已完成应用数量;三是运维上定期用zkCleanup.sh手工清理,但要注意别在主节点上跑,清理过程会占用大量 IO。

4. HBase 对 ZooKeeper 的依赖,比 Hadoop 本身还重

4.1 三个核心 znode 撑起了整个 HBase 的寻址

如果 HDFS 对 ZooKeeper 是"用一下选举",那 HBase 就是"深度绑定"。默认情况下 HBase 会自己去管理一个 ZooKeeper 集群(hbase.zookeeper.quorum指向独立部署的集群更稳妥),所有 RegionServer 和 Master 都通过它来发现彼此。最关键的三个节点:

# 进入 zk 客户端 zkCli.sh -server zk1:2181 # 1. meta 表所在的 RegionServer 地址,持久节点 get /hbase/meta-region-server # 返回类似:<主机名>,<端口>,<startcode> # 2. 当前 Active Master,临时节点,Master 挂了自动消失 get /hbase/master # 3. 所有存活的 RegionServer 列表,每个都是一个临时节点 ls /hbase/rs

meta-region-server这个节点是整个 HBase 客户端寻址的起点:客户端第一次连上集群,先查这个节点拿到hbase:meta表在哪台 RegionServer 上,再通过 meta 表找到自己关心的 rowkey 落在哪个 Region、哪台机器。这个节点要是丢了或者内容过期,客户端会反复重试、报RegionServerFinder相关的错误,表现就是"明明集群是好的,客户端就是连不上"。

/hbase/rs下面挂的是每个 RegionServer 的临时节点,节点名一般是主机名,端口,启动时间戳的格式。这个列表就是整个集群的"活体名册",Master 靠它判断哪些机器在线,客户端也靠它做负载均衡的候选。

4.2 RegionServer 断连之后的自杀逻辑

这是 HBase 设计里一个很值得说的点。RegionServer 跟 ZooKeeper 的会话一旦超时,它的临时节点就从/hbase/rs里消失了,Master 会认为它死了,然后把这个 RS 上原来负责的 Region 重新分配给别的机器。问题在于:那个"死掉"的 RS 可能还活着,只是跟 ZooKeeper 断了。如果它还继续处理客户端请求,就会出现两个 RS 同时服务同一个 Region 的情况,数据必然不一致。

所以 HBase 在 RegionServer 里加了一个自杀机制:一旦检测到跟 ZooKeeper 的会话丢失且重连失败,RS 会主动调用abort()把自己干掉。日志里能看到类似Aborting region server ... because we lost the ZK session的内容。这个逻辑听着粗暴,但在分布式系统里是正确做法——宁可让服务中断一下,也绝不能容忍脏写。

这也解释了一个常见现象:集群跑得好好的,突然一批 RegionServer 集体退出,日志里全是 session expired。十有八九不是 HBase 的问题,而是 ZooKeeper 那边卡住了(GC、磁盘 IO、网络),或者 session timeout 配得太激进。

4.3 session timeout 与 maxSessionTimeout 的联动,这里有个经典坑

HBase 的zookeeper.session.timeout默认是 90000 毫秒(90 秒),这个值明显大于 ZooKeeper 默认的maxSessionTimeout(20 * tickTime,在tickTime=2000时是 40000 毫秒)。后果就是:你配了 90 秒,ZooKeeper 实际只给你 40 秒,集群对网络抖动和 GC 的容忍度比你预期的低了一大截,稍有抖动就集体 abort。

<!-- hbase-site.xml --> <property> <name>zookeeper.session.timeout</name> <value>90000</value> </property> <property> <name>zookeeper.znode.parent</name> <value>/hbase</value> </property> <property> <name>hbase.zookeeper.quorum</name> <value>zk1,zk2,zk3</value> </property> <property> <name>hbase.zookeeper.property.clientPort</name> <value>2181</value> </property>
# zoo.cfg 里必须同步放开上限 tickTime=2000 initLimit=10 syncLimit=5 maxSessionTimeout=120000

两边对不上,你写的 90 秒就是自欺欺人。上线前拿zkCli连上去,用get随便建个临时节点,观察实际会话超时时间,比看配置文件靠谱得多。

5. Hive 和其它组件的用法:轻量但同样不能少

5.1 Hive 的并发控制在 ZooKeeper 上的落点

Hive 早期做表级并发控制,用的是 ZooKeeper 上的锁实现(ZooKeeperHiveLockManager)。多个会话同时写同一张表时,先在 ZooKeeper 上抢一个锁节点,抢不到就等待或者报错。相关配置在hive-site.xml:

<configuration> <property> <name>hive.zookeeper.quorum</name> <value>zk1,zk2,zk3</value> </property> <property> <name>hive.zookeeper.client.port</name> <value>2181</value> </property> <property> <name>hive.zookeeper.namespace</name> <value>hive_zookeeper_namespace</value> </property> <property> <name>hive.support.concurrency</name> <value>true</value> </property> </configuration>

需要说明的是,现在主流部署里 Hive 的锁更多走的是 Metastore 自带的数据库锁(DbLockManager),ZooKeeper 锁反而用得少了。但hive.zookeeper.quorum这组配置在很多发行版里仍然是必填项,因为 HiveServer2 的实例注册和高可用发现仍然依赖 ZooKeeper。填错的直接后果是 HS2 启动时卡在注册阶段,或者 beeline 连不上。

5.2 HiveServer2 的多实例注册与客户端发现

多个 HiveServer2 同时服务时,每个实例启动后都会在 ZooKeeper 的指定 namespace 下创建一个临时节点,把自己的 host 和端口写进去。客户端用服务发现模式连接时,URL 里不用写死某一台机器,而是把 ZooKeeper 地址交给驱动去动态查找:

jdbc:hive2://zk1:2181,zk2:2181,zk3:2181/; serviceDiscoveryMode=zooKeeper; zooKeeperNamespace=hiveserver2

这条链接的意义在于:某台 HS2 挂了,它的临时节点自动消失,客户端重新查找时就不会再连到那台死机器上。这套机制和 HBase 的 RS 注册是同一个思路,用的还是"临时节点 + Watch"这个万能组合。配置时唯一要注意的是zooKeeperNamespace必须和 HS2 服务端配的一致,默认值在不同版本里有差异,最好显式写死。

5.3 调度和批处理系统里的分布式锁

除了这几个核心组件,围绕 Hadoop 生态跑的一堆调度系统、元数据服务、配置中心,很多也会直接连同一个 ZooKeeper 集群做分布式锁和任务互斥。典型场景是"同一个任务在同一时刻只能有一个实例在跑",做法就是去 ZooKeeper 上抢一个临时节点,抢到才执行,执行完释放,进程崩了节点自动消失,不会留下死锁。

这类用法有,但要有节制。同一个 ZooKeeper 集群上挂的东西越多,任何一个业务方写出"高频创建删除节点"的代码,都会拖累整个集群。我见过某个自研的定时任务框架每秒往 ZooKeeper 上打几百次写请求,直接把 ZooKeeper 的请求队列打满,连带 HDFS 的自动切换都失灵。所以我的经验是:核心组件(HDFS、YARN、HBase)共用一个 ZK 集群,外围业务系统另开一个,物理隔离,别让它们互相伤害。

6. ZooKeeper 自己出问题时,Hadoop 侧会是什么表现

6.1 三种典型现象,指向完全不同的根因

现象一:NameNode 主备频繁来回切。业务侧看到的是连接闪断、重试增多,ZKFC 日志里是成片的ConnectionLossException或SessionExpiredException。这基本可以锁定 ZooKeeper 侧的会话不稳定,跟 NameNode 自身无关。

现象二:HBase 一批 RegionServer 同时退出。日志里是Aborting region server、region server opening反复出现,/hbase/rs下的节点数量剧烈波动。这种情况通常意味着 ZooKeeper 出现了全局性的卡顿,所有 RS 的会话在同一时间段集体超时。

现象三:YARN 提交新作业全部失败。报错往往是连不上 RM 或者状态存储写入失败。如果 RM 的 znode 还在但状态存储那部分(/rmstore)写入超时,作业提交会一直卡在ACCEPTED状态,既跑不起来也不报明确错误。

这三种现象的排查起点都一样:先去确认 ZooKeeper 集群本身的状态,别急着翻 Hadoop 的日志。方向错了,翻一天也翻不出来。

6.2 从现象到根因的排查顺序

我一般的顺序是这样的:

# 1. 先看进程活没活,返回 imok 说明进程在 echo ruok | nc zk1 2181 # 2. 看整体运行指标,延迟、连接数、请求队列 echo mntr | nc zk1 2181 # 3. 看当前连接都是从哪些机器来的,找异常来源 echo cons | nc zk1 2181 # 4. 看这台机器的角色和 znode 数量 echo srvr | nc zk1 2181

mntr里我最关注这几个指标:

指标含义异常阈值参考
zk_avg_latency平均请求延迟(ms)超过 50 就要警惕,超过 200 基本已经在抖
zk_max_latency最大请求延迟(ms)出现几千的值说明有过明显卡顿
zk_outstanding_requests排队中的请求数持续大于 0 说明处理不过来了
zk_num_alive_connections当前连接数突然暴涨要查是不是客户端重连风暴
zk_znode_count节点总数几十万级别要考虑清理
zk_watch_count注册的 Watch 总数异常增长会拖慢写操作

注意ruok返回imok只能说明进程还活着,不代表服务正常。我遇到过 ZooKeeper 进程在、端口通、ruok正常,但整个集群因为磁盘 IO 卡死而完全无法处理请求的情况。所以监控上只看ruok是绝对不够的,mntr的延迟指标才是真正能提前预警的东西。

6.3 常见根因对照表

现象可能的根因验证方式
ZKFC 反复切换ZooKeeper 节点 Full GC 或磁盘 IO 卡顿看 GC 日志、zk_max_latency、机器iostat
会话频繁过期网络抖动、tickTime与客户端超时不匹配逐跳 ping、对照客户端与服务端超时区间
客户端连不上、报连接拒绝maxClientCnxns打满cons数连接数,对比zoo.cfg配置
重启加载慢、启动十几分钟znode 数量过多、快照文件巨大看dataDir目录大小和zk_znode_count
HBase RS 集体 abort客户端 session timeout 大于服务端 maxSessionTimeout对比hbase-site.xml与zoo.cfg
四字命令无响应3.5 之后默认只允许srvr检查4lw.commands.whitelist

7. 我在生产上踩过的坑和对应的调参思路

7.1 session timeout 不是越小越好,也不是越大越好

这两年被问最多的问题就是"超时设多少合适"。答案取决于你能容忍多长的故障发现时间,以及你的网络和 GC 状况有多稳。

设小了,一次网络微抖或者一次 3 秒的 GC,就足以让会话过期,触发误切换。HDFS 一次误切换的代价是几十秒的写入中断,HBase 一次误 abort 的代价是 Region 重新分配、几分钟的服务不可用。设大了,真故障时要等很久才能发现,期间业务一直报错。

我的经验值:HDFS ZKFC 用 10 秒左右,HBase 用 90 秒(前提是 ZooKeeper 侧放开了maxSessionTimeout),YARN RM 用 10 秒左右。这个组合在多数环境里能兼顾灵敏度和稳定性。如果集群跨机房部署,网络 RTT 波动大,HDFS 那边可以再往上抬到 15 到 20 秒,别硬扛。

7.2 ZooKeeper 绝对不要和 Hadoop 混部

这条我说得重一点,因为踩过太多次。ZooKeeper 对延迟极其敏感,它的写请求要落盘(事务日志),对磁盘 IO 的要求很高。如果跟 DataNode、RegionServer 这类 IO 大户混在一台机器上,DataNode 一次大的 compaction 或者一次批量写入,就能把磁盘打满,ZooKeeper 的写延迟跟着飙升,然后整个 Hadoop 集群开始抖。

正确做法是单独的 3 台(或 5 台)小规格机器,只跑 ZooKeeper,磁盘用 SSD 最好,至少保证事务日志和数据目录分开:

dataDir=/data/zookeeper/data dataLogDir=/data/zookeeper/log

dataLogDir跟dataDir分开是必须的。ZooKeeper 的事务日志是顺序追加写,快照是定期全量写,两者混在一个盘上会互相干扰,日志写延迟一高,所有客户端跟着遭殃。这个改动成本极低,收益极大。

7.3 几个零碎但致命的细节

  • maxClientCnxns默认是 60。这是一个 ZK 节点对单个客户端 IP的连接数上限。一套 HBase 集群几十台机器还好,但如果有一批应用跑在同一台机器上,全连同一个 ZK,很容易打满,客户端报Too many connections。要么调大这个值,要么在客户端侧做连接复用。
  • jute.maxbuffer默认 1MB。znode 里存的数据不能太大,往/rmstore或 HBase 的节点里写大数据会直接超限报错。这也是为什么 YARN 的状态存储只存元信息,不存日志内容。
  • 四字命令白名单。3.5 之后的版本默认只放行srvr,mntr、cons这些都要在zoo.cfg里加4lw.commands.whitelist=*或者逐个列出。很多监控系统升级之后突然采集不到数据,就是这个原因。
  • 快照清理。不开autopurge.purgeInterval的话,dataDir下的快照会一直堆,最后把磁盘撑满。磁盘一满,ZooKeeper 直接拒绝所有写请求,表现就是选举失灵、HBase 全线报警。这个我建议purgeInterval=24、snapRetainCount=5,别等出事再补。
  • 别把数据目录放 NFS 或网络存储上。ZooKeeper 对 fsync 的语义要求很严,网络存储的延迟波动会让它表现极不稳定,而且出问题时的现象非常迷惑人——所有指标看着都还行,就是偶尔超时。

7.4 关于监控,我想多说一句

很多人监控 ZooKeeper 就配一个端口探活,这跟没配差不多。真正有用的监控是这几条曲线放在一起看:zk_avg_latency的 P99、zk_outstanding_requests是否长期大于 0、单机的连接数变化、zk_znode_count的增长趋势、以及磁盘 IO 的 await 值。

这几条曲线里,只要出现"延迟走高 + 排队请求增加"的组合,就说明 ZooKeeper 已经在处理不过来的边缘了,这时候 HDFS 和 HBase 那边的会话超时随时可能触发。能在切换发生之前从监控上看到这个趋势,才叫有效监控。

我个人的习惯是,每次做完集群变更(加节点、改配置、上线新业务),都会盯一天 ZooKeeper 的延迟曲线,确认它没有比变更前更抖。这个习惯帮我提前发现过好几次问题——有一次是新上线的某个服务把连接全堆在了一台 ZK 上,延迟曲线肉眼可见地歪了一边,及时调整客户端配置才没演变成事故。分布式系统的很多故障,其实在指标上都有几十分钟甚至几小时的"预热期",只是大多数时候没人去看。

返回列表