ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

HBase RegionServer批量掉线排查:ZooKeeper会话与GC调优

HBase RegionServer批量掉线排查:ZooKeeper会话与GC调优

1. 先把“批量”两个字吃透:现场第一手判断

凌晨两点十七分,监控系统连着推了七条告警,内容几乎一模一样:HBase RegionServer 批量掉线。我打开面板,八个 RegionServer 节点里有六个变灰,HMaster 日志里正在刷Server ... expired,Region 的 RIT 数量以肉眼可见的速度往上涨。这种场面见过的都知道,最忌讳的就是先冲进去点重启按钮——你手上唯一的现场证据会被自己亲手毁掉。

这篇内容想聊的就是这类问题的完整排查路径:HBase RegionServer 批量掉线到底怎么判、为什么掉、从哪些角度查、哪些参数是真正起作用的、以及事后怎么防止再犯。适合正在维护 HBase 集群的运维和平台开发看,也适合刚接手一套集群、对 ZooKeeper 会话机制还停留在“听说过”层面的同学。我会把判定链路、参数计算、日志关键字、监控指标、踩过的坑都摊开讲,命令和配置可以直接抄。

先把结论放前面:“批量”这个词本身,就是最重要的线索。单台 RegionServer 掉线,九成是那台机器自己的事——内存、磁盘、GC、进程被 OOM Killer 干掉。而同一时间窗口内多台一起掉,问题几乎一定出在它们共享的东西上:ZooKeeper 集群、机架交换机、HDFS、HMaster,或者一个被所有人同时踩中的配置陷阱。搞清楚这个分水岭,排查范围能瞬间缩小一半。

1.1 一次真实的批量掉线现场还原

那次的现场是这样的:六个节点在 90 秒内先后告警,但时间戳并不完全一致,最早的和最晚的差了大约 50 秒。这个细节很关键——如果是网络设备整体断电或者交换机重启,掉线时间会非常集中,甚至日志里能看到连接被同时 reset。而“陆续掉”说明存在一个渐进的触发条件,比如某个共享资源越来越慢,各节点陆续撑不住心跳。

我做的第一件事是把所有 RS 的hbase-root-regionserver-*.log拉下来,用一条命令找出所有的会话过期记录:

grep -n "Session expired\|ConnectionLoss\|Expired session" \ /var/log/hbase/hbase-root-regionserver-*.log | head -50

结果很有意思:六台机器上都有Session expired,但在这之前,都出现了同一类告警——MemStore is above the upper limit和Blocking updates。也就是说,真正的原因是写入压力导致 MemStore 堆积,触发全局 flush 和写阻塞,随后 JVM 进入长时间 GC,心跳发不出去,ZooKeeper 判定会话超时,临时节点被删除,HMaster 宣布这些 RegionServer 死亡。

这里插一个新手容易混淆的点:日志里出现Session expired,不代表网络断了,也不代表 ZooKeeper 挂了。绝大多数情况下,是RegionServer 自己“卡住”了,卡到连心跳线程都调度不上。JVM 在做 Full GC 的时候是整个进程停摆的(Stop-The-World),心跳线程再高优先级也跑不了。所以看到会话过期,第一反应应该是去查 GC 日志,而不是去查网络。

1.2 单个掉线和批量掉线的分水岭

我把判断逻辑整理成一个简单的判断框架,出问题时照着走一遍,基本能定性:

现象特征大概率方向优先查看
单台掉线,其余正常该节点资源问题该机 GC 日志、dmesg、磁盘健康
同一机架多台同时掉机架交换机、上联链路交换机日志、网卡错误计数
全部 RegionServer 同时掉ZooKeeper 集群、核心网络ZK 日志、ZK 四字命令、核心交换机
陆续掉线,间隔数十秒共享资源逐渐劣化HDFS 慢、磁盘 IO、MemStore 阻塞
掉线后反复重连又掉参数配置不当或 ZK 会话被截断session timeout 协商日志
只有高负载时段掉容量不足、GC 参数不合理堆内存、MemStore 上限、handler 数

