3. Flink 基础概念:架构、API 与时间机制
好,咱们进入第三章。这一章是 Flink 的基石,说白了就是搞懂 Flink 到底怎么跑起来的,以及它怎么处理时间这个最头疼的问题。我见过不少同学,API 写得飞起,但一问 Watermark 就卡壳。嗯,咱们今天就把这些硬骨头啃下来。
3.1 Flink 架构与核心组件
先看一张图,把 Flink 的运行时架构刻在脑子里。
Flink 的架构其实不复杂。一个集群里,JobManager 是大脑,负责调度、协调和检查点管理。TaskManager 是干活的,里面分了多个 Slot,每个 Slot 跑一个子任务。
3.2 DataStream API 概览
DataStream API 是 Flink 最核心的编程接口。你写 Flink 作业,本质上就是在组装一个 DAG(有向无环图)。
一个典型的流处理程序长这样:
// 1. 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 2. 接入数据源
DataStream<String> sourceStream = env.addSource(new FlinkKafkaConsumer<>(...));
// 3. 转换操作
DataStream<Event> parsedStream = sourceStream
.map(line -> JsonUtil.parse(line, Event.class))
.filter(event -> event.getStatus() == 1);
// 4. 窗口聚合
DataStream<Result> resultStream = parsedStream
.keyBy(Event::getUserId)
.window(TumblingEventTimeWindows.of(Time.minutes(5)))
.aggregate(new MyAggregateFunction());
// 5. 输出结果
resultStream.addSink(new FlinkKafkaProducer<>(...));
// 6. 启动作业
env.execute("My Flink Job");
这里有几个关键点:
- Source:数据从哪来。Kafka、文件、Socket 都行。
- Transformation:map、filter、flatMap 这些算子,说白了就是数据变形。
- Sink:数据往哪去。MySQL、Elasticsearch、Kafka 都常见。
3.3 时间语义:Event Time vs Processing Time
时间,是流计算里最绕不开的话题。Flink 支持三种时间语义,但实际生产中,Event Time 才是主角。
| 时间语义 | 定义 | 典型场景 | 注意事项 |
|---|---|---|---|
| Event Time | 事件实际发生的时间,由业务数据携带 | 日志分析、交易统计、IoT 传感器 | 需要 Watermark 处理乱序 |
| Processing Time | 数据到达 Flink 算子的系统时间 | 实时监控、简单聚合、测试 | 结果不稳定,受系统负载影响 |
| Ingestion Time | 数据进入 Flink 集群的时间 | 较少使用,介于两者之间 | Flink 1.12 后建议直接用 Event Time |
为什么 Event Time 这么重要?你想想看,用户下单的时间是固定的,但网络延迟、系统抖动可能导致数据晚到几分钟。如果用 Processing Time,那统计出来的结果就是错的——明明 10 点的订单,可能被算到 10:05 的窗口里。
核心原则: 只要业务对时间敏感,一律用 Event Time。Processing Time 只适合做「当前状态快照」这种对精度没要求的场景。
3.4 Watermark 机制详解
Watermark,中文叫水位线。它是 Flink 用来处理乱序数据的核心机制。说白了,Watermark 就是一个「时间截止线」——告诉 Flink:到这个点了,后面再来的数据,就别往这个窗口里塞了。
3.4.1 Watermark 的工作原理
假设你设置了一个 5 分钟的滚动窗口,Watermark 延迟为 10 秒。那么:
- Flink 收到一条数据,时间戳是 10:00:15。
- Flink 计算当前 Watermark = 10:00:15 - 10s = 10:00:05。
- 当 Watermark 推进到 10:00:00 时,触发 [09:55:00, 10:00:00) 窗口的计算。
- 如果 10:00:05 之后又来了一条 09:59:59 的数据,它会被丢弃或放入侧输出流。
代码实现也很直接:
DataStream<Event> streamWithWatermark = sourceStream
.assignTimestampsAndWatermarks(
WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(10))
.withTimestampAssigner((event, timestamp) -> event.getEventTime())
);
3.4.2 两种 Watermark 生成策略
| 策略 | 说明 | 适用场景 |
|---|---|---|
| forBoundedOutOfOrderness | 固定延迟,允许数据最多晚到 N 秒 | 大部分生产场景,延迟可控 |
| forMonotonousTimestamps | 假设数据严格有序,不延迟 | 测试或数据源保证有序 |
3.4.3 多并行度下的 Watermark 对齐
这里有个容易忽略的点:当你有多个并行子任务时,每个子任务都有自己的 Watermark。Flink 会取所有输入分区的 最小 Watermark 作为全局 Watermark。为什么?因为要保证数据完整性——只要有一个分区还在等数据,就不能贸然触发窗口。
但这也带来一个问题:如果某个分区一直没数据,Watermark 就卡住了。我建议你在空闲数据源上开启 withIdleness(Duration.ofSeconds(60)),让 Flink 自动跳过空闲分区。
WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(10))
.withTimestampAssigner(...)
.withIdleness(Duration.ofMinutes(1)); // 1分钟无数据则标记为空闲
3.5 小结
这一章我们拆解了 Flink 的三大基础:架构、API 和时间机制。我个人觉得,理解 Watermark 是区分「会用 Flink」和「用好 Flink」的分水岭。你只要把 Event Time + Watermark 这套组合拳打熟了,后面窗口、状态、容错那些概念,学起来会顺很多。