3. 流处理基础

要理解 Kafka 与 Flink 如何组成 Exactly-Once 链路,必须先回到 Flink 自身的运行时模型。Flink 不是“一个进程跑一个作业”的简单程序,而是一套分布式流处理引擎:用户写的 DataStream 程序在客户端被翻译成作业图,提交后由 JobManager 调度,TaskManager 执行,Slot 提供资源隔离,算子负责计算,状态负责记忆。这六个概念串起来,才构成后续端到端一致性的地基。

一、核心原理:从逻辑图到物理执行

Flink 作业的生成经历三个层次。第一层是用户代码中的 DataStream API,它描述数据从哪里来、经过哪些转换、到哪里去,属于逻辑视图。第二层是 StreamGraph 与 JobGraph,客户端把算子链优化后提交给集群。第三层是 ExecutionGraph,JobManager 根据并行度把每个算子拆成多个并行子任务,最终映射到 TaskManager 的 Slot 上执行。

其中最关键的是算子链(Operator Chain):当两个算子并行度相同且连接方式为 forward 时,Flink 会把它们合并进同一个 Task,减少线程切换与序列化开销。这也是为什么调优时经常看到“一个 Slot 里跑了整条链”。

Flink 作业从逻辑图到物理执行的映射 DataStream API source → map → keyBy → window → sink 逻辑视图 JobGraph 算子链优化 合并 forward 算子 客户端提交 ExecutionGraph 按并行度拆分 子任务调度 JobManager 生成 TaskManager Slot 执行 Task 运行 物理视图 JobManager(主节点) Dispatcher / ResourceManager JobMaster 调度与 Checkpoint 协调 心跳、故障恢复、状态元数据 TaskManager(从节点) Slot 1 Slot 2 Slot 3 Slot 4(内存隔离)

二、方法流程:JobManager、TaskManager 与 Slot 的协作

JobManager 是作业的“大脑”,内部包含 Dispatcher、ResourceManager 和 JobMaster。Dispatcher 接收客户端提交的 JobGraph;ResourceManager 负责向集群申请资源;JobMaster 负责单个作业的执行图、Checkpoint 触发与故障恢复。TaskManager 是“肌肉”,每个 TaskManager 是一个 JVM 进程,内部划分出若干 Slot。

Slot 是 Flink 的资源调度单位,代表 TaskManager 上一份固定的资源子集(默认按内存切分,CPU 不严格隔离)。一个 Slot 可以运行一条算子链中的多个 Task,但同一 Slot 内的 Task 共享内存。并行度决定需要多少 Slot:并行度为 4 的作业至少需要 4 个 Slot,可以分布在多台 TaskManager 上。

关键关系:并行度 = 算子子任务数 = 所需 Slot 数(在无 Slot 共享组冲突时)。Slot 数量不足时,作业会停留在 CREATED 或等待资源状态,而不是自动降级并行度。

下面是一段典型的 DataStream 代码,展示了 source、map、keyBy、window、sink 的链路:

StreamExecutionEnvironment env =
    StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(4);
env.enableCheckpointing(10000); // 每 10s 一次 Checkpoint

DataStream<String> source = env
    .addSource(new FlinkKafkaConsumer<>("topic-a", schema, props));

DataStream<Order> orders = source
    .map(new ParseOrderMap())
    .keyBy(Order::getUserId)
    .window(TumblingEventTimeWindows.of(Time.minutes(1)))
    .aggregate(new OrderAggregate());

orders.addSink(new FlinkKafkaProducer<>("topic-b", schema, props));
env.execute("kafka-flink-eos-demo");

三、算子与状态:流处理真正“记住”的东西

算子分为无状态与有状态两类。map、filter 通常无状态;keyBy 后的 window、aggregate、process 则是有状态算子。状态按作用范围分为 Keyed State 与 Operator State:前者绑定到具体 key,随 key 分组分布;后者绑定到算子并行实例,常用于 source 的 offset 管理。

状态后端决定状态存哪里、怎么存。常见选择包括 HashMapStateBackend(内存)与 EmbeddedRocksDBStateBackend(本地磁盘,适合大状态)。Checkpoint 时,JobManager 触发屏障(Barrier)在数据流中流动,各算子把状态快照写入持久化存储,全部完成后形成一次全局一致快照。这正是 Flink 实现 Exactly-Once 的内部基础。

概念 所在位置 核心职责 与 Exactly-Once 的关系
JobManager 主节点 调度、Checkpoint 协调、故障恢复 决定快照何时触发与回滚
TaskManager 从节点 执行 Task、管理 Slot 与内存 承载状态与数据流
Slot TaskManager 内 资源隔离与任务分配单位 影响并行度与状态分布
算子 Task 内 数据转换与计算逻辑 有状态算子参与快照
状态 算子/Key 维度 保存中间结果与进度 快照内容的核心

四、案例与误区复盘

假设一个 Kafka → Flink → Kafka 的实时订单统计作业,并行度为 4,使用事件时间窗口。若 TaskManager 只有 2 个 Slot,作业无法启动,日志会出现:

org.apache.flink.runtime.jobmanager.scheduler.NoResourceAvailableException:
Could not allocate all requires slots within timeout of 300000 ms.
Slots required: 4, slots allocated: 2

常见误区有三个。第一,把 Slot 当成 CPU 核数,实际上 Slot 主要隔离内存,CPU 是共享的。第二,认为设置了 Checkpoint 就自动 Exactly-Once,实际上还需要 source 可重放、sink 支持事务或幂等,才能端到端一致。第三,忽略 keyBy 后的状态倾斜:某个 key 数据量过大时,对应子任务的状态和内存会显著高于其他子任务,导致反压甚至失败。

复盘要点:排查 Flink 作业问题时,先看 JobManager 日志中的调度与 Checkpoint 异常,再看 TaskManager 的 Slot 使用率、内存与反压指标,最后定位具体算子的状态大小与倾斜情况。顺序错了,容易在错误层面反复调参。

把这一节的概念串起来:DataStream 是表达方式,JobManager 与 TaskManager 是运行载体,Slot 是资源单位,算子是计算单元,状态是记忆机制。后续讨论 Kafka 与 Flink 的 Exactly-Once 链路时,所有一致性保证都建立在这套模型之上。