Distributed-Shell 是理解 YARN 应用模型的一个好工具。它本身不复杂:用户提交一个 shell 命令,ApplicationMaster 通过 ResourceManager 申请若干 Container,真正执行命令的是 NodeManager 在 Container 内启动的进程。正因为它简单,反而能把 YARN 应用的骨架暴露出来。
MapReduce 会把很多细节封装起来。用户看到的是 Map、Reduce、InputSplit、Shuffle、Counter、JobHistory,很容易把注意力放在 MapReduce 框架上。Distributed-Shell 没有这些业务语义,它只保留 Client、ApplicationMaster、Container、队列、资源、日志这些 YARN 基础对象。用它观察 YARN,干扰更少。
这次我提交了一个 3 个 Container 的 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 YarnDistributedShellDemo \
-queue adhoc \
-master_memory 512 \
-container_memory 256 \
-num_containers 3 \
-shell_command 'echo YARN_DISTRIBUTED_SHELL_20250305; hostname; date; sleep 8'
运行结果如下:
Application ID: application_1741162032445_0001
Application Name: YarnDistributedShellDemo
Application Type: YARN
Queue: root.adhoc
State: FINISHED
Final-State: SUCCEEDED
Log Aggregation Status: SUCCEEDED

1. Distributed-Shell 的对象模型
1.1 Distributed-Shell 是最小化的 YARN 应用
一个完整 YARN 应用至少包含三个部分:
| 部分 | Distributed-Shell 中的体现 |
|---|---|
| Client | 解析参数、准备资源、提交 Application |
| ApplicationMaster | 注册 RM、申请 Container、构造启动上下文 |
| Container | NodeManager 启动的运行时资源单元,内部进程执行用户命令 |
Distributed-Shell 的好处是这三层非常清楚。Client 不负责执行 shell;ApplicationMaster 不负责全局调度;Container 不负责申请资源。每一层只做自己的事情。
这和 MapReduce on YARN 的底层运行模型一致,但应用语义不同。MapReduce 的普通 Container 执行 Map/Reduce Task,并额外包含 InputSplit、Shuffle、Counter、JobHistory 等作业语义;Distributed-Shell 的普通 Container 只执行 shell 命令。YARN 看到的不只是 Application 和 Container,还包括 application attempt、resource request 和 container lifecycle;框架语义由 AM 自己管理。
1.2 参数背后对应的是 YARN 对象
Distributed-Shell 的命令行参数看起来很多,但可以按对象归类:
| 参数 | 对应对象 | 含义 |
|---|---|---|
| -appname | Application | 应用名称 |
| -queue | Scheduler | 提交队列 |
| -priority | Scheduler | 应用优先级 |
| -master_memory | AM Container | AM 内存 |
| -master_vcores | AM Container | AM vCore |
| -container_memory | 普通 Container | shell Container 内存 |
| -container_vcores | 普通 Container | shell Container vCore |
| -num_containers | AM 逻辑 | 要启动多少普通 Container |
| -shell_command | ContainerLaunchContext | Container 执行命令 |
| -shell_script | LocalResource | 上传并执行脚本 |
| -localize_files | LocalResource | 本地化额外文件 |
这样看,Distributed-Shell 不是命令手册,而是一组 YARN 对象的入口。每个参数最终都会落到 ApplicationSubmissionContext、ResourceRequest、ContainerLaunchContext 或 LocalResource 里。
这也解释了为什么一个看似简单的 shell 命令,进入 YARN 后会变成多个组件协作。Client 要提交应用,RM 要调度 AM,AM 要申请普通 Container,NM 要完成本地化并启动脚本,日志聚合要收集输出。
1.3 shell_command 和 shell_script 的差别
shell_command 适合短命令,例如:
echo YARN_DISTRIBUTED_SHELL_20250305; hostname; date; sleep 8
它最终会进入 ContainerLaunchContext,再由 NodeManager 写入 Container 启动脚本并执行。短命令的优点是直观,缺点是复杂逻辑容易被 shell 引号、特殊字符、重定向和环境变量影响。如果命令来自用户输入或平台拼接,还要特别注意 shell injection 风险。
shell_script 更适合复杂逻辑。Client 会把脚本作为资源上传,AM 再把它作为 LocalResource 提供给普通 Container。这样做的好处是脚本内容更可控,也更容易版本化。生产环境里,如果命令超过一两行,我更倾向于使用脚本,而不是把所有逻辑塞进 shell_command。
这次测试里我使用短命令,是为了让日志输出更直接。日志聚合中能看到每个普通 Container 都执行了标记输出:
Container: container_1741162032445_0001_01_000002
YARN_DISTRIBUTED_SHELL_20250305
Container: container_1741162032445_0001_01_000003
YARN_DISTRIBUTED_SHELL_20250305
Container: container_1741162032445_0001_01_000004
YARN_DISTRIBUTED_SHELL_20250305
这说明 AM 成功申请并启动了 3 个普通 Container,每个 Container 内都由 NodeManager 启动进程执行了同一条 shell 命令。
1.4 num_containers 决定并行度
num_containers=3 的意思不是启动 3 个 ApplicationMaster,而是一个 AM 管理 3 个普通 Container。AM Container 由 YARN 为应用控制面自动创建,不计入用户指定的 num_containers。ResourceManager 页面里的 Attempt 表只显示一个 ApplicationMaster attempt,这是正常的。
这次应用的 Container 关系可以写成:
application_1741162032445_0001
appattempt_1741162032445_0001_000001
container_1741162032445_0001_01_000001 ApplicationMaster
container_1741162032445_0001_01_000002 shell command
container_1741162032445_0001_01_000003 shell command
container_1741162032445_0001_01_000004 shell command
这个结构很重要。YARN 应用的并行度通常由普通 Container 数量表达,而不是 ApplicationMaster 数量。AM 是控制面,普通 Container 是执行面。这里的并行度也不等于任务切分,Distributed-Shell 只是启动多个执行单元,并不会自动把输入数据拆成多份。
如果 num_containers 很大,AM 会持续申请资源,调度器会按队列容量、节点可用资源、用户限制和 AM 资源比例逐步分配。并不是用户写了 100,集群就一定同时启动 100 个 Container。资源不足时,AM 只能等待、处理部分分配,或者按自身策略做 retry/backoff。
1.5 master_memory 和 container_memory 不要混淆
Distributed-Shell 很适合说明 AM 资源和普通 Container 资源的区别。
master_memory=512
container_memory=256
master_memory 控制 AM Container。AM 需要运行 Java 进程、维护状态、和 RM/NM 通信,所以它有自己的资源请求。container_memory 控制普通 shell Container。它们的生命周期和职责不同,不能混在一起调优。这里的内存是 NodeManager 管理的资源边界,不等于进程或 JVM 实际只会使用这么多内存。
常见误判是:普通任务 OOM,就只调 master_memory;应用停在 ACCEPTED,却只调 container_memory。正确做法是先看卡在哪一层:
| 现象 | 优先检查 |
|---|---|
| 应用停在 ACCEPTED | AM 资源、队列 AM 限制、节点可用资源 |
| AM 启动后普通 Container 不分配 | 普通 Container 资源、队列容量、用户限制 |
| 普通 Container 被杀 | container_memory、命令内存、NodeManager 日志 |
| AM 自己失败 | master_memory、AM 日志、LocalResource |
Distributed-Shell 虽然简单,但这些边界和复杂框架完全一致。
1.6 队列决定应用进入哪套资源规则
这次应用提交到 adhoc 队列,RM 页面显示完整队列是 root.adhoc。这说明短队列名最终会映射到调度器里的完整队列路径。
队列不是分类标签或显示字段,而是资源策略集合。应用进入 root.adhoc 后,要受这个队列的容量、最大容量、用户限制、ACL、应用数量限制和 AM 资源限制影响。Distributed-Shell 只是一个轻量应用,但它同样受调度器约束。Capacity Scheduler 更强调容量和层级队列,Fair Scheduler 更强调公平共享,不同调度器会影响等待、分配和抢占行为。
这对平台封装很重要。很多调度平台会把队列参数藏在页面选项里,用户只知道选择“临时查询”或“ETL”。但最终提交到 YARN 时,仍然要变成一个明确的队列路径。排查队列问题时,不要只看平台页面,要看 RM 应用详情里的实际 Queue。
1.7 日志聚合证明 Container 真正执行过
ResourceManager 页面能证明应用成功,但不能证明每个 Container 执行了什么。要确认 shell 命令真正跑过,需要看日志聚合:
yarn logs -applicationId application_1741162032445_0001
这次日志显示 3 个普通 Container 都输出了 YARN_DISTRIBUTED_SHELL_20250305。这比只看 SUCCEEDED 更可靠,因为它证明执行发生在普通 Container 里,而不是只证明 AM 汇报了成功。
日志里还可以看到命令被包装成:
exec /bin/bash -c "echo YARN_DISTRIBUTED_SHELL_20250305; hostname; date; sleep 8 ..."
这说明 NodeManager 最终执行的是生成后的启动脚本。用户提交的是 shell 命令,AM 构造 ContainerLaunchContext,NM 完成本地化、生成 launch_container.sh,最后 exec 用户命令。这个链路一旦理解,很多日志路径、stdout/stderr/syslog、Container 工作目录的问题就好解释了。
日志也要分层看:stdout 是用户命令的标准输出,stderr 更容易包含脚本错误和异常栈,syslog 则通常包含启动脚本、环境准备和 NodeManager 侧的执行信息。
2. 验证价值和使用边界
2.1 Distributed-Shell 适合做验证工具
Distributed-Shell 不适合当生产计算框架,但很适合做 YARN 验证工具。它能快速验证:
| 验证点 | 为什么有用 |
|---|---|
| Client 能提交应用 | 配置和 RM 地址可用 |
| AM 能启动 | 队列和 AM 资源可用 |
| 普通 Container 能启动 | NM、资源、本地化可用 |
| shell 输出能聚合 | 日志聚合可用 |
| 多 Container 能完成 | AM 申请和监控链路可用 |
相比 MapReduce Pi,Distributed-Shell 更接近“框架无关”的 YARN 验证。它不依赖输入数据,不需要 Shuffle,也不需要 MapReduce JobHistory。只要 YARN 基础链路可用,它就能跑起来。
因此,在搭建或调整 YARN 集群后,我会把 Distributed-Shell 当作补充验证:MapReduce Pi 验证 MapReduce on YARN,Distributed-Shell 验证普通 YARN 应用模型。两者结合,能覆盖更多边界。
2.2 它也暴露了自定义应用的成本
Distributed-Shell 看起来简单,但它也说明自定义 YARN 应用并不轻。哪怕只是跑 shell 命令,也要处理:
- Client 参数解析。
- Application 提交。
- AM jar 上传和本地化。
- AM 注册和注销。
- 资源申请。
- ContainerLaunchContext 构造。
- NodeManager 启动协议。
- Container 状态监控。
- 日志收集和失败处理。
这就是为什么很多团队不会直接写原生 YARN 应用,而是使用 MapReduce、Spark、Flink、Tez 等更高层框架。原生 YARN API 给了最大控制权,也带来了更多工程负担。
如果确实要写自定义 YARN 应用,Distributed-Shell 是很好的起点。它展示了最小可运行结构,也暴露了哪些地方必须自己处理。把它改造成生产应用时,至少要补齐参数校验、资源清理、失败重试、日志可观测性、安全凭据和幂等提交。
2.3 Distributed-Shell 和调度平台的关系
很多数据平台都有“Shell 任务”类型。表面看,它们和 Distributed-Shell 很像:用户填写一段命令,平台提交到集群执行,最后收集日志和状态。但两者关注点不同。
Distributed-Shell 是 YARN 示例应用,重点是演示 YARN Client、AM 和 Container 如何配合。调度平台的 Shell 任务则通常还要处理依赖关系、周期调度、补数、失败告警、参数替换、权限控制和结果归档。
如果平台底层使用 YARN,那么它最终仍然要回答 Distributed-Shell 暴露出来的这些问题:
| 问题 | 平台也必须处理 |
|---|---|
| 命令在哪个 Container 里执行 | 需要记录 Container ID |
| 提交到哪个队列 | 需要和租户/项目绑定 |
| AM 和普通 Container 资源是多少 | 需要防止用户随意抢资源 |
| stdout/stderr 在哪里 | 需要能从平台跳转日志 |
| 失败退出码是什么 | 需要区分平台失败和命令失败 |
| 命令依赖哪些文件 | 需要上传或分发资源 |
所以 Distributed-Shell 不只是一个示例,它也可以作为平台设计的参照。一个平台如果连 Application ID、队列、Container 日志都不展示,用户排查 Shell 任务时会非常困难。
2.4 shell 任务的退出码和应用状态
YARN 应用的最终状态和 shell 命令的退出码之间有映射关系,但不是同一个层次。shell 命令在普通 Container 里执行,退出码先被 NodeManager 记录,再由 AM 接收 Container 完成状态,最后 AM 决定向 RM 汇报成功还是失败。
这条链路可以写成:
shell exit code
-> Container exit status
-> AM completed container handling
-> FinalApplicationStatus
-> RM application final state
如果 shell 命令返回非 0,普通 Container 会失败。AM 可以选择重试,也可以直接让应用失败。Distributed-Shell 的策略比较直接,复杂框架则可能有更丰富的重试策略。
这对使用者很重要。看到 RM 上应用 FAILED 时,不要只看 RM 诊断信息,还要进入 Container stderr/stdout。RM 只能告诉你应用最终失败,真正的命令错误通常在 Container 日志里。反过来,如果应用 SUCCEEDED,也不代表业务输出一定正确,只能说明 AM 向 RM 汇报的 FinalApplicationStatus 是成功。
生产平台最好同时展示:
- YARN Application 状态。
- AM 最终汇报状态。
- 失败 Container ID。
- shell 退出码。
- exit code、diagnostics 和 stdout/stderr 里的唯一日志标记。
这几层合起来,才能说清楚一次 shell 任务到底发生了什么。
2.5 多 Container 并行不等于分布式计算框架
Distributed-Shell 可以启动多个 Container,但这不等于它就是完整的分布式计算框架。它只是把同一条命令分发到多个 Container 执行。Container 之间默认没有数据交换、任务切分、结果合并、容错协议或全局调度语义。
这点很容易被误解。num_containers=3 只是让三个 Container 都执行命令。它不会自动把输入数据切成三份,也不会自动合并输出。如果命令本身没有处理并行数据的逻辑,多个 Container 只是重复执行。
要把它变成真正的分布式应用,还需要补充:
| 能力 | Distributed-Shell 默认是否提供 |
|---|---|
| 输入切分 | 否 |
| 任务分配 | 很弱,只按数量启动 |
| 结果汇总 | 否 |
| 任务重试语义 | 有基础能力,但不面向复杂作业 |
| 数据本地性 | 不像 MapReduce 那样内建 node-local/rack-local 语义 |
| 作业级 Counter | 否 |
| Shuffle / merge | 否 |
因此,Distributed-Shell 更适合验证和轻量任务,不适合替代 MapReduce、Spark 或 Flink。它能展示 YARN 的资源运行模型,但不会替你实现计算框架语义。
2.6 用 Distributed-Shell 验证集群时的推荐命令
我更推荐准备两类命令。
第一类是最小验证命令,用来证明应用能提交、AM 能启动、日志能聚合:
hadoop jar $DS_JAR \
-jar $DS_JAR \
-appname YarnSmokeTest \
-queue default \
-master_memory 512 \
-container_memory 256 \
-num_containers 1 \
-shell_command 'echo YARN_OK; hostname; date'
第二类是多 Container 验证命令,用来证明 AM 能申请多个 Container:
hadoop jar $DS_JAR \
-jar $DS_JAR \
-appname YarnMultiContainerTest \
-queue adhoc \
-master_memory 512 \
-container_memory 256 \
-num_containers 3 \
-shell_command 'echo YARN_MULTI; hostname; sleep 10'
执行后至少检查三件事:
yarn application -status <application_id>
yarn logs -applicationId <application_id>
以及 RM 页面里的队列、状态和 AM attempt。yarn application -status、yarn logs 和 RM UI 三处的 Application ID、队列、状态、日志标记能对上,才说明 YARN 的基础提交、调度、执行和日志链路是通的。
2.7 Distributed-Shell 的局限也要写清楚
用 Distributed-Shell 做文章或验证时,要避免把它说得过大。它不是生产级编排系统,也不是通用任务调度器。它的局限包括:
- 对复杂参数和 shell 引号敏感。
- 不负责业务级输入输出管理。
- 不提供强作业语义。
- 不适合长时间服务治理。
- 失败处理能力有限。
- 安全和权限仍依赖 YARN/HDFS 配置。
- 缺少计算模型抽象,不能替代 Spark、MapReduce 或 Flink。
这些局限不影响它作为学习工具的价值。相反,正因为它功能少,才容易看清 YARN 本身的边界。MapReduce 把任务模型、Shuffle、Counter 都封装起来;Distributed-Shell 把这些都拿掉,只留下 YARN 应用骨架。
我更愿意把它定位为三个用途:
| 用途 | 说明 |
|---|---|
| 学习 YARN 应用结构 | 看清 Client、AM、Container |
| 验证集群基础链路 | 检查提交、调度、日志聚合 |
| 自定义应用起点 | 参考最小 AM 实现 |
超过这三个用途,就要谨慎评估是否应该使用更高层框架。
2.8 它适合验证哪些 YARN 配置
Distributed-Shell 可以覆盖不少基础配置,但也有边界。适合用它验证的配置包括:
| 配置方向 | 验证方式 |
|---|---|
| RM 地址 | Client 能否提交应用 |
| 队列配置 | 应用是否进入指定队列 |
| AM 资源 | 应用是否能从 ACCEPTED 进入 RUNNING |
| NM 可用性 | 普通 Container 是否能启动 |
| 日志聚合 | yarn logs 是否能看到 stdout |
| HDFS staging | AM jar 和资源是否能上传 |
| 用户权限 | 队列 ACL 和日志访问是否正常 |
不适合用它验证的内容也要分清。比如 MapReduce Shuffle、InputSplit、Reduce 合并、Spark executor 行为、Flink checkpoint,这些都不是 Distributed-Shell 的职责。它能证明 YARN 基础链路可用,不能证明所有计算框架都可用。
因此,集群验收时可以组合使用:
- Distributed-Shell 验证普通 YARN 应用链路。
- MapReduce Pi 验证 MapReduce on YARN。
- 业务框架自带示例验证具体框架。
这样比只跑一个 Pi 更完整。Pi 成功说明 MapReduce 链路好,Distributed-Shell 成功说明 YARN 原生应用链路好,两者互补。
2.9 它也能帮助解释 Container 数量
初学 YARN 时,经常有人问:为什么我设置了 num_containers=3,日志里却看到 4 个 Container?答案是 AM 自己也是 Container。
这次应用就是:
application_1741162032445_0001
appattempt_1741162032445_0001_000001
1 个 AM Container
3 个普通 shell Container
ResourceManager 应用详情页的 Attempt 表展示的是 AM attempt,不会直接把所有普通 Container 列成作业任务列表。普通 Container 需要从日志聚合、NM 记录或 RM/NM 相关接口里看。
这个现象对理解 Spark、Flink 也有帮助。很多框架也会有 driver、AM、executor、taskmanager 等不同角色,它们和 Container 的对应关系并不总是一对一。先用 Distributed-Shell 看清“AM 也是 Container”,后面理解复杂框架会容易很多。
3. 命令组织和环境差异
3.1 命令越复杂,越应该脚本化
Distributed-Shell 支持直接传 shell_command,但这只适合简单命令。命令复杂以后,建议使用脚本文件。原因很实际:
| 直接命令的问题 | 脚本化的好处 |
|---|---|
| 引号容易错 | 脚本可以本地测试 |
| 多行难维护 | 脚本结构清晰 |
| 参数不可控 | 可以统一解析参数 |
| 日志难读 | 可以加明确输出 |
| 失败定位困难 | 可以分步骤返回错误 |
如果脚本要随应用分发,就把它作为 LocalResource 上传。Container 启动后在本地化目录执行脚本。这样既不依赖每台节点预装脚本,也能保证版本和任务一致。安全模式下还要确认 HDFS 路径权限、delegation token 和 localization 权限都可用。
对于平台来说,这也意味着“Shell 任务”最好不要只保存一段命令文本。更好的做法是保存脚本版本、提交参数、资源配置和 Application ID。否则任务失败后,很难还原当时到底执行了什么。
3.2 用它校验队列和用户边界
Distributed-Shell 很适合做队列和权限验证。原因是它的业务逻辑足够简单,成功或失败通常不会被计算框架内部逻辑干扰。把同一条命令分别提交到 default、etl、adhoc 队列,可以很快确认队列是否存在、是否允许提交、资源是否能分配、日志是否能查看。
如果提交到错误队列,RM Application 页面会直接显示队列归属。这个字段比命令行参数更可信,因为它是 RM 接收后的事实。平台封装队列参数时,也应该以 RM 返回结果为准,而不是只相信用户传入的值。
权限验证同样如此。一个用户能否提交到某个队列,能否查看日志,能否 kill 应用,都应该用真实应用验证。配置文件里写了 ACL,不代表实际链路一定正确;代理用户、HDFS 权限、日志目录权限都会影响结果。
建议把验证拆成几组:
| 验证项 | 观察点 |
|---|---|
| 队列提交 | Application 是否进入目标队列 |
| 资源分配 | AM 是否从 ACCEPTED 进入 RUNNING |
| 普通 Container | stdout 是否包含预期标记 |
| 日志读取 | yarn logs 是否返回完整输出 |
| 终止权限 | 非管理员用户是否能 kill 应用 |
这些验证不需要复杂数据,也不需要准备输入目录。一个简单命令就能覆盖 YARN 的核心控制面。
3.3 它能暴露 shell 环境差异
很多在登录 shell 中能执行的命令,放进 YARN Container 后会失败。Distributed-Shell 正好可以暴露这种差异。Container 不是用户 SSH 登录后的终端,而是 NodeManager 启动的隔离运行环境,通常也不是 login shell。它的工作目录、环境变量、PATH、文件可见性都由 NodeManager 和启动上下文决定。
因此,用它做验证时,不要只执行 echo ok。更有价值的命令应该打印环境信息:
hostname
pwd
id
umask
printenv | sort | head -30
ls -la
这些输出能回答几个实际问题:Container 在哪台节点运行,运行用户是谁,当前目录是什么,LocalResource 是否已经进入目录,PATH 是否包含必要命令,HADOOP 相关变量是否存在。
如果一个平台要支持“提交 shell 脚本到 YARN”,这些信息尤其重要。用户经常以为脚本会继承自己登录机器上的环境,实际并不会。平台应该在文档和错误信息里明确说明:脚本运行在 YARN Container 中,依赖文件要通过资源分发或共享存储提供,环境变量要显式传入,不能默认继承提交机的 PATH 和 shell 配置。
4. 平台验收和示例价值
4.1 它不是只给开发者看的示例
Distributed-Shell 常被当成示例应用,但它对运维也有价值。集群出现问题时,复杂框架失败不一定能立即定位原因。Spark 失败可能是 executor 参数问题,MapReduce 失败可能是输入目录或 Shuffle 问题,Flink 失败可能是 checkpoint 或作业图问题。Distributed-Shell 失败则更接近 YARN 基础链路问题。
例如:
| 现象 | 优先怀疑 |
|---|---|
| Client 无法提交 | RM 地址、认证、网络 |
| 一直 ACCEPTED | 队列资源、调度器、AM 资源 |
| AM 启动失败 | LocalResource、启动命令、HDFS 权限 |
| 普通 Container 不启动 | NM 状态、资源分配、队列限制 |
| 日志不可读 | 日志聚合、HDFS 权限、History 服务 |
这套判断可以作为集群巡检脚本的一部分。每天提交一个短命令,检查它能否成功、耗时是否异常、日志是否可读。相比只看进程存活,这种端到端验证更接近用户真实体验。
4.2 和 MapReduce 示例的区别
MapReduce Pi 也能验证 YARN,但它验证的是 MapReduce on YARN。它会启动 MRAppMaster,创建 map/reduce task,涉及 JobHistoryServer、Counter、Shuffle 等组件。Distributed-Shell 则不关心这些,它只验证 YARN 应用骨架。
两者的差异可以这样理解:
| 对象 | MapReduce Pi | Distributed-Shell |
|---|---|---|
| 应用类型 | MAPREDUCE | YARN |
| AM 行为 | 管理作业和 task | 管理 shell Container |
| 业务语义 | map/reduce/counter | shell 命令退出码 |
| Shuffle | 需要 | 不需要 |
| 日志 | 任务日志和作业历史 | AM 与 shell Container 日志 |
因此,不能用一个替代另一个。MapReduce Pi 成功,说明 MapReduce 链路可用;Distributed-Shell 成功,说明 YARN 原生应用链路可用。如果两个都失败,优先查 YARN 基础设施;如果只有 MapReduce 失败,再查 MapReduce 配置和 Shuffle。
4.3 如何把它用于平台验收
如果要把 YARN 接入一个内部平台,Distributed-Shell 可以作为最小验收任务。平台只需要暴露应用名、队列、Container 数量、内存和 shell 命令几个参数,就能完成一次端到端提交。
验收结果至少要保存:
| 字段 | 说明 |
|---|---|
| application_id | 底层应用编号 |
| app_name | 平台任务名 |
| queue | 实际进入的队列 |
| state/finalStatus | YARN 最终状态 |
| container_count | 期望和实际 Container 数 |
| log_marker | stdout 中的唯一标记 |
| elapsed_time | 从提交到完成的耗时 |
这里的 log_marker 很实用。每次提交都生成一个包含 UUID 和 timestamp 的唯一字符串,让 shell 命令打印出来,再通过 yarn logs 查找。这样可以证明日志来自本次任务,而不是历史输出或误打开的页面。
平台验收还要关注失败路径。比如传入不存在的脚本、非法队列、过大的内存、无权限用户,平台应该给出明确错误,而不是只显示 YARN 的原始异常。Distributed-Shell 足够轻量,适合用来覆盖这些负向测试。
4.4 不要忽略返回码语义
Distributed-Shell 最终是否成功,很大程度取决于普通 Container 中命令的退出码。echo ok 会成功,exit 1 会失败,多条命令串联时如果没有正确处理 set -e 或管道返回码,就可能出现业务步骤失败但 Container 仍然成功的情况。
这点对平台封装很重要。平台如果允许用户提交 shell,应该要求脚本明确返回码,并把关键错误写入 stderr。否则 YARN 只能看到进程退出状态,看不到业务语义。一个脚本内部吞掉错误、最后返回 0,RM 页面会显示应用成功,但业务结果可能已经错误。
更稳妥的脚本习惯包括:
set -euo pipefail
echo "start job"
run_step_one
run_step_two
echo "finish job"
同时要避免把所有命令写成一行。命令越长,越难判断哪个步骤失败。YARN 能负责启动和收集日志,但不能替脚本设计错误处理。Distributed-Shell 把这个边界暴露得很直接:Container 成功不等于业务一定正确,业务正确性仍然要由脚本或上层平台负责表达。
5. 小结
Distributed-Shell 的价值不在于执行 shell,而在于把 YARN 应用模型简化到最小。它可以看作 canonical YARN execution model reference:RM 是 scheduler,AM 是 coordinator,NM 是 executor,Container 是 runtime unit,Task 或 shell 进程才是 user logic。日志聚合负责留下证据。
通过 application_1741162032445_0001 可以看到,一条 shell 命令进入 YARN 后,会变成一个完整的分布式应用生命周期。理解这个过程后,再看 MapReduce、Spark 或其它框架,就不会只盯着框架自己的概念,而能看见底层共同的 YARN 运行模型。