核心概念与架构:特征、特征组、特征视图、在线存储与离线存储、实时写入与批量回填
好,咱们正式开始聊特征存储的核心概念。说实话,我刚接触这个领域的时候,也被一堆术语搞得有点晕——特征、特征组、特征视图,听起来都差不多嘛。但实际做项目你就会发现,这几个概念分不清楚,后面架构设计一定会出问题。
我习惯先把它们拆开理解,再串起来看整体架构。这样你心里就有张地图了。
特征(Feature)——最小的数据单元
特征是什么?说白了,就是一条记录里的一个字段。比如用户的年龄、最近一次登录时间、过去7天的下单金额。每个特征都有三个属性:
- 名称:比如
user_age、last_7d_order_amount - 类型:整型、浮点型、字符串、布尔值等
- 时间戳:这个特征是在什么时间点产生的
嗯,这里要注意:特征存储里的特征,通常都是加工后的结果,不是原始日志。原始日志是数据工程师的事,特征存储是给模型用的。
created_at 时间戳。别小看这个字段,排查数据延迟问题时,它能帮你省半天时间。
特征组(Feature Group)——特征的逻辑集合
特征组就是把一批逻辑上相关的特征打包在一起。比如「用户基础信息」特征组,里面包含年龄、性别、注册渠道;「用户近7天行为」特征组,包含下单次数、浏览时长、加购次数。
为什么要有特征组?我遇到过这样的场景:一个模型用了50个特征,分别来自5张不同的源表。如果没有特征组的概念,你根本不知道哪些特征是一起更新的,哪些特征依赖同一个数据源。出了问题排查起来,那叫一个痛苦。
特征组通常对应一张数据库表或一个数据源。举个例子:
-- 特征组:user_profile
CREATE FEATURE GROUP user_profile (
user_id STRING,
age INT,
gender STRING,
reg_channel STRING,
updated_at TIMESTAMP
) PRIMARY KEY (user_id);
特征视图(Feature View)——模型视角的特征切片
特征视图这个概念,我刚开始也觉得有点多余。后来做推荐系统时才明白它的价值。
特征视图是从特征组里按需选取一部分特征,再加上一些时间窗口逻辑,形成一个模型可以直接使用的特征集合。一个特征视图可以跨多个特征组。
举个例子:你的点击率模型需要用户年龄、性别、过去7天点击次数、过去24小时曝光次数。这些特征分别来自 user_profile 和 user_behavior 两个特征组。特征视图就是把这些特征拼在一起,告诉系统:「模型要的就是这4个特征,时间窗口按这个规则算」。
一句话总结三者的关系:
- 特征是最小单元
- 特征组是特征的逻辑分组(按数据源)
- 特征视图是模型需要的特征组合(按使用场景)
在线存储与离线存储——两条腿走路
特征存储最核心的设计,就是在线+离线双存储。为什么这么搞?我直接说痛点吧。
离线存储(一般是 HDFS、S3 或数据湖)用来做批量训练。模型训练时,你需要拉取过去30天甚至更长时间的历史特征。这时候不在乎延迟,在乎的是吞吐量和数据完整性。离线存储通常用 Parquet 格式,按天分区。
在线存储(一般是 Redis、Cassandra、DynamoDB)用来做实时推理。用户请求来了,模型要在毫秒级拿到最新的特征值。这时候不在乎数据量有多大,在乎的是查询延迟。在线存储通常用 KV 结构,key 是实体ID,value 是最新的特征值。
| 对比维度 | 离线存储 | 在线存储 |
|---|---|---|
| 典型技术 | HDFS、S3、Iceberg | Redis、Cassandra、DynamoDB |
| 数据格式 | Parquet、ORC | KV、JSON |
| 查询延迟 | 秒级到分钟级 | 毫秒级 |
| 数据范围 | 全量历史数据 | 最新值(或最近N条) |
| 主要用途 | 模型训练、批量评估 | 实时推理、在线服务 |
你想想看,如果只用一套存储,要么训练时查不到历史数据,要么推理时慢得没法用。所以必须两条腿走路。
实时写入与批量回填——数据怎么进去
数据写入特征存储,主要有两条路径。
实时写入:数据从 Kafka、Pulsar 等消息队列流进来,经过 Flink 或 Spark Streaming 做特征计算,然后同时写入在线存储和离线存储。在线存储直接覆盖最新值,离线存储追加到历史分区。
我在项目中遇到过一个问题:实时写入时,在线存储和离线存储的一致性怎么保证?比如 Flink 任务挂了,重启后在线存储可能丢了最近几秒的数据,但离线存储因为还没写入所以没问题。解决方案通常是:在线存储允许短暂的不一致,离线存储保证最终一致性,模型训练时以离线数据为准。
批量回填:这个场景很常见。比如你新开发了一个特征「用户过去30天复购率」,需要把过去半年的历史数据都算出来。这时候用 Spark 跑一个批量任务,把计算结果回填到离线存储,同时把最新值推到在线存储。
批量回填有个坑,我吃过亏:回填时如果直接覆盖在线存储,会导致正在推理的模型拿到「未来数据」。比如你回填了昨天之前的数据,但在线存储里昨天的值被覆盖成了历史值,模型就乱套了。正确的做法是:批量回填只写离线存储,在线存储通过实时流慢慢追上来,或者用时间戳做版本控制。
event_time,读取时只取最新的那个。
整体架构图
下面这张图,是我自己画的特征存储核心架构。你看一眼,应该就能把上面这些概念串起来了。
从这张图你能看到:数据从源进来,经过实时或批量两条路径,进入特征组。特征组是逻辑组织单元,特征视图是模型视角的裁剪。最终,在线存储服务实时推理,离线存储服务批量训练。两条链路通过时间戳和版本控制保持一致性。
嗯,核心概念和架构就这些。你把这些搞清楚了,后面选型的时候就知道该关注哪些点了。