第三章:特征工程平台——实时特征与离线特征的统一管理

特征工程平台,说白了就是推荐系统的“后勤部”。

我见过太多团队,算法模型搞得风生水起,一到上线就卡在特征上。离线特征算好了,线上拿不到;实时特征写好了,跟离线对不上数。嗯,这种坑我踩过不止一次。

今天我们就聊聊,怎么用 Feast 和 Tecton 这类工具,把离线特征和实时特征管起来。

3.1 为什么需要统一管理?

先问个问题:你的推荐模型,离线训练时 AUC 0.78,上线后掉到 0.72。为什么?

我告诉你,八成是特征不一致。

离线训练时,你用 Hive 表里的“用户过去7天点击量”。线上服务时,你用 Redis 里实时攒的“用户过去7天点击量”。两个逻辑一模一样,但数据源不同、计算时间不同、甚至精度都不同。模型能不懵吗?

核心痛点:

  • 离线特征和实时特征,两套代码、两套存储、两套运维
  • 特征口径不一致,模型训练和推理存在偏差
  • 特征复用率低,每个团队自己造轮子

统一管理,就是要解决这三个问题。让特征像数据库里的表一样,谁都能查,谁都能用,线上线下一个样。

3.2 特征存储的架构设计

我个人习惯把特征存储分成三层:

层级名称职责典型组件
L1特征源层原始数据接入Kafka、Hive、Kinesis
L2特征计算层特征变换与聚合Spark Streaming、Flink、Python UDF
L3特征服务层在线低延迟读取Redis、DynamoDB、Cassandra

你想想看,这三层如果各管各的,那肯定乱套。Feast 和 Tecton 做的事,就是在这三层之上加一个统一的“特征注册中心”。

3.2.1 Feast 架构解析

Feast 是开源方案里比较成熟的。它的核心概念就两个:Feature View 和 Feature Service。

Feature View:定义了一个特征的“是什么”和“怎么算”。比如:

# 离线特征定义
user_click_features = FeatureView(
    name="user_7d_click_count",
    entities=["user_id"],
    features=[
        Feature(name="click_count", dtype=Int64),
        Feature(name="click_item_ids", dtype=Array(Int64))
    ],
    batch_source=BigQuerySource(
        table_ref="project.dataset.user_click_7d"
    ),
    ttl=timedelta(days=1)
)

# 实时特征定义
user_realtime_features = FeatureView(
    name="user_realtime_click",
    entities=["user_id"],
    features=[
        Feature(name="realtime_click_count", dtype=Int64)
    ],
    stream_source=KafkaSource(
        kafka_bootstrap_servers="localhost:9092",
        topic="user_click_events"
    ),
    ttl=timedelta(hours=1)
)

这里有个细节:Feast 要求你同时定义 batch_source 和 stream_source。为什么?

因为实时特征需要“回填”。比如用户刚注册,没有实时数据,那就用离线数据兜底。我曾在项目中遇到过,新用户因为没有实时特征,推荐结果全是空的。后来加了离线回填逻辑,才解决。

避坑指南:我曾经犯过一个错——实时特征的 TTL 设得太短。用户晚上10点刷了一下,第二天早上8点再打开,特征过期了,模型直接拿默认值。建议 TTL 至少覆盖用户两次访问的最大间隔。

3.2.2 Tecton 的企业级特性

Tecton 是商业版,比 Feast 多了几个杀手锏。

自动特征血缘:Tecton 会自动记录每个特征从原始数据到最终服务的完整链路。出了问题,一键回溯。我记得有一次线上特征异常,靠 Tecton 的血缘图,5分钟就定位到是某个 Flink 作业的窗口参数配错了。

特征监控与漂移检测:Tecton 内置了特征分布的监控。如果某个特征的均值、方差突然变化,它会自动告警。嗯,这个功能在推荐系统里特别有用——用户行为变了,特征分布肯定先变。

特征版本管理:Tecton 支持特征的多版本。你可以同时运行 v1 和 v2,做 A/B 测试。等 v2 验证通过,再一键切换。这个我在做特征升级时用过,确实省心。

3.3 实时特征与离线特征的对齐策略

统一管理最难的不是存储,而是“对齐”。

离线特征算的是“过去7天”,实时特征算的也是“过去7天”。但离线是 T-1 的精确值,实时是近似的滑动窗口。两者天然有差异。

我的做法是:以离线为准,实时做近似补偿

  1. 离线特征每天凌晨计算一次,存入 Feast 的离线存储
  2. 实时特征在在线服务时,基于离线特征做增量更新
  3. 如果实时特征不可用,直接降级到离线特征

举个例子:

# 伪代码:实时特征补偿逻辑
def get_user_click_count(user_id):
    # 先拿离线特征
    offline_count = feast.get_offline_feature(
        "user_7d_click_count", user_id
    )
    
    # 再拿实时增量
    realtime_delta = redis.get(f"realtime_delta:{user_id}")
    
    if realtime_delta is not None:
        return offline_count + realtime_delta
    else:
        # 实时数据不可用,降级
        return offline_count

这样,离线特征保证“准”,实时特征保证“新”。两者结合,既准又新。

注意:实时增量不能无限累加。我建议每小时做一次“全量同步”,把实时增量合并到离线特征中,然后清空增量。否则误差会越来越大。

3.4 特征存储的选型建议

选 Feast 还是 Tecton?我个人的经验是:

场景推荐方案理由
中小团队,预算有限Feast + Redis开源免费,社区活跃,够用
大厂,需要 SLA 保障Tecton + DynamoDB商业支持,监控完善,运维省心
实时要求极高(毫秒级)自研 + 内存网格Feast/Tecton 的 SDK 有网络开销

你想想看,如果你的推荐系统每天只有几百万请求,Feast 完全够用。但如果像抖音那样每秒几十万 QPS,那还是得自研。

3.5 实战:搭建一个最小特征平台

最后,我分享一个最小可用的特征平台搭建步骤。嗯,这个我在内部培训时经常用。

  1. 安装 Feastpip install feast
  2. 初始化项目feast init my_feature_repo
  3. 定义特征:在 feature_store.yaml 里配置离线源和在线源
  4. 注册特征feast apply
  5. 离线写入:用 Spark 作业每天计算特征,写入 Feast
  6. 在线读取:服务端用 feast.get_online_features() 获取

就这么简单。但要注意,Feast 本身不存储数据,它只是个“元数据管理”+“数据路由”。真正的数据存在你配置的 Redis 或 DynamoDB 里。

小技巧:我建议把 Feast 的元数据存在 PostgreSQL 里,比默认的 SQLite 稳定得多。SQLite 在并发高时会锁表,我吃过这个亏。

好了,特征工程平台的核心思路就这些。说白了,就是“统一注册、分层存储、离线兜底、实时补偿”。下一章我们聊聊模型训练平台,怎么把特征喂给模型。