这张表不是拍脑袋来的。共享资源是批量掉线的第一嫌疑对象,因为只有共享的东西才具备“同时影响多个节点”的能力。单机 CPU 打满不会传染,但一台被打满的 DataNode 会让所有写 WAL 的 RegionServer 一起变慢;一个 ZooKeeper 节点的 GC 停顿,会让所有连在它上面的客户端一起心跳超时。

1.3 判定链路:到底是谁宣布了 RegionServer 的死亡

很多人以为“掉线”是 HMaster 判的,其实不是。HBase 的存活判定是一条链,ZooKeeper 在最前面:

  1. RegionServer 启动时在 ZooKeeper 的/hbase/rs路径下创建一个临时节点(EPHEMERAL),节点名是主机名,端口,启动时间戳。
  2. 之后 RegionServer 通过 ZooKeeper 客户端与服务端保持会话,靠定期心跳续约,不需要自己写心跳逻辑。
  3. 一旦会话超时,ZooKeeper 服务端会删除该临时节点。
  4. HMaster 里的RegionServerTracker监听/hbase/rs的子节点变化,收到删除事件后调用expireServer,把该节点标记为 dead。
  5. 随后RegionServerShutdownHandler接管:拆分该节点遗留的 WAL,把它上面的 Region 重新分配到其他活着的节点。

所以“掉线”这个动作,真正按下按钮的是 ZooKeeper。这也解释了为什么排查永远从会话超时开始。如果再往深一层问:为什么会话会超时?答案只有两种可能——心跳没送到(网络、ZK 侧问题),或者心跳没发出(RegionServer 侧卡住)。后者在实际生产里占绝大多数。

2. 掉线判定的底层机制与关键参数

2.1 ZooKeeper 临时节点与会话续约的真实过程

ZooKeeper 的会话机制可以类比成“租约”:客户端连上服务端后,服务端会给它一个 session,并记录一个过期时间。客户端每隔tickTime / 2左右发一次 ping,服务端收到就往后顺延。只要在约定的会话时长内至少有一次有效交互,会话就活着。

关键在于,ping 是由 ZooKeeper 客户端的独立 IO 线程发出的,跟业务线程池是分开的。这本来是好事——业务再忙也不该影响心跳。但 JVM 层面的 Stop-The-World 是“全进程暂停”,IO 线程也跑不了。所以真正能杀死会话的,是超过会话时长的 GC 停顿,而不是业务线程忙。

我见过的最典型场景:堆内存给了 64GB,用的是 CMS,老年代碎片化之后触发 Full GC,一次停顿 40 秒以上。会话时长如果只有 40 秒,那就正好卡在边缘,时好时坏,表现为“偶发性掉线”,特别难查。这种情况下日志里通常能看到:

WARN [main-EventThread] zookeeper.ClientCnxn: Client session timed out, have not heard from server in 40001ms for sessionid 0x...

注意里面的40001ms——这个数字直接告诉你实际生效的会话时长,比配置文件里写的值更可信。

2.2 session timeout 的协商过程,以及那个被截断的坑

这是我最想强调的一节,因为它在生产里坑过太多次。

ZooKeeper 的会话时长不是客户端说了算,而是协商出来的。客户端在连接时提议一个值,服务端会拿它和两个边界比较:

  • 最小值:2 * tickTime
  • 最大值:20 * tickTime

如果客户端提议的值超过最大值,服务端会直接截断到最大值,并且不会报错,只在日志里留一句negotiated timeout = xxx。

举个具体例子。假设 ZooKeeper 集群的tickTime是默认的 2000 毫秒,那么:

  • 最小会话时长 = 2 × 2000 = 4000 毫秒(4 秒)
  • 最大会话时长 = 20 × 2000 = 40000 毫秒(40 秒)

现在你在hbase-site.xml里写了:

<property> <name>zookeeper.session.timeout</name> <value>90000</value> </property>

你以为自己有 90 秒的缓冲,实际上 ZooKeeper 只给了你 40 秒。于是原本“GC 停顿 50 秒才会掉线”的容忍度,变成了“GC 停顿 40 秒就掉线”,而你的 GC 参数可能压根没按 40 秒去设计。这个偏差在低峰期完全看不出来,一旦写入量上来,批量掉线就开始了。

