HBase中的列族TTL(Time To Live)是一种基于时间戳的数据生命周期管理手段。每个列族可以独立设置TTL值,单位为秒,客户端写入的单元格在达到存活上限后,会在合适的时机被服务端判定为过期并清理。这种机制不需要业务层编写定时删除任务,由RegionServer在内部 compaction 过程中统一处理,既降低了开发成本,也减少了频繁删除对线上读写造成的压力。很多团队用它将日志类、轨迹类数据自动老化,从而控制存储成本。

列族TTL的基础配置与写入影响
在HBase中设置列族TTL主要通过表描述符完成。可以在建表时用HBase Shell或者Java API指定,也可以后续通过alter命令修改。TTL值以秒为单位,默认值约等于Integer.MAX_VALUE对应的秒数,也就是几乎不过期。需要注意的是,TTL是列族级别属性,同一张表的不同列族可以配置完全不同的存活时间,这让我们能在一张表里把核心交易数据和临时辅助数据分开管理。
当客户端写入数据时,RegionServer会用当前服务器时间或者指定的时间戳作为单元格的timestamp。TTL的判定就是拿当前系统时间减去单元格timestamp,如果差值大于配置的TTL,就认为该单元格已过期。如果写入时显式带了未来的时间戳,会导致数据在很长时间内不被清理,甚至造成空间暴涨,因此生产环境通常建议禁止客户端随意指定时间戳,或者开启时间戳校验。
下面是一段Java API配置列族TTL的示例,展示如何在建表时设置三十天过期:
Configuration conf = HBaseConfiguration.create();
Connection conn = ConnectionFactory.createConnection(conf);
Admin admin = conn.getAdmin();
// 定义列族并设置TTL为30天(单位秒)
ColumnFamilyDescriptor cfDesc = ColumnFamilyDescriptorBuilder
.newBuilder(Bytes.toBytes("cf"))
.setTimeToLive(30 * 24 * 60 * 60)
.build();
TableDescriptor tableDesc = TableDescriptorBuilder
.newBuilder(TableName.valueOf("order_log"))
.setColumnFamily(cfDesc)
.build();
admin.createTable(tableDesc);
admin.close();
conn.close();
服务端如何判定并删除过期数据
HBase并不会在写入后立刻扫描并删除过期单元格,真正的清理发生在compaction阶段。HBase的存储文件是HFile,随着写入会产生多个小文件,系统会定期做minor compaction合并小文件,并在major compaction时读取全部数据、丢弃过期内容以及被标记删除的内容。TTL过期数据的删除只在major compaction中保证彻底物理清除,minor compaction通常只合并不深究过期逻辑,这也是为什么修改TTL后磁盘空间不会立即下降。
在源码层面,StoreFileScanner在遍历单元格时会调用StoreScanPolicy判断是否过期。如果当前时间减去单元格timestamp超过TTL,且不满足MIN_VERSIONS保留条件,该单元格会直接跳过并不写入新HFile。也就是说,过期数据是在文件重写过程中被“过滤掉”的,而不是先读出来再发删除指令。这样设计避免了大量delete标记对写入放大,也减少了HLog负担。
我们可以通过HBase Shell查看列族TTL并手动触发major compaction,观察过期数据被清理的过程:
# 查看表结构中的TTL describe 'order_log' # 手动触发指定表的major compaction major_compact 'order_log' # 查看region状态与文件数量 status 'detailed'
从上述过程可以看出,如果一张表写入量很低,可能很久都不会自然触发major compaction,导致过期数据一直占用空间。此时可以借助运维脚本周期性执行major_compact,或者在HBase配置中调低hbase.hregion.majorcompaction间隔。但要注意major compaction非常消耗IO,必须在业务低峰期进行,否则容易引发读写毛刺。
TTL与版本数、MIN_VERSIONS的协同规则
列族除了TTL之外还有MAX_VERSIONS和MIN_VERSIONS两个参数,它们共同决定保留策略。简单来说,MAX_VERSIONS控制最多保留几个版本,TTL控制时间边界,而MIN_VERSIONS保证即使数据已过期,也至少保留设定数量的版本不被清理。举例来说,若MIN_VERSIONS设为1,TTL设为一天,那么超过一天的老数据正常情况下会被删,但每个qualifier至少留下最新的一条,防止全部清空造成历史完全断档。
这种组合在监控指标场景中非常实用。比如每秒采集一次CPU使用率,我们想保留最近一小时明细,同时又不想因为TTL把唯一的历史点删光,就可以设置TTL为3600,MIN_VERSIONS为1。这样即使采集停了,表里依然有最后一条记录可供展示,而不是查出来空空如也。下表总结了三者组合的常见效果:
| TTL | MAX_VERSIONS | MIN_VERSIONS | 实际保留行为 |
|---|---|---|---|
| 86400 | 3 | 0 | 仅保留一天内最新3个版本,超期全删 |
| 86400 | 5 | 1 | 超期数据仅留1条,未超期最多留5条 |
| 2147483647 | 1 | 0 | 等效不过期,只留最新版本 |
在代码层面修改这些参数同样简单,以下示例将已有列族的MIN_VERSIONS改为1,并保证TTL生效:
ColumnFamilyDescriptor newCf = ColumnFamilyDescriptorBuilder
.newBuilder(Bytes.toBytes("cf"))
.setTimeToLive(86400)
.setMaxVersions(5)
.setMinVersions(1)
.build();
admin.modifyColumnFamily(TableName.valueOf("order_log"), newCf);
最后需要提醒的是,TTL依赖RegionServer系统时钟。如果集群机器时间不同步,可能出现某些region过早清理或迟迟不清理的问题。因此部署NTP时间同步是TTL正确运作的前提。此外,在跨数据中心复制场景下,被清理的数据不会通过 replication 再传一遍删除操作,目标集群需自身也配置相同TTL来保持一致性。