JobManager调度架构:JobMaster职责、SchedulerNG接口、调度策略

好,咱们进入第二章。上一章我们聊了延迟问题的全貌,这一章得把枪口对准JobManager——尤其是它的调度核心。说白了,一个任务从提交到真正跑起来,中间那几百毫秒甚至几秒的延迟,很大一部分就耗在调度决策上。

我个人习惯,看任何分布式系统的调度器,先抓住三个东西:谁在管(职责)、怎么插拔(接口)、什么策略(算法)。Flink的调度架构也不例外。咱们一个一个拆。

JobMaster:调度动作的总指挥

先问个问题:一个Flink集群里,谁负责具体某个作业的调度?不是JobManager这个笼统的概念,而是JobMaster。

每个作业在提交后,Dispatcher会为它创建一个JobMaster。这个JobMaster就是该作业的“大脑”。它管什么呢?我列一下核心职责:

  • 生命周期管理:从作业初始化、调度执行,到失败恢复、最终停止,全由JobMaster掌控。
  • 资源申请与释放:它向ResourceManager要Slot,拿到Slot后再把Task部署上去。资源不够?它会触发pending或等待。
  • Checkpoint协调:虽然具体协调者是CheckpointCoordinator,但它是JobMaster内部的一个组件。调度延迟和Checkpoint的交互,后面章节会细讲。
  • 作业状态维护:维护整个作业的ExecutionGraph,记录每个Task的执行状态、位置、重试次数。

关键点:JobMaster是单点的。一个作业只有一个活跃的JobMaster。这意味着调度决策的延迟,直接受JobMaster的处理能力和当前负载影响。我在项目中遇到过JobMaster所在JVM Full GC导致调度停顿几秒的情况——嗯,后面讲调优时会专门说怎么避免。

SchedulerNG:调度逻辑的标准化接口

Flink 1.11之后,调度器被抽象成了SchedulerNG接口。NG代表Next Generation。为什么要搞这个接口?说白了,就是为了让调度策略可以灵活替换,不用每次都改一大坨代码。

我们看看这个接口长什么样:

public interface SchedulerNG {
    void startScheduling();
    void cancel();
    void updateGraph(ExecutionGraph executionGraph);
    CompletableFuture<Void> getGlobalTerminationFuture();
    // ... 其他方法
}

核心方法就几个:

  • startScheduling():触发调度流程。JobMaster在拿到ExecutionGraph后,调用这个方法开始干活。
  • cancel():取消调度。比如用户手动停止作业。
  • updateGraph():更新ExecutionGraph。这在动态扩缩容或失败恢复时很关键。

你可能会问:那具体怎么调度?接口只定义了“做什么”,没定义“怎么做”。怎么做,由具体的实现类决定。Flink内置了三种实现,也就是三种调度策略。

三种调度策略:Eager、Lazy、Pipelined

这三种策略,对应不同的作业拓扑和资源需求。我当年刚接触时也容易搞混,后来用一句话记住了:Eager是“全都要”,Lazy是“要了再说”,Pipelined是“边要边干”。

策略 核心思想 适用场景 延迟特点
Eager 一次性申请所有Task需要的Slot 批处理、小作业、资源充足 调度延迟低,但资源等待可能高
Lazy 按需申请,一个Task一个Task来 资源紧张、大作业、动态扩缩 调度延迟分散,整体可能更长
Pipelined 上游Task启动后立即调度下游 流处理、有pipeline连接 延迟最低,但依赖拓扑结构

Eager调度:简单粗暴,全量分配

Eager策略的逻辑很直接:JobMaster拿到ExecutionGraph后,一口气把所有Task的Slot需求算出来,然后一次性向ResourceManager申请。等所有Slot都到位了,再统一部署所有Task。

这样做的好处是:调度决策快,没有中间等待。坏处是:如果资源不够,整个作业就卡在“等待资源”状态,一个Task都跑不起来。我曾经在一个大集群上跑一个1000个并行度的批作业,Eager策略下等了30秒才凑齐所有Slot——那30秒就是纯浪费。

