3. 特征回放系统架构:离线回放 vs 在线回放、回放管道设计

特征回放系统的架构设计直接决定了回放实验的准确性、时效性与资源成本。在 A/B 测试场景中,我们需要根据实验目标(离线验证 vs 线上实时干预)选择不同的回放模式,并构建标准化的回放管道。

3.1 离线回放 vs 在线回放

两种模式的核心差异在于“回放时机”与“数据来源”。离线回放使用历史日志批量计算特征,适合模型迭代与策略验证;在线回放则基于实时事件流,在线上服务中即时计算特征,适合低延迟干预实验。

对比维度 离线回放 在线回放
数据源 历史日志(HDFS / Hive / S3) 实时消息队列(Kafka / Pulsar)
计算模式 批处理(Spark / Hive SQL) 流处理(Flink / Spark Streaming)
延迟要求 小时级 ~ 天级 毫秒级 ~ 秒级
特征时效性 使用历史时间点的快照特征 使用当前最新特征
适用场景 模型离线评估、特征重要性分析、长周期实验 线上策略实时干预、动态定价、实时推荐
资源成本 较低(可复用离线数仓) 较高(需常驻流计算资源)
回放一致性 易保证(可回溯至精确时间点) 难保证(依赖事件时间对齐)
实践建议:在 A/B 测试的特征回放中,建议采用“离线回放为主、在线回放为辅”的策略。离线回放用于大规模特征验证与模型训练,在线回放仅用于需要实时反馈的线上实验。两种模式应共享同一套特征计算逻辑,避免因实现差异导致实验偏差。

3.2 回放管道设计

标准化的特征回放管道由四个核心阶段组成:数据源 → 特征计算 → 存储 → 服务。每个阶段在离线与在线模式下有不同的技术选型与处理逻辑。

3.2.1 管道四阶段详解

  1. 数据源(Data Source)
    • 离线:从 Hive 表或 HDFS 文件中读取用户行为日志、物品属性快照、上下文日志。
    • 在线:从 Kafka Topic 中消费实时行为事件,并关联 Redis 中的最新状态数据。
    • 关键要求:数据源必须包含精确的时间戳(event_time),用于对齐回放时间窗口。
  2. 特征计算(Feature Computation)
    • 离线:使用 Spark SQL 或 PySpark 执行特征 SQL 逻辑,支持窗口聚合、跨表 Join、UDF 扩展。
    • 在线:使用 Flink SQL 或自定义 ProcessFunction 实现增量特征计算,维护状态后端(RocksDB)。
    • 关键要求:离线与在线必须使用同一份特征配置(如 YAML 或 JSON 定义的特征元数据),确保计算逻辑一致。
  3. 存储(Storage)
    • 离线:计算结果写入 Hive 分区表或 Parquet 格式的列存文件,按日期 + 实验标识分区。
    • 在线:计算结果写入在线特征存储(如 Redis Cluster / HBase),设置 TTL 以控制内存占用。
    • 关键要求:存储层需支持时间旅行查询,即能够根据回放时间戳获取当时计算的特征值。
  4. 服务(Serving)
    • 离线:通过 Presto / Trino 或 Spark Thrift Server 提供 SQL 查询接口,供分析师与模型训练脚本调用。
    • 在线:通过高并发 RPC 服务(gRPC / Thrift)暴露特征获取接口,响应时间 < 10ms。
    • 关键要求:服务层需支持特征版本管理,确保实验组与对照组使用同一版本的特征逻辑。
管道设计核心原则:“一次定义,多处运行”。特征计算逻辑在特征元数据中统一声明,离线批处理与在线流处理共享同一份配置,从源头消除计算偏差。

3.3 系统组件图

下图展示了特征回放系统的完整组件架构,涵盖离线与在线两条管道,以及它们共享的特征元数据层与存储层。

特征回放系统组件架构图 数据源层 离线日志 HDFS Hive 行为表 实时 Kafka 事件 Redis 状态 特征计算层 离线批处理 Spark SQL / Hive SQL 在线流处理 Flink SQL / ProcessFunction 特征元数据配置 YAML / JSON 统一声明 存储层 Hive 分区表 / Parquet Redis Cluster / HBase 特征版本快照存储 服务层 Presto / Trino SQL 接口 gRPC / Thrift 高并发服务 特征版本路由 & 实验分流 说明:离线管道(蓝/紫/绿)与在线管道(青/红/绿)共享特征元数据配置,存储层支持时间旅行查询

3.4 管道设计关键考量

  • 特征一致性保障:离线与在线管道必须使用同一份特征计算代码或配置。推荐将特征逻辑封装为独立的计算库(如 Java/Python SDK),在批处理和流处理中统一调用。
  • 时间旅行能力:存储层需要支持按历史时间戳查询特征值。离线场景下通过 Hive 分区字段实现;在线场景下通过 Redis 的带时间戳的 Sorted Set 或 HBase 的版本号实现。
  • 实验隔离:服务层需根据实验标识(experiment_id)路由到对应的特征版本,确保实验组与对照组使用正确的特征逻辑,避免相互干扰。
  • 监控与告警:在管道各阶段埋点监控数据延迟、计算耗时、特征缺失率等指标。离线管道关注数据产出时间(SLA),在线管道关注 p99 延迟与吞吐量。
常见陷阱:离线回放与在线回放使用不同的特征计算实现(如离线用 Hive UDF,在线用 Flink UDF),导致同一特征在两种模式下计算结果不一致。这种偏差会直接污染 A/B 测试结论。务必通过特征元数据配置强制统一计算逻辑。

3.5 回放管道代码示例

以下是一个简化的特征回放管道配置示例,展示如何通过 YAML 统一声明特征计算逻辑,并分别用于离线 Spark 任务与在线 Flink 任务。

# feature_replay_config.yaml
features:
  - name: user_click_count_7d
    type: window_agg
    window: 7d
    source: user_behavior_log
    aggregation: count
    filter: action_type = 'click'
    offline_sql: |
      SELECT user_id, COUNT(*) AS click_count_7d
      FROM user_behavior_log
      WHERE action_type = 'click'
        AND event_time >= DATE_SUB('${replay_date}', 7)
        AND event_time < '${replay_date}'
      GROUP BY user_id
    online_flink: |
      SELECT user_id, COUNT(*) AS click_count_7d
      FROM user_behavior_stream
      WHERE action_type = 'click'
      GROUP BY user_id, TUMBLE(event_time, INTERVAL '7' DAY)

  - name: item_price
    type: lookup
    source: item_catalog
    key: item_id
    offline_sql: |
      SELECT item_id, price AS item_price
      FROM item_catalog_snapshot
      WHERE dt = '${replay_date}'
    online_flink: |
      -- 从 Redis 中 lookup 最新价格
      CREATE TEMPORARY TABLE item_catalog_redis (
        item_id STRING,
        price DOUBLE,
        PRIMARY KEY (item_id) NOT ENFORCED
      ) WITH (
        'connector' = 'redis',
        'redis-mode' = 'cluster',
        'lookup.cache.max-rows' = '10000'
      );
      SELECT item_id, price AS item_price FROM item_catalog_redis
工程落地要点:上述配置中的 offline_sql 与 online_flink 字段虽然语法不同,但语义完全对齐。建议在 CI/CD 流程中增加“特征一致性校验”步骤,对同一份输入数据分别运行离线与在线逻辑,比对输出结果是否一致。