摘要
讲透 Flink on YARN 中 Per-Job 模式的完整原理:每作业一个临时集群的架构、两阶段启动与提交流程、main() 在本地 Client 执行的三个后果,以及它为何被 Application 模式取代的演进逻辑;并给出配置命令、老集群迁移要点与四个真实踩坑点。
关键词
Flink、Per-Job、yarn-per-job、临时集群、Application Master、main() 本地执行、两阶段启动、废弃、Application
Flink on YARN 的三种模式里,Session 和 Application 都还在被广泛使用,唯独 Per-Job 夹在中间,处境尴尬:它既有 Application 的「每作业一集群」隔离性,又保留了 Session 的「main() 在本地执行」------然后被官方在 1.15 标记废弃。为什么一个「看起来两头都占」的模式会走向终结?这一篇讲透它的原理,答案自然就出来了。
理解 Per-Job 还有个现实意义:很多老集群(Flink 1.14 及以前)的存量作业还在以 per-job 方式运行,迁移前必须知道它内部怎么工作。
一、Per-Job Cluster 架构:每作业一个临时集群
Per-Job 的架构一句话能概括:每次 flink run 都向 YARN 申请一个专用集群,作业结束(或失败放弃)集群随即销毁。

架构要点:
- 本地 Client 执行用户 main()(与 Session 相同),生成 StreamGraph → JobGraph。
- AM 容器内运行 JobManager,只服务这一个作业,无共享、无竞争。
- TaskManager 是本作业专属的,资源独享,背压和状态问题不会传导给其他作业。
- 集群生命周期与作业严格绑定:提交时创建,作业结束时销毁,YARN 资源随之回收。
它的隔离性其实和 Application 一样好------只是实现路径绕了一个弯:先让 YARN 把集群起来,再让本地代码把作业塞进去。
二、启动与提交流程:两阶段模式
Per-Job 的提交是两阶段串行的,这也是它启动最慢的根源:

阶段一:先起临时集群
- Client 把 flink-dist JAR 和配置上传到 HDFS;
- 向 YARN RM 提交应用(申请一个临时集群);
- RM 选择一个 NM 启动 AM,AM 内启动 JobManager 框架;
- 集群就绪------此时还没有任何用户代码,集群在「空等」。
阶段二:本地跑代码并提交作业
- Client 在本地执行用户 main(),生成 JobGraph;
- 通过 REST 把 JobGraph 提交到新集群的 Dispatcher;
- Flink 的 ResourceManager 申请本作业专属的 TaskManager 容器;
- 作业运行;结束时集群销毁。
两阶段的核心问题是串行等待:起集群要等 YARN 分配 AM 容器,这段等待发生在任何用户代码执行之前。更糟的是,如果第 5 步 main() 在本地抛异常(缺依赖、配置错),集群刚起来就白起了,一次资源申请白白浪费。
三、main() 在本地执行的三个后果
这是 Per-Job 与 Application 唯一的本质差异,也是它所有问题的来源:
- 提交机的 classpath 必须完整 。main() 在本地跑,
flink-connector-kafka、自定义依赖等必须在提交机上有。缺了就是NoClassDefFoundError,而且发生在集群已经启动之后。 - 提交机成为作业链路的故障点。main() 执行期间提交机断网、宕机、进程被杀,作业直接提交失败或半途而废。
- 本地环境与集群环境脱节。本地的 Flink 版本、依赖版本必须与集群严格兼容,否则序列化对不上------这是老集群最常见的提交报错来源。
这三个后果在 Application 模式下全部消失:main() 搬进 AM 后,提交机退化成纯粹的「JAR 上传者」。
四、Per-Job vs Application:一字之差,差在哪
两者都是「每作业一个集群」,对比下来唯一本质区别就是 main() 的位置:

| 维度 | Per-Job | Application |
|---|---|---|
| 集群生命周期 | 每作业独立 | 每作业独立 |
| main() 位置 | 本地 Client | 集群内 AM |
| 启动方式 | 两阶段串行 | 单阶段并行 |
| 本地依赖 | 必须完整 | 只传 JAR |
| 状态 | 1.15 起废弃 | 生产推荐 |
结论很直接:Application = Per-Job 的「main() 搬家」------把本地执行的代码搬进集群,其余机制全部保留。这不是革命,是一次精准的演进。
五、为什么被废弃
结合上面的分析,官方废弃 Per-Job 的理由很清晰:
- main() 在本地执行是设计缺陷:把提交机变成依赖点、故障点,与现代「提交即分离」的运维理念冲突。
- 两阶段启动又慢又浪费:起集群空等 + main() 失败白起,资源与时间双损耗。
- 功能被 Application 完全覆盖:Application 保留 Per-Job 全部优点(每作业隔离),同时消除全部缺点------没有理由再维护两套实现。
所以它不是「不好用才被废弃」,而是「更好的替代品出现后被收编」。Flink 1.15 的 release notes 里明确建议:新作业一律使用 run-application。
六、配置与命令
bash
# Flink 1.14 及以前:per-job 提交
bin/flink run -t yarn-per-job -d -c com.example.MainClass ./app.jar
# Flink 1.15+:同一个作业迁移到 Application
bin/flink run-application -t yarn-application -c com.example.MainClass ./app.jar
# 查看 per-job 应用(YARN 上每个作业是一个 application)
yarn application -list
yarn application -kill <applicationId>
yaml
# flink-conf.yaml(两种模式通用)
jobmanager.memory.process.size: 2048m
taskmanager.memory.process.size: 4096m
taskmanager.numberOfTaskSlots: 4
# 作业失败时集群行为:per-job 下作业结束即销毁集群
老集群迁移到 Application 的注意点:提交命令从 flink run -t yarn-per-job 换成 flink run-application -t yarn-application,JAR 的依赖打包方式不变,但要检查 main() 里是否依赖了本地文件路径或环境变量------这些在 Application 模式下是拿不到的。
七、四个真实踩坑
- 迁移后 main() 里的本地路径失效 。Per-Job 的 main() 在提交机跑,
new File("/opt/conf/rule.json")能读到;Application 的 main() 在 AM 里跑,这个路径在集群节点上不存在。迁移时必须把这类资源改为打包进 JAR 或放 HDFS。 - 升级 Flink 版本时把 per-job 命令照搬 。1.15+ 的
flink run -t yarn-per-job会输出废弃警告,某些版本甚至直接报错。升级脚本里必须同步改run-application。 - 「提交机挂 = 作业挂」的错觉残留。Per-Job 时代养成的「提交后本地进程不能关」的习惯,在 detached(-d)模式下不成立;但很多人迁移到 Application 后仍担心这个问题------记住 Application 的提交机只是上传者,关掉无影响。
- 把 per-job 当 Session 用。有人以为 per-job 集群会复用,反复提交后发现每次都要等集群启动------per-job 集群是每次新建的,没有复用一说。要复用就用 Session,要隔离就用 Application,per-job 两头不靠。
Per-Job 的价值在于它是 Flink 部署模式演进史上的「中间产物」:把 Session 的共享问题解决了一半(每作业一集群),却因为 main() 留在本地引入了新的依赖问题,最终被 Application 模式收编。理解它,其实是在理解一条演进线索:从共享到隔离、从本地到集群内------Per-Job 正好站在这个转折点上,作为参照系,Session 与 Application 的差异反而看得更清楚。