所以排查的第一步,是去确认实际生效值,而不是配置值。有三个地方可以看:

# 1. RegionServer 日志里的协商结果 grep "negotiated timeout" /var/log/hbase/hbase-root-regionserver-*.log | tail -5 # 2. ZooKeeper 服务端的配置 grep -E "tickTime|maxSessionTimeout|minSessionTimeout" /path/to/zoo.cfg # 3. 通过四字命令看当前连接(需要服务端开启白名单) echo cons | nc zk-host 2181 | head -20

cons命令的输出里,每一行连接的最后一列就是该会话的 timeout,一目了然。如果发现所有 HBase 连接的 timeout 都是 40000,而你的配置写的是 90000,那就坐实了被截断。

修正方式有两条路,二选一:

  • 改 ZooKeeper 侧:把tickTime提到 4000,最大值就变成 80 秒;或者显式配置maxSessionTimeout。改 ZK 需要滚动重启,影响面大,要排窗口。
  • 改 HBase 侧:把zookeeper.session.timeout调整到不超过20 * tickTime的值,同时把 GC 目标压到这个值以内。这条路不动 ZooKeeper,风险小,我更推荐先走这条。

提醒:会话时长调大不是万能药。它只是给你更长的 GC 容忍窗口,代价是真正的故障被发现得更晚——一台真的挂掉的 RegionServer,要等更久才被摘除,这段时间里打到它上面的请求全部超时。所以调参数必须配合 GC 优化一起做,单纯拉长会话时长只是把问题往后推。

2.3 会话过期之后,HMaster 到底做了什么

很多人只关心“为什么掉”,忽略了“掉了之后发生了什么”,结果在恢复阶段踩第二个坑。

RegionServer 被标记 dead 之后,HMaster 要做三件事,每一步都有代价:

第一步,拆分 WAL。该节点上所有未持久化的写入都还在 HLog(WAL)里,必须按 Region 拆开,分别回放到对应的 Region 目录。拆分是并行做的,线程数由hbase.regionserver.thread.splitwal(老版本)或hbase.splitlog.manager.threads控制。如果一次掉六个节点,就是六份 WAL 同时拆,HDFS 的读压力会瞬间拉高。

第二步,重新分配 Region。几百上千个 Region 要分散到剩下的节点上。这个过程不是瞬间完成的,HMaster 有分配线程池,hbase.master.loadbalancer.class的策略也会介入。Region 在过渡状态(RIT)期间对外不可用,业务侧表现为大量超时。

第三步,其他节点承压。原本六台机器的写入量压到两台身上,剩下两台的 MemStore 会立刻飙升,很可能触发第二轮 flush 风暴和 GC,然后再掉——这就是雪崩。我那次事故就经历了这个循环,第一批掉六个,重新分配之后半小时内又掉了两个。

理解了这三步,你就明白为什么处置批量掉线时,不能一上来就让 HMaster 全量恢复。正确做法是先稳住剩余节点:临时关闭自动 balance、必要时对写入做限流、把非核心业务的写入降级,等 Region 分配稳定了再逐步放开。

3. 六大方向逐一排查

3.1 内存与 GC:出现频率最高的元凶

我把这条放第一位,因为在我处理过的批量掉线里,GC 相关占了七成以上。

排查入口是 GC 日志。确认 JVM 启动参数里有没有开:

ps -ef | grep HRegionServer | tr ' ' '\n' | grep -E "Xmx|Xmn|GC"

如果没有-Xloggc之类的参数,先补上,这是基础设施,不是可选项。有了日志之后,重点看两个指标:单次停顿最大值和Full GC 频率。

# 统计每次 GC 的停顿时长,找最长的几次 grep -o "Total time for which application threads were stopped: [0-9.]*" gc.log \ | awk '{print $NF}' | sort -rn | head -10

如果最长停顿接近或超过实际会话时长,那基本就是它了。

