深夜的老王大排档门口挂着两块菜单:一块是清晨就卤好的老汤卤味,客人来了舀一勺就能上桌;另一块是明档现炒的小炒菜单,客人点一份爆炒牛肉,师傅立刻开火、下葱姜、颠勺、出锅,全程不过三分钟。前者像离线特征——提前批量算好,便宜但不够新鲜;后者像实时特征——数据刚到就立刻加工,新鲜但需要一套“现炒”的流水线。本文要讲的流处理引擎,就是那位灶台前的老师傅:他盯着源源不断送来的订单(事件流),掐着大火快炒的时间窗口,靠手上的小本子记录每桌进度(状态),再根据订单上的时间签判断该不该出锅(水位线),最后保证每份菜和每张账单都分毫不差(精确一次)。读完你会明白,实时特征计算不是把离线任务加速,而是一套全新的“现炒”哲学。
理解实时特征计算与流处理引擎,是成为数据工程师、AI 开发者和架构师的必修课。它决定了你的模型在风控、推荐、物联网等场景中能否“看一眼数据就立刻做决定”。
可视化演示
展示流处理架构数据流转、窗口聚合机制(滚动/滑动/会话窗口)、以及延迟与吞吐优化平衡三大核心维度。
本节配有交互动效(进入视口后自动加载,约 3KB)。
实时特征计算 流处理引擎:数据源→解析→计算→聚合→特征存储→在线服务 窗口机制:滚动窗口固定边界、滑动窗口重叠覆盖、会话窗口动态间隔 Latency ≈ α·e^(-β·Throughput) 延迟与吞吐权衡:寻找最优工作点平衡性能指标
实时特征概述
老王大排档门口有两块菜单:卤味提前一天备好,客人点了就切盘上桌;小炒则必须等客人下单后,师傅才开火现炒。卤味的优势是便宜稳定,劣势是没法根据客人当下的口味临时调整;小炒的优势是新鲜热辣,劣势是每一步都怕来不及。实时特征计算,就是给 AI 模型开出一条“小炒档口”,让模型在收到请求的瞬间,能拿到刚刚发生的数据。
在特征工程里,特征就是模型用来做判断的“食材”。离线特征通常由批处理任务每天或每小时计算一次,适合模型训练和一般性画像;实时特征则在事件发生后几百毫秒到几秒内完成计算,直接服务于在线推理。比如支付风控模型需要判断“这笔交易发生前 1 分钟内,这张卡在多少台设备上刷过”,如果只靠离线特征,盗刷早就得手了。实时特征要解决的核心问题,是端到端的“新鲜度”,也就是从数据发生到特征被模型读取的总耗时。
所以,实时特征不是简单地把离线任务“跑快一点”。老王就算把卤味热得再快,也变不成根据当下订单现炒的小炒。实时特征要求整条链路从接入到服务都以事件驱动,数据一到就触发计算,这才是“即点即炒”的本意。
- L_total(总延迟)端到端延迟,表示从事件真实发生到特征可被模型使用的总时间。
- t_ingest(接入耗时)事件从业务系统进入流处理引擎所花的时间,相当于食材从后门送到灶台。
- t_compute(计算耗时)流引擎执行窗口聚合、过滤、关联等操作的时间,对应师傅切菜、颠勺的过程。
- t_serve(服务耗时)特征写入在线存储并被模型服务读取的时间,相当于炒好的菜端上桌。
流处理基础
老王后厨的食材不是一次性整筐采购到位的,而是送货师傅每隔几分钟就从后门送一批进来:葱姜蒜、牛肉、河虾,源源不断。这就是数据流。离线批处理像是一次性把整筐菜搬进仓库,再统一择洗;流处理则像食材一到后门,师傅马上决定是切丝、切片还是直接下锅。
流数据可以分成有界流和无界流。有界流有明确的开始和结束,比如“今天 0 点到 24 点的所有订单”,本质上可以用批处理;无界流没有终点,比如用户点击日志、传感器读数,必须用流引擎持续处理。流处理中还有一个关键区分:事件时间与处理时间。事件时间是订单上打印的“下单时间”,处理时间是师傅实际看到订单的“墙上时钟时间”。网络抖动、排队都会让两者不一致,这就像外卖订单上写着 19:30 下单,但厨师 19:45 才拿到单。
处理时间与事件时间的差异,决定了实时特征到底“实时”到什么程度。如果只按处理时间来算,一顿操作猛如虎,结果却可能是把 20 分钟前的菜当新鲜菜端上桌。流处理框架的种种机制,本质上都是在帮师傅把“订单时间”和“墙上时间”对齐。
- t_processing(处理时间)流引擎真正开始处理这条数据的时间,是师傅看到订单的时刻。
- t_event(事件时间)业务上事件真实发生的时刻,是客人下单时打出的时间签。
- t_network(网络耗时)事件从源头传到流引擎经过的网络延迟,相当于订单从收银台传到后厨的传单时间。
- t_queue(排队耗时)事件在缓冲区或算子队列中等待的时间,类似饭点灶台前等着下锅的订单排成一串。
窗口计算
大排档的师傅不会等十份订单全部到齐才开始炒菜,而是按时间“凑一锅”:每 5 分钟把新到的订单理一遍,或者每来 3 个菜就开一次火。这就是窗口计算。它是把无界流切成一块块有限片段的方法,每块片段计算一次实时特征。
常见的窗口有四种。滚动窗口固定长度、互不重叠,比如每 5 分钟统计一次交易次数;滑动窗口有固定长度和固定滑动步长,窗口之间会有重叠,适合“最近 1 分钟窗口、每 10 秒刷新一次”的需求;会话窗口按活动的空闲间隔切分,用户连续操作 10 秒算一场会话,超过 30 秒没操作就切开;计数窗口则不按时间,而是攒够指定条数的事件就触发一次计算。对实时特征来说,窗口内所有事件往往会被聚合成一个值,比如均值、求和、最大值。
窗口计算决定了“哪些数据凑在一起算一个特征”。窗口设得太短,特征容易抖动;窗口设得太长,特征又不够敏感。师傅要根据菜品的性质和客人的耐心来选择火力窗口,才能在“稳”和“快”之间找到平衡。
- W(t)当前时间 t 对应的窗口集合,窗口内的数据才是这锅菜该用的食材。
- |W(t)|窗口内事件的数量,告诉师傅这一锅一共有多少份食材。
- 求和符号把窗口内每个事件值 x_i 全部加起来,对应把食材都倒进锅里翻炒。
- F(t)计算得到的实时特征,是这锅菜最终的“口味指标”,被送到模型里做判断。
状态管理
师傅炒菜时不光要下锅,还得记得“三号桌的菜不要放香菜”“五号桌正在催单”。这些记在小本子上的信息,就是流处理中的状态。状态让算子拥有“记忆”,能够跨多条事件保存进度。没有状态,事件就像失忆的顾客,每来一道菜都像是第一次。
在流引擎里,状态分为算子状态和键控状态。算子状态是全局的,比如统计整个流处理任务处理了多少条数据;键控状态按 key 隔离,比如同一张银行卡的交易记录、同一个用户的点击序列,都维护一份独立的状态。状态需要存放在状态后端中,常见的选择包括内存和 RocksDB。内存快但容量有限,RocksDB 用磁盘换容量但访问稍慢,就像师傅有的菜谱记在脑子里,有的菜谱存在厚本子里。状态还必须定期做快照,一旦机器宕机,就从最近一次快照恢复,避免“小本子被水泼了全忘光”。
状态是实时特征计算的“记忆中枢”,但它不是越大越好。小本子记太多,翻起来也累;状态过大会拖慢性能、拉长故障恢复时间。好的状态设计要尽量精简、设置过期时间、按业务维度合理划分 key,让师傅既记得住,又翻得快。
- S_total(状态总量)整个流作业需要保存的全部状态大小,决定后厨需要多大的柜子来放手写菜谱。
- K(键数量)有多少个独立 key,比如多少张卡、多少个用户,相当于小本子上登记了多少桌客人。
- S_per_key(单键状态)每个 key 平均保存的数据量,比如每桌客人记了多少条忌口和偏好。
时间与水位线
老王给每张小票都盖了时间戳,但客人经常把小票攥在手心里很久才递给师傅。于是,师傅面前的小票可能有先来后到乱序的情况:19:31 的小票先到,19:32 的小票反而后到。如果师傅只有一面墙上的挂钟,就会错误地判断订单的新鲜度。流处理也面临同样的问题,因此必须区分事件时间和处理时间,并用“水位线”来标记事件时间的进度。
水位线是一条特殊的时间标记,它向流引擎宣布:“到目前为止,所有事件时间早于这个水位线的事件我都见过了,窗口可以放心关闭。”但现实总有迟到数据,所以水位线通常会留出一定的余量。比如事件最大可能迟到 60 秒,那么水位线就是当前观察到的最小事件时间再减 60 秒。水位线太保守,窗口会迟迟不关,特征延迟变高;水位线太激进,又会漏掉很多迟到数据。这就像师傅不能为了等一桌迟迟不来的客人,让其他所有桌都饿着。
水位线不是越快越好,也不是越稳越好。它像大排档的打烊时间:定早了,晚到的客人吃不上菜;定晚了,大家陪你一起熬夜。工程上一般会配合“允许延迟”和“旁路输出”来处理迟到数据,既保证窗口及时出结果,又不会把迟到事件白白扔掉。
- WM(水位线)当前事件时间的进度标记,窗口只负责处理水位线之后到达的“过期订单”。
- min(最小值)在所有已见事件里挑出事件时间最小的那一个,代表目前时间线上最落后的参照点。
- t_e(事件时间)单个事件真实发生的时间,是小票上盖的原始时间戳。
- ε(缓冲量)允许的最大乱序幅度,相当于师傅给迟到客人预留的宽限时间。
精确一次语义
客人吃完饭最关心账单:不能多算一道菜,也不能漏算一道汤。流处理也有类似的承诺,叫做交付语义。至少一次(at-least-once)保证数据不丢,但可能重复处理,像账单上同一道菜可能被重复算了两次;最多一次(at-most-once)保证不重复,但可能丢数据,像师傅忙乱中忘了把某个菜记上账;精确一次(exactly-once)则要求最终结果既不多也不少。
要实现精确一次,光靠引擎内部还不够。常规做法是“三件套”:源头可重放,让失败后能重新读数据;状态检查点,定期给全部状态拍快照;输出幂等,让重复写入最终只生效一次。检查点相当于后厨每周一拍的库存照片,万一停电了,就根据最近一张照片把食材和账目恢复原样。恢复需要时间,这段时间直接影响服务可用性。
精确一次并不是魔法,而是把“结账”这件事设计得可以重算、不怕重复。只要最终账单一致,中间过程重新打一遍小票也没关系。实时特征系统只有做到端到端精确一次,模型拿到的特征值才不会因为故障重启而突然跳变。
- T_recovery(恢复耗时)从故障发生到状态完全恢复、可以继续服务所花的总时间。
- T_load(加载耗时)把最新的状态快照读入内存的时间,相当于翻出库存照片并照着把货架摆好。
- T_replay(重放耗时)从快照之后重新消费源头数据,把缺失的计算补回来的时间。
实时特征实践
老王的档口再厉害,也得有一排整齐的调料罐和配菜盒:青椒丝、葱段、姜末,都分门别类放在明档里,客人点单后伸手就能抓。实时特征工程的“明档”,就是特征存储。离线算好的特征和实时算出的特征都会写入特征存储,在线服务再用极低的延迟读出来喂给模型。
工程实践中有一个重要理念叫批流一体:同一套特征逻辑,既能在批处理上全量回填历史数据,也能在流处理上实时更新增量数据。这样训练时用的特征和在线推理时用的特征完全一致,避免“训练和上线两张皮”。监控也不可或缺,需要持续跟踪特征年龄、缺失率、分布漂移等指标。如果特征值长期不更新,相当于调料罐已经空了,师傅还在假装撒椒盐。
特征存储、在线服务、批流一体、特征监控,这四个环节加在一起,才构成一个可信赖的实时特征平台。老王再会颠勺,如果调料罐空空如也,也做不出一道真正新鲜的小炒。
- Age_feature(特征年龄)衡量特征“放了多少天”的新鲜度,是实时特征健康度的核心指标。
- t_query(查询时刻)在线推理发起特征读取的时间,相当于客人点菜的那一刻。
- t_update(更新时刻)该特征上次被流任务更新到特征存储的时间,相当于师傅最后一次往调料罐里加料。
流引擎选型
不同档口需要不同的灶台:老王炒小炒用猛火灶,卤味用慢炖锅,预制菜则用蒸箱。流处理引擎也是各有所长。Flink 擅长低延迟、强状态、复杂事件时间处理,适合风控和实时特征计算这类对新鲜度敏感的任务;Spark Structured Streaming 采用微批模型,延迟通常在秒级,但吞吐高、与 Spark 生态集成好;Kafka Streams 则轻量内嵌在业务服务里,适合“顺手在消息队列边上算个特征”的场景。
选型不能只看谁的名气大,而要综合延迟、吞吐、状态规模、运维成本和技术栈匹配度。用统一公式来想:引擎的“效用”等于吞吐除以成本与延迟的乘积。对交易风控来说,延迟权重 α 要大,哪怕多花成本也要更低延迟;对日志分析类任务,吞吐权重更大,延迟稍高也能接受。没有最好的引擎,只有最适合你菜单的灶台。
所以,实时特征团队在选型时要像老王一样冷静:卖卤味的不一定需要猛火灶,但做小炒的一定要火力跟得上。把业务对新鲜度的要求换算成延迟指标,再和自己的技术栈与运维能力对齐,才能选出真正合用的引擎。
- U(效用)流引擎选型综合评分,代表这套方案在成本、延迟、吞吐之间的整体性价比。
- T(吞吐)单位时间内能处理的事件数量,相当于师傅一分钟能炒几份菜。
- C(成本)部署、维护和资源消耗的总成本,相当于灶台、燃气和厨师的工资。
- L(延迟)事件从进入系统到输出特征的时间,是客人从点单到上菜等待的时长。
AI 应用场景
实时风控特征
阶段:特征计算与在线推理
解决的问题:传统风控依赖离线批处理特征,通常 T+1 才能更新,盗刷发生时模型根本看不到“这张卡刚刚在异地刷过”的信号。实时特征把交易频次、金额突变、设备切换等信号在毫秒到秒级算好,让风控模型在每笔交易发生时立即判断风险。
具体案例:某支付平台把每笔交易事件接入 Flink,用 1 分钟滚动窗口统计“同卡交易次数”,用 5 分钟滑动窗口计算“同设备金额均值”,特征写入 Redis 特征存储,风控模型读取延迟控制在 20 毫秒内,盗刷拦截率提升了 37%。
个性化推荐实时特征
阶段:特征存储与模型服务
解决的问题:用户刚点击了一个商品,如果推荐系统还在用昨天算好的离线兴趣特征,就无法捕捉“此刻想买什么”的意图。实时特征让点击、收藏、加购等行为立即更新用户兴趣向量,推荐结果更贴合当下。
具体案例:某短视频平台的推荐团队用 Kafka Streams 在消息队列旁直接计算用户“最近 5 分钟浏览类目分布”,更新后的特征写入特征库,推荐服务每次请求都拿到最新兴趣画像,页面人均点击率提升 12%。
物联网设备异常检测
阶段:流式窗口计算与告警
解决的问题:工厂传感器每秒上报温度、振动、电流等数据,离线分析只能事后补救。实时特征计算用滑动窗口维护均值、方差和变化率,配合水位线处理传感器上报乱序的问题,能在异常发生后的几秒内触发告警。
具体案例:某制造企业在生产线上用 Spark Structured Streaming 处理 5 万台设备的传感器流,每 10 秒计算一次“最近 1 分钟温度标准差”,超出阈值立即推送维修工单,非计划停机时长下降了 28%。
Python 演示
import timefrom collections import deque class RealtimeFeatureComputer: def __init__(self, window_seconds=5.0): self.window_seconds = window_seconds self._buffer = deque() self._sum = 0.0 def push(self, ts, value): self._buffer.append((ts, value)) self._sum += value cutoff = ts - self.window_seconds while self._buffer and self._buffer[0][0] < cutoff: _, old_value = self._buffer.popleft() self._sum -= old_value def mean(self): if not self._buffer: return 0.0 return self._sum / len(self._buffer) if __name__ == "__main__": computer = RealtimeFeatureComputer(window_seconds=5.0) base_ts = time.time() events = [ (base_ts - 4.0, 1.5), (base_ts - 3.0, 2.5), (base_ts - 1.0, 3.5), (base_ts - 0.5, 2.0), ] for ts, value in events: computer.push(ts, value) print("window event count:", len(computer._buffer)) print("window mean feature:", round(computer.mean(), 4))
self._buffer = deque()创建一个双端队列,相当于师傅灶台边正在等待处理的“小炒订单队列”。self._buffer.append((ts, value))新事件带着时间戳进入窗口,相当于新订单夹到排队夹子上。cutoff = ts - self.window_seconds计算窗口的过期门槛,超过这个时间戳的事件都算“凉了”。while self._buffer and self._buffer[0][0] < cutoff从队首把过期事件逐个剔除,保证窗口只保留最近 N 秒的数据。self._sum += value` 与 `self._sum -= old_value用累加和代替每次全量求和,让特征计算保持 O(1) 增量更新。return self._sum / len(self._buffer)返回窗口内的实时特征均值,也就是这锅菜的“平均口感指标”。
常见误区
误区我一直以为实时特征计算就是原来的离线代码跑得快一点,比如把每天跑的批处理改用分钟级调度,就算“准实时”了。
正解离线批处理是“卤味提前炖好”,实时特征计算是“小炒按单现炒”,两者的计算范式本质不同。实时特征由事件驱动,数据一到就触发窗口和状态更新;而调度再频繁的批处理,也仍然是等一批数据攒齐后再统一计算,无法做到逐事件响应。
为何常见:因为很多实时系统为了降低成本,确实用了“微批”方案,看起来像批处理加速,让人误以为实时等于“更快的批处理”。
误区我觉得水位线设得越大越好,这样所有乱序和迟到的数据都能被正确归入对应窗口,特征肯定更准确。
正解水位线越大,窗口关闭越晚,特征的延迟就越高。实时特征的核心是“及时”,如果为了等所有迟到数据把结果拖到几分钟后,这个特征就失去了实时价值。正确做法是根据业务容忍度设置缓冲量,并用 allowed lateness 和旁路输出兜底。
为何常见:初学者容易被“窗口关闭前必须等齐所有数据”的直觉带偏,忽略了实时计算里延迟与准确率本来就是一对需要权衡的矛盾。
误区我倾向于把所有历史数据都塞进状态里,觉得状态越多,算子能参考的信息越全,算出来的特征肯定越准。
正解状态需要内存或磁盘存储,状态太大会拖慢读写性能,也会让检查点和故障恢复变得漫长。就像师傅的小本子记了十条街所有客人的口味,真到出菜时反而翻不到该找的那一页。状态设计要精简,配合 TTL 和合理的 key 设计控制规模。
为何常见:因为“信息越多越好”在离线建模中常常成立,但流处理受资源和延迟约束,是一条有边界的生产流水线。
误区我以为 Flink 开了 exactly-once 之后,每条数据从源头到输出都绝对不会被处理两次,等于神仙保证。
正解精确一次通常指引擎状态更新和最终输出的效果一致,但故障恢复时数据仍然可能被重放,只是通过检查点和幂等输出让重复不会污染最终结果。想要端到端精确一次,还需要源头支持重放、sink 幂等,三件套缺一不可。
为何常见:“精确一次”这个词太有迷惑性,让人以为它消除了所有物理层面的重复,而实际上它消除的是“重复造成的错误影响”。
误区团队里只要引入 Flink,我就觉得所有特征都变成实时特征了,Spark 之类的框架都不算数。
正解实时与否取决于架构和延迟指标,而不是某个引擎的牌子。Spark Structured Streaming 的微批也能达到秒级延迟,Flink 如果配置不当也可能产生高延迟;反过来,Flink 的批模式也常被用来跑离线任务。选型要看是否满足窗口、状态、水位线等“现炒”能力。
为何常见:厂商和社区的宣传容易给人留下“Flink=实时”的刻板印象,忽略了真实业务里延迟目标才是判断标准。
面试题
Q1. 实时特征计算与离线批量特征最本质的区别是什么?
- A.离线用 SQL 实时用 Java
- B.批量按固定时间调度,实时按事件驱动← 正确答案
- C.离线算得慢实时算得快
- D.离线准确率低实时准确率高
答案:B。实时特征的核心是事件到达即触发计算,而不是单纯的速度快慢。离线任务即使缩短到分钟级调度,本质仍然是“攒一批再算”,无法做到逐事件响应,因此 B 正确。
Q2. 流处理中“事件时间”指的是什么?
- A.数据进入流引擎的时间
- B.事件真实发生的时间← 正确答案
- C.批处理任务的启动时间
- D.窗口关闭的那一瞬间
答案:B。事件时间是数据在业务侧真实发生的时刻,相当于小票上盖的下单时间。处理时间才是数据到达流引擎的时间,两者不一致时需要用水位线和窗口机制来对齐。
Q3. 下列哪一项不是常见的流处理窗口类型?
- A.滚动窗口
- B.滑动窗口
- C.会话窗口
- D.透视窗口← 正确答案
答案:D。常见的窗口有滚动窗口、滑动窗口、会话窗口和计数窗口,并没有“透视窗口”这种标准窗口类型。看到陌生概念时要警惕,它不是主流流处理框架支持的原语。
Q4. 流计算状态管理通常需要考虑哪些问题?
- A.状态存储介质选型← 正确答案
- B.故障时的状态恢复← 正确答案
- C.状态键规模与清理策略← 正确答案
- D.数据可视化颜色方案
答案:ABC。状态管理关心存哪里、怎么恢复、状态多大、怎么清理,对应状态后端、检查点和 TTL 等机制。数据可视化颜色属于图表设计问题,与状态管理无关。
Q5. 水位线(Watermark)的主要作用是什么?
- A.记录累计的处理延迟
- B.标记事件时间的进度← 正确答案
- C.加快网络的传输速度
- D.压缩状态后端的数据
答案:B。水位线向流引擎宣告事件时间的进度,帮助系统判断哪些窗口可以安全关闭。它既不是延迟指标,也不是网络优化或压缩手段,而是处理乱序数据的关键工具。
Q6. 端到端精确一次通常需要哪三部分配合?
- A.可重放数据源、状态检查点、幂等输出← 正确答案
- B.高吞吐队列、内存计算、高并行度
- C.数据压缩、序列化、反序列化
- D.窗口、水位线、事件触发器
答案:A。精确一次不是引擎单点能力,而是源头能重放、中间状态能快照、最终输出能幂等去重,三件套缺一不可。队列吞吐、序列化、窗口水位线都只是实现过程中的技术环节。
Q7. 下列关于滑动窗口的说法,正确的是哪些?
- A.窗口之间可以有重叠← 正确答案
- B.每个数据可能属于多个窗口← 正确答案
- C.窗口大小等于滑动步长时退化为滚动窗口← 正确答案
- D.滑动窗口不能处理实时数据
答案:ABC。滑动窗口的滑步小于窗口长度时,相邻窗口必然重叠,一条数据会进入多个窗口;当窗口长度等于滑步时,窗口不再重叠,就变成了滚动窗口。滑动窗口本身就是为实时流设计的能力。
Q8. 实时特征存储在工程中通常需要满足哪些要求?
- A.支持低延迟读取← 正确答案
- B.支持高并发更新← 正确答案
- C.特征带时间戳与版本← 正确答案
- D.必须部署在离线环境
答案:ABC。实时特征存储要扛住在线推理的高并发读和高频流式写,还要记录特征更新时间和版本,方便回溯与监控。部署在离线环境无法提供在线服务,显然不是要求。
Q9. 下列哪种延迟目标最接近实时特征计算?
- A.延迟目标为天级
- B.延迟目标为小时级
- C.延迟目标为秒到毫秒级← 正确答案
- D.延迟目标为月度级
答案:C。实时特征的价值就体现在“当下”,通常要求从事件发生到特征可用在秒级甚至毫秒级。天级、小时级、月度级都属于离线或准离线特征,无法支撑实时在线推理。
Q10. 下列关于有界流与无界流的理解,正确的是?
- A.有界流通常适合批处理
- B.无界流可按时间窗口切片计算← 正确答案
- C.有界流不能交给流引擎处理
- D.无界流不需要水位线机制
答案:B。无界流没有终点,但可以通过滚动、滑动等时间窗口切成有限片段来持续计算,这正是水位线发挥作用的地方。有界流也能交给流引擎并行处理,A 的说法不难理解,B 才是对无界流最准确的描述。