Flink 的运行时架构,也就是 Runtime 层,是很多初学者绕不过去的坎。我见过不少同学,写 Flink SQL 溜得很,一部署到集群上就懵了,作业跑不起来、资源不够、不知道提交到哪,归根结底就是对运行时这块的“地图”没建立起来。这篇我把 Flink 运行时架构的整体框架、核心组件和任务提交流程讲透,用大白话拆开揉碎,希望你看完之后,再打开 Flink UI 或者看日志时,心里能有个清晰的坐标。
先给个定位:这篇聊的是 Flink 的 Runtime(运行时)层,也就是作业提交上去之后,系统内部由哪些角色协作,任务怎么被调度、分发、执行和容错。这块知识适合所有人——不管是写 DataStream 还是 Flink SQL,不管是在本地学习还是集群生产,理解了它,排查问题时效果立竿见影。
1. Flink 运行时先搭个骨架:整体架构画像
1.1 为什么非要搞“主从架构”
很多人第一次接触大数据框架时会困惑:为什么 Flink 不能像写普通 Java 程序一样,main 函数一跑就完了?答案很简单:因为单台机器的算力、内存和磁盘都是有限的,处理海量数据必须“多机并行”。而一旦机器多了,就需要有人“发号施令”、有人“埋头干活”,这就是主从架构的由来。
Flink 的运行时架构本质上就是一套“主从模型”:一个主节点(JobManager)负责全局调度和协调,多个从节点(TaskManager)负责真正干活。这个模型跟 Hadoop 的 NameNode/DataNode、Spark 的 Driver/Executor 是一个思路,但 Flink 在细节上有自己的讲究——它把“干活”这个事拆得更细,并且用异步、事件驱动的机制来保证低延迟和高吞吐。
打个比方。你把一个作业想象成一场大型演出:JobManager 是总导演,负责分剧本、定流程、救场;TaskManager 是演员团队,按导演的安排上台表演;而提交作业的 Client,就相当于把剧本递进后台的场务。导演不亲自演戏,演员也不操心整体排期,各司其职才能把演出跑稳。Flink 的主从架构就是这个道理——职责分离,逻辑才清晰。
1.2 角色分配:一句话记住谁干什么
Flink 运行时里最核心的大角色,其实就四个:Client、JobManager、TaskManager、还有背后的资源管理组件。这里我用一张表把它们的职责先立起来,后面的内容都围绕这张表展开。
| 角色 | 通俗叫法 | 核心职责 | 生命周期 |
|---|---|---|---|
| Client(客户端) | 场务/快递员 | 负责把作业提交到集群,生成 JobGraph,把任务“打包发货” | 提交完成后即可退出 |
| JobManager(作业管理器) | 总导演 | 调度任务、分配资源、协调 Checkpoint、故障恢复 | 常驻或随作业创建 |
| TaskManager(任务管理器) | 演员团队/工人 | 真正执行任务,跑用户代码逻辑,管理本地内存和数据流 | 常驻,一个进程跑多个任务 |
| ResourceManager(资源管理器) | 后勤主任 | 管理集群资源,负责分配 TaskManager 的槽位 | 常驻 |
这个模型里的核心思路是“两阶段分布式”:第一层,多个 JobManager 组成的调度层;第二层,多个 TaskManager 组成的工作层。上面这张表你可以先记住,后面看到术语就不会慌。
1.3 架构设计的三个底层逻辑
只看角色还不够,你得理解架构背后的三个设计逻辑,否则后面学调度和容错时容易一头雾水。
第一个逻辑是“动静分离”。整个 Flink 集群里,一个作业的执行被拆分成了“静态的作业图(JobGraph)”和“动态的执行图(ExecutionGraph)”。Client 提交前生成的是静态拓扑,也就是你代码里定义的数据流结构;JobManager 拿到后,会把它转成可调度的执行图,再根据当前集群资源情况做动态安排。这一步的意义在于:作业的“形状”是稳定可分析的,但跑在哪个机器、用几个并发,是弹性可变的。
第二个逻辑是“失败自恢复”。Flink 的容错机制(Checkpoint)需要整个集群协调配合——JobManager 要记录全局状态,TaskManager 要周期性上报进度。所以主从架构天然适合做全局一致的快照,因为所有任务的状态都由 JobManager 统一协调。这也是 Flink 相较于早期 Storm 等框架的重要优势——它把“状态一致性”作为了一等公民。
第三个逻辑是“资源抽象”。TaskManager 不做无限并行,而是把自身资源切成一个个 Slot(槽位),资源分配最小单位是 Slot,不是整个进程。这一点特别重要,后面第 2 章我会展开讲。先把结论放这儿:Slot 机制让多个小任务能共享一个进程,减少启动开销,同时又能隔离资源。
2. 核心组件逐个拆:JobManager 和 TaskManager 到底在忙什么
2.1 JobManager:集群的“大脑”也是“总调度室”
JobManager 是 Flink 集群里的控制中心。它不是一个概念性的角色,而是一个真正的进程,在集群里通常称为“Master 进程”。你可以在 Flink UI 上看到它,也会在日志里频繁看到以JobManager开头的记录。
它的核心工作有三大块:
第一块是调度。作业提交上来后,JobManager 会把它转成 ExecutionGraph,然后按照拓扑顺序逐个调度执行节点。它要决定哪些 Task 可以并行跑、哪些 Task 之间有依赖需要等待、每个 Task 分配到哪个 TaskManager 的哪个 Slot 上。你可以理解为:导演拿到剧本后,要决定哪个演员在什么时间上什么台。
第二块是容错协调。Flink 的 Checkpoint 机制通常由 JobManager 发起,它会通知所有 TaskManager“现在做一次状态快照”,然后收集各个 Task 的确认结果。一旦某个 Task 挂了,它要负责重新调度失败的任务,并让任务从最近一次成功的快照恢复。这块日常运维价值极高:没有 JobManager 的统一协调,分布式状态一致性基本无从谈起。
第三块是对外服务。JobManager 对外提供 Web UI、REST API,内部还要处理心跳、维护作业元数据。比如你在 Web UI 上看到的作业 DAG、各 Task 的耗时、延迟指标,都是 JobManager 汇总上来的信息。
这里有个容易误解的点:一个 JobManager 可以同时跑多个作业吗?要看模式。Flink 集群里有两种会话模式(Session Mode)和单作业模式(Per-Job Mode):会话模式下,JobManager 是常驻进程,可以接收多个作业;单作业模式下,一个作业对应一个 JobManager,作业结束进程也就退出。这个区别会直接影响资源隔离和运维方式,后面讲到提交流程时我会再强调。
2.2 TaskManager:干活的“工人”和它的“工位”
TaskManager 是真正执行用户代码的地方。每个 TaskManager 也是一个独立进程,里面可以运行多个 Task(每个 Task 是执行图上的一个节点实例)。为了让多个 Task 能在同一个进程内安全共存,TaskManager 把内存分成了若干块,每块就是一个 Slot。
这里可以类比成“工位”:每个工人(Task 线程)占用一个工位(Slot),工位上有自己的内存边柜,不能跟旁边的人混用。一个 TaskManager 进程相当于一个车间,车间里有多少工位决定它能同时容纳多少工人。
TaskManager 内部主要负责三件事:
第一,接收并执行 JobManager 分配下来的 Task。每个 Task 本质上是一个线程,它执行用户定义的操作(map、filter、window、join 等),并负责维护自己的状态。
第二,管理本地状态存储。在有状态计算(比如窗口聚合)中,TaskManager 要把状态数据保存在本地(内存或 RocksDB),等 Checkpoint 时把状态快照异步上传到持久存储。
第三,处理网络数据交换。Task 之间是要通信的,数据从上游 Task 传到下游 Task,需要经过网络。TaskManager 内部维护了一套网络缓冲区和反压机制,这个我在第 3 章讲数据流时会展开。
还有一个你需要知道的概念:TaskManager 的“槽位数”最好根据 CPU 核心数来配置。一般设置成“CPU 核数”或“核数减一”。比如一台 8 核的机器,配 8 个或 7 个 Slot 比较合理。如果配多了,会导致大量线程频繁切换上下文,反而变慢。
2.3 Dispatcher、ResourceManager、JobMaster:Flink 1.x 引入的三个新角色
如果你翻过老版本的 Flink 代码或者用过早期版本,会发现现在的 JobManager 其实被拆分成了三个更细的角色。这是 Flink 演进过程中的一个重要设计变化,搞懂它,对理解整个运行时架构是质的飞跃。
简单说,现在 JobManager 进程内部由三个组件组成:
- Dispatcher:可以理解为“前台接待”。它接收客户端提交的作业请求,为每个作业创建一个 JobMaster,并提供一个用于提交作业的 REST 接口。会话模式下,Dispatcher 是常驻的,负责接收源源不断的作业。
- ResourceManager:负责资源的分配与释放。它的职责包括:向外部资源调度系统(如 Hadoop YARN、Kubernetes)申请 TaskManager 资源,以及在这些资源上分配 Slot 给具体作业。
- JobMaster:每个作业一个,相当于“导演本人”。它管理单个作业的整个生命周期,包括生成执行图、调度、Checkpoint、失败恢复。作业跑完后,JobMaster 退出,但 Dispatcher 和 ResourceManager 还活着,还能继续接收新作业。
为什么要拆分?因为早期版本把“接收作业、管理资源、调度作业”全揉在一个 JobManager 里,长期运行时容易出现单点瓶颈,也不利于不同部署模式(比如 Session 模式与 Per-Job 模式)复用逻辑。拆开之后,每个角色的职责单一、可测试、可扩展。
这个拆分带来的直观变化是:在集群的 Web UI 上,你会看到多个 Job 各自有对应的 JobMaster 信息,但集群级别的 Dispatcher 和 ResourceManager 是共享的。你排查问题时,最后会经常落在“到底是 ResourceManager 没申请到资源,还是 JobMaster 调度卡住了”这两个方向。
2.4 Slot 机制:为什么一个 TaskManager 能跑多个 Task
Slot 是 Flink 资源分配的最小单位,但它的设计比表面看起来精巧。关键点在于:Flink 的一个 Slot 并不是“1 个 Slot = 1 个 Task 线程”这么简单,而是“1 个 Slot = 1 个可执行的共享资源单元”。
什么意思呢?一个 Slot 通常可以容纳多个 Task 共享内存资源和线程组。比如一个作业有 source、map、window、sink 四个算子,如果 Slot 共享开启(默认开启),那么这四个算子可以放在同一个 Slot 里执行。这样做的目的是减少线程开销和网络通信——同一个 Slot 内的任务之间可以直接通过内存传递数据,不用序列化和走网络。
Slot 共享机制的另一个重要设计是“负载均衡”。比如并行度为 10 的作业,它可能只需要 4 个 Slot 就能跑起来,因为多个算子的任务可以打包到同一个 Slot 内。但代价是并发度会受 Slot 数量的限制。如果你总并发度(所有算子的并行度之和)超过了 Slot 总数,作业就会卡在“等待资源”状态。
在实际生产里,Slot 数量的配比直接决定集群能同时跑多少作业。配少了利用率低,配多了会导致同进程内的任务互相争抢内存,严重时甚至触发频繁的 Full GC。我一般建议:如果作业并行度不高、以 IO 为主,Slot 数 = 核数即可;如果是 CPU 密集型的计算,Slot 数 = 核数 - 1,留一个核给系统进程和框架线程,这是实践下来比较稳的做法。
3. 任务提交全流程:从命令行到磁盘的全链路
3.1 先分清三种部署模式
Flink 的提交流程跟部署模式强相关,先把这个讲清楚,后面才不会乱。目前主流的部署模式有三种:
| 部署模式 | 资源来源 | 生命周期 | 适用场景 |
|---|---|---|---|
| Standalone | 自己管理集群机器 | TaskManager 常驻 | 学习、测试、小规模环境 |
| YARN Session / Per-Job | 复用 Hadoop 集群 | 按需申请/释放 | 生产环境,与大数据生态集成 |
| Kubernetes | 容器平台 | 动态伸缩 | 云原生生产环境 |
三种模式下,JobManager 和 TaskManager 的代码逻辑是一样的,区别在于“容器是怎么拉起来的”“资源是谁分配的”。比如在 YARN 模式下,ResourceManager 会与 YARN 的 ResourceManager 对接,申请容器来启动 TaskManager;在 Kubernetes 模式下,则是联系 K8s API 创建 Pod。
我们的讨论以 Standalone 模式为主,因为它最简单,最适合搞清楚运行时架构本身。但你心里要明白:换部署模式只是“资源获取方式”变了,提交后 JobManager 内部那一套调度逻辑是通用的。
3.2 提交那一刻:Client 在做什么
客户端提交作业命令(比如flink run)时,整个流程的第一个动作发生在 Client 环节。不少人以为提交完命令就完事了,实际上 Client 在后台悄悄干了好几件关键的活儿。
第一步,构建作业拓扑。Client 读取你写好的代码(DataStream 程序或 SQL),执行并生成 StreamGraph。这是最原生的数据流图,里面包含所有算子(source、transform、sink)。
第二步,优化和打包。Client 会把 StreamGraph 做一些优化,主要是算子合并(Operator Chain),把能合并的算子合并成一个 Task,减少线程和数据序列化开销。生成的是一个逻辑层面的 JobGraph。这个过程你可以理解为“导演把剧本压缩成分场表”,减少现场调度开销。
第三步,把 JobGraph 提交到集群。具体提交到哪里,取决于部署模式:Standalone 模式下提交给 Dispatcher(或早期版本叫 JobManager);YARN 模式下先启动一个集群(或申请一个容器),再把 JobGraph 提交进去;K8s 模式类似。
此时 Client 可以退场了。注意:Flink 的 Client 和任务执行之间是“分离”的,Client 退出不会影响运行中的作业。这点与 Spark 的 Driver 模式有差别,也是大家常问的一个点。
3.3 JobManager 拿到作业后:调度与资源分配
JobGraph 交给 Dispatcher 之后,真正的调度大戏开始了。Dispatcher 会为这个作业创建一个 JobMaster,接着 JobMaster 要完成一系列复杂动作。
首先,生成 ExecutionGraph。JobGraph 是逻辑层面的,JobMaster 会把它转换成可执行的 ExecutionGraph——每个 JobVertex(逻辑节点)会被拆成多个并行子任务(ExecutionVertex),每个 ExecutionVertex 才是最终要跑在 Slot 上的单元。
然后,申请资源。JobMaster 会向 ResourceManager 请求足够的 Slot。如果集群里已有空闲 Slot,直接分配;不够的话,ResourceManager 会启动新的 TaskManager(或向 YARN/K8s 申请新的容器资源)。这一步正是最容易卡住的环节:如果底层资源不足,你会看到作业一直处于“RUNNING 但调度中”的状态,日志里反复提示“Waiting for resources”。
资源到位后,JobMaster 按拓扑顺序调度任务。这里的调度顺序是有讲究的:必须先启动需要输入的上游 → 再调度下游,还要考虑数据分区策略。比如下游任务要接收所有上游分区的数据(shuffle),它的启动时机可能要在上游开始输出之后。不能一开始就把所有任务全拉起来,那样内存直接爆掉。
最后,JobMaster 与每个 TaskManager 建立通信,分配具体 Execution 到对应 Slot,并持续跟踪每个 Task 的心跳和状态。整个作业的运行状态、指标、异常日志,都是由这个环节汇总上报的。
这里我给你一个实操提示:当你在 Web UI 看到作业长期处于“SCHEDULED”或“DEPLOYING”状态,优先检查 ResourceManager 那边有没有申请到资源;如果自己建了 Standalone 集群,检查 TaskManager 的 Slot 是否被其他作业占满。从“资源→调度→部署”这个顺序排查,效率最高。
3.4 Task 之间的数据流动:Shuffle 与算子链
任务被调度起来了,接着就要执行业务逻辑了。此时我们要关注数据是怎么在 Task 之间流动的。Flink 里的数据流动有两种典型方式:算子链和 Shuffle。
算子链(Operator Chain)是 Flink 引以为傲的优化。如果两个算子之间没有需要重新分区的操作(也就是数据流是 one-to-one 的),比如source.map.filter,Flink 会把它们合并成一个 Task,数据直接在线程内部传递,不走网络不序列化。这就解释了为什么很多简单的作业在 Web UI 上看到的 DAG 节点数量比代码里少——算子被压扁了。算子链能大幅提升吞吐和降低延迟。
Shuffle 则是需要重新分区的场景。比如keyBy、rebalance、broadcast,数据必须从上游的一个 Task 分发到下游的多个 Task。这里就涉及网络传输和反压机制。Flink 每个 Task 的输出端都有对应的缓冲区,网络传输时会经过一个“信用协议”来调节发送速率。当下游处理不过来时,下游会向上游反馈“慢一点”,这就是反压(Backpressure)的底层机制。理解了这一点,排查反压问题就不再玄学——你只需要定位哪个节点队列被塞满,就知道瓶颈在哪。
关于最终一致性:Flink 通过 Checkpoint barrier 在数据流中“插旗”,当 barrier 到达所有算子时,系统就知道该触发一次全局快照。这个过程中,数据流的顺序和状态一致性都有精妙的协议保障。这一部分内容更进阶,本篇先点到为止:你只需要记住,整个数据流的运行并不是“各扫门前雪”,JobManager 时刻在扮演协调者的角色。
4. 常见问题与排查心得:这些都是真实踩过的坑
4.1 JobManager 挂了的后果
很多初学者以为 JobManager 挂了,任务会自动恢复。这要分情况。在默认配置下(没有启用高可用),JobManager 是一个单点:一旦进程退出,所有运行中的作业都会失败,即使你事后重启 JobManager,TaskManager 也会因为没有“领导”而无法继续工作。
解决办法是部署时启用 Flink 的高可用模式(比如结合 ZooKeeper 或 K8s),让 JobManager 有备节点。生产环境里,如果你跑的是 7x24 小时作业,这块不能省。实际经验:我在某公司遇到过白天作业跑得好好的,夜里机房停电导致 JobManager 节点宕机,整个作业重启了好几个小时。而那之后我学乖了,凡是生产作业,一律把高可用配置写成强制检查项。
4.2 Slot 不够怎么办
“作业提交后一直卡在 Running,但任务就是起不来”——这是我被问得最多的生产问题之一。大多数情况下,原因只有一个:集群 Slot 被人占满了。
快速验证方法:打开 Flink Web UI,看“Task Managers”页面里的 Slot 使用情况;或者看 JobManager 日志里有没有Not enough resources available之类的关键字。解决办法有三个方向:第一,等正在跑的其他作业释放 Slot;第二,给集群加 TaskManager 或扩大资源;第三,调低自己作业的并行度。注意,调并行度不是随便调,涉及状态重分布,如果作业是有状态的,改并行度会触发状态重分配,反而可能更慢。所以更稳妥的是保底先加资源,再慢慢调整。
4.3 反压到底是怎么回事
反压是 Flink 运维里最经典的现象。症状是下游算子处理速度跟不上,整个链路延迟升高,Web UI 上某个节点显示为红色或背压状态。很多人第一反应是“加并行度”,但往往治标不治本。
你需要想清楚反压传导的链路:数据从 source 进入,经过每个算子,如果某个算子计算很慢,它的输出缓冲区会积压;积压到一定程度,上游算子也变慢(因为下游不给它发数据的信用额度),最终反映为整个作业速率下降。
排查反压的经典顺序是:先找到首个出现反压的节点,看它的 CPU/内存/GC 情况;如果 CPU 高,说明是该算子计算逻辑太重或数据倾斜;如果 CPU 低但反压,可能是下游或网络 IO 瓶颈;再看看数据是否符合直觉,比如是否发生了严重的数据倾斜。我之前处理过一个实时报表作业,就是某个 key 的数据量是其他 key 的上百倍,导致单个子任务被拖死。这种问题加并行度没用,得从业务上拆 key 或者加随机前缀。
4.4 部署模式选择的经验建议
最后说点选型的经验。如果你是在学习阶段或者跑个人 Demo,直接start-cluster.sh用 Standalone 就够了,简单直接。如果是公司生产环境,现在业界主流是 Kubernetes 部署,因为弹性伸缩和资源隔离更好;如果公司已经重度使用 Hadoop,YARN 模式会更容易与已有数据体系打通。
不过选型不能只看“好看”。YARN/K8s 模式最大的价值是资源动态申请:作业需要多少资源,就临时启动多少 TaskManager,作业结束就回收。但代价是启动开销和维护复杂度。所以如果你的作业生命周期很短(比如批处理跑一次就结束),用 Per-Job 模式反而更合适;如果是长期跑的数据流,Session 模式可以复用 TaskManager,资源利用率更高。
说到底,Flink 运行时架构的这些概念不是书本上的抽象名词,你越早把它们和 Web UI、日志、监控面板对上号,就越能快速定位问题。前面那张角色表你可以存着,遇到看不懂的日志就回来对照一下。这一篇先把“整体架构 + 组件 + 提交流程”这条主线铺完,后面我们再深入讲状态管理、Checkpoint 和容错恢复,那才是 Flink 真正体现“分布式状态计算”魅力的地方。