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 的核心概念、API 层次、状态管理和容错机制串在了一起。你可以把它当作本章的“地图”。从底层概念到上层 API,再到实践要点,每一层都有对应的技术选型和注意事项。
好了,这一章的内容就到这里。Flink 的知识点很多,但核心就是“流、状态、容错”这三个词。把这三个词吃透了,后面学什么算子、什么 API 都顺理成章。