1. 为什么死磕HBase的这些底层机制
我还在带HBase集群那会儿,最怕的不是进程直接挂掉,而是某个RegionServer的GC突然飙到几十次每秒,写入延迟从几十毫秒直接跳到几秒。那时候第一反应是看Region数量是不是又超了、MemStore是不是刷得不够快、或者是Compaction又堵在队列里了。后来才想明白,HBase本质上是把内存、磁盘和元数据管理这几个环节包装成了一套自动机制,平时你根本看不见它,但一旦流量上来,底层这些flush、compact、split、merge的互相纠缠就会全部暴露出来。
这篇文章想聊的,就是HBase里这几个和“数据生命周期”强相关的核心机制:读写数据时的完整链路、MemStore刷写(flush)、HFile合并(compact)、Region自动拆分(split)、建表时的预分区(pre-splitting),以及Region的手动/自动合并(merge)。如果你刚接触HBase,这篇文章能帮你把“数据从客户端进来到最终落盘”这条线走通;如果你已经在维护集群,里面那些参数阈值、触发条件和排障思路,可能正好能解释你某个下午遇到的延迟毛刺。
先说个最气人的场景。某个业务表,每天写入几个亿条数据,Region数量从几个慢慢涨到几百个,然后某天开始一个个RegionServer轮流报警。看监控,读写的P99延迟高得离谱。继续挖,HFile数量已经堆到几百个,因为MemStore刷写跟不上写入速度,compaction又要等队列;同时Region数量突破了单台服务器合理承载的上限,请求分配又偏斜。进一步查,是split触发得太被动,拆完以后又没有及时做balance,major compaction更是没在合理窗口跑完。这个case里没有一个是陌生概念,但它们互相纠缠,就成了事故现场的导火索。所以搞懂这几个机制,不是面试做题,是真的能救人命。
值得一提的是,HBase的底层世界观其实非常“朴实”。一张表会被按RowKey范围切分成多个Region,一个Region里按列族拆成若干个Store,每个Store先写内存里的MemStore,满了就刷成磁盘上的HFile。数据和索引文件都按RowKey字典序排列,HBase天然就是为“海量随机读写+范围扫描”这个组合而生的。这个模型里,Region是分布式的最小单元,也是拆、合、迁移的最小粒度;而MemStore和HFile则决定了数据在单节点上存得稳不稳、读得快不快、磁盘碎不碎。后文所有机制,都围绕着这三个层级转。
2. 读写数据的完整链路拆解
2.1 写路径:WAL、MemStore与HFile的顺序交响
HBase的写入设计,说白了就是四个动作:写日志、写内存、返回成功、异步落盘。
客户端发来一个Put请求时,会先定位到RowKey对应的Region,然后落到对应的RegionServer。服务端不是先改磁盘,而是先把变更同时写入两个地方:一个是HLog(也叫WAL,Write-Ahead Log),另一个是内存里的MemStore。只有等这两个动作都成功,操作才会返回客户端成功。这个设计背后的原因很朴素:内存快,但断电就丢;日志结构是顺序追加写,虽然也是写入磁盘,但比随机写快得多,正好用来做崩溃恢复。如果RegionServer在数据还没刷盘之前挂了,内存里的数据没了,没关系,WAL还在,进程重启后会逐条回放WAL把数据重建回MemStore,再继续后续处理。
MemStore本身是一个按RowKey排序的内存缓冲结构。为什么不能让每个Put都直接写HFile?因为每个小请求各自去刷磁盘,会产生大量小文件,随机写也慢得难以忍受。HBase选择先把一批写入聚在内存里,攒到一定规模再一次性刷成有序的HFile,这就继承了LSM(Log-Structured Merge-Tree)的核心思想:把随机写转化为顺序写加上批量写盘,写吞吐自然上一个台阶。这也是HBase写入性能优秀的重要原因。
生产环境里,和写路径强相关的参数有几个值得记住。一个是客户端侧的hbase.client.write.buffer,默认大约是2MB,如果一次请求里批量Put很多行,这个缓冲区大小会影响批次效率。另一个是服务端的hbase.hregion.memstore.flush.size,默认128MB,控制单个MemStore涨到多大就触发刷写。还有hbase.wal.provider,新版本里一般用async wal来减少写日志的同步等待开销,但异步WAL如果配置不当也存在小概率数据的取舍问题,不是所有场景都无脑换。
2.2 读路径:三层缓存与磁盘文件的协同
读数据比写数据要麻烦,因为一份数据可能散落在三个地方:最新的在MemStore里,刚被读过的热块在BlockCache里,历史数据都在HFile里。一次Get或Scan,RegionServer会先从MemStore开始找,因为这里保存的是还没落盘的最新写入;如果没找到,就会去BlockCache里查,这是放在读路径里的LRU缓存,专门缓存HFile里的数据块;最后还是没命中,才真正去磁盘扫HFile。
这里有一个关键优化:不是所有HFile都会被打开检查一遍。每个HFile都有自己的Bloom Filter(布隆过滤器),可以快速判断某个RowKey在不在这个文件里。对于Get类的点查,Bloom Filter能把匹配范围大幅缩小,避免无谓的读盘开销。这也是为什么生产上点查性能好的表,通常都要确保读取时能命中合理的HFile数量,一旦compact不及时、HFile越堆越多,滤过的文件少了要打开的文件多了,读延迟就会被拖垮。
BlockCache的规模也直接决定热数据的表现。老的HBase版本里默认有个hfile.block.cache.size,控制LRU缓存占堆内存的比例,默认值大约占40%。如果读多写少,可以适当调大BlockCache;写多读少则相反,内存要更多地留给MemStore。所谓读路径调优,本质就是在BlockCache和MemStore两个堆内存消耗大户之间做取舍,而这个取舍还要考虑RegionServer堆大小以及Region数量,不是单独调一个参数就能了事的。
2.3 读写一致性与版本问题的现场体验
HBase的每个Cell(单元格)都会保存多个版本,具体由版本数参数控制。写一条数据时可以指定时间戳版本,查询时默认拿最新版本,也可以指定读取某个历史版本。这个设计在业务上很好用,比如你要保留用户最近10次的登录时间,把列族版本数设为10,每个覆盖写都会天然保留历史,不需要应用层额外建表。
但版本特性也会带来坑。如果你把版本数调得很大,又长期不执行major compaction,那些删除标记和因版本覆盖而废弃的老数据会一直藏在HFile里,磁盘空间只增不减,扫描性能也会下降。真正物理删除这些数据,只有等major compaction触发,因为它会把一个Store下的所有HFile合并成一个文件,并且在合并过程中丢弃已删数据、过期数据和超出版本数的旧版本。这也是为什么很多运维老手对major compaction又爱又恨:它能治愈空间膨胀,但执行起来又非常吃IO,时机不对就是一场灾难。
从一致性角度看,HBase默认保证RegionServer层面的“读己之写”一致性。只要写入返回成功,后续读必然能看到,因为它在内存里且带有排序,新的覆盖会拥有更新的时间戳。但跨Region、跨RowKey的操作是没有事务的,这和传统关系型数据库差异很大。所以设计表格时,尽量把需要强一致的多个列放在同一行、同一列族里,或者通过RowKey的巧妙设计保证它们落进同一个Region,这会省掉大量应用层的补偿逻辑。
3. flush与compact:MemStore落盘与HFile合并的奥秘
3.1 flush由谁触发:四个常见条件
MemStore不会无限膨胀下去,它在多种条件满足时会触发送往磁盘的刷写。这四种情况在生产中最常见。
第一种是单个MemStore大小超限。默认情况下,一个MemStore达到128MB就会开始flush,可以通过hbase.hregion.memstore.flush.size调整。真正运行的时候要注意,HBase不是到了128MB就立刻一次性刷完,而是会有部分的flush过程,所以监控里看到MemStore略高于阈值是正常的。
第二种是整个RegionServer上的全局内存比例超限。这是比单Store更上层的一道保险:所有Region的MemStore总和达到RegionServer堆内存的一定比例时,就会触发强制刷写,默认比例由hbase.regionserver.global.memstore.size控制,通常是堆内存的40%左右,具体取决于版本。当这个全局条件被触发时,服务端会按照MemStore占用从大到小选取Region优先刷写,以最快速度腾出内存。
第三种是WAL文件数量膨胀。因为WAL是顺序追加写的,如果MemStore一直不刷写,WAL就会一直变大、变多。当WAL文件数量超过上限时,HBase也会强制触发flush,把WAL里的变更全部落盘,然后清掉旧日志。这个上限相关的参数不同版本不一样,但思路就是限制日志增长对磁盘空间的消耗。
第四种是定期自动刷写。HBase有个后台线程会每隔一段时间检查所有Region,如果有MemStore超过一段时间没有刷写(默认大概1小时),即使大小不达标也会强制刷掉。这个设计很重要,防止高写入场景下冷Region的内存数据长期不落盘,导致WAL积压后恢复时间过长。
手动执行flush '表名'可以把指定表的MemStore立刻刷写,通常用在维护窗口和下线Region之前。但我想提醒一句,手动flush在业务高峰期要慎重。它会立即产生大量小HFile,小HFile一多,后续minor compaction压力就会变大,反而可能让集群更慢。很多时候所谓的“flush一下就好了”,第二天延迟又变本加厉,就是因为小文件膨胀后compaction积压成了新瓶颈。
3.2 compact的作用与类型:小文件治理的两条路线
时间长了,HFile只增不减。为什么?因为每次flush都生成一个不可变的新HFile,除非主动合并,否则它永远不会和别的HFile产生联系。HFile多了,读操作就必须打开更多文件筛选,Bloom Filter再多也要付出打开文件的开销;同时那些删除标记和废弃版本也散落在各个文件里,清理不掉。
HBase用compaction来治理这个局面,分两种粒度。minor compaction会把多个相邻的小HFile合并成一个较大的HFile,但不会处理已删除的数据和旧版本。它动作快、IO开销相对小,适合日常的小文件整理,触发条件主要看文件数量和大小关系,比如当前Store下的HFile达到一定数量(默认由hbase.hstore.compaction.min控制,一般大于3),会选出一批大小相近的文件进行合并。
major compaction则完全不同。它会把一个Store里所有HFile合并成一个文件,做一次全量清理:删除标记的记录被真正丢弃,超过版本数的旧数据被过滤,散落文件的大小HFile归并压缩。这个过程能最大程度回收空间、提升读性能,但代价也最大:一个大型Store的major compaction可能持续几个小时,期间大量读写盘占用IO和带宽,如果撞上业务高峰,延迟必然飙升。所以生产上通常把hbase.hregion.majorcompaction默认的7天周期关掉或拉长,改成在低峰期手动触发,或者设置HBase的offpeak时间段让它自动避峰执行。
不少同学问我,什么时候该手动触发major compaction?我的经验是这几种情况值得做:表整体删除了大量数据需要真正回收空间时;某张热表的读延迟升高而HFile已经堆了很多个小文件时;以及Region刚做完merge或迁移,文件结构需要重整时。反过来,如果集群本身处于高负载、磁盘IO已经不小、或者HDFS在做均衡,就尽量不要手动触发,等窗口。
3.3 实战调优:避免flush与compaction引发的延迟毛刺
我踩过一次比较典型的坑。某个分析系统的流量集中在晚上8点到12点,白天基本没多少读写。某天晚高峰,业务方反馈写入P99从10ms涨到300ms。当时我先看CPU和GC,发现Full GC不少;再看RegionServer日志,发现有大量Region在触发全局MemStore刷写,而且compaction日志里同时有好几个大Region在跑major compaction。本质原因很简单:白天写入少,MemStore长期不达标,定期flush攒了一堆小文件;晚高峰一开始,数据一涨,compaction自动启动,而MemStore又在高峰期不断触顶,两个高开销动作撞在一起,GC和IO直接被拉满。
后来的调整方案是:把major compaction的自动周期关掉(其实HBase提供了配置可以禁用定时major compaction),通过定时脚本在每天凌晨4点低峰期手动对指定表触发;对某些高写入表适当调大hbase.hregion.memstore.flush.size,减少flush频次,但同时要观察全局内存比例是否扛得住;同时把hbase.regionserver.thread compaction相关线程池配置保持一个合理范围,避免compaction线程太少导致堵塞。这套组合下来,晚高峰立刻恢复了正常。
一个更隐蔽的细节是:Region数量也直接影响compaction的表现。一个RegionServer上Region数量过多时,每个Region的Store都可能在各自做compaction,线程池和磁盘IO会被分散冲击。所以不要只盯着单个表,最好能从集群维度观察compaction队列长度和阻塞情况。如果发现某个RegionServer长期“Queue size”很高,优先处理它的Region分配和文件积累,效果往往比盲目调参数来得更快。
4. Region拆分机制:集群自动扩容的引擎
4.1 三种分裂策略:从老版本到默认策略的演进
我见过不少刚接触HBase的人会问,为什么一张表只有几个Region,数据量涨着涨着它就自动变成几百个Region了?这就是Region拆分机制在起作用。当某个Region的数据量或文件总量达到一定阈值时,RegionServer会把它从中间劈开,变成两个子Region,两个子Region仍然属于同一张表,但各自负责不同的RowKey范围。这样一来,原本属于单个Region的读写压力就被分散到了两个Region上,如果它们恰好能迁移到不同RegionServer,还能获得分布式能力。
拆分阈值不是拍脑袋定的,历史上HBase用过几种策略。最老的是按固定大小触发,不管集群里有多少个Region,Region文件总量超过一个固定阈值就拆。这种方式实现简单但不够智能,小表容易被拆得七零八落,大表又拆得太慢。后来引入了基于Region数量的动态阈值策略,默认策略名称比较长,用公式表达就是当前同表Region数量的三次方、再乘以单个MemStore的flush size和2。简单说,一个表Region越少,阈值越小、越容易拆;Region多了,阈值不断调高,避免集群里Region数量爆炸式增长。比如一个Region时阈值可能就是256MB左右,两个Region时变成2GB,三个时就会接近7GB,之后随着数量增长,阈值慢慢趋近一个上限,这个上限由hbase.hregion.max.filesize控制,老版本默认10GB,部分发行版会调整得更大。
到了新版本,默认策略又迭代了一版:不再从特别小的阈值起步,而是从一个较低的固定值开始增长,再走同样按Region数量的立方增长路径。本质都是为了控制一个表在单个RegionServer上的Region增长速度不要太狂野,又不至于让大表长时间落在单Region上成为热点。
4.2 拆分过程:从中间点决策到RIT短暂状态
Region拆分不是简单地拿一把大砍刀从文件中间切开。实际过程大概是:RegionServer根据当前Store里最大HFile的文件大小,找到文件中央的RowKey作为拆分点,然后把原Region的元数据状态暂时标记为SPLITTING,紧接着在HDFS上不新拷贝数据,而是通过引用文件的方式来创建两个子Region的数据视图(新版本中这个细节已演变为不同的引用清理机制,但整体流程依然短暂)。父Region在被拆分的短暂期间是不可读写的,这个窗口非常短,对业务的影响通常很小。两个子Region随后经历从REGION_BEGIN到OPENING再到OPEN的过程,从此各自接管半个RowKey范围的数据和请求。
从日志里观察这个动作时,可以看到明显的split字样和新的Region编号。我们要养成看日志的习惯。如果某个Region拆完又拆、拆完又拆,短时间内产生了大量子Region,就要警惕是不是阈值设置得太低;反过来,如果某个大Region的文件早就超过阈值却迟迟不拆,就要看看是不是Region数量已经增长到了阈值上限,主动干预。
拆分完成后,HBase内部的LoadBalancer会在一段时间之后察觉到Region分布不均,把新产生的Region迁移到负载较低的RegionServer上。这个过程不是瞬时的,而是周期性执行。所以在数据快速增长阶段,你可能看到一个Region被拆成两个,其中一个还留在原服务器,另一个被搬走,然后两台服务器的Region数量逐渐趋于均衡。这个“拆”和“搬”是分开的两个机制,很多人会把它们混为一谈,实际排查问题时要分开看。
4.3 拆分风暴:不得不防的连锁反应
拆分机制本身是好事,但它会带来一个经典副作用:拆分风暴。想象一张表的写入一直很均匀,所有Region几乎同时达到拆分阈值,于是同一时刻几十个Region集体发起拆分。每个拆分动作都涉及元数据变更和文件引用创建,虽然单个开销不大,但几十个一起挤过来,RegionServer的RIT(Region In Transition)瞬间就拥堵了,请求也会抖动。更麻烦的是,拆出来的一堆新Region大概率还会触发一轮迁移风暴,因为Balancer要把它们分配到其他服务器。
应对拆分风暴的思路有两种。第一种是预分区,直接从建表阶段就把一张表切成足够的Region,避免在流量高峰期被动拆分,这是最推荐的办法,具体的做法后面专门讲。第二种是把阈值上限调大,或者根据流量节奏选择性地禁用自动拆分,在低峰期手动执行split。手动split某个Region的语法很简单,在HBase Shell里执行split '表名,RowKey前缀'或指定Region名来完成。
我个人的建议是,生产表一定要设计好预分区方案和RowKey散列策略,让拆分机制成为一个兜底保险,而不是主要依赖它来扩容。数据量不大时自动拆分没毛病;一旦业务进入快速上升期,指望自动拆分跟上流量,十有八九会被打个措手不及。
5. 预分区:性能优化从建表开始
5.1 默认建表为什么不够用
如果我们在HBase Shell里直接执行create 'user_log', 'cf',这张表初始只有一个Region,所有数据无论RowKey是什么,在表数据量达到自动拆分阈值之前,都会落在这个Region上。先不说单机写瓶颈的问题,单是“热点”这一项就够受了。因为HBase的访问路由是按RowKey范围定位到Region的,单Region就意味着所有客户端请求都会集中到同一台RegionServer上,别的机器只是空转。
等数据量涨到阈值以后,自动拆分虽然能缓解,但拆分发生的时间点往往正是业务压力上升的时候,容易撞上性能毛刺。与其被动的“炸开”,不如在建表时就提前把表“切成”多个Region,让写请求从一开始就被分散到多台机器上。这也是预分区最朴素、最核心的价值:把未来的扩容动作提前到建表这一刻完成,用空间换稳定。
5.2 三种常用的预分区实现方式
第一种是用SPLITS手动指定分割点。在HBase Shell里这样写:
create 'user_log', 'cf', {SPLITS => ['1000','2000','3000','4000','5000']}这个意思是以RowKey的字符串字典序作为分界,创建6个Region,各自的范围分别是小于1000、1000到2000、2000到3000、3000到4000、4000到5000、大于5000。适合RowKey规律已知、每个区间写入量比较平均的场景,比如按时间戳前缀分表,或者按用户ID的范围分片。
第二种是用SPLITS_FILE指定一个分界点文件。适合分界点数量很多、写在Shell里太长的情况。文件每行一个分界RowKey,Shell创建表时会自动读取生成多个Region。这种方式可复用性比较好,同一套分界规则可以在多表之间共用。
第三种是利用内置的Splitter算法,比如HexStringSplit,它会按照十六进制字符顺序生成一系列比较均匀的分界点。这类做法适合RowKey本身是十六进制字符串、而且前缀分布接近随机的场景,比如通过MD5或SHA哈希生成的RowKey。如果RowKey前缀本身就是散列值,HexStringSplit能把RowKey空间切得非常均匀,避免个别区间过热。
生产环境最常用的做法,其实是先根据业务确定RowKey的散列策略,然后用脚本生成SPLITS_FILE,批量创建同构的预分区表。比如一批业务表都用“前4位MD5 + 业务ID”作为RowKey,那么直接用一份十六进制分界点文件建表,所有表都能获得相近的负载分布。
5.3 预分区实战:分区数量怎么定,RowKey怎么配
分区数量不是拍脑袋定的,有两条线索可以参考。一条是单Region的服务能力。假设预计单Region稳定承载的数据量是10GB,表未来一年的总数据量是200GB,那么预分区数量大约20个起步;另一条是RegionServer数量,通常建议单台RegionServer承载的Region数量控制在几十到几百之间(具体取决于堆内存和请求量),所以分区总数也要参考集群规模。
有一个常见的争议点是:预分区到底分区多一点好还是少一点好?我个人的经验是“宜多不宜少,但不要多到不分”。因为后续想要新增Region,你得手动找一个热点Region去split,调整成本更高;而分区稍微多一点,单个Region数据量小,split的触发被推迟,集群的稳定性反而更高。当然也不能分得过于夸张,比如集群只有10台机器,你建表时分了3000个Region,每台机器平均300个,这已经接近单机管理上限的临界点了,反而会增加内存和元数据压力。
RowKey设计要和预分区强绑定,这是我特别想强调的。如果你用SPLITS => ['a', 'b', 'c']建了4个Region,但业务RowKey全是日期前缀,比如“20260101_10001”,那大部分数据会挤在“2026”开头所在的Region,前面的分区形同虚设。所以预分区不只是建表动作,它要求RowKey前缀必须和分界点同分布。常见的散列方式是在业务ID前面拼上MD5或哈希前缀,让前缀均匀离散,再配合十六进制分界点切分,这样基本能保证每个Region接收的写入量差不多。
一个可以用在生产上的示例:假设RowKey设计为MD5(userId)前4位 + userId,前缀空间是0000到ffff共65536个取值。如果计划16个Region,可以用HexStringSplit生成16个分界点来创建表。这样,同一userId的数据大概率落在同一个Region内(因为前缀一样),同时整体分布又足够均匀。这兼顾了“查询时能合并同用户数据”和“写入时均匀打散”两个目标。
6. Region合并操作与实战问题记录
6.1 Region合并的意义:控制元数据规模与资源碎片化
提完了拆分和预分区,好像HBase总是在把Region越切越小,那Region合并又是怎么回事?其实很简单:Region并不是越多越好。当一个表的数据被大量删除,或是某些Region经过长期拆分后已经变成几乎没有数据的“空壳”,它们仍然占据着RegionServer的元数据槽位和内存,影响着整个集群的稳定性。HBase的RegionServer能管理的Region数量是有上限的,超过以后,Master协调和迁移的耗时都会增加,单机GC压力也会明显上升。这时候就需要把相邻的小Region合并成一个更大的Region,主动回收资源。
实践中有两种典型场景会用到merge。第一种是split过度:某张表的RIT或BlockCache被大量小Region撑爆,但每个Region的实际数据量很小,此时合并掉一批相邻Region可以显著降低元数据开销。第二种是删除潮之后:业务清理了大量历史数据,Region里的HFile经过major compaction后已经缩水很多,原先生成的Region布局不再匹配当前数据规模,合并会让Region数量回到合理区间。
6.2 合并操作的正确打开方式
在HBase Shell中,合并操作的基本格式是:
merge_region 'Region_编码ID_1', 'Region_编码ID_2', true这里的Region编码ID可以在Master页面或者通过list_regions '表名'查到。注意一个关键前提:只有RowKey范围相邻的两个Region才能合并。HBase的Region是按RowKey顺序线性排列的,两个Region如果中间还隔着别的Region,它们合并出来的结果就没法表达为一个连续的RowKey范围。所以先通过表的Region分布确认相邻关系,再执行合并。
此外,合并动作本身不是简单地把两堆数据文件拷到一起,它更像是在元数据层面把两个Region的区间并成一个,物理数据文件原封不动。这让合并动作的代价相对小,但对后续访问路径会有影响:被合并进来的数据还在原HFile里,只是归属Region变了。如果这两个Region的数据量差异很大,合并后的大Region未来可能再次触发拆分,所以不要指望合并能解决数据热点,它主要解决的是Region数量过多和零碎化问题。
执行合并的时候最好选业务低峰期,并且执行前留意一下两个Region当前有没有正在执行split、migration或compaction。如果Region处于不稳定状态,强执行merge可能会导致卡在RIT里面。我曾经在一个Region还在做自动split时手动触发merge,结果它就在PENDING_CLOSE和CLOSING状态之间反复横跳,最后只能重启RegionServer才恢复过来。所以操作前用脚本确认Region状态是必要的。
6.3 易踩的坑:合并失败、元数据残留与RIT卡住
合并相关的坑,我遇到过三次印象都比较深。
第一次是合并一个刚发生过split的Region对。因为拆分后子Region的内部文件引用关系还没完全清理,直接merge导致HDFS上文件引用数和Region元数据不一致,Region打开时报错。后来学乖了,合并前先看看这个表的HFile和Region状态,等过渡期完成再操作。
第二次是跨RegionServer合并,当时直接指定了两个不在同一台机器上的Region。HBase会先把两个Region迁移到同一台RegionServer上再执行合并,这个过程可能比较耗时,当时我没注意看迁移日志,误以为命令卡死了。其实只要等待足够时间,或者在操作之前先把其中一个Region move到另一个所在节点,合并会更顺利。
第三次是误把大表的一个大Region和旁边的小Region合并,结果新Region膨胀又触发了自动拆分,等于白做。做region合并前,最好先通过工具估算一下两个Region的实际存储大小和请求量,确认合并产物仍然保持在合理范围内。HBase Shell里的major_compact不解决Region数量问题,而balancer只负责在服务器间搬动Region,也不解决小Region合并问题。如果你真想对Region数量做精细管理,merge才是对症的动作。
还有一个许多人忽略的层面:合并也是配合拆分做“破而后立”的治理工具。比如某张表因为之前预分区设得过大,导致大量空Region长期占着元数据,这时候主动把这些空Region合并回少量Region,再配合调整刷新和拆分配置,能让表重新进入健康状态。HBase本身是自动化的分布式系统,但它的自动化不等于免维护。定期观察Region数量、HFile数量、MemStore和compaction队列的变化,在合适时机出手干预,这才是MongoDB... 不,这才是HBase集群运维里最有价值的实战能力。
如果让我总结一个实操上的诀窍,那就是:不要在线上环境“裸操作”HBase的任何状态变更命令。无论是split、merge、major_compact还是balancer,每次执行前都要明确三件事:目标状态是什么、影响范围是哪些Region/表、回滚方案是什么。比如merge前保留两个Region的完整编码信息和RowKey范围,如果合并后发现异常,至少知道原本的边界在哪。虽然HBase设计的目标是让你尽量不需要人工干预,但真正到了需要干预的时候,你的判断决定了这台集群是平稳过渡还是变成事故现场。