3. Flink 基础架构:JobManager / TaskManager 角色、任务槽与并行度、数据交换策略
好,咱们今天聊聊 Flink 的骨架。说白了,你写的那堆流处理逻辑,最后得有个地方跑起来。Flink 是怎么组织这些计算资源的?我刚开始接触 Flink 时,觉得它跟 Spark 差不多,后来踩了几个坑才发现,这两者的设计哲学其实很不一样。
3.1 两大核心角色:JobManager 与 TaskManager
Flink 集群里,最核心的就是两个角色:JobManager 和 TaskManager。你可以把 JobManager 想象成项目经理,TaskManager 就是干活的工程师。
JobManager —— 大脑
JobManager 负责三件事:
- 调度任务:把你的 JobGraph(作业图)转成 ExecutionGraph,然后分发给 TaskManager。
- 协调检查点:做 Checkpoint 时,JobManager 是协调者。它告诉每个 TaskManager 什么时候开始快照。
- 故障恢复:某个 TaskManager 挂了,JobManager 会重新调度失败的任务。
我个人习惯把 JobManager 的 HA(高可用)配置放在首位。为什么?因为 JobManager 挂了,整个集群就瞎了。我在生产环境里见过一次 JobManager 单点故障,那叫一个惨——所有作业全部重启,数据延迟飙升到小时级。后来我学乖了,生产环境必须配 ZooKeeper + Standby JobManager。
TaskManager —— 苦力
TaskManager 是真正干活的。每个 TaskManager 是一个 JVM 进程,里面跑着多个 Task Slot(任务槽)。每个 Slot 可以运行一个线程(Subtask)。
我举个例子:你有一个 TaskManager,配置了 4 个 Slot。那么它最多可以同时跑 4 个 Subtask。这些 Subtask 可能是 Map 算子,也可能是 Window 算子,取决于你的作业拓扑。
3.2 任务槽(Slot)与并行度
这两个概念经常被搞混。我面试时问过不少人,能说清楚的不多。
Slot 是什么?
Slot 是 TaskManager 内部的资源隔离单元。每个 Slot 占用固定的内存(由 TaskManager 的堆内存均分)。但注意,Slot 不隔离 CPU。同一个 TaskManager 里的多个 Slot,共享 CPU 资源。
这里有个关键点:Slot 是静态的。你在 flink-conf.yaml 里配了 taskmanager.numberOfTaskSlots: 4,那这个 TaskManager 就永远只有 4 个 Slot。运行时不能动态调整。
并行度是什么?
并行度是逻辑概念。它决定了一个算子有多少个并行实例(Subtask)。比如你的 Source 算子并行度设为 3,那就会启动 3 个 Subtask,分别读取不同的数据分区。
并行度可以在多个层面设置:
- 作业级别:
env.setParallelism(4) - 算子级别:
dataStream.map(...).setParallelism(2) - 客户端提交时:
./bin/flink run -p 4
优先级:算子级别 > 作业级别 > 客户端参数 > flink-conf.yaml 默认值。
Slot 与并行度的关系
我画个图你就明白了:
你看,并行度设成 6,但只有 4 个 Slot。那 2 个 Subtask 就得等着。等前面的 Subtask 处理完,释放 Slot,它们才能跑。
3.3 数据交换策略
数据在 Subtask 之间怎么传?Flink 提供了三种策略:Forward、Keyed、Broadcast。选错了,性能会差很多。
Forward(直传)
最简单。上游 Subtask 直接把数据发给下游对应的 Subtask。一对一,不分区。
- 条件:上下游并行度必须相同。
- 特点:零序列化开销,数据在同一个 TaskManager 内直接传递。
- 场景:
map()后接filter(),这种不需要重新分区的操作。
我遇到过一个问题:两个算子并行度都是 4,但数据倾斜严重。为什么?因为 Forward 策略下,上游 Subtask 1 的数据只会发给下游 Subtask 1。如果上游 Subtask 1 的数据量是其他的 10 倍,下游 Subtask 1 就成瓶颈了。
Keyed(按 Key 分区)
这是最常用的策略。根据数据的 Key 做 Hash,决定发给哪个下游 Subtask。
- 条件:上下游并行度可以不同。
- 特点:保证相同 Key 的数据进入同一个 Subtask。
- 场景:
keyBy()之后的所有操作。
Keyed 策略的核心是 Hash 分区。Flink 默认用 murmurHash 计算 Key 的哈希值,然后对下游并行度取模。但这里有个坑:如果 Key 的分布不均匀(比如大部分数据 Key 都是 "default"),那 Hash 结果会严重倾斜。
rebalance() 打散。
Broadcast(广播)
上游的每条数据,复制 N 份,发给下游所有 Subtask。
- 条件:无限制。
- 特点:网络开销大,数据量会放大 N 倍。
- 场景:广播配置表、规则引擎中的动态规则更新。
Broadcast 要慎用。我见过有人把百万级的数据流做 Broadcast,结果下游每个 Subtask 都要处理全量数据,网络带宽直接被打满。正确的做法是:只广播小数据量(几百 KB 级别)的配置或规则。
三种策略对比
| 策略 | 数据路由方式 | 上下游并行度 | 网络开销 | 典型算子 |
|---|---|---|---|---|
| Forward | 一对一,不分区 | 必须相同 | 无(本地传递) | map, filter, flatMap |
| Keyed | 按 Key Hash 分区 | 可以不同 | 中等 | keyBy, window, reduce |
| Broadcast | 复制给所有下游 | 无限制 | 高(数据放大 N 倍) | broadcast, connect |
3.4 一个完整的任务执行流程
咱们把上面这些串起来,看看一个 Flink 作业到底是怎么跑的:
- 提交作业:客户端把 JobGraph 提交给 JobManager。
- JobManager 解析:JobManager 把 JobGraph 转成 ExecutionGraph,算出需要多少 Slot。
- 申请资源:JobManager 向 ResourceManager(资源管理器)申请 Slot。如果集群里有空闲 Slot,直接分配;没有就等。
- 部署任务:JobManager 把 Task 部署到对应的 TaskManager 上。每个 Task 占用一个 Slot。
- 数据流转:Task 之间根据数据交换策略(Forward/Keyed/Broadcast)传递数据。
- Checkpoint:JobManager 定期触发 Checkpoint,TaskManager 把状态快照写入持久化存储(如 HDFS)。
- 故障恢复:某个 Task 挂了,JobManager 重新调度,从最近的 Checkpoint 恢复状态。
嗯,这里要注意:Slot 的分配是懒加载的。不是所有 Task 同时启动,而是 JobManager 按拓扑顺序,先启动 Source,再启动下游算子。这样可以避免资源浪费。
好了,这一章的内容就到这儿。记住这些基础概念,后面讲状态管理、时间语义、容错机制时,你会回来感谢我的。