HBase这层分布式存储协议,平时写读多用不着看,可一到线上出问题——写入毛刺、Region热点、节点宕机恢复,没把底层协议搞清楚的人基本只能靠重启和加机器硬扛。我印象特别深的一次是某团队凌晨报障:某个表写入忽快忽慢,RegionServer的CPU不高、磁盘IO也不高,但就是有周期性的毫秒级停顿。查到最后,问题出在MemStore刷写与compaction的触发时序上,而这背后就是HBase存储协议里最容易被忽略的调度逻辑。
那次之后我就开始系统梳理HBase从数据落地到读取返回的完整协议链路。今天这篇文章就围绕“分布式存储协议”这个核心,从表的物理拆分、写入链路、读取链路、Region生命周期、故障恢复这几个角度拆开讲,最后再补一些压过生产环境后的调优心得。适合正在用HBase,或者打算做HBase选型、想搞清楚它内部到底怎么工作的读者。废话不说,直接进入正题。
1. HBase把一张表拆成了什么:从Region到HFile的存储层级
很多刚接触HBase的人会把表和“文件”划等号,以为HBase和MySQL一样,一张表对应若干个固定大小的小文件。实际上HBase的分布式存储协议建立在一个非常不一样的思路上:表是逻辑概念,物理存储的单位是Region,而Region再往下拆,才是最终落盘的文件。
1.1 RowKey范围决定Region边界,而不是行数
HBase的表在逻辑上是一张稀疏的、基于RowKey排序的巨型表。Region是什么?是对这张表按RowKey的字节序范围做的一次水平切分。每个Region有明确的StartKey和EndKey,比如RowKey从“0000”到“1999”的数据落入Region A,从“2000”到“3999”落入Region B。这里的范围是连续的、有序的、完全不重叠的。
这也就意味着写入一行数据时,客户端必须能算出这行数据属于哪个Region。具体做法是:客户端先查hbase:meta表(现在也叫hbase:meta,老版本的.META.表),拿到RowKey范围和RegionServer的对应关系,然后把请求直接发给负责那个Region的RegionServer。这里有个细节——客户端会缓存元数据,不会每次请求都去查meta,所以写之前还是需要先尝试直接向目标RegionServer发请求,如果Region发生了拆分或迁移导致缓存失效,服务端会返回一个重定向错误,客户端再重新拉取meta并重试。
1.2 Region内部的结构:HLog加多个Store
打开一个RegionServer,里面挂着多个Region。每个Region除了自身的数据之外,还附着一个写前日志HLog(也就是WAL,Write-Ahead Log)。HLog承担所有写入的崩溃恢复职责,后面讲写入链路和故障恢复时再细说。
Region往下拆分则是按照列族(ColumnFamily)来的。一个Region内有几个列族,就有几个Store。每个Store又有两个组成部分:一个MemStore内存缓冲区和一组不可变的HFile文件。到这里,HBase的物理层级就很清晰了:
- Table → 多个Region
- Region → HLog + 多个Store
- Store → MemStore + 多个HFile
这样的设计解决了两个核心问题。第一,按RowKey范围切分后,Region本身可以独立分配和迁移,一台机器挂掉后,它上面的Region可以被其他机器接管,这是“分布式”的基础。第二,把内存写缓冲和磁盘文件分开,写入可以先落内存再批量刷盘,读的时候先查内存再查磁盘,协议设计上就有了缓存和批量聚合的可能。
1.3 为什么HFile必须不可变
HFile是最终落盘的存储格式,但在HBase的存储协议里,HFile有一个强制约束:写入后不可修改。这个约束非常重要,可以说HBase一切读写策略都围绕着它展开。
为什么不可变?因为可变文件需要原地更新、需要锁、需要处理并发,分布式环境下还要协调多个节点同时改一个文件,成本极高。HFile设计成不可变之后,写入就变成了顺序追加:新数据写进新的HFile,旧文件永远不碰。读取时则通过多个文件的版本合并,将最新版本返回给客户端。代价是数据删除和修改并不真正“删”,而是写入一条带最新时间戳的新记录。时间一长,小文件越来越多,就需要“压缩合并”——这就是HBase里Compaction机制的由来。
HFile具体的物理格式、文件内部如何组织索引和布隆过滤器,我在第三节讲读取路径时再展开,这里先把存储层级这个骨架立起来。
2. 写数据时的那条协议链:WAL刷盘、MemStore阈值与HFile落盘
写入路径是理解HBase事务性和性能模型的关键。很多人问我,HBase写那么快,是不是牺牲了持久性?其实不是,HBase是先写日志再写内存,日志丢了才算真正丢数据。整条链路是:客户端 → RegionServer的Region → HLog → MemStore → HFile。
2.1 先落WAL还是先落MemStore,顺序是硬规则
一次Put请求到达RegionServer后,协议要求必须先把写入操作以日志形式追加到HLog里,之后再把数据写入MemStore,最后才给客户端返回成功。这个顺序不能反。原因很好理解:如果先写MemStore再写日志,一旦写到一半机器断电,MemStore里未落盘的数据就会丢失,而日志里还没有记录,数据就永远找不回来了。
HLog里每个日志项都带一个全局递增的SequenceId,这个ID是RegionServer级别维护的,用来标记日志写入的顺序。为什么需要递增ID?因为在恢复阶段,同一行数据可能出现在多个HFile和多个MemStore中,HBase依靠SequenceId来确定哪个版本更新,回放日志时也能按顺序把数据准确地恢复到崩溃前的状态。可以说SequenceId是整个写入协议的时间轴。
2.2 WAL刷盘的两种模式:同步落盘和批量延后
HLog写到HDFS上,这里有一个关键参数:写日志时要不要立刻调hsync把数据真正刷到磁盘持久化。HBase提供了WAL的同步/异步两种语义选择。
默认情况下,为了保证一条记录返回成功后就真的不丢,WAL会做一次同步刷盘。但每笔请求都刷盘,在高并发下会引入明显的IO延迟,这也是为什么HBase官方文档后来引入了hbase.durable.sync这类批量同步策略——把多个写入攒在一起,在达到一定条件时统一执行一次sync。这样做的代价是故障窗口扩大了几毫秒到几十毫秒,但吞吐量提升非常明显。
分批sync的典型配置长这样:
<property> <name>hbase.durable.sync</name> <value>true</value> </property> <property> <name>hbase.wal.sync.interval</name> <value>5</value> </property>hbase.durable.sync开启后,WAL会周期性执行sync,而不是每个Put都同步。对延迟不敏感但对吞吐敏感的业务(比如日志采集、轨迹上报),这个配置值得测一测。对金融、订单这类强一致场景,建议保持每次同步,别拿这笔数据开玩笑。
2.3 MemStore什么时候刷成HFile
MemStore是Region内每个Store(每个列族)独立的内存缓冲区。写入数据先在这里攒着,攒到一定规模再整体刷成一个HFile。触发刷盘的条件有几种:最经典的是MemStore大小达到hbase.hregion.memstore.flush.size,默认128MB;另外还有RegionServer总MemStore占堆内存比例过高时触发强制刷写、WAL文件数量或大小超过阈值时触发、手动flush等等。
这里要重点提醒一个生产环境常见的坑:同一个Store的多个文件、以及多个Store的MemStore,刷盘是各自独立的。如果一个列族写入特别大,它的MemStore先到阈值刷盘,另一个列族的MemStore还没到,HFile的生成就是不平整的。更麻烦的是,如果某个列族的MemStore持续占满,导致整个Region被阻塞,你在监控里看到的表象就是写入延迟陡增,或者报RegionTooBusyException。压测时如果发现写入有周期性抖动,第一件事就是看MemStore的刷盘节奏和Compaction的触发频率是不是撞在一起了。
2.4 HFile落盘时到底写了一些什么
MemStore刷成HFile这个动作,相当于把内存中有序的键值对集合序列化到磁盘。HFile的物理结构有几个关键区块:数据块(Data Block,默认64KB一个)、叶索引块、中间索引块、根索引块、布隆过滤器块、FileInfo元信息块、Trailer尾部块。
你记住一个核心结论就够了:HFile是一个“自带索引”的自描述文件。文件尾部的Trailer记录各个区块的偏移量,根索引可以定位到中间索引,中间索引再定位到叶索引,最终通过叶索引找到目标数据块的位置。读HBase不扫全文件,而是靠这套索引逐层定位,这就是HBase读路径能命中“点查”的原因。数据块默认压缩是可选的,通常生产环境会开SNAPPY或ZSTD压缩,压缩在块级别进行,索引和布隆过滤器通常不压缩。
只要理解了HFile不可变、自索引、数据组织成64KB块这三点,后面读路径和Compaction的很多行为都可以推出来。
3. 读数据时协议怎么逆行:BlockCache缓存、布隆过滤与索引定位
写入是顺序追加,读取则是另一种逻辑。HBase的读取协议实际上是一个“多级数据源合并”的过程:先查内存中的MemStore,再查缓存中的BlockCache,最后才去HDFS上定位HFile。这个过程和写入路径完全反过来,但同样有严格顺序。
3.1 一条Get请求在RegionServer内部的轨迹
客户端发来一条Get请求,目标RegionServer收到后,先定位到目标Region,然后访问该Region内对应列族的MemStore。MemStore里是最新的未刷盘数据,符合条件就直接返回。如果MemStore没有完整命中(比如只需读取某一行的一部分字段),接着就去BlockCache里找对应的数据块。
BlockCache是RegionServer级别的读缓存,默认实现是BucketCache或LruBlockCache,缓存的是从HDFS读出来的原始数据块。如果能命中,就不需要访问HDFS。如果BlockCache也miss了,才去HDFS上加载HFile。这里有个很多人忽略的点:一次Get可能涉及多个HFile,因为一个Store下可能同时存在好几个HFile文件,每个文件都得查一遍,再合并返回最新版本。HFile数量越多,读放大就越明显,这也是为什么Compaction对读取性能影响如此之大。
3.2 布隆过滤器如何帮助协议“跳过”无关文件
在每个HFile刷盘时,HBase顺手生成一个Bloom Filter(布隆过滤器),用于快速判断“某个RowKey是否可能在这个文件中”。
布隆过滤器的本质是一个位数组加多个哈希函数,它有两种返回值:不存在(一定不在)和可能存在(可能误判)。由于“一定不在”的结论是绝对正确的,读取时协议就能用过滤器淘汰一批明显不包含目标数据的HFile,省去打开文件、加载索引、查找数据块的一系列IO开销。
听起来很好,但有代价:布隆过滤器占内存,而且不同表的特性不同。如果业务是短RowKey点查,建议开启ROW级别的布隆过滤器;如果是典型的“按RowKey前缀扫描”型业务,ROWCOL级别(行+列)效果更好,但内存开销也更高。如果是纯Scan全表数据的场景,布隆过滤器几乎帮不上忙,开了反而浪费内存。生产环境里,对点查密集的大表开启布隆过滤器,命中率通常能去掉一半以上的无效文件访问,收益非常明显。
3.3 Scan的全表协议和缓存块策略
Scan和Get不同,Get是单行点查,Scan是区间扫描。Scan协议是按RowKey顺序逐块读取:先拿起始RowKey,通过索引定位到第一个数据块,然后按块顺序往下读,直到越过结束RowKey。
Scan协议里有几个参数对性能影响极大,其中最关键的是BlockCache的缓存优先级。扫描读取出来的数据块会被标记为SCAN类型,属于低优先级缓存;点查Get的数据块标记为SINGLE,优先级更高。这样设计的目的很明确:Scan产生的数据块通常是一次性使用,不应该把缓存里长期有用的热数据挤掉。实际跑数据分析类任务时,如果Scan和线上点查共用同一个RegionServer,监控BlockCache的命中率很有必要,命中率掉到90%以下,大概率就是Scan把缓存污染了。
另外提一下setCaching和setBatch这两个Scan参数。setCaching控制每轮RPC向RegionServer请求的行数,默认配置偏保守,设置为几百到上千通常能显著降低RPC次数。但如果某些行特别大、一行有好几MB,caching设太高容易导致RegionServer端OOM,合理的做法是配合setBatch一起控制单次返回的数据量。简单说:大行用小batch,小行用大caching。
4. Region的拆分与搬迁协议:预分区、自动Split和负载均衡
Region是HBase数据分布和负载均衡的基本单位。一个Region的RowKey范围如果不断增大到上限,或者某个Region写入热度集中,存储协议就需要对Region做拆分,并在多台RegionServer之间做搬迁。这里面的协议逻辑,直接决定了集群在数据增长过程中的表现。
4.1 自动Split的触发时机和策略选择
Region不会无限变大。当Region内的HFile总大小或者数据量达到某个阈值时,Region会发生自动拆分,把一个Region按RowKey范围切成两个子Region。
HBase内置了几种Split策略,默认的是IncreasingToUpperBoundRegionSplitPolicy。这个策略的逻辑比较复杂,简单说就是:RegionServer上的Region数量越少,拆分的阈值越小;随着Region数量增多,拆分阈值逐渐变大,最终上限是hbase.hregion.max.filesize(默认10GB)。这个设计的初衷是避免在一张表的Region数量还很少时,就被拆出大量过小的Region,导致元数据膨胀和调度开销激增。
但生产环境里,纯靠自动Split有一个很现实的问题:热点。如果RowKey是纯粹递增的时序字段(比如时间戳),数据永远往最大RowKey的Region写入,其他Region全部空闲。自动Split把一个热点Region拆成两个后,新Region依然首尾相接,写入又集中到最后一个Region上,热点根本解决不了,只是把瓶颈从一个Region变成了两个。这就是所谓的热点写问题。
4.2 预分区才是应对热点的正解
针对热点写,经验做法是预先创建足够的Region,让数据从一开始就分散到多个RegionServer上。预分区的本质是提前把RowKey空间按业务特征切好:比如按用户ID的哈希值分成100个区间,建表时直接创建100个Region,每个Region负责一个哈希区间。
预分区时,RowKey的设计方式直接决定分区的效果。最常见的两种设计,一种是纯哈希加盐:RowKey = Hash(用户ID) + 用户ID,这样不同用户落在不同区间,天然分散。另一种是反向RowKey:把原本递增的RowKey倒过来写。时间戳正写时是0000开头的越来越大,反写后是9999开头的越来越小,数据会均匀分散到所有Region,而不是全部塞进最后一个Region。
预分区也不宜切得过多。Region数量不是越多越好:每个Region都要占用内存(MemStore)、维护元数据,Region过多还会导致小的Compaction频繁发生。经验上,单台RegionServer承载的Region数量通常建议控制在1000以内,一张热表的Region数量一般几百个就足够支撑很高的并发吞吐了。压测阶段就把Region数量和RowKey分布测出来,比上线后靠自动Split救火稳妥得多。
4.3 负载均衡的协议调度:从重到轻的搬迁
Region拆分之后,新的Region默认还留在原来的RegionServer上,这时多台机器之间可能会出现数量不均。HBase的HMaster后台有一个负载均衡器(默认是StochasticLoadBalancer),它会周期性检查各RegionServer上的Region数量、Region总大小、热点读写等指标,生成一个均衡计划,然后通过Region的assign/unassign协议,把Region从负载高的机器搬到负载低的机器。
Region搬迁协议的关键点是“搬数据”和“搬日志”的分离。Region上实际的数据文件在HDFS上,是共享存储,不需要真的拷贝文件,搬迁只需要转移Region的在线管理权。真正的开销在于:源RegionServer要把Region内未刷盘的MemStore刷成HFile,并把HLog中的相关记录做一次清理,然后通知HMaster该Region已经下线;目标RegionServer再加载该Region的HFile列表,重新打开Region。整个过程会有短暂的服务不可用窗口,搬迁前需要注意。
实际的教训是:不要在大流量高峰期让Balancer做大规模搬迁。曾经有团队在晚间业务高峰执行了一次手动balance,几十个Region同时从两台机器迁走,结果写WAL的并发瞬间上升,HDFS的NameNode出现短暂瓶颈,整集群RPC延迟整体抖动了几秒。稳妥的做法是设置一个维护窗口,或者调节hbase.balancer.period,让均衡动作发生在低峰期。“宁可少搬一点,不要搬完后悔”,这个原则在Region搬迁上也适用。
5. 宕机之后协议如何兜底:WAL分割、日志回放与一致性恢复
聊协议就一定绕不开故障处理。分布式存储协议的价值,恰恰体现在节点崩溃时能不能把数据捞回来、能不能继续提供服务。HBase在这方面的设计围绕ZooKeeper的会话机制和WAL分割展开,值得重点讲。
5.1 ZooKeeper会话超时如何触发故障转移
每个RegionServer在启动后都会向ZooKeeper注册一个临时节点,并维持一个会话。RegionServer与本机ZooKeeper之间会周期性地发送心跳。一旦RegionServer宕机、网络分区,或者JVM发生长时间Full GC导致心跳中断,ZooKeeper端就会在会话超时时间(默认几十秒)后把这个临时节点删除。
HMaster监听这些节点,一旦发现某个RegionServer的节点消失,就立刻进入故障转移流程。首先要做的是把该RegionServer名下的所有Region标记为RIT(Region In Transition)状态,然后开始处理WAL。这一步有个容易被忽视的细节:HMaster不会自己直接回放所有日志,而是把日志分割任务分发给存活的RegionServer去并行执行,这样能大幅缩短恢复时间。
5.2 WAL分割到底在干什么
WAL是一个RegionServer级别的日志文件,里面混着该机所有Region的写入记录。如果想要把宕机前未刷盘的数据恢复到各个Region,就必须先把这份混在一起的日志,按照Region归属拆分开,再分发到对应Region的新主人那里去回放。
WAL分割的经典流程是:HMaster把宕机RegionServer的HLog先复制一份,然后分发到其他RegionServer上,每个RegionServer只负责分割出属于自己将要接管的那部分日志,再把这些日志按SequenceId回放到相应的Region上。回放结束后,MemStore里就有了宕机前已写入但未刷盘的数据,这些数据随后会随正常的刷盘流程落成HFile。
这里就看出前面讲的SequenceId有多重要了。日志回放的本质是“把时间倒回去重放一遍”,但重放不能重复生效。SequenceId让回放逻辑能做到幂等:如果某个Region已经持有了大于等于日志中SequenceId的数据,这条日志就会被跳过,避免重复写入。分布式系统里的幂等设计,往往就是靠这样一个单调递增的序列号解决的。
5.3 单行一致性与跨行事务的边界
HBase的存储协议在一致性方面给出的承诺是:单行级别是强一致的。也就是说,同一行数据的更新,无论并发多少,读到的要么是旧值,要么是最新值,绝不会出现中间脏状态。这得益于行级别的行锁机制和MVCC快照隔离:写入时以行为单位加锁保证顺序,读取时通过Reader从当前的MVCC版本里取数据,不受并发写的干扰。
但跨行、跨Region的操作,HBase原生并不提供多行事务。一个经典误解是“HBase和MySQL一样支持事务”,实际上你要对多个RowKey做原子性更新,原生API是不保证的。如果业务确实需要跨行事务,通常只能通过业务层的补偿机制来做,或者引入其他事务增强方案(比如协同处理器侧的二次提交),但那已经属于协议之外的扩展了,复杂度会明显上升。选型前务必要把一个底线想清楚:HBase擅长的是海量数据下的高并发单行读写,不是复杂事务。
另一个容易踩的坑是列族级别的一致性边界。一个Region有两个列族时,两个Store各自flush各自的,各自的HFile各自合并。如果某一个列族的MemStore刷盘失败,而另一个列族已经刷盘成功,中间其实会出现“一个Region内部分列族新旧数据不一致”的窗口。恢复机制最终会通过日志回放把它补上,但这段时间里的读取行为在极端场景下并不好预测。所以设计上尽量把列族数量控制在两三个以内,避免多列族带来的刷盘和恢复复杂性。
5.4 集群间复制协议:基于WAL的异步接力
HBase还提供集群间的复制能力,常见用途是跨数据中心灾备或读写分离。这个复制的数据源,正是WAL。主集群的RegionServer在写完本地WAL后,复制源线程会把日志中属于复制表的写入事件读取出来,通过复制协议推送到备集群的RegionServer上,备集群再在自己的WAL和MemStore里写入这条数据。
复制协议有几个需要注意的语义。第一,它默认是异步的,主备之间存在延迟,备集群读到的是最终一致的副本。第二,复制只支持同Key的覆盖写,不保证冲突合并。第三,复制模式下RingBuffer如果堆积,主集群的写入也会受到影响。做灾备规划时,一定要把复制延迟监控纳入告警体系,平时不检查,真正切流量时才发现备集群落后了一大截,这种事在运维圈子不算少见。
6. 压过生产环境之后,我对存储协议调优的几点体会
前面把协议链路讲得差不多了,最后分享一些我在生产环境里压过的坑和调整思路。这些建议不保证适配所有场景,但都是经过线上验证的,可以作为你排查时的参考方向。
6.1 列族数量能少就少,Flag别乱竖
HBase的列族在逻辑上允许很多个,但物理上每多一个列族,就意味着每个Region内部要多一个Store,多一份MemStore,多一份刷盘和Compaction的调度。我见过有人把几十个业务字段拆成十几个列族,结果刷盘时十几个Store同时触发、Compaction互相抢IO,整个RegionServer被拖得极慢。
列族一般建议控制在两到三个以内。如果字段很多但访问模式不区分冷热,就放在同一个列族里;如果确实有少数字段需要极高读取频率而大部分字段很少读,才可以考虑按访问频率拆成两个列族。额外提醒一句:flush是列族级别独立触发的,多列族在故障恢复时的一致性窗口也会更复杂(上一节讲过)。能用单列族解决的问题,绝不用两个。
6.2 BlockSize的取舍:随机点查优先小块,Scan优先大块
HFile的块大小默认为64KB。这个参数很多人照着默认选就完事了,但它在不同读写场景下的效果差异很大。
- 点查密集型业务,数据块设小一点(比如16KB~32KB)更好。因为读一个RowKey只需要加载一个数据块到BlockCache,块越小,缓存粒度越细,随机读的缓存命中率越高,内存浪费越少。
- 全表Scan型业务,块设大一点(比如128KB)更好。大块顺序读取时能减少索引寻址次数,顺序IO的性能更好。
- 大字段业务(如存储图片元信息、大JSON),块太小会导致一条记录横跨多个块,读一条记录要加载多个块,性能反而不如设大。
块大小在刷盘时生效,对已存在的HFile不会自动生效。需要调整块大小时,要靠修改列族描述后触发一次major compaction来重写文件,这个过程代价不小,所以块大小最好在压测阶段就通过实测定下来,上线后再改代价很高。
6.3 Compaction不是越快越好,关键在错峰
Compaction协议分为minor compaction和major compaction。Minor是自动把几个小HFile合并成一个中等文件,触发频率高、代价小;Major是把一个Store下的所有HFile合并成一个文件,能清理删除标记和历史版本,但代价极大,运行期间会大量消耗磁盘IO和CPU。
生产环境最常见的故障之一就是“Compaction风暴”:多张表多个Region在同一时段同时做Major compaction,磁盘IO被拉满,写入延迟飙到几百毫秒甚至秒级。这个问题的规避手段很朴素,一是靠监控,观察HBase的Compaction队列长度和磁盘IO的周期性峰值;二是主动错峰,把Major compaction的时间窗口配置到业务低峰期(比如凌晨),甚至对非关键表直接关闭自动Major,在维护窗口手动统一执行。
还有一个参数值得关注:hbase.hstore.compaction.min和hbase.hstore.compaction.max,控制一次minor compaction合并的文件数量。默认是3和10,如果小文件生成速度极快,可以适当把min调大一点,让每个minor批次合并更多文件,减少调度次数,代价是单次压缩的时间会边长。总之,Compaction调参的核心不是把速度调到最快,而是让它别打扰线上写入。
6.4 WAL的同步级别取决于你对“丢失”的容忍度
最后再聊一次WAL同步。这个参数是典型的“吞吐和可靠性二选一”的权衡点,没有绝对答案。
如果业务是强一致、不能丢数据的(交易记录、账户流水),请保持每次写入的同步刷盘,不要动hbase.durable.sync。这时候真正的优化方向是调整批量写(multiPut)的大小,把单行Put改成批量提交,减少网络和WAL刷盘的次数,这个优化通常能把写入吞吐提升好几倍。
如果业务是弱一致的(比如用户行为日志、埋点采集),丢失几百毫秒的窗口数据可以接受,那么就开启批量sync,并适度调大WAL文件的大小和保留数量。实测中,在同样的硬件条件下,批量sync通常能让写入吞吐提升30%~80%,换来的代价是极端故障下可能丢失最近一小段时间的数据。每次给团队做选型时我都会问一句:如果某天机器突然断电,你能接受丢多久的数据?这个问题想清楚了,参数自然就定了。
6.5 监控协议链路,比背参数更有用
参数和协议细节很多,但真正帮助我发现线上问题的,是一组基础的监控指标。这里分享一个我认为比较完整的HBase监控清单:
| 关注维度 | 关键指标 | 我的建议阈值 |
|---|---|---|
| 写入延迟 | 写请求P99延迟、WAL同步耗时 | P99高于50ms需要关注 |
| 读延迟 | Get P99延迟、Scan P99延迟 | 点查P99高于30ms需要排查 |
| BlockCache | 命中率、淘汰次数 | 命中率低于90%要查Scan是否污染缓存 |
| MemStore | 占用比例、flush队列长度 | 占用超过堆内存40%要留意刷盘压力 |
| Compaction | 队列长度、Major耗时 | 队列持续堆积且和延迟波形吻合,需要错峰 |
| Region分布 | 单个RegionServer上的Region数量 | 超过均衡指标时考虑手动均衡或拆表 |
| WAL | WAL文件数量、复制延迟 | 文件数持续增长,检查flush和归档是否正常 |
监控的价值在于把“协议运行得怎么样”变成可以量化观察的指标。HBase的存储协议本身一直在后台默默运转,靠肉眼是看不出问题的,一旦某项指标持续偏离正常范围,顺着指标再去看对应的协议环节,问题定位就快得多了。
HBase的分布式存储协议,表面上只讲了“数据怎么读写、怎么恢复”这几件简单的事,真正用起来才会发现,每个环节都串着大量取舍。写这篇文章的初衷,就是把我踩过的坑和梳理过的链路整理出来,希望你在碰到写入抖动、热点分布、节点恢复的问题时,能先在心里画出一条数据流的轨迹,再按协议去定位问题而不是靠猜。最后说句实在话:RPC、WAL、HFile、Compaction,这些东西背得再熟,不如亲手在一个百GB级别的表上做一次读写压测,再观察一次RegionServer的监控面板。数据会告诉你,协议在真实负载下到底是怎么运转的。