接下来要回答“为什么 GC 这么慢”。常见原因有三个:

  • 堆太大且用了 CMS。CMS 在老年代碎片化后只能退化成串行 Full GC,堆越大停顿越久。64GB 堆的 Full GC 动辄几十秒。我的建议是把单节点堆控制在 16~32GB,用 G1,-XX:MaxGCPauseMillis=200,靠多节点横向扩展而不是靠单机大堆。
  • MemStore 堆积触发的 flush 风暴。写入突增时,MemStore 迅速涨到上限,触发全局 flush 并阻塞写入,大量对象在短时间内产生,GC 压力陡增。
  • Region 数量过多。单个 RegionServer 上挂了几百个 Region,每个 Region 都有独立的 MemStore 和一堆对象,堆里的存活对象多,GC 效率就低。

这里给一个实打实的容量计算,帮你判断 MemStore 配置是否合理:

假设单台 RegionServer 堆内存 32GB,hbase.regionserver.global.memstore.size设为 0.4,那么全局 MemStore 上限是 12.8GB。单 Region 的 flush 阈值hbase.hregion.memstore.flush.size是默认的 128MB。写满全局上限需要 12.8GB / 128MB = 100 个 Region 同时达到阈值。如果你的节点上挂了 300 个活跃写入的 Region,那么平均每个 Region 只写到 42MB 左右就会触发全局 flush——远低于单 Region 阈值,意味着大量的中小体积 flush,磁盘上小文件暴增,compaction 压力跟着上来。

这种配置下正确的做法是反过来调:把单 Region flush 阈值降到 64MB 甚至 32MB,让 flush 更平滑;或者减少单节点 Region 数量,把 300 个拆到两台机器上。配置不是越大越好,要和 Region 密度匹配。

3.2 存储与 HDFS:被拖慢的 WAL 写入

RegionServer 的每次写入都要先写 WAL,WAL 落在 HDFS 上。如果 HDFS 慢,WAL 写就慢,写请求在 handler 线程里堵着,MemStore 涨,然后回到 3.1 的链条。

判断 HDFS 是否是瓶颈,看这几个信号:

# RegionServer 日志里的慢 WAL 同步 grep -i "sync.*took\|Slow sync\|wal.*slow" hbase-root-regionserver-*.log | tail -20

如果出现大量Slow sync且耗时在几百毫秒以上,说明 WAL 落盘有问题。往下一层查:

  • DataNode 磁盘是否打满。用iostat -x 1看%util和await,如果%util长期 90% 以上、await超过 50ms,磁盘就是瓶颈。
  • 是否有节点处于慢盘状态。HDFS 写 pipeline 里只要有一个 DataNode 慢,整条 pipeline 都被拖住。查hdfs dfsadmin -report里的坏盘和慢节点。
  • 小文件是否过多。flush 产生大量小 HFile,compaction 跟不上时,HDFS 的 NameNode 压力和读放大会同时上升。

一个容易忽略的点:RegionServer 的 WAL 目录和数据目录如果配在同一块盘上,flush 和 compaction 的 IO 会和 WAL 写抢资源。生产环境建议至少分开——WAL 单独一块盘(或者干脆用 SSD),HFile 用大容量盘。这个改动在部署阶段做成本极低,事后补救就得停机迁数据。

3.3 网络与基础设施:批量掉线的放大器

虽然纯网络问题导致的批量掉线比例不如 GC 高,但它的特征最明显,也最容易误判。

先看是不是机架级别的。把掉线节点的主机名列出来,对照机架拓扑:

# 从 HMaster 日志里提取掉线节点 grep "expired" /var/log/hbase/hbase-root-master-*.log \ | grep -oE "[a-z0-9-]+,[0-9]+,[0-9]+" | awk -F, '{print $1}' | sort -u

如果掉线的全在同一个机架,那九成是那台机架的交换机或上联链路出了问题。这时候去查交换机的端口错包计数、STP 收敛日志,比在 HBase 里翻日志有效得多。

