2. Flink基础回顾

在进入事件时间与多流关联之前,有必要把 Flink 运行时最核心的一层重新梳理清楚。很多“窗口不触发”“水位线不推进”“关联结果为空”的问题,根源并不在窗口本身,而在于对 DataStream API 的执行模型、并行度与资源分配理解不到位。本节围绕六个关键词展开:DataStream API、执行环境、数据源与数据汇、转换算子、并行度与算子链、任务槽与资源模型。

一、核心原理:从程序到分布式执行

Flink 程序本质上是“惰性构建 + 显式触发”的两段式结构。你在 main 方法里调用 env.addSource(...)、map、keyBy、window 时,只是在构建一张逻辑数据流图(StreamGraph),并不会立刻执行。只有调用 env.execute() 时,Flink 才会把逻辑图翻译成 JobGraph,提交到集群,再由 JobManager 生成 ExecutionGraph 并调度到 TaskManager 上运行。

执行环境 StreamExecutionEnvironment 是这一切的入口。它决定了作业运行在本地还是集群、默认并行度是多少、是否开启检查点。数据源(Source)负责把外部数据引入流,转换算子(Transformation)负责加工,数据汇(Sink)负责把结果写出。三者之间通过数据流连接,而并行度决定了每个算子被拆成多少个并行子任务。

Flink 作业从逻辑图到物理执行的映射 Source 并行度 2 map / filter 并行度 2 keyBy + window 并行度 4 Sink 并行度 2 TaskManager 任务槽(Task Slot) Slot 1 Source[0] → map[0] window[0] Slot 2 Source[1] → map[1] window[1] Slot 3 window[2] → Sink[0] window[3] → Sink[1]

二、方法流程:一个最小可运行骨架

下面这段代码覆盖了执行环境、数据源、转换算子、数据汇与并行度设置,是后续所有实战章节的模板:

StreamExecutionEnvironment env =
    StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(2);
env.enableCheckpointing(10_000);

DataStream<String> lines = env.addSource(new FlinkKafkaConsumer<>(
    "topic_events", new SimpleStringSchema(), props));

DataStream<Event> events = lines
    .map(new ParseJsonMap())
    .filter(e -> e != null)
    .name("parse-and-filter");

events.keyBy(Event::getUserId)
      .window(TumblingEventTimeWindows.of(Time.seconds(10)))
      .aggregate(new CountAgg())
      .addSink(new FlinkKafkaProducer<>("topic_result", schema, props));

env.execute("flink-basics-demo");

流程可以归纳为四步:建环境 → 接数据源 → 串转换算子 → 写数据汇并 execute。其中 name() 建议显式命名,便于在 Web UI 中定位算子;setParallelism() 可作用于整个作业,也可单独作用于某个算子。

三、并行度、算子链与任务槽

并行度(Parallelism)决定一个算子被拆成多少个子任务。算子链(Operator Chaining)是 Flink 的优化:当相邻算子并行度相同、且没有 shuffle 时,Flink 会把它们合并成一个算子链,放在同一个线程中执行,减少线程切换与序列化开销。例如 Source → map → filter 若并行度一致,通常会被链在一起。

任务槽(Task Slot)是 TaskManager 上的资源单位。一个 TaskManager 有若干 slot,每个 slot 可以运行一条算子链。slot 数量决定了单个 TaskManager 能并行执行多少条链。资源模型上,Flink 采用“slot 共享”机制:同一个 slot 中可以运行同一个作业不同算子的子任务,从而提高资源利用率。

概念作用常见取值/建议
并行度算子子任务数量与 Kafka 分区数对齐,避免空闲
算子链合并相邻算子减少开销默认开启,可用 disableChaining 关闭
任务槽TaskManager 的资源单位slot 数 ≥ 单作业最大并行度
Slot 共享同作业子任务共享 slot默认开启,提升利用率
排查“算子没有链在一起”时,先看并行度是否一致,再看中间是否存在 keyBy、rebalance 等 shuffle 操作,它们会打断算子链。

四、误区与复盘

  • 误区一:并行度越高越快。并行度超过数据源分区数时,多出的子任务会空闲,反而增加调度开销。
  • 误区二:忘记 execute。只构建 DataStream 不调用 env.execute(),作业不会提交,日志中也不会有任何运行痕迹。
  • 误区三:混淆算子链与并行度。算子链是执行优化,并行度是任务拆分,两者维度不同,不能互相替代。
  • 误区四:slot 不足导致作业卡在 created 状态。此时应检查 TaskManager 数量与 slot 配置,而不是反复重启作业。
在后续事件时间与多流关联章节中,keyBy 会引入 shuffle,窗口算子通常无法与上游链在一起。理解这一点,才能正确解读 Web UI 中的算子链与背压信息。

把这一层基础打牢,后面讨论水位线推进、窗口触发与双流 join 时,你就能清楚地区分:哪些现象是时间语义问题,哪些其实是并行度、算子链或资源模型配置不当造成的。