理解 YARN 应用,第一步不是写 ApplicationMaster,而是先理解客户端到底提交了什么。很多人把 yarn jar 或 hadoop jar 看成一个普通命令:命令发出去,集群就开始跑任务。这个理解太粗。YARN Client 负责构造并提交应用描述:它读取配置,连接 ResourceManager,申请新的 Application ID,准备 ApplicationMaster 需要的本地资源,构造启动上下文,指定队列和资源,再把这份应用描述提交给 ResourceManager。
也就是说,YARN Client 提交的不是“一个进程”,而是“一个应用的启动说明”,核心对象就是 ApplicationSubmissionContext。ResourceManager 收到以后,负责调度并分配第一个 AM Container,再由对应 NodeManager 按启动上下文启动 ApplicationMaster。后面的任务拆分、普通 Container 申请、失败重试、状态汇报和 Container 管理,才是 ApplicationMaster 的职责。
为了验证这条链路,我提交了一个很小的 Distributed-Shell 应用:
hadoop jar /opt/hadoop/share/hadoop/yarn/hadoop-yarn-applications-distributedshell-3.4.1.jar \
-jar /opt/hadoop/share/hadoop/yarn/hadoop-yarn-applications-distributedshell-3.4.1.jar \
-appname YarnClientModelDemo \
-queue default \
-master_memory 512 \
-container_memory 256 \
-num_containers 2 \
-shell_command 'echo YARN_CLIENT_MODEL_20250219; hostname; date; sleep 8'
任务完成后的关键状态如下:
Application ID: application_1739926092533_0001
Application Name: YarnClientModelDemo
Application Type: YARN
Queue: root.default
State: FINISHED
Final-State: SUCCEEDED
Log Aggregation Status: SUCCEEDED

