3. 实时计算引擎:Apache Flink 核心概念、DataStream API、Table & SQL API、状态管理与容错机制

聊到流批一体,Flink 是绕不开的基石。我最早接触 Flink 是在 1.4 版本,那时候它还是个“小众”框架,很多人还在用 Spark Streaming。但说实话,我第一次看到 Flink 的“事件时间”语义和“精确一次”保证时,就觉得这东西不简单。后来在几个实时数仓项目里,Flink 帮我解决了不少 Spark Streaming 搞不定的问题。

这一章,我们就把 Flink 的核心概念、API 用法、状态管理和容错机制串一遍。嗯,都是实战中高频用到的。

3.1 核心概念:流、算子、时间语义

Flink 把一切看作流。批处理?不过是“有界流”而已。这个思想很关键——你想想看,如果批和流在底层是同一套引擎,那开发起来就省事多了。

几个核心概念,我列个表,方便你对照理解:

概念 说明 我的一点经验
Stream 数据流,有界(批)或无界(流) 别纠结有界无界,API 基本一样
Operator 算子,如 map、filter、keyBy 算子链(chaining)能省网络开销
Event Time 事件发生时间,业务最关心 乱序数据全靠它,别用 Processing Time 做统计
Watermark 水位线,触发窗口计算的“闹钟” 设得太紧丢数据,太松延迟高,要调参
Window 窗口,滚动、滑动、会话 滑动窗口要注意重叠,性能开销不小

我个人习惯,在实时 ETL 里优先用 Event Time + Watermark。Processing Time 只用来做简单过滤或打标。为什么?因为业务方要的是“用户下单那一刻”的数据,不是“服务器收到那一刻”的数据。这个坑,我踩过。

核心原则:Flink 的流处理模型是“一次处理一条记录,但状态可以跨记录”。这个模型让 Flink 既能做低延迟的流处理,也能做高吞吐的批处理。

3.2 Flink DataStream API:实战中的主力

DataStream API 是 Flink 最底层的流处理 API。我大部分实时 ETL 和复杂事件处理(CEP)都用它。说白了,它就是给你一个 DataStream,你往上挂各种算子。

一个典型的实时 ETL 流程,大概长这样:

DataStream<String> sourceStream = env.addSource(new FlinkKafkaConsumer<>(...));

DataStream<Event> parsedStream = sourceStream
    .map(json -> JsonUtil.parse(json, Event.class))
    .filter(event -> event.getType() != null);

DataStream<Event> keyedStream = parsedStream
    .keyBy(Event::getUserId);

DataStream<AggResult> windowedStream = keyedStream
    .window(TumblingEventTimeWindows.of(Time.minutes(5)))
    .aggregate(new MyAggregateFunction());

windowedStream.addSink(new FlinkKafkaProducer<>(...));

env.execute("Real-time ETL Job");

这段代码里,有几个点我特别想强调:

  • Source:Kafka 是最常见的 source,记得设置消费起始位置和 offset 提交策略。
  • Transformation:map、filter、flatMap 这些算子,尽量用 lambda 或匿名类,别搞太复杂的继承链。
  • KeyBy:这是状态分区的基础。keyBy 之后,相同 key 的数据会进入同一个 subtask。
  • Window:滚动窗口适合做 T+0 的分钟级聚合,滑动窗口适合做趋势分析。
  • Sink:Kafka sink 要开启 exactly-once 语义,配合 checkpoint 使用。

避坑指南:我曾经在 keyBy 时用了不合适的 key 类型(比如自定义对象没重写 hashCode),导致数据倾斜严重。后来统一用 String 或 Long 做 key,问题就解决了。

3.3 Flink Table & SQL API:批流一体的“甜点”

Table & SQL API 是 Flink 的高层 API。它把 DataStream 和 DataSet 统一成了 Table,然后你就可以用 SQL 来查询了。我个人觉得,对于简单的过滤、聚合、join,SQL 比 DataStream API 省事得多。

举个例子,同样是 5 分钟窗口聚合,用 SQL 写是这样的:

Table orders = tableEnv.fromDataStream(orderStream, $("orderId"), $("userId"), $("amount"), $("ts").rowtime());