Lazy调度:按需分配,逐步推进

Lazy策略就聪明一点。它不会一次性要所有Slot,而是先调度Source Task,等Source Task跑起来、产生数据了,再调度下游Task。说白了,就是“用到哪个调度哪个”。

这种策略对资源紧张的环境很友好。但代价是:调度延迟被拉长了。每个Task的启动都要经历一次“申请Slot→等待→部署”的完整流程。如果你的作业有10层算子,那调度延迟就是10倍的叠加。

Pipelined调度:流水线式,上下游联动

Pipelined策略是流处理的默认选择。它的核心是:上游Task一旦部署成功,立即触发下游Task的调度,不需要等所有上游都就绪。

举个例子:Source和Map之间有pipeline连接。Source部署完成后,JobMaster马上开始为Map申请Slot、部署Map。这时候Source可能还没开始处理数据,但调度动作已经并行推进了。

这种策略的延迟最低,因为它最大化利用了“调度”和“执行”的重叠时间。我在做实时数仓时,把默认的Eager改成Pipelined,作业启动时间从12秒降到了4秒——效果立竿见影。

避坑指南:我曾经在一个有多个Input的算子(比如CoGroup)上踩过坑。Pipelined策略下,如果多个上游的调度速度不一致,下游算子可能因为部分上游还没部署而一直pending。这时候需要检查ExecutionEdge的连接类型,确保pipeline关系正确。

调度策略选择:实战建议

说了这么多,到底怎么选?我个人的经验是:

  • 流处理作业:无脑选Pipelined。这是Flink流处理的默认策略,也是延迟最优解。
  • 批处理小作业(并行度 < 50):Eager。简单直接,资源够用的话最快。
  • 批处理大作业(并行度 > 200):Lazy或Pipelined。避免一次性资源申请失败导致整个作业卡死。
  • 混合场景:如果作业既有批处理又有流处理,建议按算子粒度手动指定调度策略。Flink允许在StreamGraph构建时通过setScheduleMode设置。

核心调度流程可视化

下面这张图,我把JobMaster、SchedulerNG、三种策略的关系画出来了。你看一眼就能明白整个调度链路。

Flink JobManager 调度架构核心流程 JobMaster SchedulerNG 接口 Eager 调度 Lazy 调度 Pipelined 调度 说明: • JobMaster 是每个作业的调度总指挥,负责生命周期和资源协调。 • SchedulerNG 是调度逻辑的标准化接口,支持策略热插拔。 • 三种策略分别对应不同的资源申请时机和拓扑依赖,延迟表现差异明显。 • 实际生产环境中,Pipelined 是流处理的首选,Eager 适合小批,Lazy 适合资源受限场景。

调度延迟的源头:藏在哪?

理解了架构,我们就能定位延迟的源头了。调度延迟主要来自三个环节:

  1. Slot申请等待:JobMaster向ResourceManager发请求,ResourceManager再向TaskManager要资源。如果TaskManager没有空闲Slot,就得等。这个等待时间,Eager策略下尤其明显。
  2. ExecutionGraph构建:JobMaster在调度前需要把StreamGraph转换成ExecutionGraph,这个转换过程涉及算子链合并、Slot共享组计算。作业拓扑越复杂,耗时越长。
  3. Task部署下发:JobMaster把Task部署请求发给TaskManager,TaskManager拉取JAR包、启动JVM、初始化算子。这部分延迟和网络、磁盘IO强相关。

注意:很多人以为调度延迟只是“申请Slot”那一下。其实不是。我见过一个案例,作业有2000个算子,ExecutionGraph构建就花了8秒——这8秒完全没开始调度。所以排查延迟时,一定要把“构建图”的时间也算进去。

好了,这一章我们把JobManager的调度骨架搭起来了。你知道了JobMaster管什么、SchedulerNG接口怎么用、三种策略各自什么脾气。下一层,我们要钻进调度器的内部,看看它到底是怎么做决策的——嗯,那是第三章的内容。