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,但它的压力不能忽视。

核心关系图:

Flink 运行时进程架构 JobManager 调度 / Checkpoint / 恢复 TaskManager 1 Slot 1 Slot 2 Slot 3 Slot 4 TaskManager 2 Slot 1 Slot 2 Slot 3 Slot 4

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 的调度过程,我习惯分成四步走:

  1. StreamGraph 生成:用户代码(DataStream API)被翻译成 StreamGraph,节点是算子,边是数据流。
  2. JobGraph 优化:Flink 把可以链在一起的算子合并成 OperatorChain,减少网络传输。这一步叫“算子链优化”。
  3. ExecutionGraph 生成:JobGraph 被展开成并行化的 ExecutionGraph,每个并行实例就是一个 ExecutionVertex。
  4. 物理部署:JobManager 把 ExecutionVertex 调度到具体的 Slot 上,TaskManager 拉起对应的 Task 线程。

这里有个关键点:调度顺序。Flink 默认采用 拓扑排序,先调度 Source 节点,再调度下游节点。为什么?因为下游需要上游的数据,如果先调度下游,它只能空等,浪费资源。

调度流程图:

Flink 调度流程 StreamGraph 逻辑 DAG JobGraph 算子链优化 ExecutionGraph 并行化展开 物理部署 Slot 分配 调度顺序:Source → Map → Window → Sink(拓扑排序) 下游算子等待上游数据就绪后才开始执行

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% 的调度问题。