再看几个具体的网络层嫌疑:

  • MTU 不一致。如果 RegionServer 和 ZooKeeper、HDFS 之间的链路 MTU 配置不统一,大包会被丢弃,表现为“小请求正常、大请求超时”。心跳包很小,往往没事,但 HBase RPC 的大包会受影响。用ping -M do -s 1472逐段探测可以确认。
  • DNS 解析超时。如果集群里用主机名而不是 IP 注册节点,DNS 一旦抖动,连接建立就会卡住。这类问题在日志里表现为UnknownHostException或者连接建立耗时异常长。
  • 连接数耗尽。netstat -s | grep -i "overflow\|drop"能看出 SYN 队列溢出。RegionServer 的hbase.regionserver.handler.count如果调得过大,配合大量客户端,可能把文件句柄和端口耗尽。

3.4 负载与热点:大 Scan 和写风暴

有一种批量掉线是“业务侧引发”的,特征是有明确的时间规律,比如每天上午十点、每次跑批任务时。

核心机制是线程池被占满。RegionServer 的 RPC handler 线程数由hbase.regionserver.handler.count控制,默认 30。如果业务里有大量全表 Scan 或者scan.setCaching()设得很大的查询,这些长请求会长时间占用 handler 线程。线程占满之后,新的 put 请求排队,客户端的写超时和重试开始出现,同时 MemStore 只进不出(因为 flush 也需要资源),最终引发 3.1 的连锁反应。

判断方法很直接,抓一次现场线程栈:

jstack <pid> > /tmp/rs-jstack-$(date +%s).txt grep -c "RpcServer.*Handler" /tmp/rs-jstack-*.txt grep -A3 "RpcServer.*Handler" /tmp/rs-jstack-*.txt | grep -c "Scan"

如果 handler 线程大部分卡在Scanner或者nextRaw上,那就是长 Scan 拖垮的。处置方向有两个:一是业务侧改造,给 Scan 加setBatch、限制返回条数、加上setMaxResultSize;二是服务端把扫描和普通读写隔离到不同的线程池,或者干脆把分析类查询引到从集群上。

写热点是另一种情况。某些 RowKey 设计不当,比如用时间戳做前缀、用单调递增 ID,写入全部集中到一两个 Region,那台 RegionServer 的负载远高于其他节点,它会第一个倒下。这种问题查Region级别的读写 QPS 监控最直观——如果某台机器的写入量是其他节点的三倍以上,基本可以确定是热点。

3.5 平台自身:ZooKeeper 集群与 HMaster

有时候问题根本不在 RegionServer 上,而在“裁判”身上。

ZooKeeper 集群的常见问题:单个 ZK 节点 GC 停顿导致的连接迁移。ZK 自己的堆通常不大(2~4GB 就够),但如果 znode 数量巨大、或者有大量的 Watch 事件堆积,也会 GC 抖动。查 ZK 的日志关键字:

grep -i "too long\|slow\|gc" /var/log/zookeeper/zookeeper.out | tail -20

ZK 日志里出现fsync-ing the write ahead log in ... took ... ms是很明确的信号,说明它的磁盘慢,事务提交被拖住。ZK 的 dataLogDir 必须放在独立的高性能盘上,这一条没有商量余地。

HMaster 侧的问题:HMaster 自己 GC 停顿,会导致 Region 分配卡住,但不会直接造成 RegionServer 掉线。不过如果 HMaster 和 RegionServer 部署在同一台机器上,HMaster 的 Full GC 会争抢 CPU,间接拖累 RegionServer 心跳。所以生产环境里 HMaster 和 RegionServer 最好分开部署,至少要把主控节点独立出来。

还有一个隐藏的坑:ZooKeeper 集群的 Leader 切换。ZK 集群在做 Leader 选举的时候(几十秒到几分钟不等),所有客户端的会话都可能受影响。如果 HBase 用的 ZK 集群和别的系统共用,别的系统把 ZK 搞挂了,HBase 会跟着陪葬。所以 HBase 集群最好用独立的 ZK quorum,或者至少做资源隔离。

3.6 操作系统与内核:OOM Killer 和句柄耗尽

这条排最后,但排查时不要跳过,因为它的证据最隐蔽——进程直接消失,日志戛然而止。

# 确认是否被内核杀过 dmesg -T | grep -i "killed process\|out of memory" | tail -20

如果看到类似Killed process 12345 (java) ... out of memory的记录,那 RegionServer 是被操作系统干掉的,跟 HBase 本身没关系。原因通常是堆外内存超限、或者容器没有设置内存限制但宿主机内存被打满。

