ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Flink on YARN部署实战:从资源模型到高频排障

Flink on YARN部署实战:从资源模型到高频排障

如果你已经有一套现成的YARN集群,还想在上面跑Flink,那就别急着另起炉灶搞一套独立Flink集群。把Flink交到YARN手里,让它像跑MR、Spark任务一样,按需申请容器、用完自动释放,这是多数数据平台团队早晚要走的路线。Flink on YARN这套方案解决的核心痛点就是资源复用:不需要再为Flink单独采购机器,也不需要单独维护一套JobManager/TaskManager进程,老集群的资源池直接共享。

这篇文章我打算按自己实际落地过的流程来写,从“为什么选YARN而不是Standalone”开始,到YARN侧参数怎么调、Flink侧内存怎么配、三种提交方式有什么区别,最后再给一份高频报错排查清单。适合手里已经有Hadoop生态、想把Flink塞进去提高资源利用率的工程师,也适合刚接触YARN、被各种Container killed折磨得头疼的新手。先说个结论:绝大多数搭建问题,不是Flink不会跑,而是YARN没给它留够位置。

1. 为什么把Flink放进YARN:先想清楚这步,后面才不返工

1.1 三种部署方式,别一上来就选YARN

Flink的部署方式主要就三种:Standalone、YARN、Kubernetes。很多人一上来就把Flink装在几台裸机器上,启一个JobManager、几个TaskManager,跑起来是挺爽,但资源是写死在配置里的,作业多了就互相抢,机器少了就得手动加节点,非常不灵活。

Standalone模式适合开发测试。它的优点是简单,不用依赖Hadoop;缺点是资源利用率低、扩展全靠人工、没有和Hadoop体系打通。如果你只是本地验证逻辑,用它没问题。但生产环境里如果你的资源大部分都在YARN上,那Flink单独占一批机器就很浪费。

Kubernetes模式是后来的趋势,适合已经云原生化的团队。它有完整的容器编排、弹性伸缩,但前提是你得有一个能稳定运行的K8s集群,而且Flink Operator的学习成本不低。现在很多新项目确实首推K8s,但对传统企业、内网机房、已经沉淀了多年YARN运维经验的团队来说,把Flink直接接到YARN上才是性价比最高的路。

YARN模式的核心好处有三个:资源按需申请,作业跑完自动释放;与其他计算框架共享队列,可以把集群水位拉满;JobManager和TaskManager的生命周期由YARN统一管理,省掉一套独立运维体系。你只需要确保YARN是健康的,Flink就跟着健康。

三种方式放到表格里更直观:

部署模式部署成本资源弹性与其他作业共享适用场景
Standalone低差否开发、测试、学习
YARN中好好已有Hadoop生态的团队
Kubernetes高最好好云原生平台、容器化团队

1.2 YARN管理Flink的完整资源流转

能在YARN上跑的Flink,本质上就是把原来的独立集群拆成了“按作业动态出生”的容器集合。提交一个Flink作业时,客户端会先通过YarnClient往ResourceManager提交一个Application申请,RM会选一个NodeManager把ApplicationMaster容器启动起来。

在Flink on YARN模式下,JobManager就运行在这个AM容器里,负责作业调度、Checkpoint协调、Web UI提供。然后JM继续向RM申请新的Container来启动TaskManager,TM启动后通过网络注册回JobManager,把Slot汇报上去。之后JobGraph被调度到各个Slot上执行。

这里有个特别容易混淆的点:JobManager和ApplicationMaster并不是两个独立进程,而是同一个进程里两块职责。很多初学着以为YARN会单独起一个AM进程、再单独起一个JM进程,实际上Flink为了降启动成本,把两者合体了。理解这点,后面查日志会省很多力气。

打个不严谨的类比:YARN是酒店前台,Flink作业提交后,前台先给领队开一个房间(AM/JM容器),领队入住后按会议规模再向前台申请更多房间(TM容器)。会议结束,房间全部退掉。整个过程不用Flink自己买楼,酒店有多少房源由前台统一分配。这就是Flink on YARN最核心的资源流转逻辑。

2. 搭建前必须确认的版本组合与环境检查

2.1 版本选型:先锁住兼容矩阵

