前置问题与边界

本篇回答一个核心问题:不可变 HFile 如何合并,旧值和 tombstone 何时真正消失。

HBase 的写入路径把 mutation 写入 WAL 和 MemStore,flush 后形成新的 HFile。HFile 不原地修改,因此旧版本、Delete 标记和过期数据会散落在多个 StoreFile 中。Compaction 通过重写新文件来减少 StoreFile 数量,并在可见性规则允许时丢弃不再需要的 Cell。

本篇不讨论跨行事务,也不把 Delete 写成“立即删除磁盘数据”。HBase 的 Delete 是写入一个或多个 tombstone;旧数据是否在物理文件里消失,取决于 compaction 类型、版本策略、TTL、快照引用、最小保留时间和扫描可见性。

对象路径图

flowchart TD
    P[Put/Delete] --> WAL[WAL]
    P --> MS[MemStore]
    MS --> FL[Flush]
    FL --> H1[HFile A]
    FL --> H2[HFile B]
    H1 --> MC[Minor Compaction]
    H2 --> MC
    MC --> H3[New HFile]
    H3 --> MA[Major Compaction]
    MA --> HC[Cleaner and archive]
    MA --> OUT[Less StoreFiles, eligible versions removed]

Minor compaction 主要合并一部分 StoreFile,降低文件数量。Major compaction 会把某个 Store 的文件集合更完整地重写一次,并提供清理 Delete tombstone、过期版本和超过版本上限数据的机会。这里说的是“机会”,不是每次都立即清理所有历史数据。

关键对象和状态

对象 作用 关键状态
StoreFile column family 下的 HFile 包装 数量决定读放大和 compaction 压力
MemStore flush 内存转不可变文件 产生新的 StoreFile
Delete tombstone 标记删除范围 读路径和 compaction 都要识别
TTL 按时间过滤过期 Cell 不生成 tombstone,由读取和 compaction 过滤
VERSIONS 保留最大版本数 与 timestamp 和 Delete 共同决定可见值
MIN_VERSIONS 最小保留版本数 可能让过期数据暂时保留
KEEP_DELETED_CELLS 删除标记保留策略 改变删除后历史数据可见和清理语义
Snapshot 引用 HFile 可能阻止旧文件立即删除

Delete、TTL 和版本策略是三种不同机制。Delete 会写入 tombstone;TTL 是列族属性,按 Cell timestamp 判断过期;版本策略决定同一 Cell 坐标保留多少历史版本。Compaction 把这些规则放在一起执行。

Delete 先写 tombstone

HBase 的 Delete API 支持删除列的某个版本、某列所有版本、某个列族内的版本范围等语义。删除操作本身也是 mutation,会进入 WAL 和 MemStore,再 flush 成 HFile。读路径遇到 tombstone 时,会用它遮蔽被删除范围内的旧 Cell。

因为旧 Cell 可能在更早的 HFile 中,Delete 不能在写入瞬间跑到所有旧文件里把数据擦掉。真正的物理回收必须等 compaction 读取新旧文件,确认 tombstone 覆盖的数据已经不需要对任何可见读保留,再把不需要的 Cell 不写入新 HFile。

TTL 不等于 Delete

TTL 不会为每个过期 Cell 生成 Delete 标记。TTL 是读取和 compaction 时的过滤规则:超过列族 TTL 的 Cell 对普通读取不可见,并可能在 compaction 中被丢弃。

MIN_VERSIONS 会影响 TTL 清理边界。即使 Cell 超过 TTL,只要还需要满足最小版本数,HBase 可能暂时保留。业务上把 TTL 当作“到点立刻物理删除”会误判空间回收和合规删除时间。

版本清理

同一 (row, family, qualifier) 可以保存多个 timestamp 版本。默认读取通常取最新版本;显式设置版本范围或版本数量时,客户端可以看到历史版本。Compaction 会根据最大版本数、最小版本数、TTL 和 Delete 标记决定哪些 Cell 不再写入新文件。

版本清理的结果也受文件组合影响。如果旧版本和 tombstone 分散在不同 HFile 中,只有把相关文件放到同一次合并上下文里,compaction 才有足够信息做最终清理。minor compaction 不一定覆盖所有文件,因此可能保留仍需等待 major compaction 或后续合并才能清理的数据。

Compaction 的代价

Compaction 把多个不可变文件读出、归并、过滤,再写成新的不可变文件。它减少未来读放大,但会消耗当前磁盘 I/O、CPU、HDFS 带宽和 RegionServer 后台线程。compaction backlog 过高时,前台读写会被 StoreFile 数量、磁盘争用和 flush 压力拖慢。

调大写入缓冲并不能替代 compaction。写缓冲影响 flush 频率,compaction 影响文件数量和历史数据回收。两者都在 LSM 代价模型里,但控制的是不同阶段。

最小实验

UNVERIFIED_RUNTIME:当前任务环境没有目标版本运行条件。以下步骤用于 HBase 2.6.6、Hadoop 3.4.3、JDK 17 环境,不包含伪造输出。

复现实验从一张保留多个版本并设置 TTL 的表开始,再向同一 Cell 写入多个时间戳版本,查看多版本可见性,写入 Delete tombstone 后触发 flush 和 major compaction。命令只描述可执行步骤,不附带未采集的清理时间或文件数量。

1
hbase shell
1
create_namespace 'lab'
1
create 'lab:t07_compaction', {NAME => 'cf', VERSIONS => 3, TTL => 3600}