另外两类系统层问题也值得一看:

  • 文件句柄耗尽。RegionServer 会打开大量 HFile 和网络连接,ulimit -n如果还是默认的 1024,跑不了多久就会Too many open files。生产环境至少 65535,最好 131072。
  • 内核参数不合理。vm.swappiness如果没调低,内存紧张时 JVM 会被换出到磁盘,GC 停顿直接翻倍;net.core.somaxconn和net.ipv4.tcp_max_syn_backlog太小,高并发下连接建立失败。

这两条本身不会直接导致批量掉线,但它们是放大器——本来只是某个节点变慢,被这些参数一放大,就变成了雪崩。

4. 从告警到定位的实操流程

4.1 保护现场:先别重启的几个理由

我给自己定了一条铁律:批量掉线时,前十分钟只做信息采集,不做任何变更。

理由很实在。RegionServer 进程如果还活着(只是会话过期),它可能仍在处理一部分请求,重启会打断这些请求;如果进程已经挂了,重启也快不了多少,因为还要重新分配 Region。而一旦重启,线程栈没了、堆转储没了、GC 日志可能被覆盖,后续复盘只能靠猜。

采集清单我一般按这个顺序走:

# 1. 确认进程是否还在 ps -ef | grep HRegionServer # 2. 还在的话,立刻抓线程栈(抓两到三次,间隔 5 秒) jstack <pid> > /tmp/rs-jstack-1.txt sleep 5 jstack <pid> > /tmp/rs-jstack-2.txt # 3. 抓一份 GC 统计 jstat -gcutil <pid> 1000 10 # 4. 保留最近的应用日志 cp /var/log/hbase/hbase-root-regionserver-*.log /tmp/backup/ # 5. 记录系统状态 iostat -x 1 3 > /tmp/iostat.txt vmstat 1 5 > /tmp/vmstat.txt netstat -s > /tmp/netstat.txt

这一套下来大概两分钟,能覆盖后续 90% 的分析需求。

注意:如果 RegionServer 已经处于写阻塞状态、业务全线超时,那就在采集完线程栈之后果断处置,不要为了“保留证据”让业务停半小时。取舍的标准是业务影响面,不是技术洁癖。

4.2 三条日志线串查

日志要串起来看,单看一条线容易误判。我的顺序是RegionServer → HMaster → ZooKeeper。

RegionServer 侧重点找三个关键字:Session expired、Blocking updates、ABORTING。第一个告诉我们会话掉了,第二个告诉我们写阻塞了,第三个说明进程主动自杀了(通常是 WAL 写入失败或者 MemStore flush 失败导致的保护性退出)。

grep -nE "Session expired|Blocking updates|ABORTING|Fatal" hbase-root-regionserver-*.log

HMaster 侧重点看 Region 分配的执行情况,找Server ... expired、Split WAL、Assign的时间戳,算出“掉线 → WAL 拆分完成 → Region 分配完成”各阶段耗时。如果 WAL 拆分花了二十分钟还没完,那说明 HDFS 有问题,而不是 HBase 有问题。

ZooKeeper 侧看会话管理的记录,找Expiring session、Close session。关键是比对时间戳——如果 ZK 在某个时间点集中驱逐了一大批 session,那问题在 ZK 或者网络;如果是零散驱逐的,那就是各个 RegionServer 自己的问题。

三条线的时间戳对齐之后,因果链就出来了。我那次是:RegionServer 先出现Blocking updates(t+0),三十秒后 GC 日志显示一次 42 秒的停顿(t+30),再过几秒 ZK 报Expiring session(t+72),然后 HMaster 报Server expired(t+75)。因果非常清楚。

4.3 监控指标交叉验证

日志给的是定性结论,监控给的是量化证据。我一般看四组图:

指标组具体指标异常特征
JVMGC 停顿时间、老年代使用率、Full GC 次数停顿出现尖峰,与掉线时间对齐
系统CPU、内存、磁盘 await、网络重传某台机器明显偏离集群均值
HBaseMemStore 使用率、flush 队列长度、handler 活跃数MemStore 打到上限,handler 满载
HDFS写 pipeline 耗时、DataNode 慢盘数写入耗时抬升,出现在掉线之前

