
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、JobMasterFlink 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 能跑多个 TaskSlot 是 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或早期版本叫 JobManagerYARN 模式下先启动一个集群或申请一个容器再把 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.filterFlink 会把它们合并成一个 Task数据直接在线程内部传递不走网络不序列化。这就解释了为什么很多简单的作业在 Web UI 上看到的 DAG 节点数量比代码里少——算子被压扁了。算子链能大幅提升吞吐和降低延迟。Shuffle 则是需要重新分区的场景。比如keyBy、rebalance、broadcast数据必须从上游的一个 Task 分发到下游的多个 Task。这里就涉及网络传输和反压机制。Flink 每个 Task 的输出端都有对应的缓冲区网络传输时会经过一个“信用协议”来调节发送速率。当下游处理不过来时下游会向上游反馈“慢一点”这就是反压Backpressure的底层机制。理解了这一点排查反压问题就不再玄学——你只需要定位哪个节点队列被塞满就知道瓶颈在哪。关于最终一致性Flink 通过 Checkpoint barrier 在数据流中“插旗”当 barrier 到达所有算子时系统就知道该触发一次全局快照。这个过程中数据流的顺序和状态一致性都有精妙的协议保障。这一部分内容更进阶本篇先点到为止你只需要记住整个数据流的运行并不是“各扫门前雪”JobManager 时刻在扮演协调者的角色。4. 常见问题与排查心得这些都是真实踩过的坑4.1 JobManager 挂了的后果很多初学者以为 JobManager 挂了任务会自动恢复。这要分情况。在默认配置下没有启用高可用JobManager 是一个单点一旦进程退出所有运行中的作业都会失败即使你事后重启 JobManagerTaskManager 也会因为没有“领导”而无法继续工作。解决办法是部署时启用 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 部署因为弹性伸缩和资源隔离更好如果公司已经重度使用 HadoopYARN 模式会更容易与已有数据体系打通。不过选型不能只看“好看”。YARN/K8s 模式最大的价值是资源动态申请作业需要多少资源就临时启动多少 TaskManager作业结束就回收。但代价是启动开销和维护复杂度。所以如果你的作业生命周期很短比如批处理跑一次就结束用 Per-Job 模式反而更合适如果是长期跑的数据流Session 模式可以复用 TaskManager资源利用率更高。说到底Flink 运行时架构的这些概念不是书本上的抽象名词你越早把它们和 Web UI、日志、监控面板对上号就越能快速定位问题。前面那张角色表你可以存着遇到看不懂的日志就回来对照一下。这一篇先把“整体架构 组件 提交流程”这条主线铺完后面我们再深入讲状态管理、Checkpoint 和容错恢复那才是 Flink 真正体现“分布式状态计算”魅力的地方。