3.2 Compaction:小文件合并的取舍 本节摘要:Compaction 是 LSM-Tree 的"分期付款"机制——写入时只追加小文件,后台再把小文件合并成大文件,顺带清除超版本与墓碑。本节讲 Minor 与 Major 两档合并的选择算法、写放大/读放大/空间放大的三角权衡,以及生产上"关自动 Major、低峰手动跑"的通行惯例。 为什么必须有 Compaction 第 2 章结束时 Store 里是什么局面:一个列族下 MemStore 加一串 HFile,最新数据在内存,历史数据散落多个文件,同一行键的新旧版本与墓碑分布在不同文件里。不管它,会发生三件事: 读越来越慢:一次点查要打开的文件数线性增长(下一节的读路径会量化); 空间虚高:被覆盖的旧版本、被删除的数据一直占着磁盘;
本节摘要:Compaction 是 LSM-Tree 的"分期付款"机制——写入时只追加小文件,后台再把小文件合并成大文件,顺带清除超版本与墓碑。本节讲 Minor 与 Major 两档合并的选择算法、写放大/读放大/空间放大的三角权衡,以及生产上"关自动 Major、低峰手动跑"的通行惯例。
第 2 章结束时 Store 里是什么局面:一个列族下 MemStore 加一串 HFile,最新数据在内存,历史数据散落多个文件,同一行键的新旧版本与墓碑分布在不同文件里。不管它,会发生三件事:
Compaction 的做法是选若干小文件,归并排序重写成一个大文件——输入文件本来各自有序,归并是线性开销。重写过程中顺带完成真正的删除:超龄 TTL、超额版本数、墓碑覆盖的旧值,都在这一步物理消失(补上 2.3 节的伏笔)。
Minor Compaction:选取部分相邻小文件合并,触发性强、单次代价小。挑选算法(默认 ExploringCompactionPolicy)会在"文件数超过阈值(hbase.hstore.compactionThreshold,默认 3)"后,评估各种候选组合,选出"合并收益/成本"最优的一组。它不保证清除墓碑(墓碑要删的数据可能还在未参与合并的文件里)。
Major Compaction:把一个 Store 的所有文件合成一个,并物理清除一切可删数据。默认 7 天触发一次(hbase.hstore.compaction.major 相关周期参数)。代价是重写整个 Store 的数据,IO 与网络(HDFS 副本流水线)压力巨大——大 Region 一次 Major 可能持续数小时。
参数速查(RegionServer 级):
| 参数 | 默认 | 作用 |
|---|---|---|
| hbase.hstore.compactionThreshold | 3 | 文件数下限,触发 Minor |
| hbase.hstore.compaction.max | 10 | 单次 Minor 参与文件数上限 |
| hbase.hregion.majorcompaction | 7 天 | Major 周期,0 表示禁用自动 |
| hbase.hstore.blockingStoreFiles | 16 | 文件数超此值阻塞写入,防止失控 |
Compaction 的所有参数本质上都在这三个量之间挪筹码:
三者不可能同时最小,这是 LSM 系统的固有约束(RocksDB、Cassandra 同病)。HBase 默认配置偏保守,留了大量旋钮让用户按负载画像调:
⚠️ 常见坑:看到磁盘告警就手动触发全表 major_compaction,结果几十个 Region 同时开跑,HDFS 带宽被打满,前台读写雪崩。手动 Major 必须分批、限流(RegionServer 级还有
hbase.regionserver.throughput.controller限速参数可用)。
7 天一次的自动 Major 会在不可预测的时刻突然带来 IO 风暴,所以生产环境几乎标配这条配置:
hbase:046:0> alter 'orders', {NAME => 'cf', hbase:047:1* CONFIGURATION => {'hbase.hregion.majorcompaction' => '0'}}
(或全局配置文件里设 0,按表覆盖更常见。)然后在业务低峰用脚本滚动触发,脚本逻辑伪码:
// 低峰期滚动 Major:每次一个 Region,跑完一个再下一个 for (RegionInfo r : tableRegions) { majorCompactRegion(r); waitUntilCompactionDone(r); // 轮询 Compaction 队列为空 sleep(interval); // 每个之间留缓冲 }
判断 Compaction 是否在拖垮机器,看两个监控点(第 7 章详述):Compaction 队列长度、Compaction 占用的磁盘吞吐。队列长期堆积说明合并速度赶不上 Flush 产文件速度,这是"写多于磁盘承受力"的明确信号,该扩容或降写入了。
把 2.2 节的阻塞线补完整:hbase.hstore.blockingStoreFiles(默认 16)——当某 Store 文件数超过它且 Compaction 排队未消化,该 Region 写入再次被阻塞。所以"写 hang"有两个上游原因:MemStore 水位(2.2 节)与 StoreFile 堆积(本节),排查时要先看 Web UI 的 Flush 与 Compaction 两个队列再下结论。
💡 关键直觉:把 Compaction 理解成"信用卡分期"——写入时爽快(只追加),账单(放大系数)后付。调优的全部艺术在于安排一个还得起的还款计划:低峰还、分批还、按负载画像定利率。
文件整理好了,读路径终于可以登场:下一节把 MemStore、BlockCache 与一串 HFile 的归并过程完整走一遍。