1. 为什么一个提交命令能折腾我一整天刚接触 Flink SQL on YARN 那会儿我以为最难的应该是 SQL 语法或者状态管理结果真正让我在工位上坐到凌晨的是那个看起来平平无奇的提交命令。flink run敲下去日志刷了几百行最后给你来一句Application application_xxx failed 2 times due to AM Container for appattempt_xxx exited with exitCode: 1然后就没有然后了。你翻遍日志它就是不告诉你到底哪里错了。这篇文章记录的就是我在 Flink SQL on YARN 提交这个环节踩过的坑。核心关键词就几个Flink SQL、YARN、提交模式、资源分配、依赖冲突、日志排查。如果你正在用 Flink 做实时计算需要把 SQL 任务跑在 YARN 集群上或者你已经被exitCode: 1折磨过那这篇内容应该能帮你省下不少时间。我会从提交模式的选择开始讲把 per-job 和 application 模式的差异说清楚然后拆解提交过程中最容易出问题的几个环节资源参数配置、依赖包冲突、日志定位、Kerberos 环境下的认证问题。每个坑我都会给出具体的排查思路和解决方案不是那种“检查配置”的废话而是告诉你去看哪个文件、搜什么关键词、怎么改参数。2. 提交模式选不对后面全是白费劲2.1 per-job 和 application 模式到底差在哪Flink on YARN 有两种提交模式这个大家都知道但很多人包括当时的我并没有真正理解它们的区别只是随便选了一个就开始跑。结果就是任务能提交上去但资源利用率极低或者一个任务挂了把整个集群的槽位都占着不放。per-job 模式的特点是每个 Flink 任务单独启动一个 YARN ApplicationJobManager 和 TaskManager 都在这个 Application 里。它的好处是隔离性好一个任务挂了不影响其他任务。但问题也很明显每个任务都要单独申请一个 ApplicationMaster启动开销大而且如果任务很多YARN 上会有一堆小 Application资源碎片化严重。application 模式则是把整个 Flink 集群作为一个 YARN Application 提交多个 Flink 任务可以共享这个集群。它的优势在于资源利用率高启动快适合任务数量多、单个任务资源需求不大的场景。但缺点是隔离性差一些一个任务出问题可能影响同集群的其他任务。我当时的场景是每天有几十个 Flink SQL 任务要跑一开始用的是 per-job 模式结果 YARN 队列里全是小 ApplicationResourceManager 的压力很大而且每个任务启动都要等 AM 分配延迟很高。后来切到 application 模式把多个 SQL 任务打包成一个 Application 提交资源利用率明显上来了启动时间也从平均 40 秒降到了 15 秒左右。注意application 模式下所有任务共享 JobManager 的内存和 CPU如果某个任务特别吃资源需要单独给它分配 slot否则会拖慢同集群的其他任务。2.2 模式选择背后的资源账选模式不能只看隔离性还得算资源账。我拿一个实际例子来说明。假设你有 10 个 Flink SQL 任务每个任务需要 1 个 JobManager2GB 内存和 2 个 TaskManager每个 4GB 内存。如果用 per-job 模式总共需要 10 个 AM每个 AM 至少 1 个容器加上 TaskManager 容器总共是 10 × (1 2) 30 个容器。每个容器都有额外的内存开销YARN 的 container overhead 一般是 10% 左右实际内存消耗会更大。如果用 application 模式只需要 1 个 AM可以给 4GB 内存然后根据任务并发度动态申请 TaskManager。假设总共需要 20 个 slot每个 TaskManager 提供 4 个 slot那只需要 5 个 TaskManager 容器。总共是 1 5 6 个容器资源利用率提升了将近 5 倍。这个账算下来application 模式在任务数量多的时候优势非常明显。但要注意application 模式下 JobManager 是单点如果它挂了所有任务都会受影响。所以生产环境一般会配置 JobManager 的高可用用 ZooKeeper 做 leader 选举。2.3 提交命令的参数陷阱不管你选哪种模式提交命令的参数配置都是最容易出问题的地方。我见过太多人直接复制网上的命令改个 jar 包路径就提交结果各种报错。一个典型的 Flink SQL 提交命令长这样flink run-application -t yarn-application \ -Djobmanager.memory.process.size2048m \ -Dtaskmanager.memory.process.size4096m \ -Dtaskmanager.numberOfTaskSlots4 \ -Dyarn.application.namemy-flink-sql-job \ -Dyarn.application.queueroot.default \ -c org.apache.flink.table.client.SqlClient \ /path/to/flink-sql-client.jar \ --file /path/to/query.sql这里有几个参数特别容易踩坑jobmanager.memory.process.size这个参数控制的是 JobManager 的总进程内存包括 JVM 堆内存、元空间、JVM 开销等。很多人只设置jobmanager.memory.heap.size结果发现 JobManager 启动后没多久就 OOM 了。因为 Flink 1.10 之后内存模型变了推荐用process.size来统一控制。taskmanager.numberOfTaskSlots这个参数决定了每个 TaskManager 能跑多少个 slot。设置太小资源利用率低设置太大单个 TaskManager 负载过高容易出问题。一般建议设置为 CPU 核数的 1 到 2 倍。比如你的 TaskManager 容器分配了 4 个 CPU那 slot 数可以设为 4 或 8。yarn.application.queue这个参数指定了提交到哪个 YARN 队列。如果队列名写错了或者队列资源不足任务会一直处于 ACCEPTED 状态不会报错但也不会有进展。我遇到过好几次任务提交后卡在 ACCEPTED查了半天才发现是队列资源满了。实操心得提交之前先用yarn queue -status queue_name看一下队列的资源使用情况确认有足够的资源再提交能省掉很多等待时间。3. 依赖冲突那个让你怀疑人生的 NoClassDefFoundError3.1 依赖冲突的典型表现依赖冲突是 Flink SQL on YARN 提交过程中最隐蔽的坑之一。它的典型表现是任务能提交AM 容器也能启动但在初始化阶段突然报NoClassDefFoundError或ClassNotFoundException然后 AM 容器退出YARN 重试几次后彻底失败。最让人头疼的是这个错误信息往往只告诉你缺了哪个类但不告诉你为什么缺。你明明在 pom.xml 里加了对应的依赖打包的时候也确认 jar 包里有这个类但运行就是找不到。我遇到过一次Flink SQL 任务里用了某个 JSON 解析库本地测试完全没问题提交到 YARN 就报NoClassDefFoundError: com/fasterxml/jackson/databind/ObjectMapper。查了半天才发现Flink 的 lib 目录下已经有一个 Jackson 的版本和我打包进去的版本冲突了。Flink 的类加载器优先加载了 lib 目录下的旧版本而我的代码用的是新版本的 API所以找不到对应的方法。3.2 类加载机制与冲突排查要理解依赖冲突得先搞清楚 Flink on YARN 的类加载机制。Flink 的类加载器分为几层Java 基础类加载器加载 JDK 核心类。Flink 框架类加载器加载 Flink lib 目录下的 jar 包。用户代码类加载器加载用户提交的 jar 包中的类。默认情况下Flink 框架类加载器优先于用户代码类加载器。也就是说如果 Flink lib 目录下有一个类的某个版本用户代码里也有同一个类的另一个版本Flink 会优先加载 lib 目录下的版本。这就是依赖冲突的根源。排查依赖冲突我一般用这几个步骤确认冲突的类来自哪个 jar 包。用jar tf命令列出用户 jar 包和 Flink lib 目录下所有 jar 包的内容搜索冲突的类名。# 查找用户 jar 包中的类 jar tf my-flink-job.jar | grep ObjectMapper # 查找 Flink lib 目录中的类 for jar in $FLINK_HOME/lib/*.jar; do jar tf $jar | grep ObjectMapper echo Found in: $jar done确认版本差异。找到冲突的 jar 包后用unzip -p查看 MANIFEST.MF 或者用javap查看类的版本信息。选择解决方案。常见的解决方案有三种排除冲突依赖在 pom.xml 中用exclusions排除掉冲突的依赖让 Flink 使用 lib 目录下的版本。调整类加载顺序在 Flink 配置中设置classloader.resolve-order: child-first让用户代码类加载器优先加载。打包时重命名用 Maven Shade 插件把冲突的类重命名避免和 Flink lib 目录下的类冲突。我一般优先用第一种方案因为最干净。如果排除不掉比如 Flink 框架本身依赖这个类就用第二种方案。第三种方案比较重一般只在特别复杂的情况下用。3.3 打包时的依赖范围陷阱除了运行时冲突打包时的依赖范围设置也容易出问题。Maven 的依赖范围有compile、provided、runtime、test等几种设置错了会导致 jar 包里缺少必要的类或者包含了不该包含的类。对于 Flink 任务一般的原则是Flink 相关的依赖flink-streaming-java、flink-table-api-java 等设置为provided因为 Flink 集群的 lib 目录下已经有这些 jar 包了打包进去反而会引起冲突。第三方依赖如 MySQL 驱动、Redis 客户端等设置为compile这些需要打包进用户 jar 包。测试依赖设置为test不会被打包。我见过有人把 Flink 的依赖设成了compile结果打出来的 jar 包有 200 多 MB提交到 YARN 后各种类冲突。改成provided后jar 包只有 20 多 MB问题也消失了。注意如果你用的是 Flink SQL Client 提交SQL 连接器的 jar 包需要放在 Flink 的 lib 目录下而不是打包进用户 jar 包。因为 SQL Client 是在 Flink 集群启动后加载连接器的用户 jar 包里的连接器不会被加载。4. 资源参数配置那些让你任务卡住的数字4.1 内存参数的计算逻辑Flink on YARN 的内存配置是另一个容易踩坑的地方。Flink 1.10 之后引入了新的内存模型把内存分成了多个部分JVM 堆内存、堆外内存、元空间、JVM 开销、网络缓冲区等。如果配置不当任务可能在启动阶段就 OOM或者运行一段时间后频繁 Full GC。先看 JobManager 的内存配置。jobmanager.memory.process.size是总进程内存它由以下几部分组成jobmanager.memory.heap.sizeJVM 堆内存默认是总内存的 50%。jobmanager.memory.off-heap.size堆外内存默认 128MB。jobmanager.memory.jvm-metaspace.size元空间默认 256MB。jobmanager.memory.jvm-overhead.fractionJVM 开销默认是总内存的 10%。如果你只设置了jobmanager.memory.process.size2048m那堆内存大约是 1024MB元空间 256MB堆外 128MBJVM 开销 204MB加起来差不多 1612MB剩下的作为预留。这个配置对于一般的 SQL 任务够用了但如果你的 SQL 任务有很多 UDF 或者复杂的查询计划可能需要更大的元空间。TaskManager 的内存配置更复杂一些因为涉及到 slot 的分配。taskmanager.memory.process.size是总进程内存它由以下几部分组成taskmanager.memory.framework.heap.sizeFlink 框架堆内存默认 128MB。taskmanager.memory.task.heap.size任务堆内存用于用户代码。taskmanager.memory.managed.size托管内存用于排序、哈希表等操作。taskmanager.memory.network.size网络缓冲区用于数据传输。taskmanager.memory.framework.off-heap.size框架堆外内存。taskmanager.memory.jvm-metaspace.size元空间。taskmanager.memory.jvm-overhead.fractionJVM 开销。我一般会这样配置一个 TaskManagertaskmanager.memory.process.size: 4096m taskmanager.memory.task.heap.size: 2048m taskmanager.memory.managed.size: 1024m taskmanager.memory.network.min: 256m taskmanager.memory.network.max: 512m taskmanager.numberOfTaskSlots: 4这个配置的意思是总内存 4GB其中任务堆内存 2GB托管内存 1GB网络缓冲区 256MB 到 512MB剩下的给框架和 JVM 开销。4 个 slot 共享这些内存每个 slot 大约有 512MB 的任务堆内存。4.2 资源不足时的表现与排查资源不足的表现有很多种最常见的是任务一直处于 ACCEPTED 状态或者 TaskManager 启动后马上退出。如果任务提交后一直卡在 ACCEPTED说明 YARN 没有足够的资源来分配容器。这时候需要检查队列资源用yarn queue -status queue_name查看队列的可用资源。集群资源用yarn node -list查看集群节点的资源使用情况。容器请求用yarn application -list查看 Application 的状态如果状态是 ACCEPTED说明在等待资源。如果 TaskManager 启动后马上退出可能是内存配置有问题。常见的原因是taskmanager.memory.process.size设置得太小导致 JVM 启动时 OOM。或者taskmanager.memory.task.heap.size和taskmanager.memory.managed.size加起来超过了总内存导致配置校验失败。实操心得提交之前先用flink run-application -t yarn-application加上-Dtaskmanager.memory.process.size4096m这样的参数试跑一下观察日志里有没有内存相关的警告。如果有Insufficient memory之类的提示就说明内存配置需要调整。4.3 并发度与 slot 的匹配关系并发度parallelism和 slot 数的匹配关系也是容易搞错的地方。Flink 的并发度决定了任务会分成多少个并行子任务每个子任务需要一个 slot 来运行。如果并发度大于总 slot 数任务会一直等待 slot 分配表现为任务卡在 RUNNING 状态但没有任何数据处理。总 slot 数的计算公式是TaskManager 数量 × 每个 TaskManager 的 slot 数。比如你有 3 个 TaskManager每个有 4 个 slot总 slot 数就是 12。如果你的任务并发度设置为 16那就需要 16 个 slot但只有 12 个可用任务会卡住。我一般会这样设置先根据数据量和处理能力估算需要的并发度然后反推需要的 TaskManager 数量和 slot 数。比如数据量是每秒 10 万条单个并行子任务能处理每秒 1 万条那并发度至少是 10。如果每个 TaskManager 有 4 个 slot那至少需要 3 个 TaskManager12 个 slot留 2 个余量。注意Flink SQL 任务的并发度可以在 SQL 里用SET parallelism.default 10;设置也可以在提交命令里用-p 10指定。如果两处都设置了提交命令里的参数会覆盖 SQL 里的设置。5. 日志排查从一堆废话里找到关键信息5.1 日志的层级与位置Flink on YARN 的日志分散在多个地方如果不熟悉日志的层级结构很容易迷失在日志的海洋里。日志主要分为三层YARN 层面的日志包括 ResourceManager 日志、NodeManager 日志、ApplicationMaster 日志。这些日志可以通过 YARN 的 Web UI 或者yarn logs -applicationId app_id命令查看。Flink 层面的日志包括 JobManager 日志、TaskManager 日志。这些日志在 AM 容器和 TaskManager 容器里可以通过 YARN 的日志聚合功能查看。用户代码层面的日志包括 SQL 执行日志、UDF 日志等。这些日志混在 Flink 日志里需要根据 logger 名称过滤。我一般先用yarn logs -applicationId app_id -log_files jobmanager.log查看 JobManager 的日志因为大部分提交阶段的问题都会在这里体现。如果 JobManager 日志里没有明显错误再看 TaskManager 日志。5.2 关键错误信息的定位技巧日志里的错误信息往往很隐晦需要一些技巧才能快速定位。我总结了几种常见的错误模式和对应的排查方向Application application_xxx failed 2 times due to AM Container for appattempt_xxx exited with exitCode: 1这是最常见的错误但信息量很少。exitCode 1 一般表示 AM 容器启动失败可能的原因包括内存不足、类冲突、配置错误、Kerberos 认证失败等。需要进一步查看 AM 容器的日志用yarn logs -applicationId app_id -containerId container_id查看具体错误。java.lang.NoClassDefFoundError或ClassNotFoundException类冲突或依赖缺失。按照前面讲的依赖排查步骤确认冲突的类来自哪个 jar 包然后调整依赖范围或类加载顺序。java.lang.OutOfMemoryError: Java heap space内存不足。需要调整 JobManager 或 TaskManager 的堆内存配置。如果是 JobManager OOM增大jobmanager.memory.heap.size如果是 TaskManager OOM增大taskmanager.memory.task.heap.size。org.apache.flink.runtime.jobmanager.scheduler.NoResourceAvailableException资源不足。需要检查 YARN 队列资源和 slot 配置确保有足够的资源来运行任务。java.net.UnknownHostException或ConnectException网络问题。可能是 Kerberos 认证失败或者 YARN 集群的节点之间网络不通。需要检查 Kerberos 配置和网络连通性。5.3 日志级别调整与调试技巧默认的日志级别是 INFO很多调试信息看不到。如果遇到难以定位的问题可以临时把日志级别调到 DEBUG获取更详细的信息。在 Flink 的log4j.properties文件里可以这样配置log4j.logger.org.apache.flinkDEBUG log4j.logger.org.apache.hadoopDEBUG log4j.logger.org.apache.flink.yarnDEBUG调整日志级别后重新提交任务日志里会输出更多的调试信息比如类加载器的加载顺序、YARN 容器的分配过程、Kerberos 认证的详细步骤等。这些信息对于定位复杂问题非常有帮助。实操心得调试完成后记得把日志级别调回 INFO否则日志量太大会影响性能也会让后续的日志排查变得更困难。6. Kerberos 环境下的认证坑6.1 Kerberos 认证失败的典型表现如果你的 YARN 集群开启了 Kerberos 认证那提交 Flink 任务时会多一层认证的坑。典型的表现是任务提交后AM 容器启动失败日志里报GSSException: No valid credentials provided或者javax.security.sasl.SaslException: GSS initiate failed。这个问题的根源是 Flink 任务在提交时没有正确获取 Kerberos 票据或者票据过期了。Flink on YARN 支持两种 Kerberos 认证方式keytab 方式和票据缓存方式。keytab 方式是在提交命令里指定 keytab 文件和 principalFlink 会自动用 keytab 登录 Kerberos。票据缓存方式是先用kinit命令获取票据然后 Flink 从缓存里读取票据。我一般推荐用 keytab 方式因为更稳定不受票据过期的影响。配置方式是在提交命令里加上-Dsecurity.kerberos.login.keytab/path/to/user.keytab \ -Dsecurity.kerberos.login.principaluserREALM \ -Dsecurity.kerberos.login.use-ticket-cachefalse6.2 票据过期与续期策略如果用票据缓存方式票据过期是常见问题。Kerberos 票据默认有效期是 10 小时续期有效期是 7 天。如果任务运行时间超过票据有效期任务会突然失败报认证错误。解决方法是配置票据自动续期。可以在提交命令里加上-Dsecurity.kerberos.login.use-ticket-cachetrue \ -Dsecurity.kerberos.login.renewal.interval3600这样 Flink 会每隔 1 小时自动续期一次票据。但要注意续期需要票据还在有效期内如果票据已经过期续期会失败。注意如果 YARN 集群的 Kerberos 配置比较严格可能不允许自动续期需要联系集群管理员确认。6.3 跨域认证的注意事项有些公司的 YARN 集群和 Flink 任务不在同一个 Kerberos 域里这时候需要配置跨域认证。跨域认证的配置比较复杂需要在krb5.conf文件里配置多个域的信任关系并且在提交命令里指定正确的 principal 和 keytab。我遇到过一次跨域认证的问题任务提交后一直报Server not found in Kerberos database。查了半天才发现是krb5.conf文件里的域配置写错了把EXAMPLE.COM写成了EXAMPLE.COM.多了一个点。这个细节很容易忽略但会导致认证失败。实操心得跨域认证的问题最好在提交任务之前先用kinit命令测试一下确认能正常获取票据再提交 Flink 任务。这样可以快速定位是 Kerberos 配置问题还是 Flink 配置问题。7. 常见问题速查表与避坑清单7.1 提交失败问题速查表错误信息可能原因排查方向解决方案AM Container exited with exitCode: 1内存不足、类冲突、配置错误查看 AM 容器日志调整内存配置、排查依赖冲突NoClassDefFoundError依赖冲突或缺失确认冲突类来自哪个 jar排除冲突依赖或调整类加载顺序OutOfMemoryError: Java heap space堆内存不足查看 OOM 发生在哪个组件增大对应组件的堆内存NoResourceAvailableExceptionYARN 资源不足检查队列资源和 slot 配置增加资源或降低并发度GSSException: No valid credentialsKerberos 认证失败检查 keytab 和 principal配置正确的 keytab 或续期票据任务卡在 ACCEPTED队列资源不足查看队列状态等待资源或更换队列任务卡在 RUNNING 但无数据slot 不足检查并发度和 slot 数调整并发度或增加 TaskManager7.2 提交前的检查清单在提交 Flink SQL 任务之前我一般会过一遍这个清单能避免大部分低级错误确认 Flink 版本和 YARN 版本兼容。Flink 1.10 和 Hadoop 2.x 的兼容性最好Flink 1.13 以上需要 Hadoop 3.x。确认 jar 包路径正确。用ls -lh确认 jar 包存在用jar tf确认 jar 包内容完整。确认 SQL 文件路径正确。如果 SQL 文件在 HDFS 上确认路径可访问。确认资源参数合理。JobManager 和 TaskManager 的内存配置不要超过 YARN 容器的最大内存限制。确认依赖范围正确。Flink 相关依赖设为provided第三方依赖设为compile。确认 Kerberos 票据有效。如果用票据缓存方式先用klist确认票据未过期。确认队列资源充足。用yarn queue -status查看队列可用资源。确认日志级别合适。调试时用 DEBUG生产环境用 INFO。7.3 几个让我印象深刻的坑坑一jar 包路径里有空格。有一次提交任务jar 包路径是/home/user/my job/flink-job.jar路径里有空格提交命令没有加引号结果 Flink 把路径截断了报File not found。后来把路径改成没有空格的问题解决。坑二SQL 文件编码问题。SQL 文件里有中文注释文件编码是 GBK但 Flink 默认用 UTF-8 读取导致解析失败。后来把文件编码改成 UTF-8问题解决。坑三YARN 队列名大小写敏感。队列名是Root.Default提交命令里写成了root.default结果任务一直卡在 ACCEPTED。后来改成正确的大小写任务正常提交。坑四TaskManager 内存超限。taskmanager.memory.process.size设置为 4096m但 YARN 容器的最大内存限制是 4096m加上 container overhead 后实际需要 4505m超过了限制TaskManager 启动失败。后来把taskmanager.memory.process.size降到 3584m问题解决。这些坑看起来都很小但每一个都能让你折腾半天。我把它们记下来就是希望后来的人能少走弯路。8. 一些个人体会Flink SQL on YARN 的提交环节说到底就是三件事资源、依赖、认证。资源问题靠算账和检查队列状态依赖问题靠排查类加载冲突认证问题靠配置正确的 keytab 和 principal。这三件事搞定了提交成功率能到 95% 以上。我现在提交任务之前会先在一个测试环境里跑一遍确认没有资源问题和依赖冲突再提交到生产环境。测试环境的配置和生产环境尽量保持一致包括 Flink 版本、Hadoop 版本、Kerberos 配置等。这样能提前发现大部分问题避免在生产环境上踩坑。另外日志是排查问题的关键。我一般会在提交命令里加上-Dlog4j.configurationfile:/path/to/log4j.properties指定自定义的日志配置把关键组件的日志级别调到 DEBUG方便排查问题。任务稳定运行后再把日志级别调回 INFO。最后分享一个小技巧如果任务提交失败但日志里没有明显错误可以试试用yarn application -list -appStates ALL查看所有 Application 的状态找到对应的 Application ID然后用yarn logs -applicationId app_id查看完整日志。有时候错误信息不在 JobManager 日志里而在 NodeManager 日志里需要多翻几个日志文件才能找到。