1. 为什么Yarn不是“另一个MapReduce”,而是整个集群的呼吸中枢
很多人第一次看到Yarn,下意识会把它当成MapReduce的升级版——毕竟前七篇讲的全是HDFS和MapReduce,第八篇突然冒出个Yarn,自然觉得是“MapReduce v2.0”。我刚接触时也这么想,直到在某次压测中,集群CPU使用率长期卡在35%不上不下,而任务队列却越堆越长,日志里反复出现ApplicationMaster failed to allocate container。排查三天后才发现:问题根本不在计算逻辑,而在Yarn的资源分配策略被默认配置死锁了。
Yarn的本质,是把“资源管理”和“任务调度”这两件事彻底剥离开来。MapReduce 1.x里,JobTracker既要管几百台机器的内存/CPU/磁盘,又要解析用户写的map()和reduce()函数、协调Shuffle、重试失败任务——它既是调度员,又是程序员,还是运维工程师。这种耦合导致两个致命问题:一是JobTracker单点瓶颈,集群规模一过2000节点就频繁GC;二是新计算框架(比如Spark、Flink)想接入,必须重写一套和JobTracker通信的协议,等于每加一个引擎就要动一次底层。
Yarn用“双层调度”破局:最底层的ResourceManager(RM)只干一件事——盯着集群总资源池,像银行柜台一样严格登记每台NodeManager(NM)上报的可用内存、vCore数、磁盘IO能力;上层的ApplicationMaster(AM)则由每个应用自己启动,它只负责和RM要资源、把资源分给自己的Task,至于怎么算、怎么容错,RM一概不管。这就意味着,同一个Yarn集群可以同时跑MapReduce作业、Spark Streaming流任务、Tez DAG查询、甚至自定义的Python批处理脚本——它们共享同一套资源账本,但各自按自己的规则记账。
提示:Yarn的“资源”概念是抽象的,不直接对应物理CPU核心或GB内存。它用
vCore(虚拟CPU)和Memory-MB作为计量单位,由管理员在yarn-site.xml中通过yarn.scheduler.minimum-allocation-mb和yarn.scheduler.minimum-allocation-vcores统一规定最小申请粒度。这就像银行规定“取款最低100元”,你不能取50元,哪怕你只需要50元。
关键词里没写,但必须点明的是:Yarn的调度器(Scheduler)才是真正的权力核心。它不是简单的“先来先服务”,而是决定谁能在资源紧张时优先拿到容器、谁的任务会被延迟启动、甚至谁的任务会被主动Kill以保障高优先级作业。默认的Capacity Scheduler和可插拔的Fair Scheduler,背后是完全不同的资源公平性哲学——前者像企业部门预算制,后者像家庭电费分摊制。这个选择,直接决定了你的集群是偏向稳定交付(适合ETL流水线),还是偏向弹性响应(适合交互式查询)。
我见过太多团队踩的第一个坑:把Yarn当成“装好就能用”的黑盒。他们调大yarn.nodemanager.resource.memory-mb,以为内存够了就万事大吉,结果发现Spark任务频繁OOM。后来才明白,Yarn分配的只是JVM堆外内存(Container内存),而Spark Driver和Executor的JVM堆内存(spark.executor.memory)是独立配置的,两者叠加可能远超物理内存上限。这种“资源视差”,正是Yarn初学者最易忽略的底层逻辑断层。
2. ResourceManager与NodeManager:集群资源的“中央银行”与“地方支行”
理解Yarn,必须先厘清RM和NM这对核心角色的关系。它们不是主从,而是“契约关系”——RM不控制NM的任何具体操作,只接收其心跳报告并下发指令;NM也不信任RM,每次收到指令都会校验合法性。这种松耦合设计,让Yarn具备了MapReduce 1.x无法企及的容错能力:RM挂了,所有正在运行的AM和NM照常工作,只是新任务无法提交;NM挂了,RM只需标记该节点为DECOMMISSIONED,自动将任务迁移到其他节点。
2.1 ResourceManager:资源账本的唯一权威
RM的核心组件只有两个:Scheduler和ApplicationsManager(ASM)。Scheduler负责资源分配决策,ASM负责应用生命周期管理。注意,Scheduler本身不存储状态——它不记录“哪个AM申请了多少资源”,只根据当前集群实时资源快照和调度策略做瞬时决策。这意味着,如果你用的是默认的Capacity Scheduler,它的队列配额(如yarn.scheduler.capacity.root.default.capacity=50)是静态配置,但实际分配时会动态计算剩余资源。比如root队列总容量100%,default子队列配额50%,当default队列有任务在跑时,它占用的资源会实时从总池中扣除,其他队列能用的资源就是100%减去已用部分。
RM的高可用(HA)实现方式很务实:部署两个RM实例(Active/Standby),共享一个ZooKeeper集群作为状态存储。Active RM的所有状态变更(如新应用注册、容器分配)都同步写入ZK;Standby RM监听ZK事件,实时重建内存状态。一旦Active挂掉,Standby在几秒内自动升为Active,客户端通过ZK发现新地址。这里的关键细节是:ZK只存轻量级元数据(应用ID、状态、AM地址),不存容器分配详情。因为容器分配是瞬时行为,旧RM挂掉时,正在运行的容器由NM自主续命,新RM只需知道“哪些AM还活着”,就能继续为其分配新容器。
注意:RM HA启用后,
yarn.resourcemanager.ha.enabled=true必须配合yarn.resourcemanager.zk-address指向ZK地址。但很多团队漏掉一步:yarn.resourcemanager.hostname.rm1和yarn.resourcemanager.hostname.rm2必须分别指向两个RM节点的主机名,且该主机名必须能被所有NM节点DNS解析。我曾遇到因hosts文件未同步导致Standby RM无法连接NM的故障,排查耗时两天。
2.2 NodeManager:资源执行的最终守门人
NM的角色常被低估。它不只是被动接收RM指令的“打工仔”,而是拥有完整资源管控权的本地守护者。每个NM启动时,会读取yarn.nodemanager.resource.memory-mb(总内存)和yarn.nodemanager.resource.cpu-vcores(总vCore)作为自身资源上限。但它真正能分配的资源,还要受三个硬性约束:
物理内存保护:
yarn.nodemanager.vmem-pmem-ratio(默认2.1)规定虚拟内存与物理内存的比例。如果一台机器有64GB物理内存,NM配置memory-mb=50000,那么它允许的最大虚拟内存为50000×2.1≈105GB。一旦某个Container的进程RSS(常驻集)超过50GB,NM会直接Kill该进程——这是Linux OOM Killer的前置防线。磁盘空间水位线:
yarn.nodemanager.disk-health-checker.max-disk-utilization-per-disk-percentage(默认90%)监控本地磁盘。当Yarn本地目录(yarn.nodemanager.local-dirs)所在分区使用率超阈值,NM会自动将该节点标记为UNHEALTHY,RM随即停止向其分配新容器。这个机制防止了日志刷爆磁盘导致系统崩溃。CPU隔离强度:
yarn.nodemanager.resource.percentage-physical-cpu-limit(默认100%)配合Linux cgroups使用。若设为80%,NM会用cgroups限制所有Container进程的CPU使用率总和不超过物理CPU的80%,避免某个疯狂计算的任务拖垮整台机器。
实操中,NM的yarn.nodemanager.log-dirs配置极易出错。该参数指定Container日志的存储路径,必须是多个用逗号分隔的不同磁盘分区(如/data1/yarn/logs,/data2/yarn/logs)。如果全指向同一分区,当该分区写满时,所有Container日志丢失,AM无法获取任务失败原因。我们曾因此误判为网络问题,实际是日志目录配置不当。
2.3 ApplicationMaster:每个应用的“微型调度中心”
AM是Yarn架构中最精妙的设计。它不是一个固定程序,而是由每个应用自己实现的“调度代理”。MapReduce的AM叫MRAppMaster,Spark的叫SparkApplicationMaster,它们代码完全不同,但都遵循同一套Yarn协议:向RM注册、周期性发送心跳、申请容器、启动任务、汇报进度。
AM的生命周期完全独立于RM。当用户提交hadoop jar xxx.jar,Client进程先联系RM创建Application,RM返回Application ID;Client再启动AM(通常在某个NM上),AM启动后立即向RM注册;此后Client退出,AM独自与RM通信。这意味着,即使Client断网,只要AM活着,任务就继续运行。
AM申请容器的过程是典型的“试探-协商”:它先向RM请求N个容器(每个指定内存/vCore需求),RM返回AllocationResponse,其中包含已分配的容器列表和numContainers(本次未满足的请求数)。AM必须持续轮询,直到所有容器到位。这里有个关键技巧:AM应设置合理的askInterval(请求间隔)。太短(如100ms)会压垮RM;太长(如5s)会导致资源等待时间过长。我们测试发现,对于千节点集群,300~500ms是平衡点。
提示:AM的健壮性直接影响应用成功率。Yarn提供
yarn.app.mapreduce.am.max-attempts(默认2)控制AM重启次数。但要注意,AM重启后,之前已分配的容器不会自动回收,新AM需重新申请——这可能导致资源碎片化。生产环境建议设为1,配合AM自身的checkpoint机制(如Spark的RDD lineage)保证状态可恢复。
3. Capacity Scheduler深度解剖:企业级资源预算制的落地逻辑
在Yarn的两种主流调度器中,Capacity Scheduler(CS)是企业数据平台的绝对主力。它把资源池划分为多级队列(Queue),每个队列有硬性容量(capacity)、最大容量(maximum-capacity)和权重(weight),形成一套类似企业财务预算的资源管理体系。理解CS,就是理解如何让不同业务线在共享集群时互不干扰。
3.1 队列层级与容量继承:树状预算模型
CS的队列结构是严格的树形。根队列(root)是所有资源的总和,其子队列(如default、etl、ml)的capacity之和必须等于100%。每个子队列又可再分,比如etl队列下分batch和streaming子队列。关键规则是:子队列的capacity是相对于父队列的百分比。例如:
- root.capacity = 100%
- root.etl.capacity = 40% → etl队列占总资源40%
- root.etl.batch.capacity = 70% → batch占etl队列的70%,即总资源的28%
这种设计带来两个重要特性:一是资源隔离性强,etl队列最多只能用40%,即使其他队列空闲,它也不能突破;二是弹性有边界,batch队列在etl队列资源充足时,可借用streaming队列的空闲资源,但借用上限由maximum-capacity控制(如batch.maximum-capacity=85%,即最多用etl队列85%的资源)。
我们曾为某金融客户设计队列:root下设finance(50%)、marketing(30%)、dev(20%);finance下再分risk(60%)、report(40%)。这样,风控部门的实时反欺诈任务(risk队列)永远有至少30%的集群资源保障,而市场部的临时报表任务(marketing队列)即使突发高峰,也不会挤占风控资源。这种刚性保障,是CS区别于Fair Scheduler的核心价值。
3.2 资源分配算法:从“抢占”到“归还”的闭环
CS的分配不是静态切分,而是动态抢占。其核心算法基于“资源使用率”和“队列权重”:
- 初始分配:新任务提交到队列A,若A有空闲资源且未达capacity,则直接分配;
- 超额借用:若A资源已满但未达maximum-capacity,且其他队列B有空闲资源,CS会从B“借”资源给A;
- 抢占触发:当B队列有新任务需要资源,而A正占用B的借用资源时,CS启动抢占——它会Kill A队列中运行时间最长、优先级最低的Container,释放资源给B;
- 归还机制:被抢占的Container不会立即重试,而是进入
PREEMPTED状态,等待A队列再次有空闲资源时自动恢复。
这个过程的关键参数是yarn.scheduler.capacity.preemption-enabled(默认false)。开启抢占后,必须配置yarn.scheduler.capacity.maximum-am-resource-percent(AM容器最大占比,默认10%),防止AM占用过多资源导致任务无法调度。我们线上集群将此值设为5%,确保即使AM密集启动,也能为Task容器留足空间。
注意:抢占不是瞬间完成的。CS有
yarn.scheduler.capacity.monitoring.interval-ms(默认3000ms)控制抢占检查频率。如果设得太低,频繁Kill/Restart会增加调度开销;太高则响应滞后。我们通过压测发现,对于万级Container的集群,1500ms是最佳平衡点。
3.3 多维度资源限制:不只是内存和CPU
CS支持对每个队列设置多维度限制,这是企业精细化运营的基础:
| 限制类型 | 配置参数 | 作用说明 | 实操案例 |
|---|---|---|---|
| 并发任务数 | yarn.scheduler.capacity.<queue>.maximum-applications | 控制队列内同时运行的应用数量 | dev队列设为50,防止单个开发人员提交大量测试任务拖垮集群 |
| 单应用资源上限 | yarn.scheduler.capacity.<queue>.maximum-am-resource-percent | 限制单个AM能占用的资源比例 | ml队列设为15%,确保一个训练任务不会独占GPU资源 |
| 用户资源配额 | yarn.scheduler.capacity.<queue>.user-limit-factor | 单用户可突破队列容量的倍数 | finance队列设为2.0,允许风控主管在紧急时临时扩容 |
特别提醒:user-limit-factor常被误用。它不是“单用户独占队列”,而是“单用户最多可用队列容量的N倍”。例如finance队列capacity=50%,user-limit-factor=2.0,则单用户最多用100%资源——但这100%是集群总资源,不是队列资源。实际生效时,CS会计算“该用户当前已用资源 / 队列总容量”,若比值超2.0才限流。
4. 从零构建Yarn调度实验:三步验证资源分配逻辑
纸上谈兵不如亲手验证。下面用最简方式,在单机伪分布式模式下,直观看到Yarn如何调度资源。整个过程不依赖HDFS,专注调度器行为,5分钟即可复现。
4.1 环境准备:极简Yarn配置
在$HADOOP_HOME/etc/hadoop/下修改三个文件:
yarn-site.xml(核心调度配置):
<configuration> <!-- RM地址 --> <property> <name>yarn.resourcemanager.hostname</name> <value>localhost</value> </property> <!-- 启用Capacity Scheduler --> <property> <name>yarn.resourcemanager.scheduler.class</name> <value>org.apache.hadoop.yarn.server.resourcemanager.scheduler.capacity.CapacityScheduler</value> </property> <!-- NM资源上限:4GB内存,4vCore --> <property> <name>yarn.nodemanager.resource.memory-mb</name> <value>4096</value> </property> <property> <name>yarn.nodemanager.resource.cpu-vcores</name> <value>4</value> </property> <!-- 容器最小申请:1GB内存,1vCore --> <property> <name>yarn.scheduler.minimum-allocation-mb</name> <value>1024</value> </property> <property> <name>yarn.scheduler.minimum-allocation-vcores</name> <value>1</value> </property> </configuration>capacity-scheduler.xml(队列定义):
<configuration> <!-- 根队列总容量100% --> <property> <name>yarn.scheduler.capacity.root.queues</name> <value>default,high</value> </property> <!-- default队列占60%,high队列占40% --> <property> <name>yarn.scheduler.capacity.root.default.capacity</name> <value>60</value> </property> <property> <name>yarn.scheduler.capacity.root.high.capacity</name> <value>40</value> </property> <!-- high队列可借用至60% --> <property> <name>yarn.scheduler.capacity.root.high.maximum-capacity</name> <value>60</value> </property> </configuration>mapred-site.xml(MapReduce作业配置):
<configuration> <property> <name>mapreduce.framework.name</name> <value>yarn</value> </property> <!-- 每个Mapper申请1GB内存 --> <property> <name>mapreduce.map.memory.mb</name> <value>1024</value> </property> <!-- 每个Reducer申请1GB内存 --> <property> <name>mapreduce.reduce.memory.mb</name> <value>1024</value> </property> </configuration>提示:所有配置值必须是
yarn.scheduler.minimum-allocation-mb的整数倍。若设mapreduce.map.memory.mb=1500,而最小粒度是1024,Yarn会向上取整为2048,导致资源浪费。
4.2 启动与验证:用命令行看透调度
启动Yarn服务:
# 格式化并启动RM $HADOOP_HOME/bin/yarn --daemon start resourcemanager # 启动NM $HADOOP_HOME/bin/yarn --daemon start nodemanager此时访问http://localhost:8088,Yarn Web UI首页显示集群总资源:Memory Total 4 GB, Virtual Cores 4。点击“Scheduler”标签页,可见队列树:root → default(60%), high(40%)。
提交两个测试作业:
# 提交到default队列(默认) $HADOOP_HOME/bin/hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-*.jar \ pi -D mapreduce.job.queuename=default 10 1000000 # 提交到high队列(高优先级) $HADOOP_HOME/bin/hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-*.jar \ pi -D mapreduce.job.queuename=high 10 1000000观察Web UI的“Running Applications”列表,你会看到:
- 第一个作业(default)启动了10个Mapper,每个占用1GB内存,共占用10GB?不对!因为NM总内存仅4GB,CS会按队列容量分配:default队列60% × 4GB = 2.4GB,所以最多启动2个Mapper(2×1GB),其余8个Mapper处于
ACCEPTED状态等待资源。 - 第二个作业(high)启动时,high队列40% × 4GB = 1.6GB,可启动1个Mapper。但CS检测到default队列有8个待分配Mapper,而high队列当前无任务运行,于是将high队列的1.6GB资源“借给”default队列,使default队列暂时可用资源达2.4+1.6=4GB,从而启动全部10个Mapper。
这就是Capacity Scheduler的弹性本质:资源不是割裂的,而是流动的池子。
4.3 故障注入实验:亲手触发抢占
现在模拟高优任务抢占场景:
- 先提交一个long-running作业到default队列,让它占满2.4GB资源(如启动24个Mapper,每个100MB);
- 再提交一个high队列的作业,指定
-D mapreduce.map.memory.mb=2048(需2GB内存); - 观察Web UI:high队列作业会立即启动,而default队列中运行时间最长的几个Mapper被Kill,状态变为
PREEMPTED。
通过yarn application -status <app_id>命令,可查看详细状态变迁。你会看到default作业的AM日志中出现Container preempted by scheduler字样——这不是错误,而是CS在履行资源契约。
经验心得:生产环境开启抢占前,务必测试AM的容错能力。有些老版本Spark AM在容器被Kill后无法正确恢复,导致整个作业失败。我们的解决方案是:在
spark-defaults.conf中添加spark.yarn.am.attemptFailuresValidityInterval=3600s,让AM在1小时内多次失败才判定为永久失败。
5. 生产集群Yarn调优实战:从参数到监控的全链路
伪分布式实验能验证逻辑,但生产环境的复杂度呈指数级增长。我们服务过的某电商客户,其Yarn集群峰值承载12万Container/天,平均响应延迟要求<200ms。以下是经过千次压测沉淀的调优清单,每一条都对应真实故障场景。
5.1 RM性能瓶颈:从GC到网络连接
RM是集群单点,其性能直接决定调度吞吐。我们发现90%的RM延迟问题源于JVM GC和网络连接池:
- JVM参数:必须禁用
-XX:+UseParallelGC(并行GC在高并发下反而更慢),改用-XX:+UseG1GC -XX:MaxGCPauseMillis=200。G1能将Full GC频率从每天数次降至每月1次。 - 连接池配置:
yarn.resourcemanager.client.thread-count(默认50)控制RM处理客户端请求的线程数。对于万级节点集群,需调至200;同时yarn.resourcemanager.scheduler.client.thread-count(默认50)也要同步提升,否则Scheduler成为瓶颈。 - 状态存储优化:RM的ZK连接数默认10,但在HA模式下,每个RM实例需独立连接ZK。我们通过
zookeeper.session.timeout.ms=30000延长会话超时,并将yarn.resourcemanager.zk-max-connection-attempts=100,避免ZK抖动导致RM频繁切换。
5.2 NM稳定性加固:规避Linux内核陷阱
NM在Linux上运行,必须绕过内核的几个经典陷阱:
OOM Killer误杀:Linux内核的OOM Killer会Kill RSS最高的进程。而NM的Container进程常是内存大户。解决方案是在
/etc/sysctl.conf中添加:vm.overcommit_memory = 1 # 允许内存过度分配 vm.swappiness = 1 # 减少swap倾向 kernel.panic_on_oom = 0 # 禁用panic,改用Kill并在NM启动脚本中添加
-XX:+UseCGroupMemoryLimitForHeap,让JVM感知cgroups内存限制。文件描述符泄漏:NM每启动一个Container,会打开若干文件(日志、本地文件等)。
ulimit -n默认1024,很快耗尽。我们统一设为65536,并在yarn-env.sh中添加:export YARN_NODEMANAGER_OPTS="-XX:MaxFDLimit=65536"
5.3 调度器精准调控:用指标驱动决策
光靠配置不够,必须建立Yarn健康度指标体系。我们在Prometheus中采集以下核心指标:
| 指标名称 | 采集方式 | 告警阈值 | 问题定位 |
|---|---|---|---|
yarn_rm_scheduler_pending_applications | JMX | >50 | RM处理积压,检查Client连接数或ZK延迟 |
yarn_nm_container_launch_delay_ms | NM日志 | >5000ms | NM本地资源不足或磁盘IO瓶颈 |
yarn_scheduler_queue_used_capacity_percent | RM REST API | >95%持续5分钟 | 队列资源饱和,需扩容或优化作业 |
yarn_scheduler_preempted_containers_total | RM JMX | >100/小时 | 抢占过于频繁,调整队列max-capacity |
特别有效的是yarn_scheduler_queue_used_capacity_percent。我们曾发现ml队列长期98%占用,原以为是训练任务太多,但深入分析发现:90%的资源被几个spark.sql.adaptive.enabled=true的Adaptive Query占用,它们动态申请大量小容器却未及时释放。解决方案是:在spark-defaults.conf中添加spark.sql.adaptive.coalescePartitions.enabled=false,强制使用预估分区数。
最后分享一个血泪教训:某次集群升级Yarn 3.3.0后,所有作业启动延迟飙升。排查发现新版本默认启用了
yarn.resourcemanager.scheduler.monitor.enable(调度监控),其采样频率过高。关闭该参数后,延迟回归正常。这提醒我们:任何新版本上线,必须先阅读Release Notes中的默认配置变更,而不是盲目相信“向后兼容”。
Yarn的深度,不在于它有多复杂,而在于它把资源这个抽象概念,变成了可计量、可预算、可抢占、可审计的实体。当你能清晰说出“此刻集群的4096MB内存,有2048MB在default队列的3个Container里运行MapTask,1024MB在high队列的1个Container里运行Spark Driver,还有1024MB空闲等待分配”,你就真正掌握了Yarn的脉搏。这不仅是技术能力,更是数据平台治理的底层语言。