大规模机器学习特征存储与检索 - 第2节

特征存储架构:在线存储与离线存储

聊特征存储架构之前,我先问个问题:你平时做特征工程,特征算完放哪儿?

很多人直接丢 Redis,或者一股脑塞 Hive。嗯,这其实是个坑。我见过一个团队,所有特征都放 Redis,结果线上延迟倒是低,但离线训练时发现特征回溯根本做不了——因为 Redis 里的数据早就过期了。

说白了,特征存储必须分两条路走:在线存储和离线存储。它们各自解决不同的问题,谁也替代不了谁。

在线存储:低延迟是命根子

在线存储服务于线上推理。模型打分时,特征必须在毫秒级返回。我个人习惯用 Redis 或 Aerospike,原因很简单:它们都是纯内存或内存+SSD 混合架构,读写延迟能控制在 1-5ms。

但这里有个关键点——在线存储不存全量特征。你想想看,线上推理只需要最近的特征值,比如用户今天的点击率、商品当前的库存。历史全量数据?那是离线存储的事。

存储系统 延迟 典型场景 我踩过的坑
Redis < 1ms 实时特征、缓存 内存爆炸,没设过期策略
Aerospike 1-3ms 高并发特征查询 集群扩容时数据重分布慢
HBase 5-20ms 大 Key 特征 Region 分裂导致毛刺
注意:在线存储不要存大 Value。我曾经把用户画像 JSON 直接塞 Redis,单个 Value 超过 100KB,结果 QPS 一高 Redis 直接 OOM。后来改成只存关键字段,其他走离线。

离线存储:全量、历史、可回溯

离线存储的核心能力就三个字:全、久、查。全量特征、历史版本、能回溯到任意时间点。

我一般用 Hive 或 Parquet 格式存离线特征。为什么?因为训练时需要拉取过去 N 天的特征数据做样本拼接。你想想看,如果离线存储只保留最新值,那训练时拿什么做时间穿越?

离线存储还有个隐藏功能——特征校验。我习惯在离线存储层做一轮特征质量检查:空值率、分布漂移、异常值。发现问题直接告警,而不是等到模型上线才发现特征坏了。

我的经验:离线特征表一定要加时间分区。按天分区是最低要求,按小时分区更好。否则回溯时全表扫描,跑一次训练要等半天。

特征存储的核心组件

一个完整的特征存储系统,不是只放个数据库就完事了。我拆开来看,核心组件有四个。

1. 特征注册中心

这是特征存储的「元数据中心」。每个特征叫什么名字、什么类型、从哪来、怎么算、谁在用,都得在这里登记。

我见过最乱的项目,特征名全靠口口相传。A 同学叫 user_click_rate,B 同学叫 user_ctr,结果同一个特征在代码里出现两个名字,线上模型加载时直接报错。

特征注册中心至少得管这几件事:

  • 特征命名规范:统一前缀、命名风格
  • 特征元数据:类型、维度、时效性
  • 特征血缘:从哪个原始数据算出来的
  • 特征版本:每次变更都要留记录

2. 特征计算引擎

特征不是凭空产生的。实时特征需要 Flink 或 Spark Streaming 来算,离线特征用 Hive 或 Spark Batch。

这里有个容易忽略的点——计算引擎要和存储层解耦。我习惯的做法是:计算引擎只管算,算完写到消息队列或中间存储,再由专门的写入服务写到在线/离线存储。这样计算挂了不影响存储,存储挂了也不影响计算。

3. 特征存储引擎

就是前面说的在线存储和离线存储的具体实现。在线用 Redis/Aerospike,离线用 Hive/Parquet。

但存储引擎不是简单的读写接口。它需要支持:

  • 批量写入:离线特征一次写几百万条
  • 点查:线上按 Key 查单个特征
  • 范围查:训练时按时间范围拉取特征
  • TTL 管理:在线特征自动过期

4. 特征服务层

这是对外暴露的接口层。模型服务、训练任务都通过这一层来读写特征。

我设计特征服务层时,一定会加本地缓存。为什么?因为线上推理时,同一个特征在短时间内会被反复查询。不加缓存,Redis 扛不住高并发。我一般用 Guava Cache 或 Caffeine,设置 1-5 秒过期,命中率能到 80% 以上。

小技巧:特征服务层要做降级。如果 Redis 挂了,直接返回默认值或空值,别让模型服务等超时。我曾经因为特征服务没做降级,Redis 抖动导致整个推荐链路雪崩。

特征存储的读写路径

理解了组件,再看读写路径就清晰了。我画了一张图,把整个流程串起来。

特征存储读写路径 数据源 实时计算 (Flink) 离线计算 (Hive) 消息队列 (Kafka) 在线存储 (Redis) 离线存储 (Hive) 特征服务层 模型服务 训练任务 读写路径说明 实线:写入路径 | 虚线:读取路径

写入路径

写入分两条线:

  • 实时写入:数据源 → Flink 实时计算 → Kafka → 在线存储(Redis)。延迟控制在秒级。
  • 离线写入:数据源 → Hive 离线计算 → 离线存储(Hive/Parquet)。每天定时跑一次。

这里有个细节:实时写入到 Redis 后,我还会同步写一份到离线存储。为什么?因为线上特征需要留底,方便以后回溯。我习惯在 Kafka 里保留最近 7 天的特征变更日志,离线任务每天消费一次,写入 Hive。

读取路径

读取也分两条线:

  • 线上读取:模型服务 → 特征服务层 → Redis。特征服务层先查本地缓存,没命中再查 Redis。
  • 离线读取:训练任务 → 特征服务层 → Hive。训练时按时间分区拉取全量特征。

关键点:线上读取一定要做超时控制。我一般设置 10ms 超时,超时直接返回默认值。别让特征查询拖垮整个推理链路。

一致性保障

读写路径讲完了,但有个问题:在线和离线存储的数据不一致怎么办?

说实话,完全一致很难做到。我一般接受最终一致性。实时特征允许几分钟的延迟,离线特征每天对齐一次。

具体做法是:每天凌晨跑一个一致性校验任务,对比 Redis 和 Hive 里的特征值。差异超过阈值就告警,人工介入排查。

避坑:别试图在写入时保证强一致。我曾经用分布式事务写 Redis 和 Hive,结果性能惨不忍睹,线上延迟直接飙到 100ms。后来改成异步写入+定期校验,问题反而少了。

嗯,特征存储架构的核心就这些。说白了就是:在线存快照,离线存全量,中间加一层服务做路由和缓存。你想想看,是不是这个理?