1. Client 提交模型
1.1 YARN Client 的职责边界
YARN Client 不负责真正执行应用逻辑。它的职责更靠前,也更基础:把一个应用包装成 ResourceManager 能理解的提交请求。
这个提交请求里至少要包含几类信息:
| 信息 | 作用 |
|---|---|
| Application 名称 | 方便在 RM 页面、日志和审计中识别应用 |
| 队列 | 决定应用进入哪个 Capacity Scheduler 队列 |
| 优先级 | 同一队列内的调度参考,默认通常是 0 |
| AM 资源 | 启动 ApplicationMaster 需要多少内存和 vCore |
| LocalResource | AM 启动前需要本地化的 jar、文件或归档 |
| 环境变量 | AM 进程启动时需要的运行环境 |
| 启动命令 | NodeManager 最终执行的 AM 命令 |
从这个角度看,Client 是 YARN 应用的第一层边界。它必须懂集群配置,也必须懂自己要提交的应用需要什么资源。但它不应该承担应用运行时的调度逻辑。Client 可以做队列、资源、文件路径这类轻量预校验;但 RM 才是集群状态和调度结果的 single source of truth。Client 提交完成后,应用是否需要 2 个 Container、20 个 Container,任务是否要重试,哪些 Container 成功,哪些失败,都应该由 ApplicationMaster 管。
这也是 YARN 能支持不同计算框架的原因。MapReduce、Distributed-Shell、Tez、Spark on YARN 都可以有不同的 ApplicationMaster,但它们提交到 ResourceManager 的第一步是相似的:构造一个 Application,并让 RM 启动 AM。
1.2 YarnConfiguration:客户端先找到集群
Client 的第一步通常是加载 YarnConfiguration。这个对象会读取 Hadoop 配置路径下的默认配置和站点配置,最终知道 ResourceManager 地址、HDFS 地址、队列配置、日志相关配置等信息。
这里有一个容易忽略的点:客户端机器上的配置也必须正确。服务端配置正确,不代表任意一台机器都能提交任务。运维机、调度机、开发机如果没有一致的 core-site.xml、yarn-site.xml、mapred-site.xml,客户端可能连不上 RM,也可能把资源上传到错误的 HDFS 地址。一般可以这样理解:core-site.xml 决定文件系统和安全基础,yarn-site.xml 决定 RM/NM 与调度相关入口,mapred-site.xml 影响 MapReduce on YARN 的提交和历史服务配置。
如果集群启用了 Kerberos 或安全模式,配置加载还会影响认证路径。客户端不仅要找到 RM 和 HDFS,还要拿到正确的 principal、keytab、token service 和 delegation token 相关配置。安全配置不一致时,应用可能在提交阶段失败,也可能表现为 AM 启动后无法访问 HDFS。
单机环境里客户端和服务端在同一台机器上,这个问题不明显;多节点环境里它很常见。比如:
| 现象 | 可能原因 |
|---|---|
| 客户端连接 RM 失败 | yarn.resourcemanager.address 配错 |
| 应用提交后找不到 AM jar | 本地路径或 HDFS staging 上传失败 |
| 应用进入错误队列 | 默认队列或平台封装参数不一致 |
| 用户有权限但提交失败 | 客户端用户、代理用户、队列 ACL 不匹配 |
所以应用提交问题不能只看 RM 页面。Client 端日志同样重要,因为很多错误发生在应用真正进入 YARN 之前。
1.3 YarnClient:从查询集群到提交应用
YarnClient 是客户端和 ResourceManager 交互的核心对象。它通常会完成几件事:
- 初始化并启动客户端。
- 获取集群指标和 NodeManager 列表。
- 查询队列状态和用户 ACL。
- 申请新的 Application ID。
- 构造 ApplicationSubmissionContext。
- 提交应用。
- 轮询应用状态,直到成功、失败或超时。
这次 Distributed-Shell 客户端输出里能看到这些步骤:
Got Cluster metric info from ASM, numNodeManagers=1
Got node report from ASM for, nodeId=lavm-bzoq5mwl1h:40331
Queue info, queueName=default, queueCurrentCapacity=0.0
User ACL Info for Queue, queueName=root.default, userAcl=SUBMIT_APPLICATIONS
Submitted application application_1739926092533_0001
这些日志说明 Client 并不是盲目提交。它会先从 ResourceManager 拿到集群视图,再根据用户输入构造提交内容。对排查来说,这些日志很有价值。如果提交前就发现队列不存在、ACL 不允许、资源超过集群上限,问题会直接暴露在客户端。
日志级别也会影响可见信息。INFO 通常足够看到队列、ACL、Application ID 和提交结果;DEBUG 才会暴露更多配置解析、资源上传、RPC 请求和重试细节。线上平台默认不一定要开 DEBUG,但失败样本最好能按需打开,否则很多提交前问题只能靠猜。
这里的 Queue info 和 User ACL Info 都来自 ResourceManager 的返回,不是 Client 在本地自己推断出来的结果。Client 本地可以做静态校验,但队列状态、ACL 和资源能力的最终判断仍然以 RM 为准。
1.4 ApplicationSubmissionContext 是提交模型的核心
YARN Client 最终要构造的是 ApplicationSubmissionContext。它可以理解成应用提交请求的主体。这个对象把“应用是什么”和“AM 怎么启动”放在一起。
典型内容包括:
| 字段 | 含义 |
|---|---|
| applicationId | ResourceManager 分配的应用 ID |
| applicationName | 应用名称 |
| queue | 提交队列 |
| priority | 应用优先级 |
| amContainerSpec | AM Container 的启动描述 |
| resource | AM 资源请求 |
| applicationType | 应用类型 |
| maxAppAttempts | AM 失败后的重试次数 |
这里最关键的是 amContainerSpec。它不是普通字符串,而是一个 ContainerLaunchContext,里面继续包含本地资源、环境变量、启动命令、服务数据、ACL 等信息。ResourceManager 并不会自己解释这些业务含义,它只会把 AM Container 分配给某个 NodeManager,NodeManager 再根据启动上下文准备环境并启动进程。
换句话说,ApplicationSubmissionContext 是 Client 和 RM 的接口,ContainerLaunchContext 是 RM/NM 启动 AM 的接口。理解这两个对象,就能把“提交应用”和“启动 AM”分开。
可以把提交对象关系简化成下面这样:
ApplicationSubmissionContext
-> applicationId / applicationName / queue / priority / applicationType
-> resource / maxAppAttempts
-> amContainerSpec: ContainerLaunchContext
-> LocalResource
-> environment
-> commands
-> tokens / ACL / serviceData
其中 applicationType 用来区分 MAPREDUCE、SPARK、TEZ、YARN 或自定义 AM。它不会替代调度决策,但对 RM 页面、平台统计、审计和排查很有用。
1.5 LocalResource:提交时必须解决资源可见性
一个应用要在集群节点上启动,不能依赖客户端机器上的本地文件。AM jar、配置文件、脚本、归档包都必须变成 NodeManager 能拿到的资源。这就是 LocalResource 的意义。
LocalResource 通常会记录:
| 属性 | 说明 |
|---|---|
| resource | 资源在 HDFS 或其它文件系统上的路径 |
| size | 文件大小 |
| timestamp | 文件时间戳,用于校验和缓存 |
| type | FILE、ARCHIVE 等 |
| visibility | APPLICATION、PUBLIC、PRIVATE |
Client 的常见做法是先把 AM jar 上传到 HDFS staging 目录,再把对应路径封装成 LocalResource。NodeManager 启动 AM Container 前会把资源本地化到节点目录。这个过程看起来琐碎,但它解决的是分布式环境里最基础的问题:同一个应用在任意节点启动时,都能拿到一致的代码和配置。
NodeManager 本地化资源时会使用 localization cache。相同用户、相同可见性、相同路径和版本的资源可以在节点级复用,避免每个 Container 都重复下载。timestamp 不只是校验字段,也参与资源版本控制和 cache hit 判断:路径相同但时间戳变化时,NodeManager 应该把它当成不同版本资源处理。
这也是很多应用启动失败的来源。比如 jar 上传失败、文件权限不对、时间戳不一致、本地磁盘满、资源路径被清理,都会导致 AM 还没开始执行业务逻辑就失败。
1.6 AM Container 资源请求要和调度器匹配
Client 提交应用时,首先申请的是 AM Container 的资源,不是普通业务 Container。比如这次命令里设置了:
master_memory=512
container_memory=256
num_containers=2
master_memory=512 是 ApplicationMaster 的资源,container_memory=256 是普通 shell Container 的资源。二者不是同一个东西。
这在排查 ACCEPTED 状态时很重要。应用长时间处于 ACCEPTED,往往说明 ResourceManager 已经接受了提交,但 AM Container 还没分配出来。可能原因包括:
| 原因 | 说明 |
|---|---|
| 队列没有可用 AM 资源 | Capacity Scheduler 对 AM 有比例限制 |
| 队列容量不足 | 队列 capacity、maximum-capacity 或当前使用量限制了分配 |
| AM 内存超过最大分配 | master_memory 大于调度器或节点上限 |
| 节点标签不匹配 | 应用要求的节点标签没有可用节点 |
| 队列被停用 | 队列状态不是 RUNNING |
| 用户无权限 | ACL 或代理用户配置不允许提交 |
普通 Container 资源不足通常发生在 AM 已经启动之后;AM 资源不足则会卡在更早的阶段。Client 端日志和 RM 页面组合起来,能很快区分这两类问题。
AM 失败还会触发 Application Attempt 重试。也就是说,AM Container 不只是启动一次就结束;如果 AM 进程失败,RM 可以根据 maxAppAttempts 再分配新的 AM Container。排查 AM 问题时要同时看 Application ID、attempt ID 和对应 Container 日志。
2. 提交结果和应用边界
2.1 提交成功不代表应用逻辑成功
Client 输出 Submitted application 只说明 ResourceManager 接受了应用。它不代表 AM 启动成功,也不代表普通 Container 成功,更不代表业务逻辑成功。
这次应用的状态变化大致如下:
ACCEPTED -> RUNNING -> FINISHED / SUCCEEDED
ACCEPTED 表示应用已进入 RM,等待或正在启动 AM;RUNNING 表示 AM 已经启动并注册;FINISHED / SUCCEEDED 表示 AM 向 RM 汇报成功完成。这里也要注意,FINISHED 只是生命周期状态,不等于成功,最终是否成功要看 finalStatus。
如果 Client 只提交不等待状态,就只能证明“提交请求被接受”。如果要做安装验证或文章截图,最好继续确认:
yarn application -status application_1739926092533_0001
yarn logs -applicationId application_1739926092533_0001
这次日志聚合里能看到两个普通 Container 都执行了 shell 命令:
Container: container_1739926092533_0001_01_000002
YARN_CLIENT_MODEL_20250219
Container: container_1739926092533_0001_01_000003
YARN_CLIENT_MODEL_20250219
这说明 Client 提交、AM 启动、AM 申请普通 Container、NodeManager 执行命令、日志聚合这条链路都跑通了。
2.2 非 MapReduce 应用和 MapReduce 应用的差别
前面几篇文章一直用 MapReduce Pi 示例,因为它稳定、容易验证。但 YARN 的价值不在于只能跑 MapReduce。Distributed-Shell 的应用类型是 YARN,不是 MAPREDUCE。它没有 Map Task、Reduce Task、Shuffle,也没有 MapReduce JobHistory 语义,但它同样有 Application、ApplicationMaster、Container、队列、日志聚合。
这能帮助区分两层概念:
| 层次 | MapReduce 示例 | Distributed-Shell 示例 |
|---|---|---|
| YARN Application | 有 | 有 |
| ApplicationMaster | MRAppMaster | Distributed-Shell AM |
| 普通 Container | Map/Reduce Task | Shell 命令 |
| 队列调度 | 有 | 有 |
| 日志聚合 | 有 | 有 |
| MapReduce Counter | 有 | 无 |
所以,YARN Client 的提交模型是通用的。区别在于 AM 启动以后,应用框架如何使用后续 Container。MapReduce 用 MRAppMaster 管理 Map 和 Reduce;Spark on YARN 会围绕 Driver/Executor 组织资源;Tez on YARN 会用 Tez AM 管理 DAG 和 task;Distributed-Shell 则用普通 shell 命令验证 Container 启动。Generic AM 只遵守 YARN 提交和资源申请协议,具体执行语义由框架自己定义。
2.3 排查提交问题的一条路径
YARN 应用提交失败时,可以按下面顺序排查:
- 客户端配置是否能找到 ResourceManager。
- 应用 jar、脚本、配置是否能上传到 HDFS。
- 队列是否存在,用户是否有提交权限。
- AM 资源是否超过队列或节点上限。
- RM 是否给应用分配了 Application ID。
- 应用是否从 ACCEPTED 进入 RUNNING。
- AM 日志是否有启动异常。
- 普通 Container 是否执行并完成。
这个顺序的好处是先分清阶段。Client 报错、RM 拒绝、AM 启动失败、普通 Container 失败,是四类不同问题。把它们都叫“YARN 提交失败”,排查会很慢。
3. 平台封装和运行治理
3.1 平台封装时最容易丢掉的信息
真实生产环境里,很少有人直接在命令行里手写完整 YARN Client 参数。更多时候是通过调度平台、数据开发平台、任务发布平台提交。平台封装可以降低使用门槛,但也很容易把关键 YARN 信息藏起来。
我更希望平台至少保留这些字段:
| 字段 | 为什么必须保留 |
|---|---|
| Application ID | 后续查 RM、日志、历史都要靠它 |
| Queue | 资源归属和调度策略的核心 |
| Application Type | 区分 MapReduce、YARN、Spark 等应用 |
| AM 资源 | 判断应用为什么卡在 ACCEPTED |
| 普通 Container 资源 | 判断任务为什么分配慢或被杀 |
| LocalResource 列表 | 判断代码、脚本、配置是否被正确分发 |
| submit user | 排查 ACL、代理用户、HDFS 权限 |
如果平台只显示“任务失败”,不显示 Application ID 和队列,使用者就只能猜。一个好平台不一定要暴露所有 YARN 参数,但必须把排查入口暴露出来。最少要能从平台任务跳转到 RM 应用详情、日志聚合和提交参数。
还有一个常见问题是平台会统一拼接命令。比如用户在页面里选择队列、内存、脚本路径,平台在后台生成 hadoop jar 或 YARN Client 调用。这里最怕的是页面显示值和最终提交值不一致,也就是参数 drift。用户以为提交到 etl,实际进入 default;用户以为 AM 内存是 1024 MB,实际平台写死了 512 MB。排查这类问题时,最终事实应该以 RM 应用详情、客户端日志和平台生成的提交记录为准。
平台最好保留 submission audit log。它至少应该记录页面入参、最终提交参数、提交用户、代理用户、Application ID、队列、资源、LocalResource、提交时间和调用结果。这个审计日志不是为了增加流程,而是为了在 UI 值、平台生成值和 RM 事实不一致时,有地方还原真实提交链路。
3.2 Client 端日志应该保留多久
很多团队只重视 Container 日志,却忽略 Client 端日志。实际上,Client 日志记录了提交前后的关键事实:
Queue info, queueName=default
User ACL Info for Queue, queueName=root.default
AM Resource capability=<memory:512, vCores:1>
Submitted application application_1739926092533_0001
这些信息在应用后续排查中很有价值。比如应用没有进入预期队列,Client 日志能说明提交时到底传了什么队列;应用一直 ACCEPTED,Client 日志能说明 AM 申请了多少资源;用户说自己没有权限,Client 日志能看到提交时读取到的队列 ACL。
所以平台化提交时,Client 日志不应该只打印在临时控制台里。至少要把提交阶段的核心字段写入任务记录:
- 提交命令或等价参数。
- Application ID。
- 队列和用户。
- AM 资源。
- 普通 Container 资源。
- LocalResource 上传路径。
- 最终应用状态。
这些数据不是为了审计好看,而是为了把一次提交变成可复盘对象。YARN 是分布式系统,失败发生后再去还原提交参数,成本会很高。
3.3 Application ID 是提交链路的主键
在 YARN 里,Application ID 是所有排查动作的主键。Client 端拿到它以后,后续几乎所有命令都围绕它展开:
yarn application -status application_1739926092533_0001
yarn logs -applicationId application_1739926092533_0001
ResourceManager 页面、日志聚合目录、Container ID、Application Attempt ID 也都围绕它派生。比如:
appattempt_1739926092533_0001_000001
container_1739926092533_0001_01_000001
container_1739926092533_0001_01_000002
container_1739926092533_0001_01_000003
这个编号结构本身就能帮助判断关系。0006 是应用序号,01 是 attempt 序号,最后的 000001、000002、000003 是 Container 序号。看到 000001 通常要先想到 AM Container,后续编号更可能是普通 Container。
可以把关系画成下面这样:
application_..._0001
-> appattempt_..._0001_000001
-> container_..._0001_01_000001 # AM Container
-> container_..._0001_01_000002 # 普通 Container
-> container_..._0001_01_000003 # 普通 Container
平台如果保存了 Application ID,就能把 RM、NM、日志和业务任务串起来;如果没保存,只能靠时间、用户、应用名去模糊搜索。应用名可能重复,时间可能接近,用户也可能相同,Application ID 才是最稳定的关联字段。
3.4 提交模型里的安全边界
Client 提交应用时,还涉及安全边界。即使测试环境为了方便使用 root 用户,生产环境里也不能忽略这些问题。
第一,谁能提交到某个队列。Capacity Scheduler 可以配置队列 ACL,允许某些用户或用户组提交应用。Client 查询到 ACL 不代表一定能成功,但 ACL 是第一道门。
第二,AM 能否访问 HDFS。Client 上传 LocalResource 到 HDFS,AM 启动后也可能继续访问 HDFS。如果用户身份、代理用户或 Kerberos 凭据有问题,应用可能提交成功但 AM 启动后失败。生产平台里还要特别注意 proxy user / doAs 机制:平台服务账号代用户提交时,必须确保代理用户配置、用户组映射和队列 ACL 都一致,否则页面上看似是用户 A 提交,RM 或 HDFS 实际看到的身份可能不同。
第三,日志谁能看。Container 日志可能包含命令参数、环境变量、路径甚至敏感信息。日志聚合解决的是集中查看,不等于所有人都应该可见。生产环境需要结合用户、队列、日志 ACL 和平台权限控制。
第四,LocalResource 的可见性。PUBLIC、PRIVATE、APPLICATION 的选择影响资源共享和访问边界。公共依赖适合 PUBLIC,用户私有脚本或配置更适合 PRIVATE 或 APPLICATION。随手把所有资源都设成公共,会留下权限隐患。
第五,delegation token 的生命周期。安全集群里,Client 提交时通常会为 HDFS 等服务获取 delegation token,并交给 AM/Container 使用。token 过期、续期失败或 service 地址不一致,都可能导致应用提交成功但运行中访问 HDFS 失败。长时间运行应用尤其要关注 token renewal。
这些安全问题不属于“后期再补”。Client 提交模型本身就包含用户、资源、队列和访问控制。应用能不能安全运行,从提交时就已经开始决定了。
3.5 提交参数要有默认值,也要允许覆盖
一个实用的 YARN 提交封装,通常要同时满足两件事:大多数用户不用理解所有参数也能提交任务;高级用户在需要时能覆盖关键参数。只提供默认值,平台会变成黑盒;所有参数都让用户填写,平台又会变得难用。
我更倾向于把参数分成三层:
| 层级 | 示例 | 处理方式 |
|---|---|---|
| 必填参数 | 应用名、主程序、脚本路径 | 用户必须明确提供 |
| 受控参数 | 队列、AM 内存、Container 内存 | 平台给默认值,允许按权限覆盖 |
| 系统参数 | staging 路径、日志配置、token | 平台自动生成,不让普通用户修改 |
这样设计的好处是边界清晰。普通用户只关注业务输入;平台维护资源和安全策略;管理员可以在必要时调整默认值。比如临时查询默认进 root.adhoc,生产 ETL 默认进 root.etl,测试任务默认较低资源。用户可以申请更高资源,但必须经过权限或审批。
如果没有这层分级,很容易出现两种极端。一种是用户随意填写队列和内存,导致资源治理失效;另一种是平台写死所有参数,导致任务稍微复杂一点就跑不起来。YARN Client 的提交模型本身很灵活,平台封装要做的是约束灵活性,而不是把灵活性完全抹掉。
3.6 提交前校验比提交后失败更便宜
很多错误可以在 Client 提交前发现,不应该等到应用失败后才暴露。比如:
- 队列名为空或不存在。
- AM 内存超过集群最大值。
- Container 内存小于 JVM 参数。
- jar 或脚本路径不存在。
- HDFS staging 目录不可写。
- 用户不具备队列提交权限。
- 应用名不符合平台命名规则。
这些校验放在 Client 端能节省大量排查时间。因为一旦应用提交到 RM,后续就会生成 Application ID、AM attempt、Container、日志目录,问题对象变多。提交前拦截能把错误控制在一个清晰的范围里。
当然,Client 端校验不能代替 RM 和调度器。队列状态和资源可用性可能在提交后变化,所以 RM 仍然要做最终判断。但 Client 至少应该做静态校验和明显错误校验。比如内存超过上限、文件不存在、队列参数为空,这些都没有必要提交后再失败。
3.7 从 REST API 复核提交结果
Web 页面适合截图和人工查看,REST API 更适合复核和自动化。提交完成后,可以通过 RM REST 接口读取应用事实:
id: application_1739926092533_0001
name: YarnClientModelDemo
queue: root.default
state: FINISHED
finalStatus: SUCCEEDED
applicationType: YARN
logAggregationStatus: SUCCEEDED
这些字段能覆盖一次提交的核心结果:应用是否进入 YARN,进入哪个队列,框架类型是什么,最终是否成功,日志是否聚合。文章里使用截图是为了便于阅读,但真正做批量验证时,我更愿意用 REST API 或命令输出。
平台也可以在应用完成后自动拉取这些字段,写回自己的任务记录。这样用户不需要手动打开 RM,也能在平台页面看到 YARN 事实。更重要的是,当平台状态和 YARN 状态不一致时,可以用 YARN 状态作为底层依据。
3.8 提交动作要考虑幂等和重试
很多平台封装 YARN 提交时,只关注“调用一次提交接口”。这在命令行验证里没问题,在平台化场景里就不够。一次提交可能发生在多个边界上:业务平台写任务记录,Client 上传资源,RM 创建 Application,AM 启动,用户页面轮询状态。任何一步都可能失败,也可能只是调用方没有收到结果。
最典型的问题是网络超时。Client 已经把 Application 提交给 RM,但平台请求超时,调用方以为失败,又提交了一次。结果 RM 上出现两个相同应用名的任务,业务侧却只有一条记录。反过来,如果平台先写了任务记录,再上传 LocalResource 失败,用户看到的是一个没有 Application ID 的半成品任务。
比较稳妥的做法是把提交过程拆成可记录的阶段:
| 阶段 | 需要记录的事实 |
|---|---|
| 准备资源 | 本地文件、HDFS 路径、时间戳、大小 |
| 创建 Application | application_id、用户、队列 |
| 提交 AM | AM 资源、启动命令、LocalResource 列表 |
| 跟踪状态 | state、finalStatus、diagnostics |
| 完成归档 | 日志地址、历史地址、最终结果 |
这里最关键的是尽早拿到并保存 application_id。它应该作为提交事务的幂等 key。只要有这个 ID,后续即使平台进程重启,也能从 RM 或 JobHistoryServer 找回状态。没有这个 ID,只能靠应用名和时间猜测,排查成本会明显上升。
重试也要分层。上传资源失败可以重试同一个 HDFS 目标,也可以清理后重新上传;提交 RM 超时不能盲目重试,应该先检查 RM 上是否已经出现该 Application。RM 如果已经存在这个 applicationId,后续逻辑应该进入状态跟踪,而不是重新创建一个业务任务。状态查询失败可以延后重试,但不能把查询失败直接等同于应用失败。提交模型看起来是一个动作,工程上应该按事务边界拆开。
4. Client 设计边界和可观测性
4.1 不要把 Client 写成另一个调度器
YARN Client 可以查询集群资源、队列状态和应用状态,但它不应该替代 ResourceManager 做调度决策。Client 适合做参数校验、资源准备和提交说明构造,真正的资源分配仍然要交给 RM 和调度器。
如果 Client 里写入太多调度判断,系统会变得难维护。比如 Client 自己判断某个队列空闲就提交过去,过几秒钟其它任务进入队列,判断马上失效。又比如 Client 根据节点剩余内存选择目标节点,这会破坏 YARN 的统一调度模型,也容易和 NodeManager 的真实状态不同步。
因此,RM 才是调度事实的 single source of truth。Client 可以读取 RM 返回的信息做提示,但不能把提示当成最终分配结果。最终进入哪个状态、拿到哪个 Container、失败原因是什么,都应该回到 RM、AM 和 NM 的实际状态里确认。
Client 更适合回答三类问题:
| 问题 | Client 应该怎么做 |
|---|---|
| 参数是否明显非法 | 提交前拒绝 |
| 资源是否准备好 | 上传并校验 |
| 应用是否已经进入 YARN | 保存 Application ID 并跟踪 |
而这些问题不应该由 Client 决定:
| 问题 | 应交给谁 |
|---|---|
| 哪个节点运行 Container | ResourceManager 调度器 |
| 某个队列本轮分多少资源 | Capacity Scheduler |
| Container 启动后是否成功 | NodeManager 和 AM |
| 任务失败后是否重试 | ApplicationMaster |
这个边界清楚以后,平台封装会更稳定。Client 不需要知道所有运行细节,只需要把应用意图表达清楚:应用叫什么、谁提交、去哪个队列、AM 怎么启动、依赖资源在哪里、需要多少资源。剩下的运行过程由 YARN 的其它组件接管。
4.2 提交模型的可观测性
提交链路如果没有观测点,失败时很容易只剩下一句“任务提交失败”。一个实用的封装至少要暴露这些信息:
| 信息 | 价值 |
|---|---|
| Application ID | 串联 RM、日志、业务记录 |
| 队列 | 判断是否进入预期资源池 |
| AM 资源 | 判断是否超出调度限制 |
| LocalResource 列表 | 判断依赖是否上传完整 |
| AM 启动命令 | 判断入口是否正确 |
| diagnostics | 判断 RM 或 AM 给出的失败原因 |
这些信息不一定都展示给普通用户,但应该进入平台日志。尤其是 AM 启动命令和 LocalResource,在安全脱敏后很有价值。很多应用不是计算逻辑错,而是入口类、脚本路径、环境变量或资源权限错。没有这些提交事实,排查只能登录机器逐层翻日志。
其中 diagnostics 是关键失败入口。RM 拒绝、AM 失败、Container 启动异常、队列权限问题,经常会把第一段可读原因写到 diagnostics 里。平台如果只展示 FAILED,不保存 diagnostics,就会丢掉最有价值的失败线索。
如果需要更完整的事件链,可以再保存 event timeline:提交请求创建、Application ID 获取、资源上传完成、submitApplication 调用、状态从 ACCEPTED 到 RUNNING、最终 FINISHED 或失败。这个事件时间线可以直接暴露提交卡在哪个边界。
YARN 自身提供了 RM 页面、REST API、yarn application -status 和 yarn logs。平台要做的不是隐藏这些入口,而是把它们组织起来。用户可以从平台任务页跳到 RM Application,也可以在平台里直接看到关键字段。这样既保留底层可验证性,又降低日常排查门槛。
5. 小结
YARN Client 是应用进入集群的 submission layer。它读取配置,连接 RM,查询集群和队列,准备本地资源,构造 ApplicationSubmissionContext,提交 AM 启动说明,并跟踪应用状态。它不负责执行业务逻辑,也不是 execution layer,但它决定了应用能否正确进入 YARN。
理解提交模型以后,YARN 应用就不再是一条黑盒命令。Application ID、队列、AM 资源、LocalResource、ContainerLaunchContext、日志聚合这些对象能串成一条清晰链路。YARN 的核心可以拆成 submission、scheduling、execution 三段:Client 负责 submission,ResourceManager 和 Scheduler 负责 scheduling,ApplicationMaster、NodeManager 和 Container 负责 execution。后面再看 ApplicationMaster,就能把注意力放在“应用如何继续申请和启动普通 Container”上,而不是把提交和运行混在一起。