news 2026/10/17 3:35:48

Flink运行时架构详解:从任务提交到集群调度与执行

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flink运行时架构详解:从任务提交到集群调度与执行

带着问题去理解架构,是一种很笨但很实用的学习方法。早期我学 Flink 的时候,上来就啃源码,结果反复在各种术语里打转。后来换了个思路:先不管代码里的细节,只问一个问题——一个用户写好的 Flink 作业,从提交到集群启动运行,中间到底发生了什么?把这条链路捋清楚,再回去看架构图,每个组件的角色就都站对位置了。

这篇文章是这个系列的第一篇,把整体架构、基本组件、任务提交流程拉通讲一遍。不要指望看完就能写出分布式调度器,但至少你再遇到“TaskManager 挂了”“Slot 不够”这类报错时,心里有个底。适合刚入门 Flink 的数据工程师,也适合准备面试时把基础概念串一遍的人。

1. 先把整体架构放在眼前:Flink 运行时到底在解决什么

分布式流处理的本质,是把一个连续的计算拆到多台机器上,还要让它们看起来像在一台机器上一样协作处理。这里最头疼的不是计算本身,而是编排:谁来接收数据,谁来转换,谁来汇总,网络抖动时怎么办,某台机器挂了怎么接续。一个靠谱的运行时架构,本质就是一套把这些问题全部标准化的分工机制。

我习惯用餐厅后厨来类比。一个夫妻店,厨师既要洗菜又要切菜又要炒菜,订单小的时候没问题,但一旦客流上来就完全乱套。连锁餐厅的后厨是分层协作:主厨定菜谱、管出品,切配师傅负责切菜,炒锅师傅负责炒菜,打荷的负责传菜,大家只需要通过固定的流程和窗口协作,各自关注自己的环节。数据流处理也是这么回事,一条数据从入口到出口会经过很多算子,这些算子可能分布在不同的机器上,机器之间靠网络传送数据。

Flink 的运行时架构可以抽象成三个核心角色。客户端是把用户写的代码翻译成作业描述的角色,相当于把菜谱写成标准流程的人;JobManager 是整个作业的大脑,负责调度和协调,相当于主厨;TaskManager 是真正干活的节点,里面跑着各类计算任务,相当于各位师傅。三个角色的职责边界很清楚,所以定位问题时也很方便:作业起不来,先看客户端和 JobManager;运行中性能差、数据倾斜,那基本是 TaskManager 这一端。

为什么必须把职责拆得这么清楚,而不是让每个节点各自为政?因为分布式系统里最危险的就是没有统一领导的资源竞争。如果每个节点都自己决定跑什么任务,出现故障时就会出现谁都不管或者谁都在管的混乱状态。JobManager 做统一调度,目的就在于用全局视角做决策:谁该休息、谁能接管、资源怎么重新分配。这是所有分布式计算框架的共同思路,只不过 Flink 把执行细节做得更细。

2. 基本组件拆解:各自的位置、职责与背后的设计逻辑

2.1 客户端:作业的入口与翻译官

客户端是启动一切的地方。你写的 DataStream 程序或者 Table/SQL 程序,最终都是在客户端完成解析和优化的。它会把用户代码转换成一张逻辑的数据流图,再对这个图做一些优化,最后打包成一个可提交的 JobGraph 发给集群。

这个阶段有个容易被忽略的细节:客户端本身不参与计算,它只是一个入口。生产环境里常见一个现象:用交互式提交脚本启动作业,然后终端一关,客户端退出,有人以为作业也挂了。其实作业是否继续运行,取决于提交时使用的模式以及是否需要保持客户端连接来接收状态。这个问题后面说提交流程时再展开。

客户端具体做的事情包括:校验作业配置,生成逻辑计划,做一部分静态优化,比如算子合并的判断。这些工作如果放到 JobManager 上做也不是不行,但会白白消耗集群 CPU,更重要的是会让提交作业的速度变慢,因为 JobManager 要处理的请求很多。把解析和优化放在客户端,是一个很明智的分工:集群只负责调度和执行,不负责翻译菜谱。

2.2 JobManager:集群的大脑与调度中心

JobManager 是集群中真正的领导角色。它在整个作业生命周期里做三件主要的事情:调度执行、协调检查点、触发故障恢复。调度执行指的是把客户端提交上来的 JobGraph 转换成可并行执行的 ExecutionGraph,然后为每个并行任务寻找合适的 TaskManager 和 Slot;协调检查点是定期通知各个 TaskManager 做状态快照,并收集快照完成情况;故障恢复是某个任务失败之后,由 JobManager 决定到底是局部重试还是整个作业重新调度。

