3. 离线特征系统架构:典型离线特征ETL流程、Hive/Spark特征表设计、离线特征仓库分层
离线特征系统是特征工程体系的基石,承担着从海量历史数据中批量加工、存储和分发特征的核心职责。一个设计良好的离线特征架构,需要同时兼顾ETL流程的健壮性、特征表结构的可扩展性以及数据仓库分层的治理规范性。本节将围绕这三个维度展开详细阐述。
3.1 典型离线特征ETL流程
离线特征的ETL(Extract-Transform-Load)流程通常以天级或小时级为调度周期,覆盖从原始数据接入到特征落表的全链路。一个标准化的离线特征ETL流程包含以下关键阶段:
- 数据抽取(Extract):从业务数据库(MySQL、PostgreSQL)、日志系统(Kafka、Flume)或外部数据源拉取原始数据。通常使用Sqoop、DataX或Spark JDBC连接器完成全量/增量抽取。
- 数据清洗(Clean):处理缺失值、异常值、重复数据,统一字段格式(如时间戳标准化、枚举值映射),剔除无效记录。
- 特征计算(Transform):执行核心特征逻辑,包括聚合统计(COUNT、SUM、AVG)、滑动窗口计算(近7天/30天行为统计)、交叉组合、分箱离散化、Embedding向量化等。
- 质量校验(Validate):对产出特征进行完整性、分布漂移、空值率等质量检查,触发告警或阻断下游依赖。
- 数据加载(Load):将最终特征写入离线特征仓库的对应分层(ODS/DWD/DWS/ADS),并更新特征元数据。
实践要点: 建议将ETL流程拆分为多个可重用的Spark Job,每个Job只负责一个特征域(如用户画像、商品属性、行为序列),通过统一的调度平台(如Airflow、DolphinScheduler)编排DAG,便于故障隔离和重跑。
3.2 Hive/Spark特征表设计
特征表是离线特征仓库的核心载体,其设计质量直接影响下游模型训练和在线服务的效率。Hive和Spark SQL是离线特征表最常用的存储与计算引擎,设计时需遵循以下原则:
3.2.1 特征表设计原则
| 设计维度 | 推荐做法 | 避坑指南 |
|---|---|---|
| 分区策略 | 按日期(dt)分区,必要时增加二级分区(如业务线、地域) | 避免过多小文件,分区数控制在1000以内 |
| 存储格式 | Parquet(列存+压缩),ORC亦可 | 避免使用TextFile或CSV,IO效率低 |
| 分桶设计 | 对高频JOIN键(如user_id)进行分桶,桶数=集群核数×2~3 | 分桶数不宜过多,否则元数据膨胀 |
| 字段类型 | 数值型用INT/BIGINT/FLOAT,字符串用STRING,时间用TIMESTAMP | 避免使用DECIMAL高精度类型,影响计算性能 |
| 空值处理 | 数值型默认-999或0,字符串默认"UNKNOWN" | 不要保留NULL,会导致模型训练报错 |
| 版本管理 | 特征表名后缀加_v1/_v2,或使用Hive分区字段标识版本 | 避免原地覆盖,保留历史版本便于回滚 |
3.2.2 典型特征表DDL示例
以下是一个用户行为聚合特征表的Hive DDL示例,展示了分区、分桶、存储格式和字段设计的标准实践:
-- 用户近7天行为聚合特征表
CREATE EXTERNAL TABLE IF NOT EXISTS dws.user_behavior_agg_features_v1 (
user_id BIGINT COMMENT '用户ID',
cnt_7d_login INT COMMENT '近7天登录次数',
cnt_7d_purchase INT COMMENT '近7天购买次数',
amt_7d_purchase FLOAT COMMENT '近7天购买金额',
avg_amt_per_order FLOAT COMMENT '近7天平均订单金额',
last_active_dt STRING COMMENT '最后活跃日期',
feature_dt STRING COMMENT '特征计算日期'
)
COMMENT '用户近7天行为聚合特征表 v1'
PARTITIONED BY (dt STRING COMMENT '数据分区日期')
CLUSTERED BY (user_id) INTO 64 BUCKETS
STORED AS PARQUET
TBLPROPERTIES ('parquet.compression'='SNAPPY');
设计提示: 特征表应尽量保持宽表结构,将同一实体(用户/商品)的所有特征放在同一张表中,避免模型训练时频繁JOIN。单表字段数建议控制在200以内,超出时可拆分为多个特征域宽表。
3.3 离线特征仓库分层
离线特征仓库借鉴数据仓库的分层思想,将特征数据按照加工程度和使用场景划分为多个层次。合理的分层能够实现数据血缘清晰、复用性高、故障影响范围可控。典型的离线特征仓库分为以下四层:
3.3.1 各分层职责与特征表命名规范
| 分层 | 英文全称 | 核心职责 | 表命名示例 | 数据保留周期 |
|---|---|---|---|---|
| ODS | Operational Data Store | 原始数据镜像,保留业务系统全量/增量日志,不做任何特征加工 | ods.user_login_log |
30~90天 |
| DWD | Data Warehouse Detail | 数据清洗、去重、格式统一,保留最细粒度行为明细 | dwd.user_behavior_detail |
30~180天 |
| DWS | Data Warehouse Summary | 特征计算核心层:聚合统计、滑动窗口、交叉组合,产出宽表特征 | dws.user_behavior_agg_features_v1 |
7~30天(模型训练用) |
| ADS | Application Data Store | 面向具体模型/业务场景的特征集市,按需裁剪字段和样本 | ads.ctr_model_user_features_v2 |
7天(随模型版本迭代) |
3.3.2 分层设计的关键考量
- 数据复用性:DWD层保留最细粒度明细,支持多个DWS特征域复用同一份清洗后数据,避免重复计算。
- 故障隔离:ODS层故障不影响DWS/ADS层的历史数据;DWS层重跑只需依赖DWD层,无需重新抽取原始数据。
- 成本控制:ODS和DWD层使用冷存储(如HDFS归档),DWS和ADS层使用热存储(如Alluxio加速),平衡查询性能与存储成本。
- 血缘追踪:每层特征表在元数据系统中记录上游依赖和下游消费方,便于影响分析和问题溯源。
常见误区: 部分团队跳过DWD层,直接从ODS层进行特征计算。这种做法会导致:① 清洗逻辑散落在各个特征Job中,难以统一维护;② 原始数据质量问题直接污染特征结果;③ 无法复用清洗后的明细数据。建议严格遵循四层架构,即使初期数据量较小,也要保留DWD层。
3.4 离线特征调度与依赖管理
离线特征ETL流程依赖严格的调度编排,确保各层数据按正确的顺序产出。推荐使用DAG调度框架(如Apache Airflow、DolphinScheduler)管理特征任务依赖:
# Airflow DAG 示例:离线特征每日调度
dag = DAG(
dag_id='offline_feature_daily',
schedule_interval='0 2 * * *', # 每天凌晨2点执行
start_date=datetime(2024, 1, 1),
catchup=False
)
# 定义各层任务
ods_task = SparkSubmitOperator(task_id='ods_load', ...)
dwd_task = SparkSubmitOperator(task_id='dwd_clean', ...)
dws_task = SparkSubmitOperator(task_id='dws_feature_compute', ...)
ads_task = SparkSubmitOperator(task_id='ads_model_features', ...)
quality_task = PythonOperator(task_id='quality_check', ...)
# 设置依赖关系
ods_task >> dwd_task >> dws_task >> quality_task >> ads_task
调度最佳实践: ① 每个特征域独立DAG,避免单点故障影响全局;② 设置任务重试机制(如失败后重试3次,间隔5分钟);③ 添加数据质量检查节点,阻断异常数据向下游扩散;④ 记录每次调度的特征产出元数据(行数、分区数、特征分布摘要),便于事后审计。