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 里跑了整条链”。
二、方法流程: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 上。
下面是一段典型的 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 数据量过大时,对应子任务的状态和内存会显著高于其他子任务,导致反压甚至失败。
把这一节的概念串起来:DataStream 是表达方式,JobManager 与 TaskManager 是运行载体,Slot 是资源单位,算子是计算单元,状态是记忆机制。后续讨论 Kafka 与 Flink 的 Exactly-Once 链路时,所有一致性保证都建立在这套模型之上。