实际运行时,JobManager 内部还会分成几个小的组件,例如负责作业调度的 JobMaster、负责资源管理的 ResourceManager、负责检查点的 CheckpointCoordinator。这些概念在不同版本里会有细微差别,但从学习的角度,你只要记住 JobManager 是一个管理集合就足够了。

高可用场景下,JobManager 通常不止一个实例。多个实例之间通过某种协调服务做领导者选举,只有一个是活跃的,其余待命。一旦活跃实例宕机,其它实例会接管职责。为什么这么设计?因为单点是分布式系统最忌讳的事。流式作业可能要跑几个月,一个节点长时间不宕机的概率其实并不高。如果 JobManager 挂了整个作业就完蛋,流式作业的可用性就无从谈起。

2.3 TaskManager:真正干活的执行者

TaskManager 是集群里真正执行数据计算的进程。每个 TaskManager 会被划分成若干个 Slot,Slot 是资源分配的最小单位,可以理解为 TaskManager 里的一块固定大小的资源租约,包含一定的 CPU 配额和内存配额。一个 Slot 可以运行一个或多个任务子任务,具体取决于是否启用了 Slot 共享。

任务线程与 Slot 的对应关系是初学者最容易搞糊涂的地方。Slot 并不是线程,它只是一个资源容器;真正执行算子逻辑的是 JVM 里运行的线程。一个 Slot 里可能有好几个子任务在排队执行。为什么这样设计?如果每个子任务都占用一个独立进程,整个集群会被进程数撑爆,内存也会被大量重复浪费。共享 Slot 之后,相同资源池里的任务可以共享内存中的一部分数据结构,减少序列化和网络开销。

从运维角度看,Slot 数量决定了一个 TaskManager 最多能同时承载多少个并行子任务。假设一个 TaskManager 配置了两个 Slot,另一个配置了四个 Slot,集群总可用 Slot 就是六个。作业并行度为五的话,会占用其中五个 Slot,剩下一个留给其它作业。如果作业的并行度大于集群总 Slot 数,调度器就只能排队等待资源,这时候你会看到作业一直在启动中或者 Restarting。

2.4 高可用集群的外围组件

除了三个核心角色,一个在生产环境真正可靠的集群还需要外围组件支撑:例如用于领导者选举和元数据存储的协调服务,以及用于保存检查点和作业元数据的持久化存储。协调服务的角色很多时候被忽略,但它才是保证 JobManager 多实例能正常配合的基础。

持久化存储的作用也很直接:JobManager 需要把 JobGraph、作业的配置信息、检查点元数据保存下来,一旦出现故障,新的 JobManager 可以从存储中恢复这些信息,继续之前的调度和恢复。如果没有持久化,宕机之后所有状态都是内存里的临时数据,一旦重启就全没了。

这些外围组件和运行时架构有什么关系?其实它们是从单机可用到生产可用的关键区别。单机环境下你可以把所有角色都塞进一个 JVM 里;生产环境里只要任何一个角色没有高可用保障,整个集群的可用性就会大打折扣。

3. 任务提交流程:从代码到分布式集群运行的完整链路

3.1 四层图的转换逻辑

理解 Flink 提交流程的核心,是搞懂四张图:StreamGraph、JobGraph、ExecutionGraph,以及最终的物理执行图。这是一个逐步细化的过程。

StreamGraph 是最接近用户代码的一层,节点就是用户对数据流的各种操作,比如 map、filter、keyBy、window,边就是数据在这些操作之间的流动关系。它基本上是按照用户书写顺序生成的,还没有做复杂优化。JobGraph 是客户端做过一轮优化之后的结果,主要变化是把可以合并的算子链融合成一个节点,同时把数据传输的具体方式,比如分区器、序列化器,都固化下来。ExecutionGraph 是 JobManager 收到 JobGraph 之后,结合并行度生成的并行化视图,每个逻辑节点会被复制成与并行度一致的并行子任务。最后的物理执行图,就是这些子任务被实际调度到具体 TaskManager 的 Slot 上时形成的部署视图。

