4、HBase 作为特征存储:RowKey 设计原则、列族与版本控制、批量写入与读取优化
HBase 做特征存储,说白了就是看中它两点:一是能扛住海量数据,二是读写延迟够低。但用好它并不容易。我见过太多团队,RowKey 设计拍脑袋,结果读写性能一塌糊涂。今天咱们就把这几个核心点掰开揉碎。
4.1 RowKey 设计:特征存储的命门
RowKey 是 HBase 的唯一索引。所有查询都得走它。设计不好,整个集群都跟着遭殃。
4.1.1 三大设计原则
| 原则 | 说明 | 反例 |
|---|---|---|
| 散列性 | 避免热点,让请求均匀分布到所有 RegionServer | 直接用用户 ID 自增前缀 |
| 唯一性 | 每条特征记录必须有全局唯一的 RowKey | 不同时间戳的特征用相同 Key |
| 长度控制 | RowKey 越短越好,建议 16~64 字节 | 把整个 JSON 塞进 RowKey |
核心思路:RowKey 设计要同时满足「查询快」和「写入不倾斜」。我个人习惯用「盐值 + 业务主键 + 时间戳」的组合模式。
4.1.2 特征存储的典型 RowKey 模式
拿用户画像特征举例。假设我们要存用户近 7 天的点击特征。直接拿 user_id 做 RowKey 会怎样?新用户集中在少数 Region,老用户数据又太大。这就是典型的热点问题。
我常用的方案是这样的:
// 方案一:哈希前缀 + 用户ID + 日期
RowKey = MD5(user_id).substring(0,4) + "_" + user_id + "_" + date
// 方案二:倒序时间戳 + 用户ID(适合查最近N条)
RowKey = (Long.MAX_VALUE - timestamp) + "_" + user_id
// 方案三:业务分桶 + 特征类型 + 主键
RowKey = bucket_id + "_" + feature_type + "_" + entity_id
方案一我用的最多。哈希前缀保证了散列,用户 ID 保证了唯一,日期支持范围扫描。嗯,这里要注意:哈希函数别用太重的,MD5 前 4 位就够了,否则写入时 CPU 先扛不住。
4.1.3 避坑指南
我曾经踩过的坑:有个项目直接用时间戳当 RowKey 前缀。结果所有写入都打到一个 Region 上,RegionServer 频繁触发 Split,写入延迟从 5ms 飙升到 2s。后来改成哈希前缀,问题立刻解决。
还有一点:RowKey 长度不是越短越好。太短会导致散列粒度不够。我一般控制在 32 字节左右,既能保证唯一性,又不会浪费存储。
4.2 列族与版本控制
4.2.1 列族设计:少即是多
HBase 的列族是物理隔离的。每个列族有自己的 MemStore 和 HFile。设计多了,管理成本直线上升。
| 场景 | 推荐列族数 | 理由 |
|---|---|---|
| 在线特征服务 | 1~2 个 | 减少跨列族扫描,降低延迟 |
| 离线批量特征 | 2~3 个 | 区分高频和低频特征,优化 Compaction |
| 混合负载 | 不超过 3 个 | 超过 3 个,RegionServer 内存压力剧增 |
我个人的习惯是:特征存储只用 1 个列族。所有特征列都放在里面。为什么?因为特征查询通常要拿全量特征,跨列族扫描会多一次 RPC,延迟受不了。
4.2.2 版本控制:保留多少历史?
HBase 默认保留 3 个版本。特征存储的场景下,这个值需要仔细掂量。
- 实时特征:保留 1 个版本就够了。旧版本没意义,还占空间。
- 时序特征:保留 3~7 个版本。方便回溯和调试。
- 模型训练特征:保留全部版本,但配合 TTL 自动清理。
小技巧:版本数别设太大。我见过有人设 100 个版本,结果一次 Major Compaction 跑了 6 小时。建议版本数不超过 10,配合 TTL 一起用。
// 创建表时指定版本和TTL
create 'user_features',
{NAME => 'f', VERSIONS => 3, TTL => 604800} // 7天过期
4.3 批量写入优化
特征存储的写入场景通常是高吞吐、低延迟。批量写入是必经之路。
4.3.1 BufferedMutator:批量写入的正确姿势
别用 Put 单条写入。那是给测试用的。生产环境必须用 BufferedMutator。
// 错误示范:逐条写入
Table table = connection.getTable(TableName.valueOf("user_features"));
for (Feature feature : features) {
Put put = new Put(Bytes.toBytes(feature.getRowKey()));
put.addColumn("f".getBytes(), "click".getBytes(), feature.getValue());
table.put(put); // 每次都是RPC
}
// 正确做法:批量提交
BufferedMutator mutator = connection.getBufferedMutator(
TableName.valueOf("user_features"));
List<Put> puts = new ArrayList<>();
for (Feature feature : features) {
Put put = new Put(Bytes.toBytes(feature.getRowKey()));
put.addColumn("f".getBytes(), "click".getBytes(), feature.getValue());
puts.add(put);
if (puts.size() >= 1000) { // 每1000条刷一次
mutator.mutate(puts);
puts.clear();
}
}
if (!puts.isEmpty()) {
mutator.mutate(puts);
}
mutator.close();
这里有个细节:缓冲区大小别设太大。我一般设 2MB 或 1000 条,哪个先到就刷。太大容易撑爆内存,太小又浪费吞吐。
4.3.2 写入参数调优
| 参数 | 默认值 | 推荐值 | 说明 |
|---|---|---|---|
| hbase.client.write.buffer | 2MB | 4~8MB | 增大缓冲区,减少RPC次数 |
| hbase.client.max.total.tasks | 100 | 200~500 | 提高并发写入能力 |
| hbase.client.max.perregion.tasks | 40 | 80~120 | 单Region并发控制 |
注意:调大这些参数时,要关注 RegionServer 的 GC 情况。我遇到过调太大导致 Full GC 频繁,写入反而变慢。建议逐步调优,观察监控曲线。
4.4 读取优化
4.4.1 批量 Get 代替单条 Get
特征服务通常要一次拿多个用户的特征。用单条 Get 逐个查,网络开销就占了 80%。
// 错误示范:循环单条查询
for (String userId : userIds) {
Get get = new Get(Bytes.toBytes(userId));
Result result = table.get(get);
// 处理结果
}
// 正确做法:批量Get
List<Get> gets = new ArrayList<>();
for (String userId : userIds) {
Get get = new Get(Bytes.toBytes(userId));
get.addFamily("f".getBytes()); // 只取需要的列族
gets.add(get);
}
Result[] results = table.get(gets); // 一次RPC搞定
批量 Get 能减少 90% 以上的 RPC 次数。我做过压测,1000 条批量 Get 比单条快 8~10 倍。
4.4.2 Scan 的合理使用
特征存储里,Scan 主要用于离线批量读取。但要注意:Scan 不加限制会扫全表,那是灾难。
// 带条件的Scan
Scan scan = new Scan();
scan.setStartRow(Bytes.toBytes("hash_001_user_20240101"));
scan.setStopRow(Bytes.toBytes("hash_001_user_20240102"));
scan.setCaching(1000); // 每次RPC返回1000条
scan.setBatch(100); // 每100列返回一行
scan.addFamily("f".getBytes());
ResultScanner scanner = table.getScanner(scan);
for (Result result : scanner) {
// 处理结果
}
scanner.close();
setCaching 和 setBatch 这两个参数很关键。Caching 控制行数,Batch 控制列数。我一般设 Caching=1000,Batch=100,这样既不会撑爆客户端内存,又能充分利用网络带宽。
4.4.3 缓存策略
特征读取的延迟敏感度很高。HBase 提供了两级缓存:BlockCache 和 MemStore。
- BlockCache:读缓存,存 HFile 的 Block。适合热点特征。
- MemStore:写缓存,但读也能命中。适合刚写入的特征。
我的经验:对于特征存储,建议把 BlockCache 设大一些。我一般给到 RegionServer 堆内存的 40%。热点特征基本都在缓存里,读延迟能控制在 5ms 以内。
4.5 本章核心知识体系
下面这张图总结了 HBase 作为特征存储的核心设计要点:
说白了,HBase 做特征存储就是一场平衡游戏。RowKey 要散列,列族要精简,版本要克制,读写要批量。把这些点都做到位了,特征服务的延迟和吞吐才能同时达标。