Flink on Yarn三种运行模式与完整提交流程详解
Flink 跑在 Yarn 上这件事,圈里人都知道是生产环境的主流选择。但你要是问一句“Flink on Yarn 到底有几种跑法”,能立刻答清楚的人还真不多。很多人用了半年 Flink,还是只知道往命令行里敲一个 flink run -t yarn-per-job,至于背后 Yarn 是怎么把资源抠出来、JobManager 和 TaskManager 分别住在哪里、作业挂了谁来管,一概是黑盒。
这篇文章我就把 Flink on Yarn 的原理彻底摊开讲,重点放在三种运行模式的区别和完整提交流程上。我会结合实操中踩过的坑来解释“为什么是这种设计”,尽量让没搞过 Yarn 的人也能看懂,让已经在用的人能把原理补全。
1. Flink on Yarn 的整体设计思路
1.1 为什么偏偏是 Yarn,而不是 Standalone 或 Kubernetes
先说一个很多新手都会困惑的问题:Flink 自己不是有 Standalone 模式吗?启动一个集群,JobManager 管调度,TaskManager 管执行,不也能跑吗?为什么生产上大家几乎都选 Yarn?
核心原因就一个字:资源。Standalone 模式下,集群资源是固定的,你启动了多少台 TaskManager,资源上限就是多少,作业再多也只能排队。而 Yarn 是一个资源调度平台,它做的事情本质上是“把集群里的 CPU 和内存,按需分配给不同的计算框架”。Hadoop 生态里的 MapReduce、Spark、Flink 都可以跑在 Yarn 上,大家共享一套物理资源池,谁有需要谁申请。
用生活化的方式理解:Standalone 就像是你自己开了一家餐厅,座位是固定的,客人再多也只能等位;而 Yarn 就像是一个商业综合体的物业,各个餐厅按需租用场地,今天这家人多就多租,明天那家冷清就退租,整个综合体的空间利用率天然就高。
另外一个不可忽视的点是多租户隔离。公司里多个团队共用一套大数据集群是常态,Yarn 天然支持队列(Queue)的概念,每个团队分一个队列,队列之间资源互不挤占。这个能力 Standalone 是没有的,Kubernetes 虽然也能做资源隔离,但在 Hadoop 生态里 Yarn 仍然是最成熟、最普及的调度方案。
1.2 Flink on Yarn 的架构映射关系
要理解 Flink on Yarn,第一步是把两套架构的人物对应关系搞清楚。Flink 里有 JobManager 和 TaskManager 两个角色,Yarn 里有 ApplicationMaster(简称 AM)和 Container 两个概念。
按运行模式不同,映射关系略有差异,但整体框架是这样的:Flink 的 JobManager 运行在 Yarn 的 ApplicationMaster 进程里,TaskManager 则各自占用一个 Container。严格来说,在 Session 模式里 AM 里跑的是 Dispatcher 和 ResourceManager,JobManager 是随后续作业提交才在 AM 内动态启动的,这个我们下文展开。所以你别把“JobManager 等于 AM”这个说法记死,只能说两者在物理上确实住在同一个进程里。
Yarn 本身有 ResourceManager(简称 RM)和 NodeManager(简称 NM)两个核心组件:RM 负责全局资源调度和分配,NM 是每台物理节点上的代理进程,负责启动和管理 Container。整个集群我只见过这几个角色,没有多余的神秘组件。
架构层层递进的关系,我这里用一张简单的关系列表来说明:
- Yarn ResourceManager:集群资源总管家,接收作业申请,分配 Container。
- Yarn NodeManager:每台机器上的执行代理,负责真正启动 Container。
- Yarn ApplicationMaster:每个应用在 Yarn 上的“代表”,负责为这个应用申请资源、监控作业状态。
- Flink JobManager:Flink 作业的调度中枢,负责任务调度、Checkpoint 协调、故障恢复。
- Flink TaskManager:工作节点,真正执行算子逻辑的进程。
这五个角色之间的关系,如果你仔细观察,会发现 Yarn 负责的是“进程能不能活、资源够不够用”这层问题,而 Flink 负责的是“计算逻辑怎么调度、状态怎么保存”这层问题。两者是通过 AM 这个桥梁结合起来的。
1.3 我为什么建议新项目直接用 Yarn 模式部署
从事 Flink 开发这几年,我见过不少团队在部署方式上反复折腾:早期用 Standalone,后来发现资源没法动态伸缩,作业一多就各种排队,于是转向 Yarn;再后来听说 Kubernetes 是大趋势,又往 K8s 上迁。折腾一圈下来发现,如果没有容器化平台的特殊需求,Yarn 依然是性价比最高、运维成本最低的选择。
Kubernetes 跑 Flink 的优势主要在弹性伸缩和资源利用率上,但前提是你得有专门的 K8s 运维团队,而且 Flink on K8s 的 Session 模式、Application 模式在 Operator 方案出现之前并不好用。反观 Yarn,Hadoop 生态里它就是“原配”,踩坑资料最多,问题排查路径最成熟,任何一个用 Hadoop 超过一年的人都能帮你看看 Yarn 的日志。
再加上 Yarn 天然支持队列资源隔离、动态资源申请、作业粒度的分配与释放,这些特性恰恰是 Flink 流作业最需要的。所以我的判断是:除非公司已经踩进了 K8s 的深水区且有成熟的容器平台,否则 Flink on Yarn 依然是生产环境最稳妥的部署方式。
2. 三种运行模式的核心差异
2.1 Session 模式:一个常驻集群,多人共享
Session 模式,也叫会话模式,是 Flink on Yarn 里最“传统”的一种用法。它的特点是:先启动一个 Flink 集群,这个集群常驻在 Yarn 上,之后可以反复提交多个作业到这个集群里运行。
整个启动过程是这样的:先执行 yarn-session.sh 命令,客户端向 Yarn 提交一个 Application,Yarn 分配 Container 并启动 AM,AM 内部启动 Flink 的 Dispatcher 和 ResourceManager 组件。此时 Flink 集群就已经“就绪”了,但还没有执行任何用户作业。后续每个作业通过 flink run -t yarn-session 提交时,实际是在向这个常驻集群的 Dispatcher 提交 JobGraph,Dispatcher 会动态地为每个作业拉起一个 JobManager,然后向 Yarn 申请 Container 来启动 TaskManager。
Session 模式最大的优势是启动开销低。因为集群已经预热好,任务提交后可以秒级开始调度执行,非常适合交互式查询、短时间运行的测试任务,以及需要频繁提交小作业的场景。
但 Session 模式也有一个非常头疼的问题:资源隔离差。多个作业共享同一个集群,所有 JobManager 都挤在一个 AM 进程中,如果其中一个作业出现内存泄漏或者频繁 Full GC,整个 AM 都可能挂掉,连累其他作业一起遭殃。TaskManager 也是共享的,一个 TaskManager 里同时跑着多个作业的 Task,JobManager 只能尽量做资源隔离,但实际上不同作业之间的影响很难完全切断。
另外一个隐含问题是:Session 集群的资源是启动时就一次性向 Yarn 申请的,如果初始申请的资源比较大,而实际提交的作业又很少,那这部分资源就白白占着了。Yarn 虽然支持动态扩容和缩容 Container,但默认行为下 TaskManager 的数量是根据负载自动调整的,这个过程并不总是灵敏,容易造成资源浪费或者资源不足的尴尬。
2.2 Per-Job 模式(作业模式):一作业一集群,用完就散
Per-Job 模式,也叫作业模式,在 Flink 1.15 版本之前一直是生产环境的主力。它的特征是:每一个作业提交时,都会单独创建一套 Flink 集群,作业运行完毕或者失败退出后,集群资源随之释放。
流程上是这样:执行 flink run -t yarn-per-job 或者 flink run -m yarn-cluster(旧版本写法),客户端会在本地解析用户代码,生成 JobGraph,然后把 JobGraph 和其他必要的依赖上传到 Hadoop 集群的暂存目录(Staging Directory),最后通过 Yarn 的 Client 向 RM 提交作业。RM 分配第一个 Container 用于启动 AM,AM 内部启动 Flink JobManager,JobManager 加载 JobGraph 并开始调度,然后向 Yarn 申请 Container 启动 TaskManager,整个集群就完全围绕这一个作业运转。
Per-Job 模式最大的优点是隔离性好。每个作业独占一套 Flink 集群,JobManager 这个逻辑角色可以单独定位问题,出了故障只影响当前作业,不会波及其他任务。这在多团队共用大数据集群的场景里尤其重要——你的流作业不稳定,不该影响别人的批作业。
缺点也很明显:启动开销大。每次提交作业都要经历“向 Yarn 申请资源 -> 启动 AM -> 启动 JobManager -> 申请 TaskManager Container -> 建立连接”这一整套流程,通常要花上几十秒甚至几分钟才能把作业跑起来。对低频次的批作业来说这个开销可以接受,但对需要频繁调整参数、反复提交的任务来说,这个等待时间就很磨人了。
Yarn 官方在 Hadoop 3.1 之后引入了 AM 复用机制试图缓解这个问题,但 Flink Per-Job 模式下每个作业都对应一个新的 AM,所以基本没吃到这个红利。
2.3 Application 模式:主函数在 AM 里跑,解决 Per-Job 的瘦客户端问题
Flink 1.11 开始引入 Application 模式,这也是当前官方主推的模式。它和 Per-Job 模式最大的区别在于:客户端提交作业后可以立即退出,用户的 main 方法(也就是用户代码的主函数)是在 AM 内部执行的。
这就解决了一个 Per-Job 模式非常魔幻的问题:Per-Job 模式下,用户主函数在客户端本地执行,本地环境必须有完整的 Flink 依赖、用户代码依赖,还要能访问 HDFS 上传文件。如果你在一个没有安装 Flink 的机器上提交作业,或者本地依赖和集群依赖版本对不上,作业就会各种花式报错。Application 模式把主函数的执行位置从客户端搬到了 AM 内部,提交作业只需要一个非常轻量的 CLI 客户端,剩下的都在集群侧完成,这就舒服多了。
Application 模式的资源申请方式也和 Per-Job 不同:Per-Job 模式是先启动 AM,等 JobManager 加载完 JobGraph 之后才知道需要多少资源,再逐步申请 TaskManager;而 Application 模式因为 main 方法在 AM 内执行,只需要在 main 里调用一个工具类方法把执行环境初始化好,并行度等信息已经写入配置,AM 在启动时就可以根据配置一次性申请好所有 TaskManager 资源。这让整个资源的申请过程更紧凑,作业启动时间也更快。
2.4 三种模式选型对照与场景判断
三种模式各有适用场景,没有绝对优劣,关键看你的业务需求和运维能力。下面这个表是我在实际项目中的判断逻辑汇总:
| 对比维度 | Session 模式 | Per-Job 模式 | Application 模式 |
|---|---|---|---|
| 集群生命周期 | 常驻,手动启停 | 随作业启停 | 随作业启停 |
| 作业隔离性 | 差,多作业共享 | 好,一作业一集群 | 好,一作业一集群 |
| 启动速度 | 快,秒级 | 慢,分钟级 | 中等,比 Per-Job 略快 |
| 客户端负载 | 需要常驻进程 | 需要完整环境执行 main | 轻量,提交即退出 |
| 适合场景 | 交互式查询、短作业 | 传统批作业、独立任务 | 生产流作业、定时批作业 |
| 运维复杂度 | 低 | 中 | 中 |
实际项目里我的建议是:生产环境的流作业一律用 Application 模式,既是官方主推,也解决了 Per-Job 的客户端问题;如果只是日常测试、写写 SQL 跑个小任务验证结果,用 Session 模式最省事;Per-Job 模式目前可以视作 Application 模式的前身,如果你们还在用旧版本 Flink,那就不得不用它。
3. 提交流程的完整拆解
3.1 客户端提交前的准备:依赖、Jar 包与配置
很多人以为提交流程是从敲命令那一刻开始的,其实不是。在真正执行提交命令之前,有几项准备工作直接决定了你的作业能不能顺利跑起来。
第一是用户代码的编译打包。无论你用 Java 还是 Scala 写 Flink 作业,最终都要打成一个 fat jar(也叫 uber jar),里面不光要有你的业务代码,还要把 Flink 连接器、自定义函数、第三方依赖库统统打进去。这个步骤很容易踩坑:比如你用了 Kafka Connector,但 pom 里忘了加依赖,或者加了但 scope 是 provided,结果集群上报 ClassNotFoundException。
第二是确认依赖的是哪个 Hadoop 版本。Flink 官方发行版会针对不同的 Hadoop 版本提供对应的预编译包,比如 flink-shaded-hadoop-2-uber 和 flink-shaded-hadoop-3-uber。如果你在编译时用的 Hadoop 客户端版本和集群不一致,提交时能通过但运行时会报各种 RPC 协议不兼容的问题。经验做法是:看你们集群的 yarn.version 是多少,然后下载对应版本的 Flink。
第三是配置和参数的核对。Flink 集群的资源配置,包括每个 TaskManager 的内存、CPU 核数、并行度、状态后端存储路径等等,都可以在提交命令时通过 -yjm、-ytm、-p 这些参数指定,也可以写进 flink-conf.yaml 里。建议把生产参数固化到配置文件里,而不是每次提交时手工敲一堆 -D 参数——后者一旦敲错很难排查。
3.2 提交流程逐阶段详解:从命令行到 AM 启动
当一个作业被提交时,整个流程大致可以分成七个阶段,我用一个实际执行的命令来完整走一遍:
这条命令背后的动作拆解如下:
第一阶段:客户端解析参数与配置。 Flink 客户端程序(CliFrontend)读取 flink-conf.yaml 以及命令行传入的 -D 参数,合并成最终的配置集合。这些配置包括作业名、队列名、并行度、内存设置、状态后端类型等等。
第二阶段:上传依赖到 HDFS。 客户端把所有需要分发到集群的文件(用户 Jar、Flink 发行包、依赖文件)上传到 HDFS 的暂存目录。默认位置是 hdfs:///user/<当前用户名>/.flink/<application_id>/。这里有个非常实用的优化参数:yarn.provided.lib.dirs。如果你把 Flink 发行包和公共依赖提前放到 HDFS 上的固定目录,客户端就不需要每次重复上传这些大文件,提交速度会快很多。
第三阶段:向 Yarn RM 提交 Application。 Flink 客户端构造一个 ApplicationSubmissionContext,里面包含要启动的 AM 命令、需要的 Container 资源规格、队列信息等,然后调用 Yarn Client API 把它提交给 RM。
第四阶段:RM 分配 Container 并启动 AM。 Yarn RM 接收到请求后,根据队列资源和调度策略,在某个 NodeManager 上分配一个 Container,然后由该 NodeManager 进程在 Container 内执行启动 AM 的指令。AM 进程启动后,会首先向 RM 注册自己,然后启动内部组件。
第五阶段:AM 内部启动 Flink 核心组件。 对于 Application 模式,AM 启动后会在内部执行 Flink 的 ApplicationDispatcherBootstrap,它会调用你 Jar 包里的 main 方法,生成 StreamGraph 并转化为 JobGraph。随后在同一个 AM 进程里启动 Dispatcher 和 JobManager(即 ResourceManager 和 JobMaster,在不同的组件线程中运行)。
第六阶段:Flink 向 Yarn 申请 TaskManager 资源。 JobManager 中的 ResourceManager 组件向 Yarn 的 AM 发资源请求,AM 作为 Yarn 应用的代表向 RM 申请 Container。每获得一个 Container,就在该 Container 里启动一个 TaskManager 进程,TaskManager 启动后会向 JobManager 注册自己,并汇报自身的可用资源。
第七阶段:JobManager 调度任务,作业开始运行。 所有必要的 TaskManager 注册完毕后,JobMaster 将作业的 ExecutionGraph 部署到各个 TaskManager 上。TaskManager 内部的 TaskExecutor 线程开始执行具体算子逻辑,作业正式进入运行状态。
3.3 三种模式下提交流程的关键差异
上面的流程是 Flink on Yarn 的通用骨架,但三种模式在具体环节上有几个关键跳跃点:
Session 模式下,阶段一到阶段五只发生一次,就是在执行 yarn-session.sh 的时候。之后每次提交作业,客户端只是通过 REST API 或 RPC 的方式把 JobGraph 传给常驻的 Dispatcher,不再向 Yarn 提交新的 Application。所以 Session 模式的提交非常轻,但代价是集群常驻资源不释放。
Per-Job 模式下,客户端本地执行 main 方法生成 JobGraph,然后通过 Yarn 提交。注意这里的 AM 里启动的是 JobManager 和 TaskManager,没有 Dispatcher 这个角色,因为有且只有一个作业,不需要“分发”这个动作。这也是 Per-Job 模式下作业管理最简单的原因之一。
Application 模式的提交流程本质上和 Per-Job 几乎一样,最大区别就是 main 方法在 AM 内执行。这意味着客户端可以非常轻量化地退出,而且在 AM 启动阶段就能确定并行度和资源需求,可以一次性向 Yarn 申请全部 TaskManager Container,作业启动的节奏更快,整体资源申请也更加平滑。
3.4 一个真实的 Yarn 调度过程
只看理论有点虚,我来还原一次我在生产环境排查资源问题时的真实场景。
我们的流作业是 Application 模式,并行度 10,每个 TaskManager 配置 4 GB 内存。提交之后发现作业状态一直是“ACCEPTED”,等了两分钟还没跑起来。我看了一眼 Yarn 的 ResourceManager 日志,注意到一个关键信息:它一直在尝试为这个作业分配 Container,但集群里某个队列的资源不够了——队列总容量 200 GB,已经被其他团队的任务占掉了 180 GB。
这就是 Yarn 的资源调度本质:RM 不是给作业分配一份“预定的资源”,而是等有空闲资源时才去分配。如果你的作业需要的资源量很大,而集群在高峰期已经几乎被打满,那么你的作业就会一直排队等待。解决方式有三个方向:把作业提交到资源空余的队列;降低作业的资源规格,用更小的 Container 慢慢等资源;或者提升队列的资源上限(但这是个资源审批问题,不是技术问题)。
另一个常见的调度场景是资源碎片化。Yarn 的调度器默认以单个 Container 为单位进行分配,比如集群里还剩 6 GB 可用内存,但你的作业申请的是 8 GB 的 Container,那这 6 GB 就暂不可用,RM 只能继续等待。这种情况在 Capacity Scheduler 和 Fair Scheduler 下的处理策略略有不同,但核心问题是相通的。
4. 提交流程中绕不开的配置与参数
4.1 Flink on Yarn 的核心配置清单
很多新手拿了一个 Flink 发行包就开始跑,跑起来之后发现各种资源不足、性能差的问题。这里我把 Flink on Yarn 最核心的配置项整理成一份清单,配置位置是 conf/flink-conf.yaml。
| 配置项 | 默认值 | 说明 |
|---|---|---|
jobmanager.memory.process.size |
1600m | JobManager 进程总内存 |
taskmanager.memory.process.size |
1728m | TaskManager 进程总内存 |
taskmanager.numberOfTaskSlots |
1 | 每个 TaskManager 可运行的 Task 数 |
parallelism.default |
1 | 默认并行度 |
yarn.application.queue |
default | 提交到 Yarn 的队列名 |
yarn.application-attempts |
2 | AM 失败后的最大重试次数 |
state.backend |
hashmap | 状态后端类型 |
state.checkpoints.dir |
无 | Checkpoint 存储路径 |
rest.port |
8081 | Web UI 端口 |
yarn.provided.lib.dirs |
无 | 预置依赖的 HDFS 路径 |
我特别想强调的是 taskmanager.numberOfTaskSlots 这个参数。很多团队喜欢把它设为和 CPU 核数相同,认为这样能“充分利用资源”。但如果你的并行度为 10,每个 TaskManager 有 2 个 Slot,那就需要启动 5 个 TaskManager;每个 TaskManager 进程自身还需要一些内存开销,比如 JVM 堆外内存、元空间等。盲目调大 Slot 数会导致每个 Task 分到的资源变少,GC 频繁,性能反而不如 Slot 少一点、进程多一点的时候好。
4.2 内存分配的秘密:为什么实际占用的内存比配置大
这是实操中非常容易让人困惑的一点。你明明配置 taskmanager.memory.process.size=4g,但看 Yarn 界面发现这个 Container 实际占用内存超过了 4.5 GB,甚至有时候作业直接被 NodeManager 杀掉,报错信息是 Container killed by the ApplicationMaster 或者物理内存超限。
原因是 Flink 的内存模型里,JVM 进程的内存不只是你配置的那部分。Flink 把进程内存划分成 JVM Heap、托管内存(Managed Memory)、网络缓冲内存(Network Memory)、JVM 元空间(JVM Metaspace)、JVM Overhead 几个部分。其中 JVM Overhead 是用来容纳线程栈、代码缓存、GC 空间等额外开销的,默认是进程内存的 10%。所以在 Yarn 上,Container 的总内存是 Flink 计算出的进程总内存再加上额外的系统开销,最终超过你的初始配置是正常现象。
解决方案很简单:给 Flink 配置内存时,要比预期多预留 15% 到 20% 的余量。Container killed by the ApplicationMaster 这个经典报错,九成以上是因为 taskmanager.memory.process.size 配小了,TaskManager 实际使用内存超过了 Yarn Container 限制。
4.3 日志与监控:作业失败时,去哪个文件看什么内容
作业跑挂了,第一件事不是看 Flink UI,而是先明确去哪里找日志。Flink on Yarn 的日志位置比较分散,但结构上是清晰的。
AM 的日志在 Yarn 的 NodeManager 日志目录下,通常路径是 /opt/hadoop/logs/userlogs/application_<id>/container_<id>/。里面有一个 syslog 文件,是 AM 进程的标准输出和错误输出,所有 JVM 打印的信息都在里面。
TaskManager 的日志路径也是类似的 userlogs 目录,对应 container_<id> 是 TaskManager 的 Container ID。如果你想快速定位一个作业对应的 Container,可以在 Flink Web UI 的 TaskManager 页面看到每个 TaskManager 的 Container 日志链接,点击即可跳转。
Flink 自己生成的日志路径则和部署方式有关:默认在 $FLINK_HOME/log/ 目录下,文件名叫 flink-<user>-taskexecutor-<host>.log。但注意,这个日志只在你有本地文件系统权限时才能看到,如果你用 Yarn 模式运行,实际的 TaskManager 日志是输出到对应 Container 的 stdout 里的。在 Yarn 上排查问题,我习惯直接进 Yarn 的 Web UI,找到对应作业的 ApplicationMaster 和 Container 日志页面,一次能看全所有角色。
5. 常见问题与排查技巧实录
5.1 各种启动失败场景:从队列权限到动态资源不足
场景一:Application is rejected by the queue。 这是在提交作业时最常见的错误之一,原因是当前用户没有向指定队列提交任务的权限。Yarn 通过 Capacity Scheduler 的 ACL(访问控制列表)来限制谁能往哪个队列提交作业。排查思路是:确认你提交时用的 -D yarn.application.queue 参数是否拼写正确;检查当前提交任务的操作系统用户(比如 yarn.scheduler.capacity.root.<队列名>.acl_submit_applications)是否在该队列的白名单里。
场景二:作业启动后一直处于 ACCEPTED 状态,但迟迟不进入 RUNNING。 这个我在前面提到过,大概率是资源不足。你需要用 yarn application -status application_xxx 查看请求的 Container 数,同时去 Yarn 的 Capacity Scheduler 页面(默认端口 8088)查看队列的资源使用率。如果集群很空但作业仍然一动不动,就要检查 Yarn 的调度器配置,比如 Fair Scheduler 的 stealing 开关、Capacity Scheduler 的 maximum-capacity 是否限制了队列。
场景三:ClassNotFound/NoClassDefFoundError。 这是 Per-Job 和 Application 模式最典型的客户端-集群环境不一致问题。Application 模式下你可能会在 AM 日志中看到“用户代码中的类”找不到,这时候不要慌,优先检查 Jar 包是否把第三方依赖完整打进去了(用 mvn dependency:tree 检查依赖树),以及你上传到 HDFS 的依赖目录和集群 Flink 版本是否匹配。
5.2 动态资源分配不工作的坑
我遇到过不只一次的情况是:JobManager 的内存配了 2 GB,作业并行度只有 2,另一个作业共用同一个队列时资源满了,任务一直等待。后来看了 JobManager 日志才发现,它不是在等待 Yarn 分配新的 TaskManager Container,而是在尝试复用已有的 Container——但那个 Container 上的资源其实已经不够了。
这是 Session 模式特有的一个坑:TaskManager 有 taskmanager.numberOfTaskSlots 的限制,Slot 被某个作业占用后,其他作业的 Task 无法调度到这个 TaskManager 上,即便这个 TaskManager 整体还有不少 CPU 和内存余量。这时候你以为“动态资源分配”会自动帮你启动一个新 TaskManager,但 Flink 的默认行为是要等到队列中没有可用资源时才会触发新的资源请求。
如果你希望 Session 模式下每个作业都能得到及时的资源响应,建议把 taskmanager.numberOfTaskSlots 设置成一个较小的值(比如 2 或 4),让资源粒度更细,调度更灵活;同时可以考虑给 Session 集群的初始 TaskManager 数量设一个合理的下限,避免资源争抢过激。
5.3 网络与 JDBC 连接器相关的异常排查思路
Flink SQL 用久了,你大概率会遇到 flink type is datev2, but arrow type is dateday 这种类型映射错误。这个错误的来源是 Flink 的 JDBC 连接器与 Doris 这样的数据库之间在做类型转换时出了问题:Flink 内部用 DateV2 表示日期类型,而 Doris 通过 Arrow 向量化读取时,把日期解析成了 DateDay 类型,两边类型不一致导致赋值失败。
排查思路是三步:确认 Flink 和 Doris 连接器的版本兼容性——老版本的 Doris Connector 往往不支持新的 Flink 类型系统;检查你建表时的字段类型是否是 DATE 而不是 DATETIME,很多情况下改变字段类型即可绕开映射问题;最后再看看连接器的配置项里有没有类似 option.date-format 这样的参数可以显式指定格式。
JDBC 连接器的另一个高频报错是 Failed to create connection,通常伴随 Communications link failure 或者 Connection refused。这个报错的排查面比较广:先排除网络问题(数据库端口是否可达、白名单是否放行),再检查连接池配置(空闲连接是否被数据库服务端主动断开),最后看数据库端的最大连接数是否已经打满。我曾经被一个 wait_timeout 参数折腾了一个下午,MySQL 服务端把空闲超过 8 小时的连接断掉了,而连接池里的连接没有及时剔除,导致每次请求都捞到死连接。
5.4 作业恢复与 Checkpoint 相关的经验
运行中的流作业挂了,最让人揪心的就是状态能不能恢复。Flink 通过 Checkpoint 定期将状态快照持久化到远端存储,作业重启时从最近的 Checkpoint 恢复。但在 Yarn 模式下,作业重启的“宿主”已经变了——新启动的 AM 和 JobManager 是一个全新的进程,它怎么知道要加载哪个 Checkpoint?
关键就在 state.checkpoints.dir 这个配置上,它必须指向一个各节点都能访问的共享存储路径,通常是 HDFS。只要这个路径在配置文件和代码中保持一致,新启动的 JobManager 就能通过作业 ID 找到最近的 Checkpoint 目录并自动恢复。
踩坑提醒:千万不要把 state.checkpoints.dir 配置成本地路径,比如 /tmp/flink-checkpoints。这在单机测试时没问题,但在 Yarn 集群上,TaskManager 分布在不同的 NodeManager 上,本地路径就变成“每台机器各存各的”,恢复时连 Checkpoint 文件都找不到。我第一次部署时就踩了这个坑,恢复作业时状态全丢,只能从头消费 Kafka 数据,那酸爽的经历至今难忘。
6. 如何从零搭建一套可用的 Flink on Yarn 环境
6.1 前置条件:Hadoop 集群与 Yarn 的安装
如果你还没有一个能用的 Yarn 集群,那第一件事是先装 Hadoop。安装 Yarn 本身不复杂,核心步骤是配置 core-site.xml、hdfs-site.xml、yarn-site.xml、mapred-site.xml 这几个文件,然后启动 HDFS 和 Yarn 相关进程。
yarn-site.xml 里最核心的配置是 yarn.resourcemanager.hostname(RM 的主机名)和 yarn.nodemanager.aux-services(必须设为 mapreduce_shuffle,否则使用 Spark 或 Flink 时各种远程拉取文件会失败)。NodeManager 的资源总量配置是 yarn.nodemanager.resource.memory-mb 和 yarn.nodemanager.resource.cpu-vcores,这两项的值决定了每台物理机能为 Yarn 提供多少资源。
需要特别注意的是磁盘资源。yarn.nodemanager.local-dirs 是 Yarn 存放中间数据和 Container 日志的本地路径,建议配置在数据盘而不是系统盘上,路径要有足够的剩余空间。我见过因为 / 盘写满导致的 NodeManager 假死,整个集群调度瘫痪的情况,这个细节千万别忽略。
6.2 适配 Flink 与 Hadoop 版本
Flink 发行包和 Hadoop 之间天然存在版本兼容问题。Flink 官方在发布包中提供了几种 Hadoop 版本,但不是所有 Hadoop 版本都有对应的预编译包。如果你用的 Hadoop 版本比较特殊,就得自己处理兼容性问题。
最稳妥的做法是使用 Flink 官方提供的 flink-shaded-hadoop-3-uber 或者 flink-shaded-hadoop-2-uber 这两个 fat jar,放在 Flink 的 lib 目录下。如果你的 Hadoop 集群是最新版(3.3 以上),建议直接下载 Flink 对应的 Hadoop 3 版本。如果你自己编译 Flink,一定要把 -Pvendor 和 Hadoop 版本参数配对正确,否则运行时会报 RPC 协议不兼容——最典型的报错是 org.apache.hadoop.ipc.RemoteException 或者各种 No such method 异常。
这里还要顺带回应一个很多人纠结的问题:Flink 一定要用 HDFS 吗? 答案是不一定。Flink on Yarn 模式必须要有一个共享存储来暂存作业 Jar、分发依赖和保存 Checkpoint,但这份存储不一定是 HDFS。只要你有一个所有节点都能访问的共享文件系统(比如 NFS、JuiceFS、CephFS)也可以。但在 Hadoop 生态里,HDFS 是最标准、最顺手的选择,所以但凡你用了 Yarn,大概率会顺手用 HDFS,两者几乎是配套出现的。
6.3 快速验证环境是否正确
环境搭好后,不要直接丢生产作业,先用一个简单任务验证整套链路。我最常用的验证方法是提交一个 Flink 自带的 WordCount 或者使用 flink run 执行一个简单的 SQL 任务,比如:
检查三个东西:第一,Yarn 上出现了新的 Application,且状态从 ACCEPTED 变为 RUNNING;第二,TaskManager 成功注册,Flink Web UI 能看到 Slot 信息;第三,作业真正处理了数据并输出结果。三步都能走通,说明 Hadoop 和 Flink 的整合没有问题。如果卡在第二步,大概率是内存配置不够——TaskManager 不能正常启动的根因,九成是资源不够或内存超限被杀了。
6.4 一个实用的启停脚本模板
最后分享一个我常用的启停脚本模板,方便你一键启停 Yarn 模式下的 Flink 集群。
脚本里的 yarn.provided.lib.dirs 是我前面强调过的优化项,它会把 Flink 发行包和公共依赖放到 HDFS 上,提交作业时不再重复上传,实测提交时间能缩短一半以上。如果你还没用过这个参数,强烈建议试试。
7. 优化建议:让 Flink on Yarn 跑得更稳
每种模式都有适配场景,但不管用哪种模式,有些优化经验是共通的,我整理几条最有价值的。
资源规划时要给 AM 留余量。 很多人在配置时只关注 TaskManager 的资源,把 AM 的内存忽略了。AM 进程虽然比 TaskManager 小,但它是整个作业的调度中枢,内存不足会导致频繁 Full GC、调度变慢甚至 AM 被 Yarn 杀掉重启。建议 AM 的内存不要低于 1.5 GB,如果并行度很高,可以调到 2 GB。
合理设置 Slot 数与并行度的关系。 并行度 20 全部塞到 1 个 TaskManager 里,和分散到 10 个 TaskManager 里,性能差异是巨大的。前者会让一个进程的线程数过多,GC 压力巨大;后者虽然进程数多,但每个进程的线程数少,CPU 缓存友好度更高,调度更灵活。一般经验是每个 TaskManager 的 Slot 数设在 4 到 8 之间,具体情况再看资源和数据量。
大作业拆分能救命。 一个超大作业如果逻辑复杂,建议拆成多个较小作业,分开提交。这样做的好处是:单个作业出问题不会影响其他作业;Checkpoint 和恢复时间显著缩短;运维时的直观性更强。代价是跨作业的状态共享需要借助外部存储或者消息队列,但收益远大于损失。
充分的 Checkpoint 策略配置。 流作业必须配好 Checkpoint,不是“有了就行”。包括 execution.checkpointing.interval、超时时间、最小间隔、以及精确一次(exactly-once)和至少一次(at-least-once)的语义选择。在 Yarn 上加上动态资源变化的影响,Checkpoint 配置不当会让作业运行几个月后在某个大流量节点上崩掉,恢复时又因为状态过大恢复缓慢,整个服务中断数小时。
用 Yarn 队列做好团队隔离。 多团队共用集群时,一定是分队列管理,不要所有人都塞到 default 队列里。队列之间不但资源隔离,还能通过 Capacity Scheduler 配置不同的调度优先级。不然一旦某个团队的任务写了个死循环,把集群资源耗尽,所有团队都会遭殃。
我个人在实际操作中的体会是,Flink on Yarn 这套体系,真正难的不是看懂的文档,而是把它真正放到高负载生产环境后,通过一次次故障和调优积累出来的经验。比如 Session 模式下 JobManager 内存怎么配、TaskManager Slot 和并行度的最佳比例是多少、Checkpoint 频率和状态大小的平衡点在哪里,这些参数没有标准答案,只有基于你业务数据量的持续迭代。
最后再分享一个小技巧:Flink 作业上线前,先做一次小规模压测,观察内存使用曲线和 GC 情况,用数据说话代替拍脑袋配置。这套部署方式只要你认真跑过一两个月,踩过几个坑、调过几次参,再回头看这篇原理,你会发现框架设计者的很多决策都是深思熟虑的,而你对它的掌控力也会上一个台阶。