4. 实时计算引擎选型

聊到实时特征链路,绕不开的一个选择题就是:用 Flink 还是 Spark Streaming?

说实话,这个问题在圈子里争论了好几年。我个人的态度很明确——没有银弹,只有适不适合。但在成本追踪这个特定场景下,两者的差异其实非常明显。

先说说我的经历。几年前我在做一个金融风控的实时特征项目,数据源是 Kafka,需要毫秒级延迟计算用户画像。当时团队里有人坚持用 Spark Streaming,理由是“大家都会 Scala,上手快”。结果上线第一周就出了岔子——滑动窗口的 watermark 处理不对,导致特征值频繁跳变,风控模型直接崩了。后来换成 Flink,问题才解决。

嗯,这个教训让我深刻意识到:选引擎不能只看团队熟悉度,更要看场景的硬约束。

4.1 核心差异:架构与延迟

先看一张对比图,把两者的核心架构差异说清楚。

Flink vs Spark Streaming 架构对比 Apache Flink Kafka / Pulsar 数据源 流式处理引擎(纯流) 事件时间 + 精确一次语义 状态后端(RocksDB / Heap) 输出到 Redis / HBase / Kafka Spark Streaming Kafka / 文件 / Socket 数据源 微批次(Micro-Batch) 处理时间 + 至少一次语义 RDD / DataFrame 批处理模型 输出到 HDFS / Redis / MySQL Flink 是真正的逐条处理流引擎;Spark Streaming 本质是微批次,延迟受批次间隔限制

从图上可以看得很清楚:Flink 是真正的逐条处理流引擎,而 Spark Streaming 本质上是微批次。这个差异在成本追踪场景下会带来一系列连锁反应。

4.2 延迟对比:毫秒级 vs 秒级

成本追踪最核心的需求是什么?实时性。特征链路上每多一秒延迟,就可能多一笔超预算的支出。

维度 Flink Spark Streaming
处理模型 逐条事件驱动 微批次(Micro-Batch)
端到端延迟 毫秒级(通常 < 100ms) 秒级(取决于批次间隔)
事件时间支持 原生支持,内置 watermark 需额外处理,精度有限
状态管理 Keyed State + 定时器 基于 RDD 的 updateStateByKey
精确一次语义 内置支持 需结合外部事务

你想想看,如果你的成本追踪需要每 5 秒刷新一次预算消耗,Spark Streaming 的批次间隔至少得设 2-3 秒,再加上处理时间,实际延迟可能在 5-10 秒。而 Flink 可以做到 100 毫秒内输出结果。

我在一个广告计费项目中遇到过类似情况。当时用 Spark Streaming 做实时计费,批次间隔设了 5 秒,结果用户点击广告后,预算扣减要等 10 秒才生效,导致超投了 15% 的预算。换成 Flink 后,延迟降到 200 毫秒以内,超投问题直接解决。

4.3 状态管理与成本计算

成本追踪离不开状态——你要记住每个用户、每个渠道、每个时间窗口的累计消耗。这一点上,Flink 的优势非常明显。

我的建议:如果成本计算涉及滑动窗口、累计求和、阈值告警等复杂状态操作,优先选 Flink。它的 Keyed State 和 ProcessFunction 可以让你精确控制状态的读写和过期策略。

看一段 Flink 状态管理的代码示例:

