离线 AUC 好、线上骤降:train-serve skew 下的 point-in-time 与双存储机制
一个推荐模型出现这类现象时,第一反应应该是查特征读取链路,而不是调超参:训练样本和线上请求拿到的同一批特征,很可能在时间口径、编码逻辑或版本上不一致,也就是 train-serve skew。特征存储正是为这两个问题设计的:特征重复开发、训练与在线推理不一致。先看它背后的两个核心机制。
point-in-time correctness:样本里不能出现“未来特征”
训练样本对应一个事件时间,这个时间点之后才产出的特征如果被 join 进来,模型等于提前看到了“未来”。离线 AUC 会虚高,线上没有未来可用,效果自然回落。这种数据泄漏在指标上非常隐蔽。
所以训练集 join 必须按事件时间做截断:
SELECT e.user_id, e.label, f.feature_value
FROM label_events e
LEFT JOIN historical_feature f
ON f.user_id = e.user_id
AND f.feature_time <= e.event_time
也就是 point-in-time join。
双存储 + registry:一份定义,两种物理落地
训练需要历史特征,推理只需要最新值,访问模式完全不同。feature store 因此维护两份物理存储:
- offline store:数仓/数据湖表,存放历史特征和训练数据;
- online store:毫秒级 KV(Redis/DynamoDB),key=实体 ID,value=最新特征值。
这两份存储不能有两个定义源。特征定义统一进 registry,只写一次,由同一份定义生成离线表和 online 同步任务。双存储本身不复杂,真正的坑在一致性、同步链路和去重。
同步链路通常三层:
- 批特征由离线调度产出,写 offline,再同步 online;
- 流特征从 Kafka → Flink 消费,实时更新 online,并额外落一份 offline 数据;
- 在线 API 通过 GetOnlineFeatures 批量取回特征。
任何一层只改了其中一侧,train-serve skew 就会重新出现。
可复现的 skew 来源
排障时可以做一组对照:固定一批实体 ID,分别从离线产出和 online store 读特征,逐字段比对。以下差异比较常见,而且都能稳定复现:
- 时区口径不一致:线上按 UTC 算“过去 1 小时”,离线 ETL 用本地时区,时间窗口边界偏移。
- 空值/默认值不一致:线上缺省填 0,离线保留 NULL,模型若对 NULL 单独编码,特征语义直接漂移。
- 特征编码不一致:线上
hash(id)%1000,离线hash(id)%10000,embedding lookup 错位。 - 表版本不一致:线上读
user_profile_v1,离线用user_profile_v2,schema 变更导致分类 ID 映射错乱。
校验方法就两条:
- 浮点值比较用
abs(a - b) < 1e-6; - 不要只信离线 AUC。AUC 对特征绝对值不敏感,特征整体 +10% 可能看不出差距,但线上固定阈值决策会受影响。
边界:何时不需要 feature store
feature store 的价值在“准”和“稳”,不是“快”,不能当普通特征缓存用。如果只有一个模型、特征数量少、特征逻辑也不跨项目共享,自己组 Redis + Hive,再写一个离线同步 job 就够了,不需要上重型平台。
真要落地,也先做离线:选多项目共用、口径稳定的特征,逐步加 online store,最后再接流特征。第一批就铺开在线和流式,会让问题定位难度成倍增加。
落地选型
真需要 feature store 时,先看已有的基础设施。如果离线以 Spark 为主、特征以批式 join 为主,可以评估 LinkedIn 开源的 Feathr(Scala/Spark 实现);它把特征定义、时间窗口聚合和在线取数放在一套配置里,适合已经有 Spark 技术栈、想快速验证 feature store 概念的团队。
如果更看重托管生态和完整度,可以看 Feast(Apache-2.0,LF AI & Data 托管,官网 feast.dev)。它把 registry、offline/online 双存储和 point-in-time join 的边界切得比较清晰,社区和云厂商集成也相对成熟。Feast 本身不绑定具体计算引擎,离线侧可以接 BigQuery、Snowflake、Spark 等,online store 支持 Redis、DynamoDB 等,适合先从离线 feature 落地、后续再接流式同步的场景。
选型的重点不是比特性列表,而是看它能不能覆盖上文的“三层同步链路”和“一份定义、两种存储”:离线表和 online 更新是否由同一份定义生成,历史回填是否内置 point-in-time join,在线读取是否支持批量实体的批量取回。Feast 和 Feathr 都在这几个点上做了实现,但各自默认的存储和引擎绑定不同,按团队已有设施取舍即可。如果只是跑通 demo,不看托管链路是否能对齐现有的调度、血缘和权限体系,后面会把 skew 从代码问题变成平台问题。