2. 实时与批量特征:两种生产模式对比、延迟与吞吐量权衡

做特征平台这些年,我见过不少团队在「实时还是批量」这个问题上反复纠结。说实话,没有银弹。两种模式各有各的脾气,选错了,后面运维起来真是欲哭无泪。

这一章,咱们就把这两种生产模式掰开揉碎,看看它们到底差在哪,以及怎么在延迟和吞吐量之间找到那个平衡点。

2.1 批量特征生产模式

批量特征,说白了就是「定时算好,存起来等用」。我最早接触特征工程时,用的就是这套路子。

核心流程

  1. 数据源:通常是 Hive 表、离线数仓、或者日志文件。
  2. 调度触发:按天/小时跑 Spark 或 Hive 任务。
  3. 特征计算:做聚合、Join、窗口函数。
  4. 结果写入:写到 KV 存储(如 Redis、HBase)或离线表。
  5. 线上读取:模型服务通过 Key 直接查。

典型场景:用户画像标签、历史统计特征(近7天点击率)、CTR 模型的基础特征。

优点

  • 吞吐量极高:Spark 集群一跑,几十亿条数据轻松搞定。
  • 逻辑简单:回溯、重算、调试都很方便。
  • 成本可控:离线计算资源便宜,按需调度。

缺点

  • 延迟高:至少 T+1,甚至小时级。用户刚发生的动作,要等下次调度才能反映到特征里。
  • 数据漂移:调度时间和事件时间对不上,容易算错。

我的经验:批量特征适合「变化慢、依赖历史长」的场景。比如用户的性别、年龄、会员等级,这些没必要实时算。

2.2 实时特征生产模式

实时特征,就是「事件一来,立刻算,立刻用」。我记得第一次做实时特征项目时,被 Flink 的状态管理和 exactly-once 语义折腾得不轻。

核心流程

  1. 数据源:Kafka 消息队列,事件流。
  2. 流式计算:Flink/Spark Streaming 做滑动窗口、计数、去重。
  3. 状态存储:RocksDB 或内存状态。
  4. 结果推送:写入 Redis/内存网格,或直接推给模型服务。
  5. 线上读取:毫秒级响应。

典型场景:实时点击率、用户实时行为序列、风控中的设备指纹特征。

优点

  • 延迟极低:秒级甚至毫秒级。
  • 时效性强:用户刚点了一下,特征立刻更新。
  • 支持滑动窗口:比如「过去5分钟点击次数」,批量模式很难做到。

缺点

  • 吞吐量受限:单节点处理能力有限,需要做分区和扩容。
  • 状态管理复杂:窗口过期、状态快照、故障恢复,都是坑。
  • 成本高:实时计算资源贵,运维复杂度高。

避坑指南:我曾经在一个项目中,把「用户实时行为序列」全量存到 Redis,结果内存直接爆了。后来改成只保留最近100条,才稳住。

2.3 延迟与吞吐量的权衡

你想想看,延迟和吞吐量本质上是一对矛盾。想要延迟低,就得牺牲吞吐量;想要吞吐量高,就得接受延迟。

为什么会这样?因为实时计算需要保持状态,状态越多,处理越慢。批量计算可以分片并行,吞吐自然高。

对比表格

维度 批量特征 实时特征
延迟 分钟~小时级 毫秒~秒级
吞吐量 极高(可扩展) 中等(受状态限制)
计算引擎 Spark、Hive、MapReduce Flink、Spark Streaming、Kafka Streams
存储 HDFS、Hive 表 Redis、内存网格、RocksDB
数据一致性 最终一致(容易回溯) 精确一次(实现复杂)
运维成本 低 高
适用特征 历史统计、画像标签 实时行为、滑动窗口

如何选择?

我个人习惯按「特征新鲜度要求」来分:

  • 新鲜度要求 < 1小时:用实时模式。
  • 新鲜度要求 1小时~1天:用微批(Mini-batch),比如每5分钟跑一次 Spark。
  • 新鲜度要求 > 1天:用批量模式,省心省力。

小技巧:很多团队会做「Lambda 架构」——批量算全量,实时算增量,最后合并。但要注意,合并逻辑容易出 bug,我建议尽量用纯实时或纯批量,别搞混搭。

2.4 核心知识结构图

下面这张图,帮你把两种模式的核心差异和权衡关系理清楚。

实时与批量特征对比结构图 实时 vs 批量特征:延迟与吞吐量权衡 批量特征生产模式 延迟:分钟~小时级 吞吐量:极高(可扩展) 引擎:Spark / Hive 适用:历史统计、画像标签 实时特征生产模式 延迟:毫秒~秒级 吞吐量:中等(受状态限制) 引擎:Flink / Kafka Streams 适用:实时行为、滑动窗口 权衡核心: 低延迟 ↔ 高吞吐量,不可兼得

2.5 混合策略:Lambda 架构

嗯,这里要注意。虽然我前面说尽量别搞混搭,但现实项目中,Lambda 架构还是很常见的。

简单来说:

  • 批量层:每天算全量特征,保证数据完整。
  • 实时层:实时算增量,保证新鲜度。
  • 合并层:把批量结果和实时结果合并,输出最终特征。

我曾经踩过的坑:合并逻辑里,实时增量覆盖批量数据时,时间窗口没对齐,导致特征值跳变。后来加了「版本号」和「时间戳校验」才解决。

代码示例:Flink 实时窗口聚合

// 实时特征:过去5分钟用户点击次数
DataStream<ClickEvent> clicks = env.addSource(kafkaConsumer);

clicks
  .keyBy(event -> event.getUserId())
  .window(SlidingProcessingTimeWindows.of(
      Time.minutes(5),   // 窗口大小
      Time.minutes(1)))  // 滑动步长
  .aggregate(new CountAggregate())
  .map(count -> {
      // 写入 Redis,供模型服务读取
      redisClient.set("user:click:5min:" + count.getUserId(), 
                      String.valueOf(count.getCount()));
      return count;
  });

个人建议:如果团队实时能力不强,先用「微批」过渡。比如每1分钟跑一次 Spark 任务,效果接近实时,但运维简单得多。

好了,这一章的核心就是:批量重吞吐,实时重延迟。选型时别贪心,想清楚你的特征到底需要多新鲜,再决定走哪条路。