写入同一个 Cell 的多个版本后查看结果:

1
put 'lab:t07_compaction', 'user#0001', 'cf:name', 'v1', 1000
1
put 'lab:t07_compaction', 'user#0001', 'cf:name', 'v2', 2000
1
put 'lab:t07_compaction', 'user#0001', 'cf:name', 'v3', 3000
1
get 'lab:t07_compaction', 'user#0001', {COLUMN => 'cf:name', VERSIONS => 3}

删除某一列,并触发 flush:

1
deleteall 'lab:t07_compaction', 'user#0001', 'cf:name'
1
flush 'lab:t07_compaction'

再次读取,观察 Delete 对可见性的影响:

1
get 'lab:t07_compaction', 'user#0001', {COLUMN => 'cf:name', VERSIONS => 3}

触发 major compaction:

1
major_compact 'lab:t07_compaction'

使用 HFile 工具观察 compaction 前后 StoreFile 元信息时,只能记录实际环境中的文件数量和元信息。示例文章不得预填固定数值。

清理实验表:

1
disable 'lab:t07_compaction'
1
drop 'lab:t07_compaction'
1
drop_namespace 'lab'

Java Public API 边界

应用代码通过 PutDeleteGetAdmin 触发或观察相关行为。下面示例只展示 Delete 写入和 Admin major compaction 的 Public API 入口,不保证 compaction 立即完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.client.Delete;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.util.Bytes;

public final class HBaseDeleteCompactionExample {
public static void main(String[] args) throws IOException {
Configuration conf = HBaseConfiguration.create();
TableName tableName = TableName.valueOf("lab:t07_compaction");
try (Connection connection = ConnectionFactory.createConnection(conf);
Table table = connection.getTable(tableName);
Admin admin = connection.getAdmin()) {
Delete delete = new Delete(Bytes.toBytes("user#0001"));
delete.addColumns(Bytes.toBytes("cf"), Bytes.toBytes("name"));
table.delete(delete);
admin.majorCompact(tableName);
}
}
}

UNVERIFIED_RUNTIME:这段代码未在目标依赖下编译运行。majorCompact 是请求后台 compaction,不是同步返回“所有文件已经清理完成”的证明。

失败恢复路径

Delete tombstone 和普通 Put 一样走 WAL。RegionServer 在 Delete 已返回但尚未 flush 时崩溃,恢复依赖 WAL replay。恢复后,tombstone 仍会遮蔽对应范围的旧 Cell。

Compaction 过程中失败时,HBase 不能让新旧文件集合同时以不一致方式暴露给读路径。实现会围绕临时文件、提交、StoreFile 列表和归档清理维护可恢复状态。Snapshot 引用的 HFile 不能简单删除,旧文件可能进入 archive 后继续被 snapshot 访问。

工程迁移

HBase 机制 通用模式 迁移场景
tombstone 先标记后回收 LSM delete、对象存储删除标记
TTL filter 读时过滤加后台回收 缓存过期、日志保留策略
compaction 后台重写控制读放大 Lucene segment merge、RocksDB compaction
snapshot 引用 引用阻止物理删除 Copy-on-write 文件、备份保留

模式公式是:visible_data = latest_versions - covered_by_tombstone - expired_by_ttlphysical_reclaim = compaction(visible_rule, retention_refs)。听到“删除后空间没有下降”,先检查 compaction 和引用保留,不要先怀疑 Delete 没生效。

常见误解

误解 更准确的说法
Delete 会立即删除所有 HFile 中的数据 Delete 先写 tombstone,物理回收依赖 compaction
TTL 会写入删除标记 TTL 是时间过滤规则,不为每个 Cell 生成 tombstone
major compaction 一定立刻清掉所有旧值 清理受版本、TTL、Delete、快照和保留策略影响
minor compaction 和 major compaction 只差名字 覆盖文件范围不同,清理能力和 I/O 代价也不同
compaction 只影响磁盘空间 compaction 同时影响读放大、后台 I/O 和前台延迟

练习

  1. 写入同一 Cell 的 5 个版本,设置 VERSIONS => 3,观察普通 get 与多版本 get 的差异。
  2. 对同一列执行 Delete 后,在 flush 前后分别读取,区分可见性和物理回收。
  3. 创建 snapshot 后触发 major compaction,观察旧 HFile 是否立即删除。
  4. 调整 TTL 与 MIN_VERSIONS,观察过期数据是否仍被保留。

系列导航

编号 文章
00 导读:row key 决定数据位置
01 数据模型:Cell、Column Family 与版本
02 集群架构:HMaster、RegionServer、ZooKeeper 与 hbase:meta
03 Schema 与 row key 设计
04 写入路径:WAL、MVCC、MemStore 与 flush
05 读取路径:BlockCache、Bloom Filter 与 HFile block
06 HFile 内部结构
07 本篇:Compaction、TTL、版本与 Delete
08 Region 生命周期:split、merge 与 assignment
09 Client 路由与重试
10 行级原子性与一致性边界
11 RegionServer 崩溃与 WAL 恢复
12 跨集群 Replication
13 Snapshot、Backup 与恢复
14 Get、Scan、Filter 与分页
15 BufferedMutator 与 Bulk Load
16 Coprocessor、Endpoint 与 Phoenix 边界
17 Kerberos、RPC 保护与 ACL
18 Metrics、hbtop、Compaction 与性能调优
19 HBase 3.0 与设计边界

参考资料