1. 故障现场地图:先搞清楚HBase到底坏在哪一层
半夜两点被电话叫醒,十有八九不是好事。有一次我睡得正沉,值班同事打过来:HBase集群的写入接口大面积报错,日志里刷屏RegionTooBusyException。远程连上去一看,某个RegionServer的MemStore已经涨到2个多G,整个集群的写请求几乎全打在那台机器上,活活把一台普通节点熬成了热点。当天折腾到天亮才恢复,事后复盘发现,根因不是硬件、不是网络,而是当初设计行键时一个看起来“没什么大不了”的细节。
这类经历,做大数据的人多少都遇过。HBase的排查难,不在于单个报错有多复杂,而在于你面对的是一个分布式系统:客户端、ZooKeeper、HMaster、RegionServer、HDFS各管一段,故障症状经常出现在入口,真正原因却埋在数据链路深处。这篇文章,就是想把我这些年处理HBase问题时沉淀下来的排查思路、常用命令、参数含义和避坑经验整理成一份可以直接照着做的指南。适合正在维护HBase集群的工程师,也适合刚接手HBase、想少走弯路的新人。
先说清楚我的基本排查框架:任何HBase异常,先分五层定位。
| 故障层面 | 典型症状 | 优先检查方向 |
|---|---|---|
| 客户端层 | 超时、重试、无法获取连接 | 连接池配置、rpc超时、重试次数 |
| 协调层 | Session过期、Master主备切换 | ZooKeeper会话、GC停顿、网络抖动 |
| 存储层 | Region不可用、RIT卡住 | Region状态、META表、Split/Move流程 |
| RegionServer层 | 写入被拒绝、读变慢 | MemStore、BlockCache、Compaction队列、GC |
| 底层HDFS | 文件损坏、SafeMode、块丢失 | fsck、DataNode状态、副本数 |
这个顺序不是我随便排的。多数线上事故,表面症状在最上面一两层,但你在最上面查半天往往什么都查不到。正确做法是:先确认RegionServer进程是否稳定,再看Region状态是否健康,最后才回到客户端配置和业务侧改造。下面各章,就按这个思路把高频坑一个个拆开。
2. RegionServer的三大隐形杀手:热点、GC抖动与写阻塞
RegionServer是整个HBase的“命门”,它一抖,整个集群跟着抖。我遇到的所有莫名其妙的故障,刨到最后基本都能归到三类:数据热点、JVM停顿、写路径阻塞。
2.1 热点问题:行键设计才是原罪
所谓热点,就是所有读写压力集中在某几个Region上,其他Region闲着。最经典的场景是:写入时用时间戳做行键前缀,比如order_20250607_0001。看起来挺自然,但同一秒内的订单前缀相同,都会落进同一个Region,而这个Region必须等上一秒的量flush并Split后才有新Region接管。于是你们看到的就是:某台RegionServer CPU 100%,其他几台负载只有个位数。
排查热点,用两条命令足够了:
# 查看每个Region的请求数,找QPS特别高的Region echo "status 'detailed'" | hbase shell # 或从HBase UI的Region列表里,看哪个Region的request count数字异常突出一旦发现某几个Region“一骑绝尘”,不用怀疑,先看行键设计。常规解法有三种:
- 加盐(Salting):在行键前加上随机前缀,如
A_20250607_0001、B_20250607_0002。前缀范围要按Region预分区数设定,否则前缀本身也会成为热点。 - 哈希:对原行键做哈希取前几位作为前缀。分布最均匀,但牺牲了范围扫描能力。
- 反转键(Reversed Key):适合时间序列场景,把高位的低位反转,比如把订单号倒序。新数据均匀落在各Region,且保留了一定的扫描性能。
我的建议很直接:上线前必须做行键设计评审,上线后第一周就要盯着Region负载分布看。热点问题在开发环境很难暴露,因为数据量小、单Region也扛得住;上了生产,几百万行数据一来,马上就现原形。等到线上才改行键,意味着要重写全表,代价极大。
2.2 GC抖动:RegionServer掉线的真正推手
RegionServer莫名被标记为宕机,ZooKeeper会话过期,Master开始做故障恢复——这种场景十有八九是老年代GC停顿造成的。
HBase是典型的“大堆Java应用”,RegionServer堆动辄几十GB。老年代GC一次Full GC如果超过几十秒,RegionServer在这段时间里无法向ZooKeeper发送心跳,ZooKeeper就判定它失联,触发Master把它的Region重新分配。等你看到日志里的“ZooKeeper session expired”,GC早就结束了,节点状态都恢复好久了,但更糟的是Master已经开始做Region重新分配,集群进入抖动期。
我见过太多次把这个问题误判成“网络抖动”的排查过程。判断是不是GC问题,不需要上高级工具,三招:
# 1. 看GC日志里有没有长Stop The World停顿 grep "Full GC" /path/to/logs/gc.log* # 2. 看RegionServer日志里,从最后一次心跳到session expired之间的时间差 grep "ZooKeeper session expired" hbase-root-regionserver-*.log # 3. 用jstat查看实时GC情况 jstat -gcutil <regionserver_pid> 1000如果确认是GC问题,调优方向通常是:堆大小与JVM版本匹配(HBase 2.x默认G1,别再沿用CMS参数)、控制老年代晋升速率、合理设置-XX:MaxGCPauseMillis。另外,一个很实用的经验是把ZooKeeper的会话超时时间从默认的90秒左右调到120秒以上,给GC留出缓冲。这不是掩盖问题,而是在修GC的同时,避免单次停顿直接引发雪崩式Region重分配。
2.3 MemStore无限增长:写阻塞的临界点
写入变慢、抛RegionTooBusyException,大多数时候不是因为CPU不够,而是MemStore到了阻塞阈值。每个Region的每个列族都有MemStore,默认刷写阈值是128MB,当超过阈值的4倍(hbase.hregion.memstore.block.multiplier=4)时,该Region会拒绝写入;同时,整个RegionServer的MemStore总大小超过堆内存的40%(hbase.regionserver.global.memstore.size=0.4)时,会阻塞所有写入并强制刷写。
这个机制本身是保护性的,问题是它触发时通常意味着刷写速度跟不上写入速度。我在排查时说过的第一句话,永远是“看MemStore大小”。在HBase UI的RegionServer页面,能直接看到每个Region的MemStore占用。如果再配合:
# 看当前有多少线程卡在flush上 jstack <regionserver_pid> | grep -A 20 "flush" # 看storefile数量是不是异常多(HFile太多会拖慢flush后的读) echo "status 'detailed'" | hbase shell基本能判断是“写入太快”还是“刷写太慢”。如果是Compaction积压导致刷出的文件无法及时合并,光加节点都没用,得先处理compaction队列。这里有个我踩过的坑:为了追求写入性能,有人把hbase.regionserver.handler.count调得很大,比如调到100以上。结果就是同一时刻大量请求进来,内存和GC瞬间被打爆,反而更容易触发阻塞。Handler数不是越大越好,默认值附近(30左右)通常够用,真不够应该加节点,而不是加线程。
3. RIT不是玄学:Region卡在转换状态时的完整排查链路
如果说GC是HBase最隐蔽的坑,那Region In Transition(RIT)就是最磨人的坑。Region状态机在OPENING、CLOSING、SPLITTING、RECOVERING之间卡住,几十分钟不动,这个Region对客户端就是“不存在”的,读写全部失败。
3.1 RIT是怎么形成的
RIT的根因,通常是Region状态与META记录不一致。常见路径有三条:一是Split过程中RegionServer宕机,新旧Region的状态都没来得及写入META;二是Master在做Region转移时,目标RegionServer没有按时完成OPENING流程;三是META表自身分裂或损坏,导致状态查询出错。HBase 2.x引入了ProcedureV2,用WAL记录操作过程,理论上能自动恢复卡住的事务,但现实里RIT依旧频繁出现,尤其是碰到HDFS底层文件异常时。
3.2 排查RIT的完整步骤
我第一次处理RIT时,走了不少弯路。正确步骤应该是这样的:
# 第1步:确认到底哪些Region卡住了 echo "status 'detailed'" | hbase shell | grep -i "transition" # 或者看日志里反复出现的同一行 grep "Failed to open region" hbase-root-regionserver-*.log | tail -50 # 第2步:打开Master日志,找这个Region最近的状态变迁记录 grep "<region_name>" hbase-root-master-*.log | tail -100 # 第3步:确认META里这个Region怎么看 echo "scan 'hbase:meta', {COLUMN => 'info:state'}" | hbase shell到第2步基本能看出卡在哪个环节。如果是“找不到RegionServer来接管”这种问题,检查目标RegionServer是否还在线;如果是“打开Region时报文件错”,则要往HDFS方向查,看HFile是否损坏或缺失。定位清楚之后才谈修复,修复手段分四个等级:
- 轻量级:手动把Region assign到另一个RegionServer上(HBase 2.x用HBCK2,1.x用旧版hbck的assign命令)。
- 屏蔽状态机:HBCK2提供了bypass命令,可以绕过卡住的Procedure。
- 重建Region状态:对于META与RegionServer状态严重不一致的,需要unassign后重新assign。
- 极端情况:Region数据已不可恢复时,停掉自动恢复,备份日志后从快照恢复。
3.3 一个卡在RECOVERING的实例复盘
有次线上集群有个Region卡在RECOVERING状态一直起不来,日志里反复出现WAL replay失败。当时我第一反应是看WAL文件本身,结果文件在HDFS上完好无损。后来打开RegionServer日志里的堆栈,发现是ZooKeeper上的一个临时节点残留,导致RegionServer认为还有别的实例在处理同一个Region,自己就一直等待。清理掉那个残留znode后,Region几秒钟内恢复正常。
这个小案例给一个重要启示:RIT排查一定要先看日志里的完整堆栈,而不是急着跑恢复命令。状态的转换是有前置条件的,盲目assign只会让同一个Region在两个节点上反复横跳,把一次性问题变成持续抖动。
4. 读写路径上的“坑中之坑”:WAL、BlockCache与Scanner超时
RegionServer层面解决了,还有一堆藏在读写链路里的细节坑。这部分问题不致命,但很影响体验,而且它们往往是“偶发”的,比持续故障更难定位。
4.1 WAL:丢数据的最后一道防线
HBase写入先写WAL(Write-Ahead Log,即HLog)再写MemStore,WAL是数据不丢的根本保证。可如果只在Put时使用默认的SYNC_WAL,每次写入都要刷磁盘,性能损失很大。于是很多人把写入改成ASYNC_WAL,追求吞吐量,然后RegionServer一宕机,几十秒内的写入全没落盘,回放时直接丢数据。
我处理过最严重的一次数据丢失,就是线上把durability设成了ASYNC_WAL,恰好一台RegionServer被运维误重启。那几十秒的写入几千条,全部消失。如果不是事后用业务日志反查,这个问题可能永远都不会被发现。我的态度很明确:默认SYNC_WAL别乱动,如果业务可以接受少量丢失再做取舍,但要提前通知业务方,并且做好对账。日志里被当成“正常优化”的改动,往往是事故报告里“没想到”的根因。
WAL相关的另一个坑是WAL文件积压。RegionServer宕机后,HDFS上的WAL文件需要重放,如果积压的文件太多,重放过程要很久,期间Region一直处于RECOVERING状态。这时别干等,先检查HDFS上/hbase/WALs目录的文件数量和大小,必要的话临时调大重新分配WAL的线程数,或把积压文件拆给多个节点并行处理。
4.2 BlockCache:全表扫描是怎么把缓存打爆的
HBase的读路径,数据先查BlockCache,再查HFile。BlockCache设计得好,读性能可能差出10倍。我见过很多人,把集群读慢归咎于磁盘IO慢,实际上一查BlockCache hit ratio,只有30%多,说明大量读根本没命中缓存。再一看,业务方每天定时跑全量扫描,把整个BlockCache冲得干干净净,其他在线查询全部降速。
解决思路有两个层面。行为层面,全量扫描优先选择在业务低峰期跑,或者直接让它走单独的只读副本集群。配置层面,合理配置缓存类型:堆内的LRUBlockCache适合小规格场景;堆外BucketCache适合读多写少、堆内存紧张的场景。判断依据不是“别人怎么配”,而是你自己的读命中率指标。我通常会在压测时直接取两个数字:cache hit ratio和历史GC时间,两者取平衡点。
4.3 Scanner超时:一个被忽略的经典场景
HBase客户端的Scan是带租约(lease)的。如果你打开一个Scan,遍历一行要处理很久,租约到期,服务端会抛ScannerTimeoutException,客户端如果没处理还继续拿next,就会抛UnknownScannerException。这个问题的特点是:小数据量测试完全正常,一到生产大数据量就“随机”失败。
排查时不要只看异常名,要确认两点:一是业务代码里是否在Scan之后做了重逻辑处理(比如逐行调远程接口)——这是最常见的慢Scanner来源;二是超大扫描的数据量是否超出了一次RPC能拉回的范围。根本解法是调整hbase.client.scanner.timeout.period超时时间(默认60秒左右),但更重要的是让业务侧分批Scan,或者用过滤器尽量在服务端裁剪数据。超时参数是兜底,不是解决方案。
5. 别急着重建集群:先把手头工具的排查价值榨干
很多人一遇到HBase异常,第一反应是重启RegionServer、重启Master、甚至重启整个集群。其实HBase自带一套排查工具链,足够你在重启前把问题定位得很清楚。重启是最简单的操作,也是最容易掩盖真实问题的操作——你重启了,问题暂时没了,但根因还在,下次换一个时间点继续爆发。
5.1 hbase shell:不只是查数据
hbase shell是排查时的第一入口,但很多人只拿它做scan和put,完全浪费了。
# 集群整体状态 echo "status" | hbase shell echo "status 'detailed'" | hbase shell # 查看某张表的region分布,确认有没有“数据倾斜” echo "get_splits 'your_table'" | hbase shell echo "desc 'your_table'" | hbase shell # 查看是否有表处于DISABLED或不一致状态 echo "list" | hbase shell有个细节值得多说一句:status 'detailed'里的每个Region后面有Requests数,它同时也是热点判断的第一手证据。我排查热点时,第一步永远是看这个数,而不是跑各种监控平台,因为监控平台的数据有延迟,shell里的是当前时刻的实时状态。
5.2 hbck与HBCK2:状态不一致的“手术刀”
HBase 1.x时代用hbase hbck,它主要做一致性检查和简单的修复。HBase 2.x引入了HBCK2,操作方式完全不同,全部通过Master的HTTP接口或者命令行hbase hbck2执行。很多从1.x迁移过来的人,还在用旧版hbck命令去管2.x集群,结果各种报错甚至把元数据搞得更乱。
我只能提醒一点:2.x集群只认HBCK2,网上搜到的旧命令不要照抄。HBCK2的常见用法:
# 查看当前状态不一致的region hbase hbck2 view --all # HBase 2.x部分版本写法是 hbase hbck2 --help 先看帮助 # 主动assign一个region hbase hbck2 assigns <encoded_region_name> # 强制绕过某个卡住的procedure hbase hbck2 bypass <procedure_id>使用这类“手术刀”命令之前,务必确认你明确知道当前Region的真实状态,并且做过数据保护(比如先快照)。HBCK2的bypass功能很强大,但用错场景,会把一个正在正常恢复中的流程强行打断,造成二次伤害。
5.3 日志、jstack与指标:三板斧组合拳
最后一个不可缺少的工具组合是三层结合:RegionServer日志、JVM线程堆栈、监控指标。日志负责告诉你“发生了什么”,jstack负责告诉你“当前卡在哪个方法上”,指标负责告诉你“这个状态持续了多久、趋势如何”。
# 拿到RegionServer的pid后,直接看线程状态 jstack <pid> | grep -E "flush|compaction|rpc" | head -50 # 看堆内存使用是否异常 jmap -heap <pid> # 看GC停顿 jstat -gcutil <pid> 5000我通常在收到告警后按这个顺序操作:先看告警对象的GC和CPU曲线 → 再开RegionServer日志找异常堆栈 → 必要时jstack确认线程卡点 → 最后才决定是否干预。按照这个流程,80%的问题能在10分钟内定位,完全用不着重启。
6. 常见异常代码逐个拆:从报错名直接反推处理思路
处理HBase问题多了,你会发现异常报错本身就是线索。我整理了一份高频异常对照表,第一次遇到时照着查,能省很多时间。
| 异常/错误 | 典型场景 | 排查方向 |
|---|---|---|
| RegionTooBusyException | 写入被拒、客户端重试堆积 | MemStore是否超阈值、Handler是否占满 |
| NotServingRegionException | Region在移动/Split期间被访问 | 查看Region状态,等待META更新,客户端自动重试 |
| NoServerForRegionException | 客户端拿到的Region地址已失效 | META不一致,查Region是否被重新分配 |
| ScannerTimeoutException / UnknownScanner | Scan租约过期 | 业务侧处理太慢,或扫描数据量过大 |
| ZooKeeper session expired | RegionServer掉线 | GC停顿、网络、ZK负载 |
| META表不可用 | Master启动失败、全部读写失败 | hbase:meta的Region是否在线,HDFS是否正常 |
| FileNotFound / BlockMissing | HFile损坏、HDFS副本缺失 | hdfs fsck检查块完整性,考虑从备份恢复 |
| TableExists / TableNotFound | 建表/删表竞态 | 操作重试机制,确认命名空间状态 |
6.1 RegionTooBusyException:最容易被误判的写入报错
这个异常出现时,客户端日志会很长,容易让人以为“集群挂了”。实际上绝大多数时候集群活得好好的,只是某个Region的保护机制启动了。除了前面说的MemStore达到阻塞阈值,还有一个常见原因是Handler数量耗尽:所有RPC处理线程都在处理慢查询(比如大Scan),新请求排队排到超时。区分两种原因的方法是看UI上该RegionServer的MemStore曲线和Handler占用曲线,MemStore高是前者,Handler全满而MemStore正常是后者。
处理建议:前者看flush和compaction;后者减少慢查询、控制单次Scan大小,必要时小幅度上调hbase.regionserver.handler.count,但别超过50。
6.2 NotServingRegionException:多数时候“自己会好”
这个异常看着吓人,其实经常发生在RegionSplit或Region移动的瞬间。客户端拿着旧的Region地址访问,服务端告诉你“这个Region不在我这儿了”。遇到它先别急,客户端自身有刷新META并重试的机制,通常重试几次就恢复了。如果持续报,才需要人工检查Region进程是否卡住、META状态是否一致。
6.3 ZooKeeper session expired:不是ZK的错
这个名字有很强的误导性,让很多人排查时盯着ZooKeeper集群本身,查ZK磁盘、网络、目录大小,却忽略了一个事实:session expired通常是因为RegionServer自己停顿太久,没来得及刷新心跳。停顿来源主要是GC,其次是CPU完全被占满。所以遇到这个报错,第一件事是回头查RegionServer的GC日志,而不是去查ZK。
7. 一套能落地的“体检清单”:上线前、故障后、日常巡检
把前面的坑汇总成清单,是我最后想分享的精华。排查能力固然重要,但更好的做法是把问题挡在发生之前。
7.1 上线前必查项
- 行键设计评审:是否可能热点?是否有盐值/哈希方案?预分区数与Region大小匹配吗?
- 预分区:用
org.apache.hadoop.hbase.util.RegionSplitter提前规划分区,避免初期所有数据挤在单Region。 - 列族设计:列族数量是否合理?尽量一个列族,避免多个列族刷写互相拖累。
- JVM参数:2.x确认用G1,堆大小与机器内存匹配,预留30%给OS页缓存。
- 客户端参数:连接池大小、重试次数、RPC超时是否与业务匹配。
7.2 故障后的恢复顺序
- 先确认HDFS健康:
hdfs fsck /hbase,排除底层块缺失和SafeMode。 - 再确认META表可用:
echo "get 'hbase:meta', 'table'" | hbase shell。 - 然后确认Region状态:是否有RIT,是否有多个Region在同台RegionServer上堆积。
- 最后才看业务侧:告警期间的写入是否可重放,是否有对账方案。
这个顺序的关键点是:永远从底层往上层查。HDFS有病,你重启多少次RegionServer都没用;META坏了,你再怎么重启Master也会再次失败。
7.3 日常巡检指标
频率建议:至少每天看一眼,每周做一次深度检查。核心指标只有这几个:RegionServer的GC频率与Full GC次数、BlockCache命中率、WAL目录文件数、Compaction队列积压、每个Region的请求数分布。我一般把这几项做成一个简单的巡检脚本,跑到一个文件里,每天上班先看一遍。不要小看这个习惯,线上80%的问题在变大之前,都会在这些指标上留下蛛丝马迹。
最后分享一个我个人的实操体会:HBase排查,真正难的从来不是某个命令不会用,而是面对一堆互相矛盾的表象时,能不能忍住不重启、不瞎试,先按“HDFS → META → Region → RegionServer → 客户端”的层次一点点剥。踩过的坑多了以后,你会发现大多数故障的根因其实就那么几类,都是在某个早期环节埋下的隐患,只是当时没有暴露。
再补一个很实用的小技巧:每次都把线上事故的排查过程写成简短记录,哪怕只有几行,标注当时的处理顺序、判断依据、真正根因。等到下一次再遇到类似的RIT或者MemStore告警,翻一下旧记录,排查速度会快非常多。我的经验是,积累五六次事故记录之后,我已经能根据症状在几分钟内锁定排查方向。这就是避坑指南最大的价值——不要重复踩同一个坑,更不要用重启来换取“暂时正常”。