4. Apache Flink 调度原理:JobManager 与 TaskManager、Slot 管理、Task 调度与部署
Flink 的调度层,说白了就是整个集群的“交通指挥中心”。你写好的 DataStream 或 Table API 作业,最终怎么分配到机器上跑起来?谁负责监控?谁负责干活?这就是本章要聊的核心。
我个人习惯把 Flink 的调度拆成三个层面来看:进程架构(谁是谁)、资源模型(怎么分 Slot)、任务调度(怎么部署)。咱们一个一个过。
4.1 进程架构:JobManager 与 TaskManager
Flink 集群启动后,会拉起两类进程。一个是“大脑”,一个是“肌肉”。
- JobManager(JM):负责接收作业、生成执行计划、协调 Checkpoint、故障恢复。它不处理数据,只做决策。
- TaskManager(TM):真正干活的进程。每个 TM 上运行多个 Task Slot,每个 Slot 里跑一个线程(Subtask)。
我在项目中遇到过一个问题:有人把 JobManager 的堆内存设得特别小,结果作业一多,JM 频繁 Full GC,整个集群的调度都卡住了。嗯,这里要注意——JM 是单点,虽然 Flink 支持 HA,但它的压力不能忽视。
核心关系图:
4.2 Slot 管理:资源隔离的最小单元
Slot 是 Flink 里最容易让人误解的概念。很多人以为一个 Slot 就是一个线程,其实不完全是。
Slot 的本质是 TaskManager 上的一组资源(CPU + 内存)的抽象。 每个 TM 可以配置多个 Slot,比如 taskmanager.numberOfTaskSlots: 4。每个 Slot 可以运行一个 SubTask 线程。
你想想看,如果两个 SubTask 共享同一个 Slot,它们就在同一个 JVM 进程里,可以共享 TCP 连接、心跳、内存池。这对网络密集型作业特别友好。
我的经验: 我曾经把 Slot 数设得和 CPU 核数一样多,结果发现内存不够用。后来我改成 Slot 数 = CPU 核数 × 0.75,留出系统余量,稳定多了。
Slot 的分配策略有两种:
- Eager 分配:作业启动时一次性申请所有 Slot。适合批处理,因为批作业的并行度是固定的。
- Lazy 分配:按需申请,边跑边要。适合流处理,特别是 Source 端先启动,后续算子再申请。
| 策略 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Eager | 批处理、固定并行度 | 启动快,无资源争抢 | 资源浪费(如果作业不需要全部 Slot) |
| Lazy | 流处理、动态扩缩容 | 资源利用率高 | 调度延迟,可能死锁(互相等待 Slot) |
避坑指南: 我曾经在流作业里用 Eager 分配,结果集群 Slot 不够,作业一直处于“等待资源”状态,Checkpoint 超时,最终导致作业失败。后来我改成 Lazy 分配,配合 slot.request.timeout 参数,问题解决了。
4.3 Task 调度与部署:从逻辑图到物理执行
Flink 的调度过程,我习惯分成四步走:
- StreamGraph 生成:用户代码(DataStream API)被翻译成 StreamGraph,节点是算子,边是数据流。
- JobGraph 优化:Flink 把可以链在一起的算子合并成 OperatorChain,减少网络传输。这一步叫“算子链优化”。
- ExecutionGraph 生成:JobGraph 被展开成并行化的 ExecutionGraph,每个并行实例就是一个 ExecutionVertex。
- 物理部署:JobManager 把 ExecutionVertex 调度到具体的 Slot 上,TaskManager 拉起对应的 Task 线程。
这里有个关键点:调度顺序。Flink 默认采用 拓扑排序,先调度 Source 节点,再调度下游节点。为什么?因为下游需要上游的数据,如果先调度下游,它只能空等,浪费资源。
调度流程图:
4.4 调度策略:你该选哪个?
Flink 提供了几种调度策略,我挑两个最常用的说说:
- 默认调度器(DefaultScheduler):基于拓扑排序,先调度 Source,再调度下游。适合大多数场景。
- 自适应调度器(AdaptiveScheduler):Flink 1.14 引入,可以根据集群负载动态调整并行度。适合资源波动的场景。
我个人习惯在流作业里用默认调度器,因为它的行为可预测。自适应调度器虽然灵活,但调试起来比较麻烦——你很难说清楚某个时刻为什么并行度变了。
小技巧: 如果你用自适应调度器,记得设置 jobmanager.adaptive-scheduler.min-parallelism 和 max-parallelism,防止调度器把并行度缩到 1 或者撑爆集群。
4.5 部署模式:Session 与 Application
调度策略定下来后,还得考虑部署模式。Flink 支持三种:
| 模式 | 集群生命周期 | 资源隔离 | 适用场景 |
|---|---|---|---|
| Session | 共享集群,作业提交即运行 | 弱(作业间共享 Slot) | 开发测试、小作业 |
| Per-Job | 每个作业独享集群 | 强 | 生产环境(已逐渐被 Application 替代) |
| Application | 作业即集群,Main 方法在集群执行 | 强 | 生产环境(推荐) |
嗯,这里要注意:Session 模式虽然方便,但作业间会抢 Slot。我曾经在 Session 集群上跑了 5 个流作业,其中一个作业的 Source 并行度突然增大,直接把其他作业的 Slot 挤没了,导致集体反压。后来我全改成 Application 模式,每个作业独立集群,再也没出过这种问题。
4.6 小结
Flink 的调度原理,说白了就是三件事:谁调度(JobManager)、谁干活(TaskManager)、怎么分资源(Slot)。理解了这个模型,你就能解释为什么作业启动慢、为什么 Slot 不够用、为什么某个 Task 一直处于 SCHEDULED 状态。
我个人建议:生产环境优先用 Application 模式 + 默认调度器,Slot 数按 CPU 核数的 0.75 倍配置,内存留 20% 给系统开销。这样能避免 80% 的调度问题。