Table result = tableEnv.sqlQuery(
    "SELECT " +
    "  userId, " +
    "  TUMBLE_END(ts, INTERVAL '5' MINUTE) AS windowEnd, " +
    "  SUM(amount) AS totalAmount " +
    "FROM orders " +
    "GROUP BY userId, TUMBLE(ts, INTERVAL '5' MINUTE)"
);

tableEnv.toRetractStream(result, Row.class).print();

你看,SQL 的语义更清晰,维护成本也更低。但要注意,Table API 的底层还是 DataStream API,所以性能上不会有本质区别。

我一般在项目里这样分工:

  • 复杂 ETL、状态计算、自定义算子:用 DataStream API
  • 简单聚合、join、维表关联:用 Table & SQL API
  • 批流一体场景:用同一套 SQL,只改 source 和 sink 的 connector

注意:Table API 的 retract 模式(toRetractStream)会输出 UPDATE_BEFORE 和 UPDATE_AFTER 两种消息,下游如果是 Kafka,要确保消费者能处理这种“回撤”语义。否则数据会翻倍。

3.4 状态管理与容错机制:Flink 的“记忆”和“保险”

状态管理是 Flink 最核心的能力之一。没有状态,Flink 就是个简单的“转发器”。有了状态,Flink 才能做累加、去重、join 这些有记忆的计算。

Flink 的状态分为两种:

状态类型 说明 典型场景
Operator State 算子级别的状态,每个并行实例一份 Kafka offset 管理
Keyed State key 级别的状态,每个 key 一份 用户累计消费金额

Keyed State 又分几种:ValueState、ListState、MapState、ReducingState、AggregatingState。我用的最多的是 ValueState 和 MapState。ValueState 存单个值,MapState 存 key-value 对,适合做维表缓存。

状态后端(State Backend)决定了状态存哪里:

  • HashMapStateBackend:存 TaskManager 内存,适合小状态
  • EmbeddedRocksDBStateBackend:存本地磁盘,适合大状态

我建议,状态超过 1GB 就用 RocksDB。否则 GC 会把你搞疯。我曾经有个项目,状态 5GB 用 HashMap,结果 Full GC 每 10 秒一次,任务根本跑不起来。换成 RocksDB 后,稳如老狗。

容错机制的核心是 Checkpoint。Flink 通过分布式快照,把状态定期保存到持久化存储(HDFS、S3)。如果任务挂了,就从最近的 checkpoint 恢复。

Checkpoint 的关键参数:

参数 默认值 我的建议
checkpoint interval 无 30s - 5min,看业务容忍度
exactly-once / at-least-once exactly-once 大部分场景用 exactly-once
checkpoint timeout 10min 别设太短,大状态容易超时
min pause between checkpoints 0 建议设成 interval 的一半

一句话总结:状态让 Flink 有“记忆”,checkpoint 给 Flink 上“保险”。两者配合,才能实现“精确一次”的语义。

3.5 知识体系总览

下面这张图,把 Flink 的核心概念、API 层次、状态管理和容错机制串在了一起。你可以把它当作本章的“地图”。

Flink 核心知识体系 Flink 核心知识体系 核心概念 API 层次 状态 & 容错 Stream / Operator Event Time / Watermark Window(滚动/滑动/会话) DataStream API Table & SQL API 批流统一:同一套 SQL Operator State / Keyed State State Backend(HashMap/RocksDB) Checkpoint / Savepoint 实践要点 优先 Event Time Watermark 调参 实践要点 复杂逻辑用 DataStream 简单聚合用 SQL 实践要点 大状态用 RocksDB Checkpoint 间隔 30s+ 核心思想:一切皆流,批是特殊的流 状态 + Checkpoint = 精确一次语义

这张图把 Flink 的核心概念、API 层次、状态管理和容错机制串在了一起。你可以把它当作本章的“地图”。从底层概念到上层 API,再到实践要点,每一层都有对应的技术选型和注意事项。

好了,这一章的内容就到这里。Flink 的知识点很多,但核心就是“流、状态、容错”这三个词。把这三个词吃透了,后面学什么算子、什么 API 都顺理成章。