3. Flink 基础概念:架构、API 与时间机制

好,咱们进入第三章。这一章是 Flink 的基石,说白了就是搞懂 Flink 到底怎么跑起来的,以及它怎么处理时间这个最头疼的问题。我见过不少同学,API 写得飞起,但一问 Watermark 就卡壳。嗯,咱们今天就把这些硬骨头啃下来。

3.1 Flink 架构与核心组件

先看一张图,把 Flink 的运行时架构刻在脑子里。

Flink 运行时架构 JobManager 调度 · 检查点 · 协调 TaskManager 1 Slot 0 | Slot 1 TaskManager 2 Slot 0 | Slot 1 TaskManager 3 Slot 0 | Slot 1 数据源 (Kafka) 数据汇 (MySQL) 💡 关键点 JobManager 是大脑 TaskManager 是工人

Flink 的架构其实不复杂。一个集群里,JobManager 是大脑,负责调度、协调和检查点管理。TaskManager 是干活的,里面分了多个 Slot,每个 Slot 跑一个子任务。

我的习惯: 调优时先看 Slot 数量。Slot 数 = TaskManager 数 × 每个 TM 的 Slot 数。这个值决定了你的并行度上限。我曾经在一个实时大屏项目里,因为 Slot 配少了,数据积压到 Kafka 消费 lag 飙到几万条,后来加了两个 TM 才压下去。

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 都常见。
注意: 别把 Flink 的 DataStream 和 Java 的 Stream 搞混了。Flink 的流是分布式的、有状态的、可以容错的。我见过有人直接用 Java Stream 在 map 里做聚合,结果数据全在单机算,集群资源全浪费了。

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 秒。那么:

  1. Flink 收到一条数据,时间戳是 10:00:15。
  2. Flink 计算当前 Watermark = 10:00:15 - 10s = 10:00:05。
  3. 当 Watermark 推进到 10:00:00 时,触发 [09:55:00, 10:00:00) 窗口的计算。
  4. 如果 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 假设数据严格有序,不延迟 测试或数据源保证有序
我曾经踩过的坑: 有一回做金融交易统计,我设了 30 秒的 Watermark 延迟,觉得够用了。结果业务方反馈说,有些交易因为跨机房同步,延迟能到 2 分钟。那批数据全被丢掉了,对账对了一周。后来我改成了 5 分钟延迟,并加了侧输出流做兜底。记住:Watermark 延迟不是越小越好,要根据业务容忍度来设。

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 这套组合拳打熟了,后面窗口、状态、容错那些概念,学起来会顺很多。