大规模机器学习特征存储与检索 - 第2节
特征存储架构:在线存储与离线存储
聊特征存储架构之前,我先问个问题:你平时做特征工程,特征算完放哪儿?
很多人直接丢 Redis,或者一股脑塞 Hive。嗯,这其实是个坑。我见过一个团队,所有特征都放 Redis,结果线上延迟倒是低,但离线训练时发现特征回溯根本做不了——因为 Redis 里的数据早就过期了。
说白了,特征存储必须分两条路走:在线存储和离线存储。它们各自解决不同的问题,谁也替代不了谁。
在线存储:低延迟是命根子
在线存储服务于线上推理。模型打分时,特征必须在毫秒级返回。我个人习惯用 Redis 或 Aerospike,原因很简单:它们都是纯内存或内存+SSD 混合架构,读写延迟能控制在 1-5ms。
但这里有个关键点——在线存储不存全量特征。你想想看,线上推理只需要最近的特征值,比如用户今天的点击率、商品当前的库存。历史全量数据?那是离线存储的事。
| 存储系统 | 延迟 | 典型场景 | 我踩过的坑 |
|---|---|---|---|
| Redis | < 1ms | 实时特征、缓存 | 内存爆炸,没设过期策略 |
| Aerospike | 1-3ms | 高并发特征查询 | 集群扩容时数据重分布慢 |
| HBase | 5-20ms | 大 Key 特征 | Region 分裂导致毛刺 |
离线存储:全量、历史、可回溯
离线存储的核心能力就三个字:全、久、查。全量特征、历史版本、能回溯到任意时间点。
我一般用 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 实时计算 → Kafka → 在线存储(Redis)。延迟控制在秒级。
- 离线写入:数据源 → Hive 离线计算 → 离线存储(Hive/Parquet)。每天定时跑一次。
这里有个细节:实时写入到 Redis 后,我还会同步写一份到离线存储。为什么?因为线上特征需要留底,方便以后回溯。我习惯在 Kafka 里保留最近 7 天的特征变更日志,离线任务每天消费一次,写入 Hive。
读取路径
读取也分两条线:
- 线上读取:模型服务 → 特征服务层 → Redis。特征服务层先查本地缓存,没命中再查 Redis。
- 离线读取:训练任务 → 特征服务层 → Hive。训练时按时间分区拉取全量特征。
关键点:线上读取一定要做超时控制。我一般设置 10ms 超时,超时直接返回默认值。别让特征查询拖垮整个推理链路。
一致性保障
读写路径讲完了,但有个问题:在线和离线存储的数据不一致怎么办?
说实话,完全一致很难做到。我一般接受最终一致性。实时特征允许几分钟的延迟,离线特征每天对齐一次。
具体做法是:每天凌晨跑一个一致性校验任务,对比 Redis 和 Hive 里的特征值。差异超过阈值就告警,人工介入排查。
嗯,特征存储架构的核心就这些。说白了就是:在线存快照,离线存全量,中间加一层服务做路由和缓存。你想想看,是不是这个理?