为什么设计这么多层?一个原因是职责分层:用户关心的是逻辑语义,客户端负责把逻辑语义翻译成可调度的计划,JobManager 负责把计划变成可并行的执行计划,TaskManager 负责把执行计划落地。另一个原因是便于优化:不同层级有不同的优化手段,比如算子链优化在 JobGraph 阶段做最合适,而并行度的分组调度在 ExecutionGraph 阶段才能做。

举个例子。你的代码是 source → map → keyBy → window → sink。StreamGraph 里这五个节点是分开的。做算子链优化时,source 和 map 如果能串在一起,就会变成一个 JobGraph 节点;但 keyBy 是重分区操作,所以 keyBy 之后的 window 和 sink 也可能在另一个链里。最终 JobGraph 可能只有两三个节点。到了 ExecutionGraph,根据并行度再展开成多个子任务。

3.2 算子链:减少开销的关键优化

算子链是 Flink 里非常重要的优化手段。简单说,如果两个相邻算子之间是窄依赖,也就是数据不需要跨分区重新打散,那么它们可以合并成一个任务,在同一个线程里执行。比如 source 之后接一个 map,又接一个 filter,这三个算子往往可以合并成一个任务链。

为什么合并能带来性能提升?因为不合并的话,每个算子都是独立任务,数据从一个任务到下一个任务需要经过序列化、网络传输、反序列化,或者线程间队列传递。合并之后,数据就在同一个线程的函数调用栈里直接传递,省掉了大量内存拷贝和线程切换开销。在高吞吐场景下,这个优化往往能带来成倍的效果差异。

什么时候算子链会断开?常见的是 keyBy、rebalance、broadcast 这类需要重新分区的操作,数据必须从一个子任务路由到另一个子任务,链路自然就断了。另一种是你手动关闭了算子链,通过配置强制每个算子单独成为一个任务。一般是为了定位某个算子的性能瓶颈,或者需要单独设置某个算子的并行度才会这么做。在 UI 上看到的 Task,很多时候是一个算子链而不是单个算子,新手排查问题时要注意这一点。

3.3 JobManager 的调度决策

接下来看调度阶段。JobManager 收到 JobGraph 之后,会为每个任务计算所需 Slot 数量。这里有一个常见误区:所需 Slot 数量不是所有算子并行度的简单加法,而是取最大并行度,因为默认开启了 Slot 共享。假设一个作业有三个算子,并行度分别是 2、2、6,那么整个作业只需要 6 个 Slot 就够。每个 Slot 里会放若干个属于不同算子的子任务,这样资源利用率更高,启动速度也更快。

调度时 JobManager 会考虑数据本地性,尽量把子任务调度到输入数据所在的机器,减少跨机网络传输。流式作业里数据是不间断的,所以这个考量对批式作业更明显,但架构理解上是统一的。然后它会向对应的 TaskManager 发送部署请求,TaskManager 在本地创建任务线程,连接上游和下游的数据通道,开始数据处理。

这里必须提一下检查点协调的时间点。调度完成、任务全部进入 RUNNING 状态之后,CheckpointCoordinator 才会开始周期性触发检查点。很多作业一启动就在做检查点,而状态很大导致检查点超时,看起来就是作业没报错但一直在恢复重启。这种问题往往不是调度架构的锅,而是检查点参数没调好。

3.4 不同提交模式的差异

从任务提交的角度,Flink 还有一个概念必须讲:提交模式,也就是会话模式、单作业模式、应用模式。会话模式是集群常驻,一个集群同时提交多个作业,优势是资源共享、提交快,短板是作业之间会互相影响,一个作业把内存打爆可能带崩其它作业。单作业模式是集群跟着作业走,作业结束集群就释放,隔离性最好,但每次提交都要重新启动集群,开销大。应用模式类似单作业,但用户的主方法是在集群的某个节点上执行,客户端提交完就可以退出,特别适合打包好的作业包。

这三种模式影响的正是架构里客户端、JobManager、TaskManager 的生命周期。会话模式下客户端只是一个发起者,集群常驻;单作业模式下客户端短暂存在,集群动态创建;应用模式下甚至连客户端的很多工作都移到了集群内部。理解这个差异对排障很有帮助。比如会话模式下作业挂了但集群还在,可以立刻再提交;单作业模式下要重新排队启动集群,时间成本完全不同。

4. 实践中的几个大坑:槽位、并行度与内存的纠缠

4.1 槽位不是线程,也不是并发度