Flink on YARN的兼容性问题比想象中多,很多作业跑不起来不是代码问题,而是Flink版本和Hadoop版本对不上。我自己在项目里常用的组合是Flink 1.17/1.18系列配合Hadoop 3.2.x或3.3.x,JDK用8或11。Flink 1.14以前的老版本对Hadoop 3.3的某些RPC协议支持不完整,会莫名其妙报一些类转换异常。

除了Flink本身,还要确认能拿到YARN配置。Flink发行包只内置了一小部分Hadoop API,真正连YARN、读HDFS还需要用到集群侧的core-site.xml、yarn-site.xml。Flink官方建议用HADOOP_CLASSPATH把Hadoop的ClassPath传进来,这一步漏了你会看到一大串“Could not connect to ResourceManager”之类的报错,问题却不在RM本身。

建议在提交Flink任务的环境变量里加上一句:

export HADOOP_CLASSPATH=$(hadoop classpath)

如果你用了Hive或者HBase,也一并把它们的ClassPath拼上。这里还有一个踩过很多遍的坑:不要在Flink的lib目录里随便放Hadoop的jar包。有些教程让你把flink-shaded-hadoop-uber直接塞进去,在旧版本还能用,新版本这种操作很容易和集群自带的Hadoop冲突,最后报一堆校验和异常。

2.2 主机网络与基础环境检查

Flink on YARN对主机环境的要求不算高,但有几个点必须要查,不然后面TM注册不上或者客户端提交超时,排查起来会绕圈子。

第一个是hostname和/etc/hosts。YARN的NodeManager会以自己的hostname在集群里亮相,Flink任务管理器启动后也要通过这个hostname回连JobManager。如果节点之间不能用主机名互通,或者大小写不一致,就会造成TaskManager一直“Waiting for registration”。我习惯是先把所有节点的hostname统一成小写,然后在每台机器的/etc/hosts里写全所有节点的IP映射,不用DNS也不依赖内网域名。

第二个是SSH免密。虽然YARN提交本身不需要直接SSH到NodeManager,但HDFS操作、日志聚合、运维排查都需要,顺手配了能省很多事。第三个是时钟同步,YARN和HDFS对时间敏感,节点间偏差过大会出现诡异的认证失败。

还有一个容易被忽略的是端口和防火墙。Flink on YARN模式下,客户端要能访问ResourceManager的Web端口和RPC端口,JobManager也需要通过YARN分配的地址回连。如果你的节点之间有防火墙限制,最好提前把Hadoop和Flink相关的端口段放行。

可以用下面这个清单自查:

检查项要求常见问题
hostname统一小写大小写不一致导致注册失败
/etc/hosts全节点IP映射齐全TM回连JM超时
SSH免密所有节点互通运维操作繁琐、易错
时间同步NTP/Chrony一致认证过期、日志时间错乱
端口放行RM、NM、Flink Web端口提交超时、UI打不开
HADOOP_CLASSPATH声明Hadoop依赖提交时ClassNotFound

2.3 YARN侧必改参数与调度器细节

Flink能不能顺利跑起来,很大程度上取决于YARN给不给你资源。有些人总在Flink配置里较劲,结果YARN队列根本没给够资源,作业一直挂在ACCEPTED状态。

第一组要改的是NodeManager的资源上限。很多默认配置只给了单节点很小的内存量,比如默认的yarn.nodemanager.resource.memory-mb可能是8GB甚至更小。如果你的节点是64GB物理内存,只给YARN分8GB,那Flink一个TaskManager要申请4GB都费劲。通常我会给YARN划走物理内存的80%到85%,剩下给系统、HDFS、其他进程留余量。

<property> <name>yarn.nodemanager.resource.memory-mb</name> <value>49152</value> </property> <property> <name>yarn.nodemanager.resource.cpu-vcores</name> <value>16</value> </property>

第二组是调度器对单个容器的限制。yarn.scheduler.maximum-allocation-mb默认值往往是8GB或16GB,如果Flink的TaskManager进程要分配到10GB以上,就会被YARN直接拒绝。申请资源和最大限制不匹配时,日志里不会明确告诉你“你改一下maximum-allocation-mb吧”,而是表现为作业一直等待或者AM启动后立刻失败。

第三组是容量调度器的AM资源占比。默认yarn.scheduler.capacity.maximum-am-resource-percent只有0.1,意思是最多只能把集群10%的资源用来启动AM容器。如果你跑的Flink作业需要给JobManager分配2GB甚至更大,而集群总资源又不多,那么AM就可能一直排队,作业状态停在SUSPENDED。生产环境我通常把这个参数调到0.5,给Flink作业留足启动空间。

