先说个真事儿:早些年我接手过一个订单明细的存储改造,前一位同事建表时图省事,create 't_order_detail', 'cf'一行搞定,连个预分区都没打。上线第三天写入就开始长尾抖动,一看 Master 上那个表只有两个 Region,一个扛着全部写流量。后来不得不停机重刷数据、重新建表、再导一遍,前后折腾了六个多小时。从那以后我形成了一个习惯——HBase 的表操作,重点从来不是"命令怎么写",而是"建之前想没想清楚"。
这篇东西聊的就是 HBase 表操作这件事:建表、改表、删表、查表状态,以及围绕这些动作的参数取舍和踩坑经验。如果你刚接手一套 HBase 集群、正在做表结构设计,或者已经被 Region 热点和 alter 卡住折磨过,下面这些内容应该能省你不少时间。命令本身不难,难的是每个参数背后对应着什么样的读写行为,改错一次要付多大代价。
1. 先搞清楚:HBase 的"表"和关系库的表不是一回事
1.1 三层结构:表 → 列簇 → 列限定符
很多人从 MySQL 转过来,第一反应是把 HBase 的表当成普通二维表用,建表时恨不得把字段都列出来。这是最容易出事的地方。HBase 的表结构只有三层:表(Table)、列簇(Column Family)、列限定符(Qualifier),而且只有前两层是建表时物理存在的,第三层是写数据时随手指定的。
这个设计的后果很直接:列限定符不需要预先定义,理论上可以无限多,但列簇是物理存储单元,每个列簇在底层对应一组独立的 StoreFile,也就是一组独立的 HFile。你在一个列簇里放多少列,扫描时就要读多少数据块。所以关系库里那种"字段多就多呗,反正查的时候只 select 需要的列"的思路,在 HBase 里是完全失效的——列簇之间才是隔离边界,列簇内部没有隔离。
我在实际项目里见过有人建了八个列簇,理由是"按业务模块分开,看起来清晰"。结果每次 Scan 一行数据,底层要打开八个 StoreFile 的读取器,随机读延迟直接翻了几倍,而且 MemStore 被切成八份,每份都很难攒到刷盘阈值,反而更容易触发小文件刷写。后来我们把八个列簇合并成两个(一个存主属性、一个存大文本),读性能恢复了正常水平。
1.2 表操作为什么"重":每一次 DDL 都是一次元数据广播
关系库里的ALTER TABLE ADD COLUMN大多数情况下改的是数据字典,秒级完成。HBase 不是这样。表的 schema 信息存在元数据表里,你执行一次 alter,Master 要把新的 schema 推送到所有持有该表 Region 的 RegionServer 上,每个 Region 都要做一次本地的 schema 更新和 StoreFile 重载。
这意味着两件事。第一,表越大、Region 越多,alter 越慢,一个几百 Region 的表改列簇属性,等上几分钟甚至十几分钟是正常的。第二,alter 过程中如果有 Region 正处于 compact 或者 split,就可能出现短暂的 schema 版本不一致,客户端拿着旧 schema 去写会报DoNotRetryIOException之类的错。
所以我在团队里定了一条规矩:DDL 操作不进业务高峰,alter 之前先确认没有正在跑的大批量导入任务。这条规矩听着像废话,但真出事的时候,十有八九是因为有人觉得"就加个 TTL,秒级的事",结果赶上了凌晨的批处理窗口。
1.3 表操作能改什么、不能改什么
先把边界说清楚,省得后面反复查文档。下面这张表是我自己贴在显示器旁边的版本,命令行和 Java API 都适用:
| 操作类型 | 是否支持在线 | 是否需要 disable | 典型耗时 |
|---|---|---|---|
| 新增列簇 | 支持 | 否 | 秒级到分钟级 |
| 删除列簇 | 支持 | 否(但数据不可恢复) | 分钟级,取决于 Region 数 |
| 修改 TTL / VERSIONS / 压缩算法 | 支持 | 否 | 分钟级 |
| 修改 BloomFilter 类型 | 支持 | 否 | 分钟级,下次 compact 生效 |
| 重命名列簇 | 不支持 | 是 | 与表规模正相关 |
| 重命名表 | 不支持 | 是 | 秒级 |
| 修改预分区(已经分好的) | 不支持 | — | 只能 split/merge 调整 |
| 修改表名/命名空间 | 不支持 | 是 | 秒级 |
disable这个动作的字面意思是"把表下线",但实际影响是所有读写请求立即失败,客户端会拿到TableNotEnabledException。所以凡是需要 disable 的操作,本质上都是一次小型停机,必须走变更流程。
2. 建表:后期 90% 的故障在这一步埋下
2.1 create 命令逐参数拆解
先把最完整的一条建表语句摆出来,然后一个个说。这是我目前项目里订单明细表的标准模板:
create 'ns_order:t_order_detail', {NAME => 'cf', VERSIONS => 1, TTL => 7776000, COMPRESSION => 'SNAPPY', BLOOMFILTER => 'ROW', BLOCKCACHE => true, DATA_BLOCK_ENCODING => 'FAST_DIFF', IN_MEMORY => false}, {SPLITS => ['1','2','3','4','5','6','7','8','9','a','b','c','d','e','f']}逐个说清楚每个参数在管什么:
NAME是列簇名,建表时必填。命名上我建议全小写、短、有语义,比如cf、meta、log。别用大写,别用点号,别用中文,虽然某些版本能过,但后面写数据、做权限、写配置文件时会一路踩坑。
VERSIONS是保留的版本数。默认是 1,很多人不知道这个默认值意味着什么——你覆盖写同一个 rowkey+列,旧值会被压掉。做需要追溯历史的场景(比如配置变更记录),就得显式调到 3 或者 5。但要注意,VERSIONS调大之后,compact 时要保留的数据量成倍增长,磁盘占用和读放大都会上升。我的经验是除了明确的变更流水类表,一律保持 1。
TTL是数据过期时间,单位是秒,不是毫秒也不是天。这个坑每年都有人踩。TTL => 7776000就是 90 天。TTL 的过期是"惰性"的,数据过期不代表立刻消失,要等下一次 major compact 才真正清理,所以磁盘看到的数据量会比理论值大一些,这是正常的。
COMPRESSION是压缩算法,常用的有NONE、GZ、LZO、SNAPPY、ZSTD。选型上,写入吞吐优先选SNAPPY,压缩比优先选GZ或ZSTD。我这里必须提醒一句:压缩算法需要集群层面装好对应的 native 库,你写了SNAPPY但集群没装,建表能成功,写数据时会一路报错。建表前先跟运维确认,或者用hbase hive之类的工具查一下hbase.regionserver.codecs配置。
BLOOMFILTER决定是否给数据块建布隆过滤器,可选NONE、ROW、ROWCOL。这是 HBase 里性价比最高的一个开关。ROW级别的布隆过滤器能让 Get 操作在打开 StoreFile 之前就判断出"这个文件里肯定没有这个 rowkey",直接跳过。对于随机读多的表,开ROW基本是默认操作;如果业务里有大量"读某行某几列"的场景且列数很少,可以考虑ROWCOL,代价是索引占用更大。
BLOCKCACHE控制数据块是否进 BlockCache,默认 true。有一种场景需要关掉:全表扫描的冷数据表。这种表的数据基本不会被重复读,塞进 BlockCache 只会把热数据挤出去,纯属污染。我一般给日志归档类表设成 false。
DATA_BLOCK_ENCODING是块内编码方式,FAST_DIFF是最常用的,对 rowkey 前缀重复度高的数据压缩效果明显。如果你的 rowkey 设计是"前缀+时间戳"这种结构,FAST_DIFF能省下可观的存储。
2.2 列簇设计:数量、命名与读写比例
前面说了列簇数量的问题,这里补充一个判断标准:一个表里的列簇,写比例和读模式应该大致相似。
原因在于 HBase 的刷盘和 compact 都是以 Store(即列簇)为单位独立进行的。假设你有一个列簇 A 每秒写一万条,另一个列簇 B 每小时写十条。A 会频繁触发 MemStore 刷盘,生成大量小文件;B 那边的 StoreFile 数量长期不变,但每次 A 触发 compact,B 其实不受影响——这一点还好。真正的问题是读:当你 Scan 一行数据时,如果只需要 A 的列,但因为同一行的 B 列簇存在,某些操作(比如带 filter 的全行扫描)会打开所有列簇的 StoreFile。
我的实践准则是:列簇数量控制在 1 到 3 个之间,超过 3 个必须给出充分理由。常见的合理拆分是"小字段列簇 + 大文本列簇",因为大文本(比如 JSON 报文、图片二进制)会显著拉高单行体积,把它单独拆出去能让小字段的扫描保持轻快。
命名方面,我用过的规范是:主数据用cf,附加属性用attr,大对象用blob,日志类用log。短名字的好处是每个 KeyValue 都能省几个字节的列簇名字开销——别小看这个,十亿行的表,列簇名从column_family改成cf,省下来的存储是以 GB 计的。
2.3 预分区:从数据量和 Region 目标大小倒推
不预分区是整个表操作里最贵的错误,没有之一。默认建表只创建一个 Region,所有写入先打到一个 RegionServer 上,等它涨到阈值(默认 10GB,由hbase.hregion.max.filesize控制)才 split 成两个。这个过程中,集群的其他机器都在看戏。
预分区的数量怎么算?我给一个可复现的推导过程:
第一步,估算半年后的数据总量。假设订单明细表每天新增 2000 万行,单行平均 800 字节,加上副本系数 3 和一些索引开销,粗略按 1KB/行 算:
每日增量 = 2000 万 × 1KB ≈ 20GB 半年总量 = 20GB × 180 ≈ 3.6TB第二步,确定单 Region 的目标大小。业界共识是 8GB 到 12GB 之间,我一般取 10GB。太小会导致 Region 数量爆炸,Master 管理开销上升;太大则 split 时抖动明显,故障恢复也慢。
第三步,算分区数并留余量:
分区数 = 3.6TB / 10GB ≈ 360 留 30% 增长余量 → 约 470,取整到 512512 这个数字看着吓人,其实很合理:假设集群 20 台 RegionServer,平均每台 25 个 Region,完全在健康范围内(单机 20~200 个 Region 是常见区间)。
如果你不想手算,用NUMREGIONS让 HBase 帮你切也行:
create 'ns_order:t_order_detail', {NAME => 'cf'}, {NUMREGIONS => 16, SPLITALGO => 'HexStringSplit'}但HexStringSplit只适合 rowkey 是十六进制字符串的场景。如果你的 rowkey 是"业务前缀 + 时间戳"这种有明确语义的结构,手写 SPLITS 数组比自动切分靠谱得多,因为你能保证每个分区的数据量真的均匀。
举个具体例子。假设 rowkey 结构是商户ID后两位 + 时间戳倒序,商户 ID 是 00 到 99 的两位数字,那切分点就很好定:
create 'ns_order:t_order_detail', {NAME => 'cf'}, {SPLITS => ['05','10','15','20','25','30','35','40','45','50', '55','60','65','70','75','80','85','90','95']}这样切出来 20 个 Region,理论上每个区间覆盖 5 个商户号段。当然实际上商户的订单量不是均匀分布的,头部商户可能占了 40% 的量。所以我还会做一件事:统计历史数据的实际分布,按分位数而不是按区间切。这一步多花半小时,能省掉后面几个月的热点告警。
2.4 建表时顺手要定的四件事
除了列簇和分区,还有四样东西我建议在建表阶段就定死,因为后面改虽然有路径,但都要付代价:
第一,命名空间。别把所有表都扔在default里。按业务线划分命名空间(ns_order、ns_user、ns_log),好处是权限可以按命名空间批量授予,配额(Quota)也能按命名空间限流。建表前先执行create_namespace 'ns_order'。
第二,表级配置。如果你的表有特殊需求,比如"这个表禁止自动 major compact",可以在建表时通过CONFIGURATION带上:
create 'ns_order:t_order_detail', {NAME => 'cf', TTL => 7776000}, {CONFIGURATION => {'hbase.hregion.majorcompaction' => '0'}}设成 0 表示关闭自动 major compact,改由定时任务在业务低峰手动触发。这个做法在写入量大的表上很常见,因为自动 major compact 的触发时机不可控,赶上高峰期就是一次性能事故。
第三,配额。set_quota能限制单表的写入吞吐或者存储上限,防止某张表把整个集群的资源吃干。具体命令是set_quota TYPE => THROTTLE, TABLE => 'ns_order:t_order_detail', LIMIT => '100M/sec'。这个在建表时就配好,比事后救火优雅得多。
第四,监控项。表建完立刻把 Region 数、StoreFile 数、读写 P99 延迟这几项加到监控面板上。很多人是等到业务方反馈"查询变慢了"才去看,那时候已经积累了一堆问题。
3. alter:能在线改的和必须停服的
3.1 安全区:哪些属性可以热更新
alter 最容易让人产生错觉的地方是——命令敲下去秒回,但实际变更可能要几分钟才生效。这是 HBase 的异步 schema 更新机制:Master 先更新元数据,然后逐个通知 RegionServer 更新本地 schema,全部完成后才算真正生效。
可以安全在线改的属性包括:TTL、VERSIONS、COMPRESSION、BLOOMFILTER、BLOCKCACHE、IN_MEMORY、DATA_BLOCK_ENCODING、MAX_FILESIZE。改法很直接:
alter 'ns_order:t_order_detail', {NAME => 'cf', TTL => 15552000, VERSIONS => 3}一条 alter 可以同时改多个属性,这样只需要走一次 schema 广播,比拆成三条快得多。这个细节很多人不知道,习惯性地一条一条敲。
需要注意的是改动的生效时机不同。TTL和VERSIONS是逻辑层面的,改完立刻对新写入生效,老数据的清理要等 compact。COMPRESSION改完之后,只有新生成的 HFile 会用新算法,已有的文件要等下一次 compact 才会重写成新格式。所以改压缩算法之后不要立刻期待磁盘占用下降,那得等一轮 compact 走完。BLOOMFILTER同理。
3.2 雷区:改列簇名、改 split 策略、删列簇
删除列簇是我认为最危险的一个操作,因为它不需要 disable,敲下去就执行,而且数据真的会被删掉,没有回收站:
alter 'ns_order:t_order_detail', {NAME => 'old_cf', METHOD => 'delete'}执行之后,Master 会给所有相关 Region 发指令,删掉对应的 Store。这个过程是后台异步做的,命令返回时可能还没删完,但一旦开始就没有回退路径。我的做法是:删列簇之前先做快照,哪怕最后证明是误操作也能捞回来。
重命名列簇在 HBase 里没有直接命令。标准做法是新建一个列簇、把数据导过去、再删旧的。如果数据量不大可以用CopyTable或者自己写个 MapReduce 扫一遍。数据量大的话,这个操作的时间成本要按小时计,必须提前规划窗口。
重命名表和移动表到别的命名空间都需要先 disable:
disable 'ns_order:t_order_detail' snapshot 'ns_order:t_order_detail', 'snap_order_20240101' clone_snapshot 'snap_order_20240101', 'ns_order:t_order_detail_v2'用快照克隆的方式"改名",比snapshot + restore更安全,因为原表还在,出问题能立刻切回去。
修改 split 策略这件事,我需要单独说一句:已经建好的 Region 边界是改不了的。你能改的只是"未来新 split 时用什么策略"。如果一开始预分区打错了,唯一的补救路径是手动split或者merge_region去调整边界。所以预分区这件事,真的要在建表前想清楚。
3.3 大表 alter 卡住的应对手法
几百个 Region 的表执行 alter,客户端可能会卡住很久。这时候别急着 Ctrl+C,因为命令中断不代表操作停止,Master 那边的任务还在跑。正确做法是另开一个 shell 窗口,用下面几个命令观察进度:
# 查看表的 schema 是否已经更新 describe 'ns_order:t_order_detail' # 查看是否有 Region 处于 transition 状态 hbase hbck -details ns_order:t_order_detail # 在 Master UI 上看 "Procedures & Locks" 页面HBase 2.x 之后,alter 是通过 Procedure 框架执行的,你能在 Master UI 上看到每个 Region 的更新进度。如果发现某个 Region 一直卡在 pending 状态,多半是那个 RegionServer 有问题(比如 GC 停顿、磁盘满、进程假死),这时候要去 RegionServer 日志里找线索。
我遇到过一次 alter 卡了四十分钟的情况,最后定位到是一台 RegionServer 的hbase:meta缓存不一致,重启那台机器之后任务瞬间完成。所以遇到卡住,先看是不是某台机器的问题,而不是怀疑 alter 命令本身。
还有一个实用技巧:如果只是想给表加一个属性,而这个属性对存量数据没有影响(比如加一个CONFIGURATION项),其实可以绕开 alter,直接在元数据层面改——但我不推荐这么做,风险远大于收益,老老实实走 alter 就好。
4. 删除、清空与快照:不可逆操作的三道保险
4.1 disable → drop 的完整链路
HBase 删表必须两步走:先disable,再drop。这个设计不是故意麻烦你,而是防止手滑。drop 一个还 enabled 的表会直接报错:
ERROR: Table ns_order:t_order_detail is enabled. Disable it first.完整的删除流程我一般这么走:
# 第一步:先打快照(保险) snapshot 'ns_order:t_order_detail', 'snap_order_before_drop_20240115' # 第二步:确认快照存在 list_snapshots 'ns_order:.*' # 第三步:下线 disable 'ns_order:t_order_detail' # 第四步:确认状态 is_disabled 'ns_order:t_order_detail' # 第五步:删除 drop 'ns_order:t_order_detail'中间那个is_disabled检查看着多余,但它能防住一种情况:disable 命令因为网络抖动超时返回,实际没执行成功。如果你直接接 drop,会拿到一个报错,然后你以为删掉了——其实表还在,还占着空间。
另外提醒一点:drop之后表的 HFile 不会立刻从 HDFS 上消失,而是进到.Trash目录,默认保留一段时间(由fs.trash.interval控制)。这意味着短期内磁盘空间不会释放。如果急着腾空间,需要手动清理 trash,但这一步一定要在确认业务真的不需要数据之后再做。
4.2 truncate 与 truncate_preserve 的差别
truncate是清空表数据但保留表结构,常用于测试环境重置或者临时表复用。它的内部实现其实是"disable + drop + create",也就是说预分区会被重置成一个 Region。
这是个巨大的坑。生产环境如果有一张预分区 512 的表,你truncate一下,它变回单 Region,所有写入重新打到一台机器上。我见过有人在压测环境反复 truncate 之后,压测数据完全不可信,因为分区结构和生产不一致。
正确做法是用truncate_preserve:
truncate_preserve 'ns_order:t_order_detail'它会保留原有的 Region 边界,只清数据。这个命令在我这里属于"必须记住"的级别,凡是生产环境的表清空操作,一律用它。
补充一个细节:truncate_preserve在某些老版本上执行时间较长,因为要逐个 Region 处理,Region 多的时候要等几分钟。执行期间表是不可用的,客户端会报错,所以还是要走变更窗口。
4.3 snapshot:删表前必须做的一件事
快照的底层原理是记录 HFile 的引用,不复制实际数据,所以创建一个 TB 级表的快照通常只需要几秒到几十秒,磁盘开销也很小。这让它成为最实用的"保险"手段。
常用命令就四个:
# 创建快照 snapshot 'ns_order:t_order_detail', 'snap_order_20240115' # 查看快照 list_snapshots 'ns_order:.*' # 从快照恢复(会覆盖原表,需要先 disable) restore_snapshot 'snap_order_20240115' # 从快照克隆出新表(不影响原表) clone_snapshot 'snap_order_20240115', 'ns_order:t_order_detail_bak'这里要区分清楚restore_snapshot和clone_snapshot的使用场景。restore 是原地恢复,会把当前表的数据全部替换成快照时刻的状态,中间产生的数据全部丢失,操作前必须 disable。clone 是生成一张新表,原表不动,适合做数据比对或者事故复盘。
还有一个高频操作:误删数据后按快照导出。ExportSnapshot是个 MapReduce 任务,能把快照导出到另一个集群或者另一个路径。如果只是想让业务方查几条历史数据,也可以用clone_snapshot生成临时表,查完再删掉,比 restore 安全得多。
快照的清理要记着做:
delete_snapshot 'snap_order_20240115'虽然快照本身占空间不多,但数量多了之后,Master 的启动时间会明显变长,而且会阻塞 HFile 的归档清理(因为被快照引用的 HFile 不能删)。我一般设置一个策略,快照只保留最近 7 天。
5. 表状态巡检:describe 之外你还要看什么
5.1 命令速查与输出解读
日常巡检用的命令不多,但每一个的输出都要会读。先看最基础的:
hbase> describe 'ns_order:t_order_detail'输出里重点看三块:TABLE_ATTRIBUTES(表级配置)、每个列簇的属性、以及REGION_COUNT。如果列簇的COMPRESSION显示none而你以为是SNAPPY,那就是建表时漏了。
看 Region 分布用:
hbase> list_regions 'ns_order:t_order_detail'这个命令在 2.x 才有,输出每个 Region 的起始 key、结束 key 和所在 RegionServer。我习惯把它导出来排序看,重点看Region 大小是否均匀。如果发现某个 Region 是别人的 5 倍大,那就是热点,需要 split 或者调整 rowkey 设计。
看表的数据量和请求量:
hbase> status 'detailed'这个输出很长,但我只看几个数字:各 RegionServer 的 Region 数、请求数(read/write request count)、以及memstoreSize。如果某台机器的 memstoreSize 明显高于其他机器,那就是 GC 或者写入不均的信号。
还有一个容易被忽略的命令:
hbase hbck -details ns_order:t_order_detailhbck常被当成全局修复工具,其实限定到单表用更有价值。它会检查表和元数据表是否一致、Region 是否有重叠或者空洞、引用文件是否有残留。正常情况下输出全是OK,一旦出现INCONSISTENT就要警惕。
5.2 Region 倾斜与热点的定位方法
热点问题是 HBase 表操作里最需要靠经验判断的部分。定位路径我一般这么走:
第一步,看 Region 大小分布。list_regions导出后手动排序,如果最大和最小差了三倍以上,说明 rowkey 分布有问题。
第二步,看写入请求分布。在 Master UI 的 Table Details 页面能看到每个 Region 的请求量,或者在 RegionServer 上查hbase.regionserver.writeRequestCount指标。如果某个 Region 的写入量占了全表的 60%,那就是典型的单点热点。
第三步,判断热点类型。常见的三种:
| 热点类型 | 特征 | 处理方式 |
|---|---|---|
| 单调递增 rowkey 热点 | 所有写都落在最后一个 Region | 改 rowkey 设计,加哈希前缀 |
| 头部商户热点 | 某个区间内的读写量远超其他 | 对该区间做二级拆分,或加盐 |
| 时序数据热点 | 新数据集中在最近时间片 | rowkey 用时间戳反转或分桶+时间戳 |
第四步,动手调整。如果是单调递增导致的热点,split只能缓解一时,根治要改 rowkey。如果只是某个区间过大,可以直接手动拆:
# 按指定 rowkey 拆分 split 'ns_order:t_order_detail', '5a' # 合并两个相邻的过小 Region merge_region 'encoded_name_1', 'encoded_name_2'split之后要注意,后台会做一次 compact,这段时间 IO 会上升,别在高峰期操作。
6. 踩坑实录:这些报错我几乎每年都遇到一次
6.1 建表/改表阶段的典型报错
报错一:TableAlreadyExistsException
看着简单,但经常有人被坑。原因是 HBase 的元数据缓存有延迟,刚 drop 的表立刻重新建,可能碰到"表已存在"。正确做法是 drop 之后等几秒,或者用exists命令确认。
报错二:MasterNotRunningException/PleaseHoldException
集群刚重启或者正在做 failover,所有 DDL 都会被拒绝。这时候别重试,重试只会加重堆积。等 Master 完全起来、Region 全部上线之后再操作。
报错三:NoSuchColumnFamilyException
写数据时指定的列簇不存在。常见于两种情况:一是建表时列簇名字打错了;二是 alter 删了列簇但客户端缓存还没更新。后者需要重启客户端或者等缓存过期。
报错四:RegionInTransitionException
表里有 Region 处于中间状态(比如 split 到一半、或者 balance 过程中),这时候 disable 或者 alter 会失败。处理方式是去 Master UI 看具体的 Region 状态,或者用hbck定位。
报错五:org.apache.hadoop.hbase.DoNotRetryIOException: java.lang.NoClassDefFoundError
十有八九是压缩算法不匹配。你建表时写了SNAPPY但某个 RegionServer 没装 native 库,写入就会在这一台机器上失败。解决方案是一致化集群配置,或者临时把压缩改成NONE——但后面那个方案意味着要重写所有存量数据,代价不小。
6.2 上线后的表级问题
问题一:Region 数量失控。表现是 Master 负载高、启动慢、list_regions输出几百行。原因通常是预分区过多,或者数据量增长太快触发自动 split。处理方法是用merge_region合并相邻的小 Region,同时把hbase.hregion.max.filesize调大。
问题二:StoreFile 数量暴涨。一个 Store 下有几十个 HFile,读性能直线下降。这通常是 MemStore 反复刷盘导致的(写入太猛或者 memstore 阈值太低)。临时缓解可以手动触发major_compact,根治方案是调大hbase.hregion.memstore.flush.size或者增加 Region 数分摊写入压力。
问题三:TTL 设了但数据没少。前面说过,TTL 清理依赖 major compact。如果表关闭了自动 major compact,又没人手动触发,过期数据会一直占着空间。我的做法是配一个定时任务,每周对带 TTL 的表跑一次major_compact。
问题四:表被锁。表现是所有 DDL 都没法执行,报TableLockException。原因是上一次 alter 或者 disable 中途失败,锁没释放。解决方式是去 ZooKeeper 里找对应的锁节点删掉,或者在 Master UI 上强行解锁。这个操作要谨慎,最好先确认没有正在跑的任务。
6.3 一份可以直接抄的建表检查清单
最后把上面所有内容收敛成一张表,我每次建表前都过一遍:
| 检查项 | 检查方法 | 不合格的后果 |
|---|---|---|
| 命名空间是否存在 | list_namespace | 建表失败 |
| 列簇数量 ≤ 3 | 人工确认 | 读放大、MemStore 碎片化 |
| 是否预分区 | 检查 SPLITS 参数 | 写入热点,单点瓶颈 |
| 分区数是否按数据量算过 | 按 10GB/Region 倒推 | Region 过多或过少 |
| TTL 单位是否用秒 | 人工确认 | 数据过期时间差几个数量级 |
| 压缩算法集群是否支持 | 查hbase.regionserver.codecs | 写入报错 |
| VERSIONS 是否符合业务需求 | 对照业务是否需要历史版本 | 覆盖写丢数据 |
| 是否配置配额 | list_quotas | 单表可能吃满集群资源 |
| 是否已做首次快照 | list_snapshots | 误操作无退路 |
| 监控项是否已接入 | 看监控面板 | 出问题无法快速定位 |
再补一个我个人的小习惯:建完表立刻写三条测试数据,再读出来,再删掉。这一步能验证 schema、权限、网络连通性、压缩配置是否全部正常。看起来原始,但它能在三十秒内暴露大部分配置问题,比等业务方写入之后报错要划算得多。
表操作这块内容,说到底是"一次性决策 + 长期维护"的组合。建表那五分钟的思考质量,基本决定了这张表未来一两年的运维成本。我踩过的那些坑,回头看几乎没有一个是命令写错导致的,全是在"这个参数要不要设""分区要不要打""列簇要不要拆"这些问题上偷了懒。所以与其背命令,不如把每个参数背后的行为模式搞清楚,后面自然就不会犯低级错误了。