交叉验证的价值在于排除。比如日志显示会话过期,但监控上 GC 停顿只有 200 毫秒,那 GC 就不是原因,得转去查网络。反过来,如果 GC 停顿明显超过会话时长,那基本可以结案,不用再折腾网络设备。

4.4 参数调整与效果验证

定位之后,调参要有依据,不能拍脑袋。我按“投入产出比”排序,优先动风险小的:

第一优先级,GC 参数。目标很明确:单次停顿必须显著小于实际会话时长。如果实际会话时长是 40 秒,那 Full GC 必须压到 10 秒以内,留足缓冲。具体做法:

# G1 的典型配置,堆 32GB -Xms32g -Xmx32g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:InitiatingHeapOccupancyPercent=45 -XX:+ParallelRefProcEnabled -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/var/log/hbase/gc-rs.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M

InitiatingHeapOccupancyPercent调到 45 是为了让 G1 更早启动并发标记,避免老年代被写满才被动触发 Full GC。这个值调低会增加 CPU 开销,但换来的是更平滑的停顿,在 RegionServer 场景下这笔交易划算。

第二优先级,MemStore 相关。根据前面算过的 Region 密度,把hbase.hregion.memstore.flush.size从 128MB 降到 64MB 或 32MB,同时确认hbase.regionserver.global.memstore.size不要超过 0.4。降阈值会增加 flush 次数,所以要同步检查 compaction 策略,避免小文件堆积。

第三优先级,会话时长。在 GC 优化到位的前提下,如果还偶发超时,再把zookeeper.session.timeout调到 ZooKeeper 允许的最大值以内。注意这一条要和 ZK 侧的tickTime一起看,不能只看 HBase 配置。

调整之后必须验证。验证方式不是“观察几天看看”,而是主动压测:用 YCSB 或者业务侧的回放工具,把写入量拉到峰值的 1.2 倍,持续跑两小时,观察 GC 停顿和 MemStore 曲线。我在一次调整后压测发现,虽然 Full GC 没了,但 Young GC 频率从 5 秒一次变成 1 秒一次,CPU 占用上升了 15%。这说明MaxGCPauseMillis设得太激进,调到 300 毫秒后恢复正常。参数调优是个来回试的过程,不是一次配好就完事。

5. 常见问题速查表与踩坑记录

5.1 掉线问题速查表

日志/现象可能原因快速验证处置方向
Session expired+ GC 停顿超时堆过大或 CMS 退化看 GC 日志最长停顿换 G1,降堆,调 IHOP
Session expired+ 无 GC 异常网络或 ZK 侧问题查 ZK 日志、交换机错包修网络,查 ZK 慢盘
Blocking updates频繁MemStore 配置与 Region 密度不匹配算全局上限 / 单 Region 阈值降 flush 阈值,减 Region 数
Slow sync大量出现HDFS 写慢、磁盘瓶颈iostat 看 await、看慢盘换盘,隔离 WAL 目录
handler 线程全满长 Scan 或大请求jstack 看线程栈限制 Scan,隔离线程池
进程突然消失无日志OOM Killerdmesg 查 killed process调堆外内存,查容器限制
Too many open files句柄上限低ulimit -n提到 65535 以上
掉线集中在同一机架交换机或链路故障对照机架拓扑修交换机,做链路冗余
重启后立刻又掉ZK 里残留旧节点查/hbase/rs子节点清理残留,检查主机名解析
恢复期 Region 长时间 RITWAL 拆分卡住看 HMaster 拆分日志查 HDFS,调拆分线程数

5.2 几个反直觉的坑

坑一:ZK 里残留的临时节点。有次 RegionServer 重启后一直起不来,日志报“节点已存在”。原因是上一次会话过期后,ZK 删节点有延迟,新进程启动时旧节点还在。这种情况不用慌,等会话彻底过期节点被删掉后会自动恢复;如果等了很久还在,才需要手工清理。但手工删节点要非常小心,删错路径会直接影响整个集群的元数据。