<property> <name>yarn.scheduler.capacity.maximum-am-resource-percent</name> <value>0.5</value> </property>

改完yarn-site.xml之后,ResourceManager需要重启才能生效。如果你怕影响线上任务,至少也要在维护窗口内做。同时建议开启日志聚合:

<property> <name>yarn.log-aggregation-enable</name> <value>true</value> </property>

不然作业跑完容器一销毁,日志就跟着没了,排查问题只能干瞪眼。

3. 实操:从下载Flink到第一个任务跑上YARN

3.1 下载与部署目录规划

搭建Flink on YARN之前,先在客户端节点下载一个对应版本的Flink发行包。版本号和YARN所在集群的Hadoop版本要匹配,这个前面已经说过。下载完最好校验一下SHA-512,避免拉了个损坏包装完一堆诡异的报错。

解压到统一目录,比如/opt/flink,然后做一个软链指向当前版本,方便以后升级。这里有一个容易掉坑的细节:用什么用户操作,一定要和运行YARN/HDFS的用户保持一致,或者至少有HDFS目录的写权限。很多权限问题都源于root跑的Flink、普通用户跑的HDFS,两边互不认账。

Flink安装目录本身只需要放在客户端提交机器上即可,YARN会把Flink的运行依赖自动传播到各节点?严格来说不是“自动传播”,而是Flink会把lib包上传到HDFS临时目录,或者你提前把依赖放到HDFS上让它直接使用。为了省时间,我习惯先把lib目录里的包传到HDFS:

