资讯详情

资讯详情

Flink运行时调度机制深度解析:从JobGraph到ExecutionGraph的调度策略与调优实践

1. 从一次作业卡顿说起为什么要啃运行时调度这块硬骨头很多人用 Flink 写作业代码跑通了、本地测试也过了一上生产就发现各种诡异现象明明并行度设了 16资源也够但某些 subtask 就是迟迟不启动或者作业重启之后数据倾斜的毛病突然消失了过一会儿又回来了。这些问题翻文档翻不到答案看日志也只能看到一堆 Deploying 和 Scheduling 的字样。说到底是没搞明白 Flink 运行时调度到底在干什么。Flink 的运行时调度简单说就是把作业图变成真正跑在机器上的任务这一整套决策过程。它要回答几个核心问题一个 JobGraph 怎么被拆成 ExecutionGraphExecutionGraph 里的节点按什么顺序被调度资源不够的时候是等还是抢失败之后是局部重启还是全局重启。这套机制直接决定了作业的启动速度、资源利用率、故障恢复行为甚至影响数据处理的正确性。这篇文章适合两类人看一类是已经能写 Flink 作业、但遇到调度相关问题只能靠猜的开发者另一类是需要做 Flink 集群运维、要调优作业启动和恢复性能的工程师。我会从调度器的整体架构讲起把 JobGraph 到 ExecutionGraph 的转换、调度策略的选择、资源分配流程、失败恢复的调度行为这几个关键环节拆开揉碎配上图解式的文字描述和实际调优经验。读完你至少能做到看到调度日志知道它在哪一步卡住了能根据作业特征选对调度模式能解释为什么你的作业重启要花那么久。需要说明的是Flink 版本迭代较快本文的机制描述以 1.14 到 1.17 这个区间的主流行为为基准不同版本在细节上可能有差异但核心调度逻辑是相通的。涉及具体参数时我会标注出来方便你对照自己集群的版本验证。2. 调度器的三层结构从 JobGraph 到物理执行的映射链路2.1 JobGraph、ExecutionGraph、物理执行图的关系要理解调度先得把 Flink 的几层图分清楚。用户写的 DataStream API 代码经过算子链优化之后在客户端生成的是JobGraph。JobGraph 的节点是 JobVertex一个 JobVertex 可能包含多个被 chain 在一起的算子。这时候的图还是逻辑层面的没有并行度的概念也没有资源的概念。JobGraph 提交到 JobManager 之后会被转换成ExecutionGraph。这是调度真正操作的对象。转换的核心动作是每个 JobVertex 按照它的并行度展开成若干个 ExecutionVertex每个 ExecutionVertex 代表一个并行子任务。同时ExecutionGraph 里还会插入 IntermediateResult 和 IntermediateResultPartition 这些中间结果节点用来描述数据交换关系。再往下ExecutionVertex 被调度到具体的 TaskManager 上执行时对应的是物理执行图也就是实际运行的 Task。这三层的关系可以这样理解JobGraph 是设计图纸ExecutionGraph 是施工计划物理执行图是实际盖出来的楼。调度器的工作就是在 ExecutionGraph 这一层做决策然后驱动物理执行。这里有个容易混淆的点ExecutionVertex 和 Execution 的区别。一个 ExecutionVertex 在它的生命周期里可能有多个 Execution每次失败重启就会产生一个新的 Execution用 executionId 区分。理解这一点对分析失败恢复行为很关键因为调度器重启的时候操作的是 ExecutionVertex而不是直接操作 Execution。2.2 调度器组件与职责划分Flink 的调度相关组件主要有这么几个各管一摊Scheduler调度策略的实现者决定下一个该调度谁。Flink 提供了三种内置调度器后面会详细对比。SchedulingStrategy和 Scheduler 配合决定在满足什么条件下开始调度。比如 Eager 策略是全部立即调度Lazy 策略是等上游数据就绪才调度。SlotPool管理 TaskManager 上报的 slot 资源负责 slot 的申请、分配和释放。它是调度器和资源之间的中间层。ExecutionGraph被调度的对象同时维护着所有 ExecutionVertex 的状态机。JobMaster整个调度的驱动者协调上面这些组件工作。这几个组件的协作流程大致是JobMaster 启动后创建 ExecutionGraphScheduler 根据调度策略遍历 ExecutionGraph 中待调度的顶点向 SlotPool 申请 slotSlotPool 从可用资源中分配分配成功后触发 Task 部署。任何一个环节出问题作业就卡在对应的状态上。提示排查调度卡顿的时候先看 ExecutionGraph 里顶点的状态。如果大量顶点停在 CREATED 状态说明调度器还没开始调度它们如果停在 SCHEDULED 状态说明在等 slot如果停在 DEPLOYING说明 slot 有了但 Task 部署有问题。状态是定位问题的第一手线索。2.3 调度触发的时机调度不是一次性完成的而是在多个时机被触发。作业刚提交时JobMaster 会做一次初始调度。之后每当有 ExecutionVertex 状态变化、有新的 slot 上报、有 Task 执行完成或失败都可能触发新一轮调度。这种事件驱动的设计让调度能快速响应状态变化但也意味着调度逻辑必须处理好并发和幂等。实际运维中我遇到过一种情况作业启动后长时间没动静日志里也没有明显报错。后来发现是 SlotPool 里一直没有可用 slot而调度器在等 slot 的过程中没有打印足够的信息。这种时候需要主动去看 TaskManager 的注册情况和 slot 的分配情况不能干等日志。3. 三种调度策略的取舍Eager、Lazy 与分阶段调度3.1 Eager 调度全部立即调度的代价Eager 调度是最直观的策略作业一启动所有 ExecutionVertex 立刻进入调度流程不管上游数据有没有准备好。这种策略的优点是启动快所有任务并行拉起没有等待。缺点是资源消耗大如果作业的并行度很高会瞬间向集群申请大量 slot可能把集群资源打满影响其他作业。Eager 调度适合什么场景我个人的经验是适合那些资源充足、追求启动速度、且作业拓扑比较简单的场景。比如一些实时性要求高的短作业或者独占集群的大作业。但如果你的集群是多租户共享的用 Eager 调度要谨慎因为它可能造成资源抢占。在 Flink 里Eager 调度对应的是EagerSchedulingStrategy它是默认策略之一。配置上可以通过jobmanager.scheduler相关参数调整不过大多数情况下不需要手动改除非你有明确的调度需求。3.2 Lazy 调度按需调度的资源友好方案Lazy 调度也叫 LazyFromSources的思路是只调度数据源顶点下游顶点等上游有数据产出后再调度。这样做的好处是资源按需使用不会一次性占满。对于流作业来说数据是从 source 往下游流的所以按需调度很自然。但 Lazy 调度有个明显的坑如果作业拓扑里有多个 source或者有复杂的 join 结构按需调度的顺序可能不符合预期导致某些顶点迟迟不被调度。我见过一个作业两个 source 做 union 之后接一个算子结果其中一个 source 的数据一直不被处理排查半天才发现是调度顺序的问题。Lazy 调度适合资源紧张、作业拓扑是简单流式结构的场景。它对应的实现是LazyFromSourcesSchedulingStrategy。需要注意的是这个策略在批处理场景下行为可能和流处理不同因为批处理的数据依赖关系更复杂。3.3 分阶段调度与三种策略的对比除了 Eager 和 LazyFlink 还有分阶段调度Phased Scheduling主要用于批处理场景。它的思路是把作业分成多个阶段每个阶段内部可以并行调度阶段之间按依赖关系串行。这样既能利用并行性又能控制资源峰值。三种策略的对比可以看下面这个表策略调度时机资源占用适用场景主要风险Eager全部立即调度峰值高资源充足、追求速度资源抢占、启动风暴Lazy按上游数据就绪峰值低资源紧张、简单流拓扑调度顺序异常、部分顶点饥饿分阶段按阶段调度可控批处理、复杂依赖阶段划分不当导致串行过长选策略的时候我的建议是先看作业类型纯流作业优先考虑 Lazy 或 Eager批作业考虑分阶段。再看资源情况共享集群用 Lazy独占集群可以用 Eager。最后看拓扑复杂度拓扑越复杂越要谨慎用 Lazy因为调度顺序不好预测。注意调度策略不是配了就完事它和并行度、slot 数量是联动的。并行度调高但 slot 不够再好的策略也白搭。调优的时候要把这三个参数放在一起看。4. 资源分配与 Slot 管理调度器背后的资源账本4.1 Slot 的概念与共享机制Slot 是 Flink 资源管理的基本单位一个 TaskManager 上的资源被划分成若干个 slot。每个 slot 能跑一个 Task或者一个算子链。Slot 的数量决定了 TaskManager 能并行跑多少个任务。Slot 共享Slot Sharing是 Flink 的一个重要机制同一个 SlotSharingGroup 里的不同算子可以共享一个 slot。默认情况下所有算子属于同一个共享组所以一个 slot 里可以跑整条流水线的任务。这样做的好处是提高资源利用率因为不同算子的资源需求在时间上可能错开。但 slot 共享也有代价。如果共享组里的某个算子特别耗资源会影响同组其他算子的执行。我遇到过因为一个算子内存占用过高导致同 slot 的其他算子频繁 GC 的情况。这种时候可以考虑把耗资源的算子单独分一个共享组用slotSharingGroup方法指定。4.2 SlotPool 的申请与分配流程SlotPool 是调度器和 TaskManager 之间的资源中介。它的工作流程是这样的调度器需要 slot 时向 SlotPool 发起申请SlotPool 先看自己缓存的可用 slot 里有没有合适的有就直接分配没有的话向 ResourceManager 请求新的 TaskManagerTaskManager 启动后把 slot 上报给 SlotPoolSlotPool 再分配给等待的申请。这个流程里有个关键点SlotPool 分配 slot 时会尽量满足申请的位置偏好locality preference。比如某个 Task 希望跑在数据所在的节点上SlotPool 会优先分配那个节点的 slot。如果等不到超时后会退而求其次分配其他节点的 slot。这个超时时间由slot.request.timeout控制默认是 5 分钟。实际调优中这个超时时间值得关注。如果集群资源充足但作业启动慢可能是 SlotPool 在等位置偏好把超时调小能加快启动但可能牺牲数据本地性。反过来如果作业对数据本地性要求高可以调大超时但启动会变慢。这是个权衡。4.3 资源不足时的调度行为资源不足时调度器的行为取决于策略和配置。Eager 策略下所有顶点都会尝试申请 slot申请不到的会排队等待。Lazy 策略下只有被触发的顶点会申请等待的顶点少一些。这里有个容易被忽略的点资源不足时作业可能部分运行、部分等待。也就是说已经拿到 slot 的顶点会正常跑没拿到的会一直等。这种半死不活的状态有时候比直接失败更难排查因为作业看起来在运行但数据处理不完整。排查这类问题关键是看 ExecutionGraph 里各顶点的状态分布。如果一部分是 RUNNING一部分是 SCHEDULED基本可以确定是资源不足。这时候要么加资源要么降并行度要么换调度策略。提示jobmanager.execution.failover-strategy这个参数和资源不足时的行为有关。默认是region意思是按区域重启。如果资源长期不足可以考虑调整这个策略但更根本的解决办法还是加资源或优化作业。5. 失败恢复中的调度行为重启背后的决策逻辑5.1 失败类型与恢复策略的对应关系Flink 的失败恢复不是简单的挂了就重启而是根据失败类型和配置选择不同的恢复范围。核心的配置是restart-strategy和failover-strategy。重启策略决定重启几次、间隔多久有固定延迟、失败率、无重启等几种。故障恢复策略决定重启哪些部分有全图重启和区域重启两种。区域重启Region Failover是 Flink 1.9 之后引入的优化它把 ExecutionGraph 划分成多个区域失败时只重启受影响的区域而不是整个作业。区域怎么划分简单说就是根据数据交换关系把有依赖的顶点划到一起。一个区域内的顶点要么一起重启要么一起不重启。这样能减少重启带来的数据重放和状态恢复开销。5.2 区域重启的调度过程拆解区域重启的调度过程可以拆成几步首先失败的 ExecutionVertex 被标记为 FAILED然后调度器找到这个顶点所属的区域把区域内所有顶点标记为需要重启接着取消这些顶点正在运行的 Task释放它们占用的 slot最后重新调度这些顶点走一遍正常的调度流程。这个过程里slot 的释放和重新申请是关键。如果 slot 释放不及时重新调度会等很久。我遇到过因为 Task 取消卡住导致 slot 迟迟不释放的情况最后是通过调整task.cancellation.timeout解决的。另外区域重启会触发状态恢复。如果作业开了 checkpoint重启后的顶点会从最近的 checkpoint 恢复状态。checkpoint 的大小和恢复时间直接影响重启耗时。所以优化重启性能很多时候是在优化 checkpoint。5.3 重启过程中的常见卡点重启卡顿是运维中的高频问题。根据我的经验卡点主要有这么几类Slot 释放慢Task 取消时如果有资源没清理干净slot 会一直占着。排查方法是看 TaskManager 的日志找取消相关的异常。Checkpoint 恢复慢状态大的作业从 checkpoint 恢复要读大量数据。优化方向是减小状态、用增量 checkpoint、把状态后端换成性能更好的。调度器处理慢ExecutionGraph 很大时调度器遍历和更新状态的开销不可忽略。这种情况可以考虑降低并行度或者拆分作业。资源申请排队重启后重新申请 slot如果集群资源紧张要排队等。这种只能靠加资源或错峰重启。排查重启问题我习惯先看时间线从失败发生到第一个 Task 重新运行中间隔了多久卡在哪一段。Flink 的日志里通常有足够的信息还原这个时间线关键是要有耐心去翻。6. 调度性能调优的实战经验6.1 并行度、Slot 数与调度开销的平衡并行度不是越高越好。并行度提高ExecutionGraph 的顶点数增加调度器的管理开销随之上升。同时slot 申请的数量增加SlotPool 的压力也变大。我做过一个测试同样一个作业并行度从 16 提到 128启动时间从 8 秒涨到了 40 多秒其中大部分时间花在调度和 slot 分配上。所以调并行度的时候要综合考虑数据量、资源量和调度开销。一个经验法则是并行度不要超过集群可用 slot 总数的 80%留一些余量给调度波动和故障恢复。另外如果作业的瓶颈不在并行度上盲目提高并行度只会增加调度负担不会提升吞吐。6.2 调度日志的阅读方法Flink 的调度日志信息量很大但很多人不知道怎么读。我的方法是分层看先看 JobMaster 的日志了解调度的整体进展再看 TaskManager 的日志了解 Task 的部署和执行情况最后看 ResourceManager 的日志了解资源的申请和分配。关键日志关键词包括Scheduling、Deploying、Allocated slot、Requesting new TaskManager、Failed。看到Scheduling说明调度器开始处理某个顶点看到Allocated slot说明资源分配成功看到Requesting new TaskManager说明资源不够在申请新资源。把这些关键词串起来就能还原调度的完整过程。日志级别也值得注意。默认的 INFO 级别可能不够详细排查复杂问题时可以临时调到 DEBUG但要注意 DEBUG 日志量很大别把磁盘写满了。6.3 几个容易被忽视的调度参数除了前面提到的还有几个参数对调度行为有影响但容易被忽视jobmanager.execution.failover-strategy故障恢复策略默认 region。如果作业拓扑是线性的region 和 full 差别不大如果是复杂 DAGregion 能显著减少重启范围。slot.idle.timeout空闲 slot 的超时时间。调小能更快释放闲置资源但可能导致频繁申请释放。slot.request.timeoutslot 申请超时。前面说过影响位置偏好和启动速度的权衡。taskmanager.numberOfTaskSlots每个 TaskManager 的 slot 数。这个值影响单节点的并行能力和资源隔离粒度。这些参数没有万能的最优值要根据作业特征和集群情况调。我的建议是每次只调一个观察效果别一次改一堆否则出了问题不知道是哪个参数导致的。7. 我在实际调度排查中踩过的坑说几个真实的排查经历都是文档里不会写的。第一个坑作业启动后一直卡在 SCHEDULED 状态日志里没有任何报错。查了半天发现是 SlotPool 里有一个僵尸 slot是之前失败的 Task 没释放干净的。解决办法是重启 TaskManager但根本的修复是升级 Flink 版本新版本对 slot 释放的处理更健壮。第二个坑区域重启后作业的数据处理结果出现重复。原因是重启的区域和未重启的区域之间的数据交换没有正确处理。这个问题的根源是 checkpoint 的 barrier 对齐没做好后来通过调整 checkpoint 配置和确认 exactly-once 语义的配置解决了。第三个坑调度器在高负载下响应变慢导致作业启动时间波动很大。排查发现是 JobMaster 的线程池被占满调度任务排队。解决办法是给 JobMaster 分配更多资源同时降低作业的并行度减少调度任务的数量。这些坑的共同点是表面现象和根本原因之间隔了好几层需要顺着调度链路一层层往下查。我的经验是遇到调度问题不要急着改配置先把 ExecutionGraph 的状态和日志的时间线理清楚大部分问题都能定位到具体的环节。最后分享一个小技巧Flink 的 Web UI 里有 ExecutionGraph 的可视化能看到每个顶点的状态和它们之间的连接。排查调度问题时这个图比日志更直观。配合日志一起看效率会高很多。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →