离线特征工程基石:Hive与Spark在特征加工中的应用
聊到特征平台,很多人第一反应是实时特征、在线推理。但说实话,我做了这么多年特征工程,离线特征才是真正的基石。你想想看,一个推荐系统里,80%以上的特征都是离线算好的。实时特征只是锦上添花,离线特征才是雪中送炭。
这一章,我就把离线特征加工的核心工具——Hive和Spark,以及存储选型、调度回溯这些实战经验,一次性讲透。
为什么是Hive + Spark?
我在2018年刚接触特征平台时,团队还在用纯Hive做特征加工。那时候每天跑一次全量特征,一次就要6小时。后来引入Spark,才把时间压缩到40分钟。为什么会差这么多?
Hive擅长的是批量、结构化、SQL友好的场景。比如用户画像、基础属性这类特征,用Hive写几个JOIN就搞定了。但遇到复杂计算——比如用户过去7天的行为序列编码、物品的实时流行度衰减——Hive就力不从心了。
Spark正好补上这块短板。它的内存计算、RDD/DataFrame API、MLlib库,让特征工程变得灵活得多。
我的经验法则:
- 简单聚合、JOIN类特征 → Hive(开发快,维护成本低)
- 复杂逻辑、多步骤、需要迭代 → Spark(性能好,可编程性强)
- 两者混合使用 → 最推荐的方式
Hive在特征加工中的实战
先说说Hive。很多人觉得Hive过时了,其实不然。在离线特征场景里,Hive的稳定性和SQL可读性是无可替代的。
举个例子,我们要加工一个「用户近30天购买金额」的特征:
-- 用户近30天购买金额特征
INSERT OVERWRITE TABLE feature_user_purchase_amt_30d
SELECT
user_id,
SUM(amount) AS total_amt_30d,
COUNT(DISTINCT order_id) AS order_cnt_30d,
AVG(amount) AS avg_amt_30d
FROM dwd_order_detail
WHERE dt >= '${biz_date}' - 30
AND dt <= '${biz_date}'
GROUP BY user_id;
这段SQL,任何一个数据分析师都能看懂。但要注意一个坑:数据倾斜。我曾经遇到过一个大促场景,头部用户占了80%的数据量,导致单个Reduce卡了3小时。
避坑指南:我曾经在双11特征加工时,因为没处理数据倾斜,整个任务跑了12小时还没跑完。后来加了salting key才解决。建议对高频用户做分桶或随机前缀打散。
Spark在特征加工中的进阶用法
当特征逻辑变得复杂,Hive就有点吃力了。比如我们要计算「用户最近7天浏览序列的Embedding均值」,用Hive写UDF很麻烦,但Spark就顺手得多。
我习惯用Spark的窗口函数 + 自定义聚合来处理时序特征:
// 用户行为序列特征加工
import org.apache.spark.sql.expressions.Window
import org.apache.spark.sql.functions._
val df = spark.table("dwd_behavior_log")
.where(col("dt") >= date_sub(current_date(), 7))
// 按用户分组,按时间排序,生成行为序列
val seqDF = df
.withColumn("rn", row_number().over(
Window.partitionBy("user_id")
.orderBy(col("event_time").desc)))
.groupBy("user_id")
.agg(
collect_list(
struct(col("item_id"), col("behavior_type"), col("event_time"))
).alias("behavior_seq")
)
这里有个关键点:collect_list的顺序。Spark的collect_list不保证顺序,必须配合窗口函数先排序再聚合。我见过不少新手在这里翻车。
小技巧:如果特征维度特别多(比如上千个),建议用Spark的VectorAssembler统一组装成特征向量,再写入Hive。这样下游训练时直接读向量列,省去反复解析的麻烦。
离线特征存储选型:HBase vs Redis
特征算好了,存哪里?这是个大问题。我见过很多团队一开始随便选个存储,后来迁移时痛不欲生。
离线特征存储的核心诉求就三个:高吞吐写入、低延迟读取、支持回溯。
| 维度 | HBase | Redis |
|---|---|---|
| 写入吞吐 | 极高(批量写入可达百万级TPS) | 较高(受限于单机内存) |
| 读取延迟 | 毫秒级(但不如Redis) | 微秒级(纯内存) |
| 存储容量 | PB级(依赖HDFS) | GB~TB级(受内存限制) |
| 回溯能力 | 强(支持多版本、时间戳查询) | 弱(默认只存最新值) |
| 成本 | 低(基于廉价磁盘) | 高(内存昂贵) |
| 典型场景 | 全量特征存储、历史回溯 | 在线高并发特征读取 |
我个人习惯的做法是:HBase做主存储,Redis做缓存。离线任务每天批量写入HBase,在线服务读取时先查Redis,没命中再回源HBase。这样既保证了回溯能力,又满足了在线延迟要求。
核心结论:如果你需要做特征回溯(比如训练历史样本),必须用HBase或类似支持多版本的存储。Redis只适合存最新特征快照。
调度与回溯:特征平台的命脉
特征加工完了,怎么保证每天按时产出?怎么在模型迭代时重新计算历史特征?这就是调度和回溯要解决的问题。
我经历过最惨的一次事故:某天调度系统挂了,特征没产出,线上模型用了昨天的特征,推荐效果暴跌30%。从那以后,我对调度系统格外重视。
调度系统的核心设计
- 依赖管理:特征A依赖特征B,必须等B产出后再跑A。用DAG调度(比如Airflow、DolphinScheduler)
- 重试机制:任务失败自动重试3次,间隔5分钟。如果还失败,发告警
- 数据校验:产出后自动校验行数、空值率、分布漂移。我习惯加一个「特征质量检查」步骤
- 链路追踪:每个特征记录加工时间、数据源、版本号,方便排查问题
回溯:为什么难?怎么解?
回溯,说白了就是「用今天的特征逻辑,重新算一遍历史数据」。比如你优化了特征计算公式,想看看在历史样本上的效果,就必须回溯。
回溯的难点在于:历史数据量大、计算资源消耗高、容易踩坑。
我分享一个回溯的最佳实践:
- 增量回溯:不要一次性回溯全部历史,按天或按周分批回溯。比如先回溯最近30天,验证没问题再回溯更早的数据
- 快照表设计:在HBase中按「特征名_日期」作为RowKey,这样回溯时只需要覆盖特定日期的数据
- 并行度控制:回溯任务很吃资源,建议控制并发数,避免影响线上任务。我一般设置最大10个并行回溯任务
- 结果对比:回溯完成后,自动对比新旧特征分布。如果差异超过阈值,触发人工审核
避坑指南:我曾经回溯一个用户画像特征,因为没注意时间窗口的边界问题,导致回溯结果和原始结果差了20%。后来加了「时间戳对齐」校验才解决。记住:回溯时的时间窗口必须和原始加工时完全一致。
本章知识体系总览
下面这张图,把离线特征工程的完整链路串起来了。你可以看到从数据源到特征加工、存储、调度、回溯的完整流程。
嗯,这一章的内容就到这里。离线特征工程看似基础,但里面的坑真不少。从Hive和Spark的选型搭配,到HBase和Redis的存储策略,再到调度回溯的工程实践,每一步都决定了特征平台的稳定性和效率。
记住一句话:离线特征做不好,在线模型跑不了。希望这些实战经验能帮你少走弯路。