hdfs dfs -mkdir -p /flink/lib hdfs dfs -put -f /opt/flink/lib/*.jar /flink/lib/

提交作业时指定:

-Dyarn.provided.lib.dirs=hdfs:///flink/lib

这样每次提交就不用重新上传几十MB的依赖,作业启动会快很多。这个细节在作业频繁发布时体感非常明显。

3.2 flink-conf.yaml的内存模型配置

Flink从1.11之后强制使用新的内存模型,这点和旧版本很不一样。老教程里教你调taskmanager.heap.size,在新版本里基本已经过时,你得按进程总内存来规划。

打开conf/flink-conf.yaml,最少要确认这几个配置:

jobmanager.memory.process.size: 2048m taskmanager.memory.process.size: 8192m taskmanager.numberOfTaskSlots: 4 state.backend: rocksdb state.checkpoints.dir: hdfs:///flink/checkpoints

简单解释一下:jobmanager.memory.process.size是JobManager整个进程的物理内存预算,taskmanager.memory.process.size是TaskManager整个进程的物理内存预算。它们包含堆内存、托管内存、网络缓冲、JVM自身开销和直接内存等所有部分。你不需要再单独去配堆内和堆外,Flink会自动拆分配置,除非你确实遇到了内存瓶颈才需要微调。

这里有一个新手很容易踩的坑:同时设置process.size和heap.size,系统会做校验,一旦发现两种配置叠加不合理就直接报错。所以我建议先只设process.size,让它自己去算堆和堆外的占比,跑一段时间看监控再决定要不要手动调整堆内存。

还有一个和YARN相关的关键点:TaskManager容器在YARN上申请的内存,必须能装得下Flink计算出来的进程总内存,还得留一点余量给容器本身的开销。例如你设置taskmanager.memory.process.size=8192m,实际YARN容器可能会申请8.8GB甚至9GB,因为Flink在底层会额外计算YARN容器开销。如果yarn.scheduler.maximum-allocation-mb恰好是8192MB,那么TM容器永远起不来。

3.3 三种提交模式:session / per-job / application

Flink on YARN有三种提交方式,很多人混着用但分不清区别。最简单的理解是:Session先建一个集群,多个作业共享;Per-job和Application每一个作业单独建一个集群。

Session模式启动方式:

./bin/yarn-session.sh -nm my-flink-session -d

启动后YARN上会出现一个常驻的Flink集群,然后你可以反复用下面的命令提交作业:

./bin/flink run -t yarn-session ./examples/streaming/WindowWordCount.jar

Session适合开发测试、小作业、作业数量多但单个资源需求量不大的场景。缺点是所有作业挤在同一个JobManager下,如果一个作业把JM拖垮或者导致Checkpoint积压,其他作业全部遭殃。所以我在生产上不太推荐把重要作业全塞进同一个Session,隔离性太差。

旧版的Per-job模式用-m yarn-cluster提交,参数看着很直观,但Flink 1.15之后已经把YARN Per-job支持移除了,官方建议统一使用Application模式。Application模式下main()方法在集群端执行,客户端提交之后即使断开也不影响作业运行,而且每个作业的资源隔离更彻底:

./bin/flink run-application -t yarn-application \ -Djobmanager.memory.process.size=2048m \ -Dtaskmanager.memory.process.size=6144m \ -Dtaskmanager.numberOfTaskSlots=2 \ -Dyarn.provided.lib.dirs=hdfs:///flink/lib \ -Dyarn.application.name="实时订单统计" \ hdfs:///app/jars/real-time-etl.jar

注意这里我是从HDFS上读取Jar包,而不是本机路径。这样做的好处是客户端没有任何业务包也能提交,而且多个客户端可以共用一套部署包。

3.4 并行度与内存规划计算案例

给一个实际的资源规划例子。假设YARN集群有3个节点,每个节点给Flink用的内存是64GB,每个节点16个vcore。

线上需求是并行度16,每个TaskManager需要2个Slot。那么TaskManager数量就是16除以2,共8个TM。假设每个TM的taskmanager.memory.process.size=6144m,那8个TM就是48GB,再加上JobManager的2GB和少量系统余量,整体资源在3个节点上完全可以铺开。

但如果并行度提到32,Slot数不变,TM数会变成16,内存总需求一下子到96GB以上,3个节点显然扛不住。此时你有两个选择,要么把每个TM的Slot数提高到4,让TM数量降回8个;要么增加节点。很多人的误区是一味调大并行度而不看集群总容量,最后任务卡在资源申请上,还以为是Flink调度器有毛病。

再提醒一下YARN容器限制的问题。如果taskmanager.memory.process.size=6144m,容器实际请求可能到6.6GB左右。假设yarn.scheduler.maximum-allocation-mb只给了6GB,那容器请求会直接超限,TM启动失败。所以最好先算好这个数,再反推YARN侧需要把maximum-allocation-mb调到多少,不要盲目改Flink配置。

4. 高频问题与排查实录

4.1 作业一直ACCEPTED/SUSPENDED或AM反复重启

这是Flink on YARN里出现频率最高的问题之一。作业提交后RM UI显示状态一直在ACCEPTED,或者到了RUNNING又很快挂掉,再被YARN重新调度,反反复复。

首先用命令看状态:

yarn application -list yarn application -status application_xxx

如果能够从RM Web UI看到AM资源等待的信息,排查方向基本可以锁定在资源配额上。最常见的原因是yarn.scheduler.capacity.maximum-am-resource-percent这个值太小,AM申请的资源超过了队列允许的百分比。这种情况修改YARN调度器参数,不是光flink-conf里调大JobManager内存就能解决的。

还有一种情况是AM进程启动后因为内存超限被NodeManager杀掉。这要看NodeManager的日志和容器日志,里面通常会出现“Container killed on request. Exit code is 143”之类的信息。这往往不是Flink自己的问题,而是YARN在强制回收容器。下面一节展开讲。

4.2 TM一直注册失败:hosts、内存、虚拟内存校验

TaskManager启动之后无法注册到JobManager,这个现象几乎每个搭过Flink on YARN的人都见过。表面症状是Flink Web UI上TaskManager列表为空,或者一直显示“Waiting for registration”,然后容器不断重启。

第一步先看TaskManager日志,里面如果出现连接失败,多半是网络或hostname的问题。这里我吃过一次亏:某个节点的hostname是大写的,YARN上报时转成了小写,但Flink在配置里用了原始hostname,两边对不上,TM就一直连不上JM。统一所有节点的hostname并配好/etc/hosts以后,问题就消失了。

第二步看YARN的NodeManager日志,检查是不是虚拟内存校验把容器杀了。YARN默认会校验容器使用的虚拟内存,而JVM在Linux上申请的虚拟内存往往远超实际物理内存,经常出现Flink实际内存正常、但虚拟内存超限被NM强杀的情况。生产环境我建议根据实际资源规划把虚拟内存校验关掉:

<property> <name>yarn.nodemanager.vmem-check-enabled</name> <value>false</value> </property> <property> <name>yarn.nodemanager.pmem-check-enabled</name> <value>false</value> </property>

但这里必须强调:关掉校验不等于可以疯狂超配内存。内存规划还是要合理,否则物理内存溢出会把整个节点拖死,到时候就不是单个作业的问题了。

4.3 ClassNotFound与依赖冲突的三种解法

业务作业提交到YARN后,常见的报错还有NoClassDefFoundError、NoSuchMethodError、各种序列化异常。这些问题大多不是代码逻辑问题,而是依赖没有正确打包或者和Flink内置依赖冲突。

第一种解法是把所有业务依赖打进一个Fat Jar,用maven-shade-plugin调整依赖合并策略。这是最稳妥的方式,能保证每个作业自包含。

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.4.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>your.MainClass</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin>

第二种解法是把多个作业共享的公司内部依赖提前放到HDFS上的lib目录,配合-yarn.provided.lib.dirs使用,减少每次打包体积,也避免重复上传。这里要注意不要把不同版本的相同依赖混放进去,否则会随机踩NoSuchMethodError。

第三种解法是处理Flink自身lib目录。不要把你自认为需要的connector或hadoop jar一股脑塞进Flink的lib目录,除非你确认版本和集群一致。生产上我见过最坑的就是有人把不同版本的flink-connector-kafka同时放进lib,结果作业一会儿用这个类一会儿用那个类,行为完全随机。

4.4 日志到底该去哪看

Flink on YARN模式下日志分散在三个地方,查错时先分清你要看的是哪一类。

第一类是Flink运行时日志,包括JobManager日志、TaskManager日志、作业运行日志。如果你能访问Flink的Web UI,直接在JobManager页面点“Logs”就能看;不同Container之间的日志可以到RM Web UI点击对应Application的Tracking URL。

第二类是YARN容器日志。作业结束后容器销毁,日志默认跟着没了,除非开了日志聚合。开启yarn.log-aggregation-enable后,可以用下面命令拉取:

yarn logs -applicationId application_xxx -containerId container_xxx -log_files stdout

第三类是YARN自己的服务日志,比如ResourceManager、NodeManager日志,位于Hadoop日志目录。遇到容器反复被杀、AM启动异常这类问题时,这里才是第一现场。总体来说,排查顺序应该是:先看YARN服务日志确认容器生命周期是否正常,再看Flink的JM/TM日志确认运行时异常,最后再到自己的业务日志里找代码问题。

4.5 问题速查表

现象可能原因第一步处理
作业一直ACCEPTEDAM资源占比不足或队列资源不够检查yarn.scheduler.capacity.maximum-am-resource-percent
AM反复重启容器内存超限被NM强杀查看NodeManager日志、确认process.size配置
TM一直注册不上hostname不一致或/etc/hosts缺失统一hostname,补全hosts映射
Container killed, exit 143虚拟内存或物理内存校验超限评估内存规划后关闭或放宽校验
提交作业找不到RMHADOOP_CLASSPATH未设置export HADOOP_CLASSPATH=$(hadoop classpath)
作业启动极慢依赖每次上传HDFS提前把lib放到hdfs,使用provided.lib.dirs
两个作业互相拖累共用同一个Session集群重要作业拆分到独立Application模式

5. 几点个人实践体会

搭过几次Flink on YARN之后,我的体会是:真正难的不是Flink本身的配置,而是你对YARN资源模型的理解。先把YARN侧的内存分配、AM占比、容器上限这些参数理清楚,再回头配Flink,你会觉得顺畅很多。第一次上手的人我建议先用Session模式把链路跑通,看到Flink Web UI正常展示TM了,再切到Application模式做生产作业。

还有一个值得养成的习惯:把Flink的lib依赖提前放到HDFS,并统一公司内部的基础镜像或Jar包版本。这样每次发布作业能省下大量上传时间,也避免不同客户端提交时的依赖不一致。另外,监控一定要早接,TaskManager的CPU、内存、堆外使用量这些指标,在作业出问题之前就能帮你发现隐患,别等到Container被杀才去翻日志。

如果你们的集群规模还会继续扩大,后面可以把Flink on YARN的部署脚本、依赖目录、参数模板全部版本化管理起来,新环境上线时直接复制一套。这套东西看着简单,但真正做成可复用的流程,能省掉未来大量的排障时间。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进