很多人把 Slot 数和并行度划等号,这是一个很危险的直觉。Slot 是 TaskManager 对外承诺的资源容量,类似餐厅里的一张桌子可以坐不同的客人,但桌子本身不是厨师,也不代表厨师数量。并行度是算子层面的并发概念,决定一个算子被拆成多少个子任务。两者之间的关系,是通过调度器的分配规则连接起来的。

举个具体例子。一个集群有两个 TaskManager,每个配置两个 Slot,所以总共四个 Slot。一个作业的并行度是三,另一个作业的并行度也是三,两个作业都想跑的话,第二个作业会一直等待,因为剩余 Slot 不够。但第二个作业的并行度如果是 1,它就能立刻跑起来。这里体现的是资源不是按作业数算,而是按 Slot 使用量算。生产环境调并行度时,一定要把集群总 Slot 数放在心里,不要把作业并行度调到比集群剩余 Slot 数还大。

还有一点值得提醒:Slot 共享虽然能提高利用率,但也会带来故障域扩大。如果一个 Slot 里塞了多个算子的子任务,其中某个子任务异常导致 Slot 被释放,同 Slot 的其他子任务也会受影响。排查问题时,可以先从 UI 上看一个 TaskManager 的 Slot 里到底跑了哪些任务,再判断是单点问题还是资源争抢问题。

4.2 并行度该调多少,才能兼顾吞吐和资源

并行度的设置是一门艺术。设大了,资源浪费,作业启动慢,状态因为分片太多而膨胀;设小了,数据倾斜和积压就会出现。经验上,流式作业建议并行度不超过集群 Slot 总数的 70%,留出一部分余量给后续的恢复、扩容或者其它作业。另外,不同算子差异很大,source 和 sink 的并行度可以跟核心业务算子不同。如果你的 source 是从消息中间件拉数据,并行度一般受分区数限制,设得再大也只会有部分子任务真正在拉数据。

还有一个值得注意的细节:状态后端的存储会与并行度强相关。并行度调高,状态分片更多,检查点压力也更大;并行度调小,单个子任务的状态可能暴涨,容易出现内存压力。所以改并行度的时候,最好顺带观察检查点的高峰时延,如果出现明显恶化,就要回头考虑是不是并行度变化带来的连锁反应。

4.3 内存:堆以外还有一笔账

Flink 的内存管理在运行时架构中非常特殊。TaskManager 的内存配额里,除了 JVM 堆,还包括堆外直接内存、网络缓冲内存、托管内存以及 JVM 自身开销。托管内存是 Flink 专门用于排序、哈希表、状态后端等操作的内存,它可以配置在堆内或堆外,不同版本默认值有差异。网络缓冲内存则用于数据在任务之间传输时的缓冲。

生产环境里最典型的坑,是容器内存限制和 JVM 参数不匹配。比如你给 TaskManager 配了 4 GB 堆,但没有给堆外和 JVM 开销留余量,在资源管理框架里申请的容器内存也是 4 GB,那很容易因为堆外内存超出限制被强制杀掉。处理方法很简单:给容器内存至少留出几百 MB 甚至 1 GB 的缓冲,同时把 Flink 的各类内存配比调得和容器规格一致。排查时看到 Container killed by memory 这类关键词,先去看各类内存的使用率,而不是直接调高堆大小。

5. 常见问题排查与速查表

5.1 先从日志的位置开始定位

排障的第一步不是看报错文本,而是先确认是哪个角色的日志。客户端日志通常在提交机器上,JobManager 日志在跑着 JobManager 进程的节点上,TaskManager 日志在执行节点上。很多新手拿着客户端报错去找 TaskManager 的问题,折腾半天才发现作业根本没有提交成功。

遇到作业起不来,我的习惯是按这个顺序查:先看客户端有没有把 JobGraph 提交给 JobManager,再看 JobManager 有没有把任务调度到 TaskManager,最后看 TaskManager 有没有正常启动任务线程。三个环节对应三类日志,定位起来非常快。

客户端日志里如果看到类似无法连接 JobManager 的报错,大概率是地址配置有问题,或者 JobManager 进程还没起来。JobManager 日志里如果看到资源不足的提示,再去算集群剩余 Slot。TaskManager 日志里如果出现任务初始化异常,那就是作业本身的问题,比如序列化器配置不对,或者状态后端初始化失败。分清楚这三层,很多问题其实不需要深挖源码就能解决。

5.2 高频问题的具体表现