// Flink 状态管理示例:实时累计成本
DataStream<CostEvent> costStream = env
    .addSource(kafkaConsumer)
    .keyBy(event -> event.getUserId())
    .process(new KeyedProcessFunction<String, CostEvent, CostAlert>() {
        
        private ValueState<Double> accumulatedCost;
        private ValueState<Long> windowStart;
        
        @Override
        public void open(Configuration parameters) {
            ValueStateDescriptor<Double> desc = 
                new ValueStateDescriptor<>("accumulated", Types.DOUBLE);
            accumulatedCost = getRuntimeContext().getState(desc);
            windowStart = getRuntimeContext()
                .getState(new ValueStateDescriptor<>("windowStart", Types.LONG));
        }
        
        @Override
        public void processElement(CostEvent event, Context ctx, Collector<CostAlert> out) 
            throws Exception {
            Double current = accumulatedCost.value() == null ? 0.0 : accumulatedCost.value();
            current += event.getAmount();
            accumulatedCost.update(current);
            
            // 检查是否超过预算阈值
            if (current > event.getBudget()) {
                out.collect(new CostAlert(event.getUserId(), current, event.getBudget()));
            }
            
            // 注册每日重置定时器
            ctx.timerService().registerEventTimeTimer(
                getEndOfDay(event.getTimestamp())
            );
        }
        
        @Override
        public void onTimer(long timestamp, OnTimerContext ctx, Collector<CostAlert> out) {
            // 每日零点重置累计成本
            accumulatedCost.clear();
        }
    });

这段代码里,我用 ValueState 保存每个用户的累计成本,用定时器实现每日自动重置。Spark Streaming 要实现同样的逻辑,得用 updateStateByKey 配合 mapWithState,代码复杂度会高不少,而且状态恢复的粒度不如 Flink 精细。

4.4 资源消耗与成本

说到成本追踪,我们还得考虑引擎本身的运行成本。毕竟,用昂贵的计算引擎去追踪成本,本身就是一种讽刺。

资源维度 Flink Spark Streaming
内存消耗 中等(状态可配置 RocksDB) 较高(批次数据全量驻留)
CPU 利用率 持续稳定 批次处理时突增
状态存储 RocksDB / Heap / 文件系统 仅内存 + Checkpoint
扩缩容 动态扩缩容(Savepoint) 需重启作业
典型集群成本 中等 中高(批次间隔越短成本越高)

这里有个容易被忽略的点:Spark Streaming 的批次间隔越短,资源消耗越高。如果你把批次间隔从 10 秒降到 2 秒,集群资源消耗可能翻倍。而 Flink 的逐条处理模式,资源消耗与数据量成正比,不会因为延迟要求变高而突然飙升。

避坑指南:我曾经见过一个团队,为了把 Spark Streaming 的延迟压到 1 秒以内,把批次间隔设成 500 毫秒,结果集群 CPU 直接打满,作业频繁失败。后来算了一笔账,那个方案的集群成本比 Flink 高了 3 倍,延迟还比 Flink 差。说白了,用微批次引擎硬扛毫秒级延迟,性价比极低。

4.5 选型决策矩阵

说了这么多,到底怎么选?我整理了一个决策矩阵,你可以根据实际情况对号入座。

场景 推荐引擎 理由
毫秒级实时成本告警 Flink 事件时间 + 低延迟,满足秒级响应
分钟级批量成本核算 Spark Streaming 批次处理更稳定,与批作业共享资源
复杂窗口计算(滑动/会话) Flink 原生窗口语义,代码简洁
已有 Spark 技术栈 Spark Streaming 降低学习成本,快速上线
状态量巨大(TB 级) Flink + RocksDB 磁盘状态存储,内存可控
与批处理 ETL 混跑 Spark Streaming 统一计算引擎,运维简单

我个人习惯是:如果成本追踪的延迟要求 < 5 秒,无脑选 Flink。如果延迟容忍度在 10 秒以上,且团队 Spark 经验丰富,Spark Streaming 也是合理的选择。

4.6 小结

最后说一个我踩过的坑。有一次我们评估两个引擎的性能,在测试环境跑了一周,Flink 的延迟和资源消耗都优于 Spark Streaming。但上线后,Flink 作业频繁出现 Checkpoint 超时,原因是状态后端配置不合理。后来调整了 RocksDB 的内存和线程参数,才稳定下来。

所以我的建议是:选型只是第一步,真正的挑战在调优和运维。无论选哪个引擎,都要预留至少两周的压测和调优时间。