ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

HBase线上故障排查实战:从Region热点到GC与ZooKeeper会话超时

HBase线上故障排查实战:从Region热点到GC与ZooKeeper会话超时 凌晨一点线上告警突然炸了某张表写入超时率直接飙到百分之三十。我登录 Master 看了一眼几个 region 卡在 RIT 状态紧接着 HBase Shell 敲个list都半天没响应。查了一整夜才定位到根因——那张表的 rowkey 是时间戳前缀热点全部打在一台 RegionServer 上加上 GC 停顿把 ZooKeeper 会话拖超时最后变成了连锁故障。这次想把自己这几年用 HBase 实际踩过的坑、解决过的高频问题系统整理一遍。网上讲 HBase 安装与配置的帖子已经很多了但真正跑起来之后那些让人睡不着的问题反而很少有人说清楚。这篇文章主要面向两类人一类是刚接手 HBase 集群的运维和开发另一类是遇到线上问题但不知道从哪里下手的同学。我尽量不搬教科书内容只讲真实环境里反复出现的故障以及可以照着做的排查思路和恢复手段。1. 连接与元数据故障RegionServer失联、META异常、RIT卡死1.1 ZooKeeper会话过期不是玄学是参数和网络在打架HBase 集群对 ZooKeeper 的依赖远超很多人的预期。RegionServer 与 Master、RegionServer 与客户端之间很多状态同步都依赖 ZK 的临时节点和会话心跳。最常见的故障表现是RegionServer 进程明明还活着日志里却打出一句ZooKeeper session expired然后 Master 把这个节点标记为异常开始把上面所有的 region 重新分配到其他机器。如果你去查这个 RegionServer 的 GC 日志大概率会发现当时发生了一次超过zookeeper.session.timeout阈值的暂停。这个超时值在 HBase 里默认通常是 90 秒不同版本略有差异但生产环境里网络抖动、磁盘 IO 卡顿、G1 Full GC 停顿任何一个超过这个阈值就会让 RegionServer 在 ZK 里死亡。我的处理建议分三层监控层必须盯着每台 RegionServer 的 GC 停顿时间曲线和 ZK 连接数不要等告警出来了才去翻日志。参数层zookeeper.session.timeout不要设得太短。90 秒其实已经是一个合理基线低于 60 秒时偶发的 GC 停顿就会被放大成故障切换太不划算。当然也不要调成十几分钟否则真宕机时故障转移慢到你想哭。网络层检查各节点之间的时钟同步和交换机丢包这个常被忽略。NTP 偏移严重时ZK 心跳包的时序会乱也会触发会话过期。另外客户端那侧也有一个常见的坑hbase.client.retries.number如果保持默认或者被人调得很大在 region 重新分配期间客户端会反复重试且指数退避最终表现为某个操作卡死很久才报错。我一般会结合业务操作超时时间把重试次数控制在合理范围内而不是一味依赖重试解决网络抖动。1.2 RegionServer连接失败背后常见的系统层原因有段时间我们集群新增了几台 RegionServer结果客户端时不时报连接失败但 HBase UI 上看节点又是正常的。排查到最后发现问题出在/etc/hosts配置不一致。RegionServer 启动时会把主机名注册到 ZK客户端解析这个名字时走到错误的 IP自然连不上。记住一个关键点HBase 的 RPC 通信默认走 16020RegionServerMaster 走 16000老版本可能是 60020/60000。运维层面至少检查这几件事所有节点的主机名和/etc/hosts必须统一正反向解析要正常。防火墙和安全组要放行对应的端口段不要只放 2181 就算完。文件句柄数要调够。ulimit -n设置太小连接数一上来日志里就会出现Too many open files客户端报告连接被拒绝。TCP 端口耗尽问题。客户端与 RegionServer 之间连接数大的时候TIME_WAIT和CLOSE_WAIT会堆积后面连接就建不上。这个在并发高的业务场景里很常见要顺带把内核参数也检查一遍。这类问题排查起来不复杂但每次发生都很折磨人因为现象看起来像 HBase 挂了实际是操作系统层或者基础网络的问题。1.3 META表异常与RIT卡死META 表是 HBase 的路由中枢这张表挂了所有表操作都会受到影响。常见的诱因包括RegionServer 在托管 meta region 时突然宕机、磁盘损坏、以及某些错误的手工修复操作。HBase 2.x 之后老版本的hbck -fix已经不建议用于修复了贸然跑可能把 META 表修出更大问题。现在的推荐做法是使用 HBCK2 工具它专门处理 HBase 2.x 中的 region 状态异常。常用场景包括region 长时间处于 PENDING_OPEN 或 PENDING_CLOSEmaster 反复尝试但无法完成状态转移。处理 RIT 卡死有一个动作顺序我踩过坑之后总结成这样先确认旧的 RegionServer 是否完全停止服务。这步不能省否则强制 assign 后可能出现两个节点同时服务同一个 region 的情况数据一致性风险极高。在 HBase Shell 中用assign encodedRegionName或unassign encodedRegionName重置状态。如果 assign 没反应再用 HBCK2 的 bypass 和 assigns 工具绕过卡住的状态机强制重新分配。到 HDFS 上确认 region 的数据目录完整文件缺失时强行复活 region 只会让问题更复杂。注意任何对 META 表和 region 状态的修复操作动手之前最好先做一次快照给自己留条后路。2. 写入路径卡顿热点、MemStore刷写与WAL阻塞2.1 Region热点时间戳前缀导致写入全部打在同一台机器写入热点可能是 HBase 生产环境里最容易被忽视、一旦发生又最难受的问题。典型场景是 IoT 或者日志类数据表设计时图省事rowkey 直接用了时间戳 设备ID这种结构。新数据产生时时间戳递增所有写入都会落到最后一个 region 上。结果就是一台 RegionServer 磁盘 IO 被打满CPU 升高其他节点闲着看戏。解决热点问题首先要回到 rowkey 设计。加盐Salting在 rowkey 前面拼一个哈希前缀比如MD5(userId).substring(0, 4) timestamp。前缀把数据打散到多个预分区写入吞吐立刻分摊。哈希分片取业务主键的 hash 值根据分片数取模后作为前缀。这个方式写入均匀但代价是有序读会变差。反转某些场景下可以反转固定宽度的时间戳让最新数据落在前面的 region。这只解决了一部分问题并不是万能的。只改 rowkey 还不够必须配合预分区。生产经验是按预期总数据量 / 单 region 建议容量10GB~20GB来估算 region 数再留至少 50% 余量。创建预分区可以这样hbase create device_log, cf, {SPLITS [01, 02, 03, 04, 05, 06, 07, 08, 09]}也可以用自带工具按分片算法生成比如hbase org.apache.hadoop.hbase.util.RegionSplitter device_log -c 10 -f cf已经上线的热点表不要直接在生产环境硬改 rowkey。我的标准做法是快照旧表 - 新建一张预分布好、rowkey 重新设计的新表 - 写迁移程序或跑一个 MapReduce 任务搬数据 - 切换代码里的表名。整个过程要有回滚预案迁移期间双写或者旁路切换都要提前想清楚。2.2 MemStore刷写阻塞为什么写入会突然卡住写入请求先落 MemStore达到一定大小后异步刷成 HFile。默认情况下单 region 的 MemStore 刷写阈值是 128MB但真正让写入阻塞的是 RegionServer 级别的全局 MemStore 上限。默认配置里全局 MemStore 占总堆的比例是 40%其中低水位是 95%也就是说总 MemStore 达到堆的 38% 左右时开始刷写到 40% 时就会阻塞所有写请求。日志里一旦出现Blocking updates或memstore is above high watermark说明 MemStore 的生产速度已经超过刷写速度。排查顺序建议这样看是全局阻塞还是某个 region 阻塞。通过 HBase UI 或者status detailed能直接看到每台 RegionServer 的 MemStore 大小和 Flush 队列长度。Flush 队列很长时看是偶尔的大流量写入还是持续刷不出去。持续刷不出去往往不是 MemStore 参数的问题而是磁盘 IO 或 HDFS 写入变慢了。如果只是某张表某个 region 的 MemStore 涨得特别快大概率又是 rowkey 热点单点写入压力太大。全局刷写都频繁同时 GC 也被拖累可以考虑调大刷写线程或者增加 RegionServer 节点来分摊压力。一个比较容易犯的错误是遇到写入阻塞就急着调大global.memstore.size。MemStore 变大刷写确实会更晚触发但一次刷写的数据量也更大GC 压力和磁盘 IO 峰值反而上去了属于饮鸩止渴。只有确认写入模型本身没问题的前提下才适合动这个参数。2.3 WAL与持久化写入慢经常藏在这两个地方WALWrite-Ahead Log是 HBase 数据不丢的保证。默认每条写入最终都要同步刷到 HDFS 上的 WAL 文件这个 fsync 开销是写入路径的主要成本之一。有些团队为了提升写入性能把hbase.durable.sync改成异步结果 RegionServer 一断电就丢了一批数据业务找上门才追悔莫及。我的观点是除非业务明确能接受小窗口丢数据否则永远不要关同步刷盘。想要写入快应该在行键设计、客户端批量提交、RegionServer 分布上下手。还有一个隐蔽的坑HDFS Erasure CodingEC不适合放 HBase WAL。EC 对追加写入支持有限而 WAL 恰恰是流水式 append典型场景撞得死死的。另有 HDFS EC 副本策略在小文件场景下性能也不理想。生产环境我的建议是WAL 目录老老实实用普通副本策略默认 3 副本数据文件也尽量不要贸然开 EC。另外写入延迟很高时先看 DataNode 的磁盘和网络状况不要一上来就怀疑 HBase。HDFS 慢节点、坏盘触发的块复制、机架感知未配置导致副本集中到同一机架这些问题都会传导成 HBase 写入超时。3. 读路径性能BlockCache、Scan扫描与过滤器3.1 BlockCache配置不当的两种典型表现BlockCache 用于缓存 HFile 的数据块默认的 LRUBlockCache 占堆的 40%。读多写少的表这个比例可以适当提高读少写多的表BlockCache 配再大也白搭反而挤占 MemStore 空间。一个常见的组合问题是Regionserver 堆配了 32GB 甚至 64GBhfile.block.cache.size和global.memstore.size加起来超过了 0.8JVM 自身可用的内存被榨干结果频繁 Full GC读延迟和写延迟一起爆炸。无论怎么配这两块加起来最好别超过堆的 80%剩余空间要留给请求处理、RPC 缓冲和 JVM 自身。如果机器物理内存充足、读缓存压力大2.x 版本可以用 BucketCache 的 offheap 模式把读缓存放到堆外明显降低 Young GC 和 Full GC 的频率。但要注意堆外缓存不是万能的磁盘 IO 没解决时它只能让缓存淘汰时对 GC 的冲击变小并不能凭空提升读吞吐。3.2 Scan慢的常见原因大多数是自己流程的问题读性能问题里Scan 慢绝对占大头。我最常看到的代码如下一个 Scan 对象没设 startRow 和 stopRow直接扫全表然后在客户端做过滤。这种写法在小数据量的测试环境看不出问题到生产环境就是灾难。Scan 慢通常要检查这几个点setCaching和setBatch是否合理。setCaching决定每次 RPC 返回多少行默认值通常偏小调大可以减少 RPC 次数但调到几千时如果每行 cell 很多客户端内存很容易被打爆。setBatch控制每次 RPC 返回的 cell 数当单行 cell 数量大时必须设置。经验值一行几十个 cell 时setCaching(500)加setBatch(100)比较稳妥。ResultScanner 用完有没有关闭。不关闭会导致客户端连接和内存泄漏时间长了整个客户端就废了。用 try-with-resources 是最稳的写法。结果集是不是太大了。一个扫大范围数据的任务一次拉几千万行无论 HBase 多快都会慢。业务上能用分页就分页能用 startRow/stopRow 限范围就限范围。3.3 过滤器、布隆和列族设计HBase 的 Filter 会下推到 RegionServer 端执行所以把过滤条件写在 Scan 的 Filter 里比把数据拉回客户端再过滤要高效很多。但不要以为用了 Filter 就万事大吉某些过滤器比如SingleColumnValueFilter在底层仍然需要读取相关的 HFile性能不一定好。真正的读优化往往从 rowkey 设计开始让查询条件天然落在连续的 key 区间上而不是靠过滤去大海捞针。布隆过滤器也是一个被讲烂但很多人没理解透的点。它对 get 操作有效对 scan 几乎没帮助。创建表时设置BLOOMFILTER ROW能加快按 rowkey 的随机读如果经常按行键 列来读ROWCOL可以进一步减少不必要的 block 加载但写入代价会更大。所以布隆配置不是越强越好要看查询模式。列族设计方面我强烈建议一张表不超过 2~3 个列族。多个列族在 flush、compaction、WAL 截断等环节会互相影响实际收益却很小。曾经接手过一张 5 个列族的表写入热点全部集中在一个列族结果其他列族也频繁被连带 flush读性能一塌糊涂。精简列族之后整个读写链路清爽多了。另外读多写少的大表建议开启数据压缩。SNAPPY 和 ZSTD 都是不错的选择压缩后磁盘 IO 大幅减少CPU 开销换取 IO 收益整体很划算。4. Compaction与存储治理小文件、Compaction风暴与HFile损坏4.1 小文件过多与store file数量失控HBase 每次 MemStore 刷写都会生成一个 HFile写入频繁时小文件数量快速上涨。读路径上一个查询要打开多个 HFile 去定位数据文件越多读放大越严重。HBase 默认会在小文件数量达到 3 个时触发 minor compaction将这些文件合并成更大的文件。但如果刷写速度比合并速度快store file 数量就会持续累积。当某个 store 的文件数超过hbase.hstore.blockingStoreFiles默认是 16旧版本存在差异时写入会被阻塞日志里出现Too many store files。这说明 compaction 已经跟不上写入速度了。处理思路先看 compaction 队列长度和磁盘 IO。如果是 IO 已经是瓶颈单纯调线程数没用要降写入速度或加节点。控制小文件产生速度。客户端尽量批量提交减少高频单条写入。适当调整 compaction 相关线程池和吞吐上限。2.x 引入了 small compaction 和 large compaction 两个线程池可以针对短任务和长任务分别配置不要把线程都占在大文件合并上。4.2 Major Compaction风暴与错峰major compaction 会把一个 store 下的所有 HFile 合并成一个大 HFile执行时磁盘 IO、CPU、网络带宽消耗都很大。HBase 默认会在 7 天左右自动触发一次 major compaction时间点带有随机性。这就容易出现一个尴尬局面业务高峰期集群里多台 RegionServer 同时在做 major compaction整体读写性能跌入谷底。我管理的大多数生产集群都会在服务端关掉自动 major compaction改成定时错峰手动执行。相关配置是property namehbase.hregion.majorcompaction/name value0/value /property注意这个参数设置为 0 不代表完全不合并。手动执行的入口很灵活可以单独对某张表、某一个 region 执行hbase major_compact device_log手动执行时有一个容易被忽略的点major compaction 期间会写出一份全新的完整数据文件磁盘空间占用会先增后减。如果 HDFS 剩余空间不足任务可能在中途失败。我一般要求 HDFS 使用率低于 80% 再跑批量 major compaction并且按表分批、错开时间避免多个大表同时合并。4.3 HFile损坏与快照恢复RegionServer 报ChecksumException或者打开 HFile 失败时第一反应不是去删文件而是先定位是哪张表、哪个 region。日志里通常会带 region 的 encoded name可以顺着这个信息到 HDFS 上找到对应目录再用 hbase hfile 工具检查具体文件是否可读。恢复处理的优先级我按实战安全程度排序有快照 - 优先恢复快照。HBase 快照是 HDFS 层面的指针复制代价极低所以我在生产环境会坚持对重要表配置定期快照任务。没有快照但 HDFS 仍有可用副本 - 先修复或隔离坏盘再看 RegionServer 能否从其他副本读取数据重新构建。损坏情况严重 - 把相关 region 下线隔离坏文件评估数据损失范围能修复多少是多少。这里再强调一次HBase 2.x 环境下不要用老hbck -fix去修复它在很多时候会删除它认为是异常的数据文件有可能造成不可逆的损失。遇到复杂恢复场景可以先保护现场再咨询有经验的同事或社区。5. 内存与GC大堆、Full GC和堆外Cache的权衡5.1 堆越大反而越容易被打挂很多人觉得 RegionServer 堆越大越好数据往内存里塞就能快。实际上堆越大Full GC 的停顿时间越长。64GB 堆一旦触发 Full GC停顿个几秒到十几秒很常见这段时间内 RegionServer 的所有 RPC 请求都会卡住。如果停顿超过 ZooKeeper 会话超时Master 会把 RegionServer 判定为宕机触发 region 重新分配然后故障开始雪崩。先讲结论HBase 2.x 环境优先用 G1 垃圾回收器合理设置停顿目标并且控制堆内缓存的比例。CMS 时代靠预留浮动垃圾空间来缓解concurrent mode failure的经验在 HBase 2.x 里已经不适合了。但 G1 也不是开了就高枕无忧Young GC 频繁同样会拖高延迟调参方向主要是降低堆内大对象的分配压力。经验之谈GC 调参更多是缓解真正根治问题要靠降低内存压力。如果某个表的热数据特别多不要死磕 RegionServer 堆大小想办法从 rowkey、缓存策略、数据生命周期上做优化。5.2 MemStore与BlockCache的堆内平衡堆内这两块大头必须按业务读写比来分配这也是排查很多 GC 问题的第一步。业务模型hfile.block.cache.sizeglobal.memstore.size说明读多写少0.4~0.50.2~0.3读缓存优先memstore 够用即可写多读少0.2~0.30.3~0.4给 memstore 留更多空间减少刷写频率读写均衡0.3~0.40.3~0.4两者加总不超过 0.8调参之后要观察一段时间不要只盯着命中率。BlockCache miss 率高不一定代表缓存配置小也可能是查询模式本身就是大量扫描缓存根本帮不上忙。5.3 OffHeap方案的实际效果有限但真实HBase 2.x 支持把部分 MemStore 和 BlockCache 放到堆外最常用的就是 BucketCache 的offheap模式。我有个读多写少的业务RegionServer 堆 64GBGC 导致的查询毛刺一直压不下去把读缓存改成 offheap 后Young GC 频率明显下降查询延迟曲线稳定了很多。但要提醒一句配置 offheap 之前先确认机器物理内存富余。堆外缓存占的是系统物理内存如果 NodeManager 或者其他进程还要共用机器很容易 OOM。另外offheap 配置会引入更多参数新手容易出现配置错误反而实例启动失败。生产环境如果当前配置跑得很稳就不要为了更先进去折腾。6. 一套能落地的排查流程从报警到定位用不了半小时6.1 收到报警先看四个维度再动服务线上出了故障第一反应不是重启也不是改参数。我会先快速做四个判断单点还是全局只有一张表超时还是所有表都超时这个决定排查方向。看 GC 曲线。如果某台 RegionServer 的 GC 时间和频率异常优先处理 GC而不是去重启节点。看磁盘和 HDFS。DataNode 是否坏盘、磁盘剩余空间是否告急、HDFS 是否有大量块复制任务。看 ZooKeeper 和 RIT 状态。有 region 卡住先处理 region 状态再关注路由和元数据。有一次集群报警所有人都在查 HBase 参数最后发现是某台 DataNode 的磁盘出现了大量慢 IO导致 HDFS 写入变慢进而 HBase WAL 写入阻塞。这种问题如果一开始就盯着 HBase 侧调参方向就彻底偏了。重启节点会带走很多现场证据。遇到可疑情况先把节点日志、jstack、jmap -histo、监控截图收集好再决定下一步。6.2 HBase Shell与HBCK2的常用排查命令日常排查最常用的命令我列成清单方便直接备查# 查看集群整体状态、region 分布和请求数 hbase status detailed # 查看某张表分布在哪些 RegionServer 上 hbase list_regions device_log # 手动分配/卸载 region处理 RIT 卡死 hbase assign encodedRegionName hbase unassign encodedRegionName # 强制刷写和合并 hbase flush device_log hbase major_compact device_log # 控制 balancer迁移/扩容期间避免 region 自动跑来跑去 hbase balance_switch false hbase balance_switch true # 验证某行数据是否可读 hbase get device_log, rowkeyHBase 2.x 的元数据异常场景我优先使用 HBCK2 而不是老 hbck。比如assigns命令可以强制批量分配 regionbypass可以跳过卡住的状态。用这些工具之前必须确认不再有旧的 RegionServer 进程在服务同一个 region否则会发生双写。6.3 日志关键字与对应问题日志是排查故障的第一手证据。HBase 的 RegionServer 日志里以下关键字基本能对应到典型问题日志关键字对应问题Blocking updatesMemStore 达到高水位写入被阻塞Too many store filesstore file 数量超阈值compaction 跟不上写入REGION_IN_TRANSITION/RITregion 状态卡住需要手工干预NoServerForRegionExceptionmeta 表路由信息异常或 region 正在重分配RegionTooBusyExceptionregion 压力过大、RPC 队列满或 MemStore 阻塞Slow Get/Slow Scan读延迟超过慢查询阈值需要进一步分析原因举例如果日志里刷了一屏Blocking updates同时Too many store files也在出现大概率是某个 region 的写入压力过大 compaction 跟不上。先不要改阻塞参数先查 rowkey 分布和 region 热点把热点切开问题才能真正解决。再回到开头那个凌晨修了一夜的案例。它其实不是某一个参数导致的而是 rowkey 热点引发写入集中在单一 RegionServer这台机器的 GC 压力变大GC 停顿触发 ZooKeeper 会话超时Master 判定节点宕机region 开始大规模重分配最终整个集群像多米诺骨牌一样倒下。事后看根因就是表设计阶段没想清楚数据分布后面无论怎么调 JVM、调刷写参数都只是把问题往后推。我个人的切身体会HBase 线上故障一半以上根源在表设计——rowkey、预分区、列族、压缩策略。排错的第一步不是改参数而是回到业务读写模型去问一句这个表的设计是不是从根本上就歪了。稳定运行的集群靠的不是雕花一样的 JVM 参数而是合理的表结构、精准的预分区、预算好刷写和 compaction 的节奏再加上定期快照这个兜底动作。
返回列表