坑二:脑裂式的“假死”。RegionServer 进程活着,但因为 GC 卡住导致会话过期,HMaster 已经把它标记为 dead 并开始重新分配 Region。等它 GC 结束醒过来,会发现自己持有的一些 Region 已经被分配给别人了。这时候它会上报错误并主动退出(ABORTING),这个行为是保护性的,不是 bug。看到这类日志不要以为是程序异常,它是在避免数据写冲突。

坑三:会话时长改大反而更糟。前面提过,会话时长拉长意味着故障发现更慢。有一次为了压掉“偶发掉线”的告警,把会话时长从 40 秒调到 80 秒,告警确实少了,但业务侧的请求超时率涨了一倍——因为那台卡住的机器还挂在那儿,客户端一直在往它上面打请求。这是个典型的“指标好看、体验变差”。正确的顺序永远是先解决 GC,再谈参数。

坑四:只看 RegionServer 不看 HDFS。我见过一个集群,RegionServer 的 GC 参数调得很漂亮,但每天还是固定时间批量掉线。最后查到是 HDFS 在跑定时的数据均衡任务,把磁盘 IO 打满了,WAL 写不进去,RegionServer 卡在 sync 上。HBase 的问题经常要跳出 HBase 去找。

6. 把一次事故变成一套体系

单次排查再漂亮,也只是救火。真正省心的是把这类问题前置拦掉。我在几个集群上落地过下面这套东西,效果比较实在。

6.1 监控指标要盯住“前兆”而不是“结果”

大多数团队监控的是“RegionServer 是否在线”,这个指标只能告诉你已经出事了。真正有价值的是前兆指标:

  • GC 停顿的 P99 和最大值。这是最灵敏的指标,比“是否在线”早出现十几分钟。
  • MemStore 使用率超过 70% 的持续时长。持续超过一分钟就该告警了。
  • flush 队列长度。队列堆积说明磁盘跟不上,早于写阻塞出现。
  • WAL sync 耗时的 P99。超过 100 毫秒就值得看一眼。
  • ZK 会话的 negotiated timeout 实际值。定期核对配置值和实际值是否一致,防止被截断而不自知。

阈值怎么定?我的方法是看历史数据的分布,取“正常波动上限”的 1.2 倍作为警告线,1.5 倍作为严重线。不要直接抄网上的数字,每个集群的负载特性都不一样。

6.2 变更管理和故障演练

批量掉线里有相当一部分是变更引发的:调了个 JVM 参数、扩了 Region、换了块盘、升级了 HDFS。这类问题最难查,因为“以前一直是好的”。

我的做法是把 HBase 相关的变更全部纳入清单管理,尤其是这几类:JVM 参数、hbase-site.xml的任何改动、Region 拆分合并、HDFS 侧的参数调整、ZooKeeper 的tickTime和磁盘变更。每次变更后强制观察一个完整的业务高峰,确认无异常再关闭变更单。

另外强烈建议做主动的故障演练。找个低峰窗口,手工 kill 掉两台 RegionServer,观察:WAL 拆分多久完成、Region 重新分配多久、业务侧超时率涨多少、剩余节点会不会连锁反应。这个演练能把恢复时间从“未知”变成“心里有数”,也能提前暴露容量问题——如果杀掉两台剩下的就扛不住,说明你现在的冗余度根本不够。

我自己在几个集群上做完演练之后,把 RegionServer 的冗余度从“N+1”提到了“N+2”,也就是任意两台同时挂掉,剩余节点仍能在不触发写阻塞的前提下承接全部流量。这个冗余度看起来浪费资源,但比起一次雪崩带来的业务损失,实在划算。

最后分享一个我个人一直在用的小习惯:给每个集群维护一份“掉线档案”,每次发生批量掉线,不管最后查没查清,都把当时的时间点、掉线节点数、关键日志片段、监控截图、处置动作记进去。时间长了你会发现,同一个集群的掉线原因往往就那么两三个,反复出现。有了这份档案,第二次遇到时五分钟就能定位,而不是再从零开始翻日志。这东西不产生任何直接产出,但它是把一个运维从“每次都在救火”变成“越来越不慌”的关键。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进