聊到大数据,尤其是Hadoop生态里的存储和计算组件,很多人第一反应是HDFS和MapReduce,但整套集群真正能稳定跑起来,靠的其实是YARN这一层。生产环境里“任务一直ACCEPTED不动”“Container被反复杀掉”“两个大作业一上来,全集群都卡死”这些破事,十有八九都出在YARN资源管理配置不合理上。而且YARN不止服务MapReduce,Spark、Flink、跑批脚本,全都要通过它来分资源。这篇文章主要给两类人看:一类是刚把集群搭起来,不知道CPU和内存该怎么分的新手;另一类是已经跑了一段时间线上任务,天天被慢任务和OOM折磨的维护者。我会从架构原理讲到调度器选型,再把最容易踩的内存坑和排查步骤整理出来,尽量让你看完就能对着自己的集群动手查。
1. YARN之前:Hadoop 1.x的瓶颈与YARN的诞生
1.1 JobTracker的至暗时刻
在Hadoop 1.x时代,整个MapReduce体系里最核心也最脆弱的进程就是JobTracker。它当时要做的事情太多了:接收客户端作业、把作业拆成Map和Reduce任务、决定任务落在哪台机器上、跟踪所有任务运行状态、任务失败后重新调度,甚至还要维护整个集群的心跳和元数据信息。听起来很全能,但你想想,一个进程既要当调度中心,又要当监控中心,还要当状态存储,几方面挤在一起,扩展性必然出问题。社区公认的经验是,当集群规模到几千台这个量级,JobTracker的压力会非常明显,GC变长、RPC超时、任务排队,主节点一出问题,恢复过程也特别痛苦。
更麻烦的是,MapReduce并不能覆盖所有计算场景。到了Hadoop 2.x时期,实时流计算、交互式查询、机器学习这些需求都长起来了,大家希望一个集群能跑多种框架。但Hadoop 1.x的资源管理跟MapReduce绑定得太死,想跑Spark就得另起一套集群。于是同批机器可能被拆成几拨,各自有各自的调度器,高峰期这边吃不饱、那边排长队,浪费非常严重。
1.2 思路转变:把“资源管理”从计算框架里抠出来
YARN的核心思路,说白了就是把集群当一台大型计算机来管。Hadoop 2.x开始,资源管理这块被单独拆出来,变成一个独立通用的调度层。整个YARN做的事情非常像一个操作系统:它维护一张全集群资源表,谁想用资源就来申请,按队列和配额分配,至于作业内部怎么拆任务、怎么协调,计算框架自己说了算。
这个设计带来的好处有三点。第一,多框架共存成为可能,MapReduce、Spark、Flink这些计算模型可以跑在同一批节点上,资源之间不打架。第二,调度策略可插拔,FIFO、Capacity、Fair三种调度器随便换,不用重新编译框架。第三,主节点职责清晰了,ResourceManager只做资源分配和宏观调度,不需要关心某个Map任务跑得怎么样,故障面被有效缩小。从Hadoop 1.x时代过来的人,第一次看到YARN的RM和NM分工时都会有明显感受:该管资源的只管资源,该跑计算的只管计算,各司其职,半夜报警都变少了。
2. YARN核心架构拆解:四个角色各管一段
2.1 ResourceManager:大脑,但不是保姆
先看RM(ResourceManager)。RM是集群层面的主控进程,通常部署在独立的主节点上。它内部必须分成两个模块:Scheduler和ApplicationManager。Scheduler只做纯粹的“资源分配”,它不关心这个作业是谁的、跑到什么状态,只管把某个节点上的Container资源分配给某个应用,分配依据就是当前各队列的容量和资源使用情况。ApplicationManager负责接收新提交的应用、为应用启动ApplicationMaster,并在AM异常退出后按策略重新拉起,同时追踪应用的最终状态。
RM还有两个容易被忽略的能力。一个是高可用,主备RM通过共享状态存储来切换,一般配合ZooKeeper做选主,切换过程对客户端相对透明。另一个是重启恢复,开启之后RM故障重启,能尽量还原已提交的应用状态。实际维护里,主备切换比“手工重启+丢失所有作业”舒服得多,所以提前配好非常关键。
2.2 NodeManager:本地管家
NM(NodeManager)是每台物理节点上的代理进程,它的职责就是管理这台机器上的Container。NM周期性向RM上报心跳,内容包含节点总资源、已用资源、剩余资源、本地磁盘使用情况等。RM就是靠这些上报数据,才能把Container调度到合适的机器上。NM收到启动或停止Container的命令后,在本机上创建进程、设置环境变量、处理日志,并且持续监控资源用量。
我经常把NM比作“本地管家”:它管内存、管磁盘目录、管Container的状态,唯独不管作业逻辑。作业逻辑归AM管。NM出了问题,影响的是单机资源调度和容器生命周期,影响范围可控。
2.3 ApplicationMaster:每个作业的项目经理
AM是YARN做得比较精巧的一环。每个提交上来的作业,都会单独启动一个AM,AM自己跑在一个Container里。它的职责非常像项目经理:向RM申请自己需要的Container资源,拿到资源后指挥NM把这些Container启动起来,过程中跟踪每个任务的执行情况,出现失败任务要重新申请资源重跑,最后统计作业结果并汇报。
举例来说,MapReduce作业的AM会把Map和Reduce任务用到的资源都算好,逐个申请;Spark on YARN作业的AM会负责Executor的启动和回收。这就把“作业内部怎么管理”和“集群怎么分资源”彻底隔离开了,RM只需要回答“要多少、优先级多少”,不需要理解作业语义。
2.4 Container:资源的最小分配单位
最后是Container。它是YARN做资源分配的基本单位,不是一台虚拟机,也不是一个常驻进程,而是一组资源描述:多少兆内存、多少个虚拟核心,必要时带上本地磁盘目录。AM本身占用一个Container,用户作业里真正跑计算的Map、Reduce、Executor也都运行在各自的Container里。
一个容易误解的点是:Container不是一次分配终身占有的。AM可以动态申请和释放Container,YARN负责随时更新资源账目。任务跑完,Container资源立刻让出来,下一个任务马上就能用。正因为它是轻量逻辑概念,YARN才能在资源不够时做到“排队等容器”,而不是卡死整个作业。如果以后涉及GPU或FPGA资源,较新的Hadoop版本还支持在Container定义里加入这些资源类型,但大多数业务集群的日常还是以内存和vCore为主。
3. 关键资源模型:内存、vCore与分配粒度
3.1 一张表看懂YARN如何计量集群容量
刚部署好的集群,yarn-site.xml里没写自定义参数时,常用的默认值大概是这样的:
| 参数 | 社区版常见默认值 | 作用 |
|---|---|---|
| yarn.nodemanager.resource.memory-mb | 8192 | 单个NM可分配物理内存上限 |
| yarn.nodemanager.resource.cpu-vcores | 8 | 单个NM可分配虚拟核上限 |
| yarn.scheduler.minimum-allocation-mb | 1024 | 单个Container最少能申请的内存 |
| yarn.scheduler.maximum-allocation-mb | 8192 | 单个Container最多能申请的内存 |
| yarn.scheduler.minimum-allocation-vcores | 1 | 单个Container最少能申请的核数 |
| yarn.scheduler.maximum-allocation-vcores | 4 | 单个Container最多能申请的核数 |
集群资源总量,就是所有NM上报资源的总和。调度器分配时,容器大小会“对齐”到最小分配粒度的整数倍。比如最小内存是1024MB,一个需要3GB的任务会被调度成申请3个单元;最小vCore是1,需要5个核的任务就得申请5个单元。把最小粒度调小,能让资源切得更细,但调度次数和心跳开销也会变大,所以不要一味追求小。后面几节我会专门讲怎么取值。
3.2 CPU核数怎么算,vCore不是物理核
vCore和物理核之间没有等号,它只是调度时用来“按比例排队”的数字。你可以把物理核看成一辆货车的载重,vCore看成货车的体积标记。YARN默认不会对CPU做强制隔离,两个各占4个vCore的Container,可能仍然在同一批物理核上争抢CPU跑。如果不开专门的隔离配置,vCore更像“调度权重”而不是“物理核承诺”。
所以我的习惯是:普通计算集群按物理核数配置,最多乘1.5倍。例如一台16核的物理机,vCore可以配16或者24,但不要盲目填64。填太高会让调度器以为机器有海量CPU能力,任务之间互相抢CPU,整体反而跑得更慢。内存方面更严格,建议给OS和常驻系统进程留出15%到20%的余量。假设机器64GB物理内存,HDFS的DataNode等服务占到8GB左右,那么把剩余44GB左右交给YARN是更稳妥的做法,直接按满物理内存配置很容易触发系统层面的swap。
3.3 最小与最大容器:为什么这两项最容易被忽略
这两个参数看似简单,但生产环境的坑基本都是它们挖出来的:
- 最小分配太大:本来512MB就能跑的小任务,也会被塞进1GB的Container,导致小任务排队时间变长,吞吐下降。
- 最大分配太小:Spark Executor需要16GB的作业直接被拒,或者容器被压到上限,任务一启动就OOM。
- 最大分配太大:一个Container可能把单台NM的可用内存全吃掉,其他作业只能干等,集群资源被某个大容器长期霸占。
一般建议,最小设为512MB或1024MB,最大设为单台NM可用内存的二分之一到三分之二。比如NM可用内存48GB,maximum-allocation-mb可以设成32768,同时保留足够空间给AM和更多小任务。需要提醒一点,这些参数改动后要重启或者滚动重启集群才生效,改完没重启就去排障,很容易把人绕晕。
4. 调度器选型与队列配置实战
4.1 三种调度器对比:FIFO、Capacity、Fair
YARN里默认提供了三种调度器,很多人文档看了无数遍,真到自己选的时候还是不知道怎么下手。我一般这样理解:
| 调度器 | 特点 | 适合场景 |
|---|---|---|
| FIFO | 先到先得,一个队列吃到底 | 单人小集群,基本没有多租户需求 |
| Capacity | 多队列按比例分配容量,彼此隔离,能弹性利用空闲资源 | 大多数生产集群,离线批处理加实时任务混跑 |
| Fair | 多队列公平调度,任务多时大家均摊资源 | 多租户共享、交互式查询居多 |
实际上,生产环境默认就是Capacity调度器,我也很少看到有人换Fair,除非业务形态特别依赖公平流动。重点是,默认配置只有root.default一个队列,多个团队共用时会互相干扰,所以根据业务建队列几乎是必经之路。
在Capacity调度器里,有两个概念容易被忽略:capacity和maximum-capacity。capacity是按百分比描述的,同级所有子队列加起来必须是100,代表“正常情况下的保底份额”。maximum-capacity代表“最多能涨到多少”。比如dev队列容量设15、maximum-capacity设20,那么集群空闲时dev最多吃到20%,但当其他队列有任务加入,dev会被逐渐挤回15%附近,不会顶到无限大。你遇到的“某个作业一上来,全集群都卡死”,很多时候就是没设置maximum-capacity把队列上限控住。
4.2 Capacity Scheduler队列配置,一份可抄的XML
假设我要建三个队列:production(生产批处理)、data_service(实时交互)、dev(开发测试),目标比例60:25:15。可以在capacity-scheduler.xml里这样配置:
<configuration> <property> <name>yarn.scheduler.capacity.root.queues</name> <value>production,data_service,dev</value> </property> <property> <name>yarn.scheduler.capacity.root.production.capacity</name> <value>60</value> </property> <property> <name>yarn.scheduler.capacity.root.data_service.capacity</name> <value>25</value> </property> <property> <name>yarn.scheduler.capacity.root.dev.capacity</name> <value>15</value> </property> <property> <name>yarn.scheduler.capacity.root.production.maximum-capacity</name> <value>100</value> </property> <property> <name>yarn.scheduler.capacity.root.data_service.maximum-capacity</name> <value>80</value> </property> <property> <name>yarn.scheduler.capacity.root.dev.maximum-capacity</name> <value>20</value> </property> </configuration>这里要注意,同级子队列的capacity总和必须等于100,否则调度器会直接报错。maximum-capacity默认是100,也就是说如果不显式配置,任何队列理论上都可能把整个集群资源吃掉。上面我对data_service限到80、dev限到20,生产队列没限,这样生产任务忙起来时可以尽量吸收集群空闲资源。
配置完成后,通常不需要重启集群,多数发行版支持在线刷新调度器配置,但保险起见我会在变更后到Web UI里确认一下队列状态和已用资源比例。Spark提交任务时可以用--queue dev指定队列,MapReduce作业通过mapreduce.job.queuename指定,这样用户作业自然会落到对应队列里。
4.3 AM资源上限与队列隔离的配合
还有一个参数很容易被忽略:yarn.scheduler.capacity.maximum-am-resource-percent,默认值通常按0.1控制,也就是每个队列最多有10%的资源可以用来启动AM。这个数字如果设得太低,作业一多,AM没地方启动,任务就会一直停在ACCEPTED状态。如果设得过高,大批AM会占掉资源,真正跑计算的容器反而没位置。
队列很多、并发作业量大的集群,我会适当把这个值调到0.2甚至0.3,但前提是AM容器本身不能太大。AM不是执行密集型任务,给512MB到2048MB就够,别把AM请求搞得跟计算容器一样大。队列隔离的意义在于:生产队列不会被开发作业挤垮,开发任务再“作”,也有自己的小天地。
5. 从提交作业到容器启动:一个任务是怎么被调度起来的
5.1 九步走:YARN处理一次作业的完整流程
很多资料会讲YARN架构,但落到实际排查时,你脑子里必须有一条清晰的链路。我把一次作业从提交到结束拆成九步:
- 客户端把作业提交给RM,RM的ApplicationManager接收并记录应用的元数据。
- RM为应用分配第一个Container,专门用来启动AM,这个容器会从当前队列的空闲容量里扣。
- NM收到RM指令,在本机把AM进程启动。AM启动后主动向RM注册,RM再把可用资源信息返回给AM。
- AM把作业拆成若干任务,向RM的Scheduler发起资源请求,请求里包含需要的Container个数、单容器内存、vCore、优先级、位置偏好。
- Scheduler根据当前队列余量、节点资源、位置约束,选择一个合适的NM,返回一个Container分配结果。
- AM带着分配结果联系对应NM,NM验证后创建Container进程。
- Container里的任务开始执行,NM持续监控资源用量,AM定期收集任务进度,RM只通过心跳跟踪应用总体状态。
- 任务失败时,AM重新申请资源拉新任务;某些阶段的失败次数超限,AM会向RM报告,由RM按用户策略决定是否重试整个应用。
- 作业结束,AM注销并向RM汇报结果,所有Container回收,资源回到池子里重新分配。
排查问题的时候,你先确认卡在哪一步:是RM没收到提交,还是AM没起来,还是AM起来了但Container分配不到,还是任务跑着跑着被杀。不同阶段对应完全不同的日志位置,别一上来就翻NodeManager日志。
5.2 数据本地化与延迟调度
分布式计算里,把任务调度到“数据所在的节点”能省大量网络IO,所以AM在请求Container时通常会带上节点偏好:第一优先数据块所在节点,第二优先同机架,第三才是随便哪台。但现实情况是,如果严格要求“一定本地”,有些节点资源紧张时任务就得一直等,集群整体吞吐会下来。
调度器为了兼顾本地化和吞吐,引入了一点延迟策略:收到请求后不会立刻放弃本地偏好,而是让任务多等几个心跳,如果本地节点还是挤不出来,才逐步往同机架、其他机架扩散。所以你在资源页面上看到某个任务排队时间变长,不一定就是卡住了,可能是节点分布不均,或者数据副本太集中。这种调度行为用户不用干预,但心里要有个数,尤其当大集群里某个队列只有少数几台机器装了大量热门数据时,任务排队会特别明显。
5.3 资源抢占与弹性:什么时候该开,什么时候别开
Capacity调度器本身支持弹性利用:当某个队列空闲、另一队列任务繁重时,空闲队列的资源可以被借出去,前提是配置了合适的maximum-capacity。但注意,这种“被借走的资源”在原队列有任务回来时,只是让新任务等着,通常不会强制把正在运行的Container打断。想强制打断,需要开启专门的抢占策略。
我的建议是,生产环境先别急着开抢占。抢占会让后台任务频繁被杀重跑,如果任务本身没有良好保存状态,整体进度反而倒退。优先去查队列的基础容量和作业资源需求,而不是靠抢占来解决饿死问题。真要开,先在测试队列压测一段时间,再逐步放宽。
6. 内存溢出的三座大山与排查实录
6.1 虚拟内存超限:最常见的Container被杀原因
YARN默认会对虚拟内存做硬性检查,默认比例是yarn.nodemanager.vmem-pmem-ratio=2.1。也就是说,Container申请1GB物理内存时,进程里的虚拟内存用量最多可以到约2.1GB。JVM启动后,堆、线程栈、类加载器、本地库都算在虚拟内存里,跑复杂计算任务时很容易超过2.1倍,于是NM直接把Container杀掉。这种问题在日志里的典型表现是“Container killed by YARN for exceeding memory limits”,看着像内存不足,实际不是。
排查顺序我建议这样:
- 用
yarn logs拉出应用日志,确认是哪个Container被杀,被杀前在跑什么代码。 - 确认当前虚拟内存比例配置是多少,官方默认2.1在很多场景其实偏保守。
- 如果频繁超限,可以把比例调到3.0,并在JVM参数上做调整,比如减少不必要的堆外内存分配。
- 不建议直接把
vmem-check-enabled关掉。关闭等于撤掉整张安全网,某个隐蔽的泄漏问题会直接打爆物理内存,到时候更难查。
6.2 物理内存超限:JVM堆与容器内存的匹配
虚拟内存检查解决了,物理内存会不会超?一样会。每台机器上Container实际占用的物理内存峰值如果超过申请值,NM的监控线程照样会杀掉它。最常见的场景是:mapreduce.map.memory.mb设成2048MB,但mapreduce.map.java.opts里直接写了-Xmx2048m,堆和容器一样大。JVM不只有堆,还有Metaspace、线程栈、JIT编译器,再加上用户代码的堆外缓存,加起来很容易超。
我的经验是,JVM最大堆至少要留出20%的余量。容器2048MB时,-Xmx设置在1536MB到1600MB比较稳。遇到过因为-Xmx恰好等于容器内存,导致某个数据倾斜任务一触发序列化就OOM,后来把堆调小,GC从疯狂增长变成可控回收,反而稳定很多。容器内存和JVM堆之间的关系,是YARN排障中最容易被忽略的坑。
6.3 作业一直ACCEPTED,多半是资源没匹配上
状态一直显示ACCEPTED,表示应用已经被RM接收,但AM还没启动成功。最常见的两种原因:AM请求的资源超过了调度器允许的最大容器规格,或者当前队列可用余量不足,AM容器分配不出来。
排查命令一般这三条就够了:
# 查看某个应用的状态和诊断信息 yarn application -status application_xxxx # 查看队列当前资源使用情况 yarn queue -status root.production # 查看节点资源总量和已用量 yarn node -all在Web UI上重点看集群剩余内存、剩余vCore和Active节点数。如果集群显示还有空闲,但作业一直ACCEPTED,那就去看yarn.scheduler.maximum-allocation-mb和yarn.scheduler.maximum-allocation-vcores,极大可能是单容器需求量超过上限。如果把最大容器调大了还是不动,再检查AM所在队列的余量。
6.4 一套快速排查命令,收藏备用
下面这组命令基本能覆盖日常80%的YARN问题定位:
# 查看节点资源状态和容器分布 yarn node -list -all # 查看应用详细状态,包括所在队列、资源申请、诊断信息 yarn application -status application_xxxx # 查看队列具体使用量,内存、vCore、AM占比都在这 yarn queue -status root.production # 拉取应用日志,定位Container被杀原因 yarn logs -applicationId application_xxxx看日志时先看三处:AM启动日志、Container所在节点的NodeManager日志、任务自身stdout和stderr。很多坑,比如虚拟内存超限、物理内存超限、文件句柄不足,在日志里都有明确描述,只是位置隐蔽。
7. 上线前的参数清单与我的调优原则
7.1 可直接对照检查的参数表
每次调整YARN配置前,我习惯把所有关键参数过一遍,下面这个表可以直接照抄:
| 配置项 | 建议值 | 备注 |
|---|---|---|
| yarn.nodemanager.resource.memory-mb | 单机内存 × 0.75~0.8 | 给OS和伴生服务留余地 |
| yarn.nodemanager.resource.cpu-vcores | 物理核数或物理核数 × 1.5 | 别极端放大 |
| yarn.scheduler.minimum-allocation-mb | 512或1024 | 越小越灵活,开销越大 |
| yarn.scheduler.maximum-allocation-mb | 单NM内存的0.5~0.66 | 防止单容器吃满整机 |
| yarn.scheduler.minimum-allocation-vcores | 1 | 常规 |
| yarn.scheduler.maximum-allocation-vcores | 单NM vCore的0.5~0.66 | 跟内存保持对应关系 |
| yarn.app.mapreduce.am.resource.mb | 1024~2048 | AM容器别太大 |
| mapreduce.map.memory.mb / reduce.memory.mb | 根据任务实际内存 | 容器和JVM堆搭配 |
| yarn.nodemanager.vmem-pmem-ratio | 2.1~3.0 | 先观察再微调 |
注意,同一份配置不要今天调一个参数、明天调另一个参数,然后搞不清哪个起作用。我建议把每次变更写进版本库,并记录变更原因,后面回滚和复现都有依据。
7.2 从任务反推容器大小,而不是盲目调大
我一直建议“从任务反推容器大小”:先分析单个task真实需要多少内存、多少CPU,再决定容器规格。举个例子,一个Mapper处理几百MB数据,峰值内存可能就1.5GB,给它2GB容器和2核就够了;强行给8GB容器反而把并行度降下来,因为总资源固定的情况下,容器越大并发数越少,整批任务完成时间反而更慢。
调优的正确路径是:选一个有代表性的任务,让它跑完整流程,看峰值内存、CPU占用、shuffle量,然后反推合适容器规格。再根据“资源总量 ÷ 单容器大小 = 租户级并行度”来评估是否满足SLA。迭代几次之后,集群利用率自然会稳定在一个理想区间。
7.3 几个让我少挨半夜报警的习惯
有些习惯是我踩过几次坑之后养成的,特别想分享出来:
第一,线上队列和开发队列一定分开。开发作业跑撞库、循环、异常数据是常态,如果跟生产混在一个队列,一次不合理的重试就可能影响定时任务。队区分开之后,开发任务再折腾,生产任务还能保住资源。
第二,改配置前先备份,改完立即确认生效。YARN支持在线刷新调度器配置,但RM主备切换、节点滚动重启这些操作在不同集群里表现不一致,不能只看进程起来了就完事。
第三,长年记录集群资源利用率。只改参数不看效果等于白改。把每个队列的利用率、Container平均生命周期、最大容器内存这几个指标定期导出来看,能帮你提前发现“某些任务越来越大,资源快撑不住”的趋势。
第四,不要轻易关掉内存检查。关闭检查能让任务暂时不被打断,但一旦有内存泄漏,会从单台机器爆掉扩展到整个节点不可用,届时的恢复成本远高于多杀几个Container。
最后说点我自己的体会。YARN调优最忌讳上来就抄大厂参数,每台机器的内存、每类任务的数据曲线都不一样。我维护的集群从几十台到几百台都经历过,看到最多的问题就是“某个参数被调到了极端值”:要么内存分配特别激进,要么队列上限完全没设,最后任务全挤在一起。我现在比较坚持的做法是,定期做一次资源审计,把各队列利用率、容器平均生命周期、最大容器内存这几个指标拉出来重新评估,再决定是否需要加节点或者调整配置。还有一个小技巧很实用:每次提交Spark或MapReduce作业时,强制在客户端脚本里打印容器资源请求,比如把AM和Executor各自要多少内存、多少核输出到日志第一行。出问题的时候不用翻代码,看日志第一段就知道资源申请和集群实际分配是否一致。YARN的调度算法再复杂,落到日常运维面前,其实就是一个个容器请求和队列余额,把这两件事盯清楚,集群基本就稳了一半。