TT Lab
开始
学习 学习路径 课程

Apache Hadoop — 在一个 Pod 里搭起并运维 HDFS 与 YARN

YARN 把内存切成容器大小再分配出去

在 TT Lab 中继续学习

一句话总结

YARN 把集群的内存和 CPU 切成容器这样的小块分给应用。小块的大小有下限和上限:小于下限的请求会被提升到下限,大于上限的请求会被拒绝。先把资源给谁,由队列来决定;已结束容器的日志,必须开启日志聚合才会集中保存在一个地方。

为什么需要它

Hadoop 1 的 JobTracker 独自承担了资源管理和作业监控。集群越大,这一个进程就越成为瓶颈;而且槽位被预先分成 Map 用和 Reduce 用,只有 Map 在运行的时段里,Reduce 槽位就闲着。此外,在那样的集群里,除了 MapReduce 之外不能运行别的东西。

YARN 架构文档说明,这是把它一分为二的设计。资源管理交给集群中唯一的 ResourceManager,作业的规划和监控交给每个应用各启动一个的 ApplicationMaster。每台机器上运行的 NodeManager 负责启动容器,并监控和汇报资源使用情况。ResourceManager 内部的 Scheduler,用文档的话说,是“纯粹的”调度器——它只分配资源,既不跟踪应用的状态,也不重新启动失败的任务。这些是 AM 的职责。所以不管是 MapReduce、Spark 还是其他框架,只要带上各自的 AM,就能共用同一个集群。

工作原理

提交作业后,ResourceManager 先占一个容器来启动 AM。AM 以“内存 X MB、vcore Y 个的容器 N 个”这样的方式提出请求,调度器按 NodeManager 的空闲资源分配。这里就要涉及大小规则。根据 yarn-default.xml,规则是这样的。

资源模型文档补充说明,请求会被调整到最小值和最大值,或者变成设定增量的倍数。也就是说,请求的大小和得到的大小可能不同。MapReduce 的 AM 能提前发现任务请求超过最大分配,在请求容器之前就把作业以 KILLED 结束,并把原因写进应用的诊断(Diagnostics)行。

NodeManager 能分给容器的总量是 yarn.nodemanager.resource.memory-mb。默认值 -1 只在开启硬件自动检测时才会计算,其他情况下取 8192MB。它不会检查机器上是否真的有这么多内存。

把 NodeManager 分给容器的 1024MB,按两种最小分配来使用的示意图。最小分配为默认值 1024MB 时,请求 512MB 的 AM 被提升到 1024MB,独占整个节点,Map 容器没有位置,作业停滞。把最小分配降到 256MB 后,512MB 的 AM 和两个 256MB 的 Map 可以一起放下。大于最大分配的请求会被拒绝

用数字看这个实验 Pod,就能明白规则为什么重要。Pod 内存是 2Gi,仅 NameNode、DataNode、ResourceManager、NodeManager 这四个守护进程的堆上限加起来就有 864MB(256、160、256、192)。NodeManager 分给容器的份额是 1024MB。如果把最小分配保持默认值 1024MB,一个容器就是整个节点。AM 拿走这一个,Map 就没有地方放,作业只启动了 AM,就永远等下去。而且 MapReduce AM 的默认请求 yarn.app.mapreduce.am.resource.mb 是 1536MB,根本放不进 1024MB 的节点。在小节点上,必须降低最小分配,同时也要降低 AM 和任务的请求。这就是实验镜像把最小分配设为 128MB、最大分配设为 1024MB、AM 设为 384MB、Map 和 Reduce 设为 256MB 的原因。

还要记住,请求和 JVM 堆是两回事。mapreduce.map.memory.mb 是容器大小,堆按默认比例 0.8 在其中确定。其余 20% 留给 JVM 自身和原生内存。NodeManager 默认同时开启物理内存和虚拟内存检查(vmem-pmem-ratio 为 2.1),会杀掉超出的容器(实验镜像关闭了虚拟内存检查,因为它会把 JVM 保留的地址空间误算成使用量)。

队列决定谁先拿到

默认的调度器是 CapacityScheduler。根据 Capacity Scheduler 文档,所有队列都是 root 的子队列,用逗号把子队列写在 yarn.scheduler.capacity.root.queues 中。每个队列的 capacity 是百分比,而且同一层的总和必须是 100。这份份额是保证,不是围栏。其他队列空闲时,可以使用超过自己份额的资源,这种弹性由 maximum-capacity 来限制。

<property>
  <name>yarn.scheduler.capacity.root.queues</name>
  <value>default,etl,adhoc</value>
</property>
<property>
  <name>yarn.scheduler.capacity.root.etl.capacity</name>
  <value>60</value>
</property>

修改配置文件之后,不必重启 ResourceManager,用 yarn rmadmin -refreshQueues 让它生效。作业通过 mapreduce.job.queuename(默认 default)选择队列,提交到不存在的队列的作业,会在提交阶段被拒绝。

在小集群中还有一个经常碰到的值。maximum-am-resource-percent 是集群资源中可用于 AM 的比例,默认是 10%。同时处于活跃状态的应用数量由它决定。1024MB 节点的 10% 比一个 AM 还小,所以同时提交两个作业,第二个很容易停留在 ACCEPTED。

日志要开启才会聚合

容器的日志保存在启动该容器的 NodeManager 的本地磁盘上。yarn.log-aggregation-enable 默认是 false,此时日志在 yarn.nodemanager.log.retain-seconds 默认的 10800 秒(3 小时)之后被删除。开启后,应用结束后日志会被移到 HDFS 的 /tmp/logs 之下,用一行 yarn logs -applicationId <앱 ID>(占位符为应用 ID)就能一次性读取所有容器的日志。实验镜像已经开启了这个值。如果有几百个节点,没有它,光是找到一个失败任务的日志就要花半天。

集群现在有多空闲,可以通过 ResourceManager REST的 /ws/v1/cluster/metrics 查看。totalMB、allocatedMB、availableMB 和 appsPending 会一并给出。

在现场相遇的样子

第一,作业停在 ACCEPTED 不动。原因通常是这三种之一:没有足够大的节点能放下 AM,队列的 AM 份额已满,或者 AM 启动了但没有位置放任务容器。把 availableMB 和请求大小并排放在一起看,大部分都能解决。

第二,请求调小了,实际占用的内存却没变。请求小于最小分配时,会被提升到最小分配。请求 256MB、最小分配 1024MB,得到的就是 1024MB。

第三,容器因内存超限被杀。堆虽然在请求范围内,但线程栈和原生缓冲区超出了剩余部分。可以降低堆的比例,或者加大请求。

第四,删除队列后,作业全部被拒绝。提交脚本使用的是默认的 default 队列,而这个队列被从列表中去掉了。

实际工作中真正重要的事

下一项实验要做什么

通过 ResourceManager 的 REST 读取集群的总内存和空闲资源,并通过应用状态的诊断行,确认一个给每个 Map 请求 2048MB(超过最大分配)的作业,拿不到容器而以 KILLED 结束。在 capacity-scheduler.xml 中加入 etl 和 adhoc 队列,把保证份额的总和调整为 100,用 refreshQueues 使其生效,然后看提交到 etl 队列的作业在该队列中运行,以及提交到不存在队列的作业在提交阶段被拒绝。通过调度器 REST 读取队列状态,再用 yarn logs 取回已结束作业的聚合日志,整理成报告。