作业一直 Restarting,看起来是任务执行失败被反复重试,但很多时候根因在检查点或者背压。检查点超时会导致作业自动回滚,表现就是周期性重启;背压严重会导致上游数据积压,下游处理不过来,但作业状态依然是 RUNNING。这两种情况的处理思路完全不同,一个要调整检查点参数或检查状态后端,一个要调并发或优化算子逻辑。

Slot 不够的报错通常写得很明确,类似 not enough free slots,但也有些场景是 JobManager 在等待资源管理器分配新的 TaskManager,这时候会出现超时类日志。如果是会话模式,可以临时扩容集群解决;如果是单作业模式,扩容和作业启动是一体的,要在申请资源前把参数调好。

还有一种容易被忽略的情况:作业提交成功了,但个别并行子任务长时间没有数据。这往往不是架构问题,而是数据源的分区策略导致的。比如外部系统的数据分布不均匀,某些分区数据量极少,对应子任务自然没有数据。这时候调整分发策略,或者给 source 设置合适的并行度,比反复重启作业更有效。

5.3 速查表:症状、可能原因与检查点

下面这张表整理了我运维中遇到频率最高的几个问题,希望对你有用。

症状可能原因第一个检查点
作业提交后一直 PENDING/启动中Slot 不足查看集群剩余 Slot 与作业并行度
任务反复失败重启代码异常或检查点超时看 TaskManager 异常堆栈,确认是否与状态相关
检查点一直失败状态太大或持久化存储慢观察状态大小和存储写入延迟
某些子任务数据量明显偏高数据倾斜在 UI 上看子任务的记录数和背压状态
容器被强杀内存超限核对容器规格与 Flink 内存相关参数
延迟突然飙升背压或 GC查 GC 日志和算子的背压指示

这张表不能覆盖所有场景,但能帮你快速建立一个排查框架。遇到表里没有的问题,也可以套用同样的思路:先分层看是哪个角色的问题,再缩小到具体报错,最后结合指标验证猜测。

6. 最后分享一点个人经验

从不懂架构的新手到能够独立处置线上问题,我走过不少弯路。最大的一条教训,是遇到问题就去查具体参数,而不先想清楚问题出在哪一层。架构知识的价值就在这里:它让你有一个坐标系,遇到报错能快速定位是客户端、调度层还是执行层的问题。另一个习惯是多看作业的执行图,在 UI 里把算子链、子任务、分区数和实际日志对应起来,比死记硬背任何文档都管用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 3:34:54

GPU基数排序:从CUB工程实现到性能调优

写 GPU 排序,我很少一上来就吹某种算法,但基数排序是个例外。对于整数、定长字节这类键,GPU 基数排序确实是最能接近“性能最好”这个说法的通用算法。它不是靠比较来排序,而是靠“按位计数 分段搬运”,一趟就是一次连…

作者头像 李华
网站建设 2026/10/17 3:34:28

Windows下Git上传失败排查指南:从环境配置到传输调优

Windows下用Git上传本地文件,最让人抓狂的往往不是命令敲错,而是所有配置都正常、本地提交也顺利,偏偏在push那一步卡住,报出一堆看不懂的错误。我前阵子帮某开发者处理过一个典型的案例:他的Windows电脑上有一个项目目…

作者头像 李华
网站建设 2026/10/17 3:33:29

从零实现malloc:深入理解堆管理器与内存分配机制

简介:这份资源围绕《深入理解计算机系统》(CSAPP)中的malloc实验展开,面向正在学习操作系统内存管理、准备完成课程实验的高校学生与自学者。内容涵盖内存池管理、块结构设计、碎片处理、内存对齐、free与realloc实现等核心知识点,并配有实验…

作者头像 李华
网站建设 2026/10/17 3:32:04

STM32F103 HAL库 FSMC驱动ILI9486 LCD移植与触摸校准实战

简介:基于STM32F103的3.5寸ILI9486触摸屏HAL库驱动完整工程,适合嵌入式入门开发者、单片机爱好者以及需要快速点亮LCD并进行触摸交互的工程师。项目提供完整源码,可直接烧录验证,主要涵盖SPI接口配置、GPIO控制、LCD初始化与绘图命…

作者头像 李华
网站建设 2026/10/17 3:31:58

从零搭建RAG知识库:让大模型真正读懂你的私有文档

之前接了个内部需求,做一套针对部门文档的问答工具。文档数量不算多,几百份产品手册和内部流程说明,需求也很直白:同事想问什么直接在对话里搜。结果第一版模型一上线就翻车——有人问"报销流程里超过5000元需要谁审批"…

作者头像 李华