在刚刚接触大数据生态时,我们往往对 HDFS 的存储机制了如指掌,但在面对集群资源管理时,难免会感到困惑。特别是当我们试图在同一个集群上同时运行 MapReduce 作业、实时流处理任务甚至是机器学习算法时,如何高效地分配 CPU 和内存就成了一个巨大的挑战。这正是 Hadoop 1.0 时代的痛点。幸运的是,在 Hadoop 2.0 中,我们迎来了 Yet Another Resource Negotiator (YARN)。在这篇文章中,我们将像拆解一台精密引擎一样,深入探讨 YARN 的架构,了解它是如何通过将资源管理和任务调度分离,从而演变成一个通用的分布式操作系统。
目录
从 Hadoop 1.0 到 YARN:架构的演进
在开始之前,我们需要回顾一下历史。在 Hadoop 1.0 中,JobTracker 承担了太多的责任。它既要负责资源分配,又要负责任务调度。这种“一身兼二职”的设计在集群规模较小时尚可,一旦节点数超过 4000,JobTracker 就会成为整个系统的性能瓶颈,内存溢出(OOM)更是家常便饭。
为了解决这个问题,我们在 Hadoop 2.0 中引入了 YARN。它的核心思想非常简单:解耦。YARN 将 JobTracker 的职责拆分成了两个核心组件:全局的 ResourceManager (RM) 和每个应用独享的 ApplicationMaster (AM)。这种架构不仅解决了扩展性问题,还让 Hadoop 从单纯的“离线批处理系统”进化为可以运行多种处理引擎(如 Spark, Hive, Pig)的通用大数据平台。
!Hadoop 1.0 vs 2.0 Architecture
为什么 YARN 如此重要?
YARN 的出现极大地提高了系统的灵活性。在此之前,HDFS 上只能运行 MapReduce 任务。而现在,你可以将数据存储在 HDFS 中,利用 YARN 管理资源,同时运行图处理(如 Giraph)、交互式查询(如 Impala)、流处理(如 Spark Streaming, Flink)以及传统的批处理任务。这种多租户的特性,使得集群资源的利用率得到了质的飞跃。
YARN 的核心特性
在深入代码和组件之前,让我们先总结一下 YARN 为什么能成为当今大数据处理的主流选择:
- 可扩展性:YARN 将资源管理功能从调度中分离出来,使得 ResourceManager 可以专注于管理成千上万个节点。只要增加节点,集群规模就可以几乎线性扩展。
- 兼容性:YARN 对 MapReduce v1 应用了良好的向后兼容。我们可以无缝迁移旧版作业,无需重写代码,这为升级提供了极大的便利。
- 集群利用率:通过动态资源分配,YARN 避免了静态分区带来的资源浪费。如果某个时刻没有批处理任务,流处理引擎可以立即利用空闲的 CPU 和内存。
- 多租户:这是企业级应用的关键。YARN 支持多种计算框架并存,不同部门可以在同一个物理集群上运行各自的任务,互不干扰。
YARN 架构详解:解剖“大象”
让我们仔细观察 YARN 的架构图,它主要由以下几个核心组件构成。我们将逐一分析它们的职责,并看看它们是如何协同工作的。
1) 客户端
一切始于客户端。它是用户与 YARN 交互的入口。当你使用 hadoop jar 命令提交一个作业时,实际上是由客户端代码向 ResourceManager 发送请求。
- 职责:提交应用程序、监控应用状态、甚至杀死作业。
2) 资源管理器
RM 是 YARN 的“大脑”,也是集群中最核心的守护进程。它通常运行在独立的专用节点上,以避免抢占业务资源。
RM 包含两个主要组件:
#### A. 调度器
调度器是一个“纯调度器”,这意味着它只负责根据资源需求和策略分配资源,不负责监控任务状态或重启失败的任务。YARN 支持多种调度策略,最常见的是:
- Capacity Scheduler (容量调度器):这是很多企业生产环境的首选。它将集群资源划分为多个队列,保证每个队列都能获得最小资源量。这非常适合多部门共享的场景。
- Fair Scheduler (公平调度器):正如其名,它试图让所有应用程序平均分配到资源。如果你只有一个大作业和一个小作业,公平调度器会逐步将资源从小作业转移给大作业。
#### B. 应用管理器
如果你把调度器比作“分配房间的前台”,那么应用管理器就是“办理入住手续的大堂经理”。
- 职责:
1. 接受客户端提交的作业。
2. 协商第一个容器用于启动 ApplicationMaster。
3. 如果 ApplicationMaster 失败,负责重启它。
3) 节点管理器
NM 运行在集群中的每个 DataNode 上。它是 RM 的“耳目”和“手脚”。
- 心跳机制:NM 会定期向 RM 发送心跳信号和资源使用情况(可用内存、CPU 核数)。如果 RM 长时间收不到心跳,就会认为该节点宕机,并重新调度其上的任务。
- 容器管理:NM 负责启动和销毁容器。它还监控容器的资源使用,如果一个容器超出了分配的内存,NM 会直接将其杀死以保护节点不崩溃。
4) 应用主程序
这是每个应用程序特有的“管家”。一旦你的应用程序开始运行,它会首先启动自己的 AM。
- 职责:
1. 向 RM 申请资源:AM 计算出作业需要多少个 Container,然后向 RM 的 Scheduler 发出请求。
2. 与 NM 协商:拿到资源后,AM 会指示具体的 NM 启动任务。
3. 监控任务:AM 负责监控任务的进度。如果一个 Map 任务失败,AM 可以决定是否重试。
5) 容器
Container 是 YARN 中资源分配的抽象概念。它不仅仅是逻辑上的概念,更是物理资源的封装。
- 资源封装:一个 Container 通常包含特定量的内存和 CPU vcore。在 Hadoop 3.x 中,还可以包含 GPU 和其它加速器资源。
- 隔离:Container 利用 Linux 的 Cgroups 和线程技术实现了资源隔离,防止一个任务占用所有资源导致整个节点死机。
应用程序提交流程:从启动到完成
理解了组件之后,让我们通过一个实战流程,来看看这些组件是如何交互的。我们可以将其类比为一个大型项目的管理过程。
!<a href="https://media.geeksforgeeks.org/wp-content/uploads/ApplicationWorkFlowYARN.jpg">Application Workflow
第一步:提交申请
当我们运行 hadoop jar my-job.jar 时,客户端会向 ResourceManager 的应用管理器提交一个请求。这个请求包含了作业的全局信息,比如需要的 jar 包、输入输出路径等。
第二步:启动 ApplicationMaster
ResourceManager 找到一个空闲的节点,并指示该节点的 NodeManager 启动一个 Container。在这个 Container 中,运行的是我们应用程序专有的 ApplicationMaster (AM)。对于 MapReduce 来说,这就是 MRAppMaster。
第三步:申请资源
MRAppMaster 启动后,它会根据作业的切片数量计算需要运行多少个 Map 任务和 Reduce 任务。然后,它会向 ResourceManager 的调度器发送请求,申请相应的 Container。
// 这是一个简化的伪代码示例,展示 AM 如何向 RM 申请资源
// AM 与 RM 通信的协议
AllocateRequest request = AllocateRequest.newInstance(0, 0.0f,
null, // 本次请求需要的资源列表
null, // 已释放的 Container 列表
null);
// 发送请求
AllocateResponse response = resourceManager.allocate(request);
// 检查 RM 分配了哪些 Container
List allocatedContainers = response.getAllocatedContainers();
for (Container container : allocatedContainers) {
// 记录分配到的容器资源,例如 2GB 内存,1 个 CPU 核
System.out.println("Allocated Container: " + container.getId() +
" on Node " + container.getNodeId());
}
第四步:分配任务
当 Scheduler 确实有空闲资源时,它会通过心跳响应将 Container 的信息(主机名、端口、资源详情)返回给 AM。AM 收到后,会根据这些 Container 的位置(尽量数据本地化),将具体的任务分配给 NodeManager。
第五步:执行与监控
NodeManager 收到任务指令后,启动一个新的 Container 来运行 Map 或 Reduce 任务。任务运行期间,ApplicationMaster 会不断监控它们的进度,直到作业完成。
实战代码解析:如何与 YARN 交互
作为开发者,我们通常不需要直接编写 YARN 的客户端代码,因为 MapReduce 和 Spark 已经帮我们封装好了。但是,理解底层的 API 有助于我们排查问题。
示例 1:配置 YARN 客户端
在 Java 中直接提交应用到 YARN,你需要配置 YarnConfiguration。
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.yarn.client.api.YarnClient;
import org.apache.hadoop.yarn.conf.YarnConfiguration;
public class YarnClientExample {
public static void main(String[] args) throws Exception {
// 1. 创建配置对象
Configuration conf = new Configuration();
// 2. 设置 ResourceManager 的地址,这通常配置在 yarn-site.xml 中
// 这里我们手动设置以便示例清晰
conf.set("yarn.resourcemanager.hostname", "localhost");
conf.set("yarn.resourcemanager.address", "localhost:8032");
// 3. 初始化 YarnClient
YarnClient yarnClient = YarnClient.createYarnClient();
yarnClient.init(conf);
yarnClient.start();
System.out.println("YARN 客户端已成功连接到集群!");
// 4. 查询集群状态
System.out.println("集群当前可用的总内存: " +
yarnClient.getYarnClusterMetrics().getAvailableMB() + " MB");
yarnClient.stop(); // 记得关闭连接释放资源
}
}
示例 2:理解 Container Launch Context
ApplicationMaster 在向 NodeManager 发送启动任务请求时,必须提供一个 ContainerLaunchContext。这就像一份“装箱单”,告诉容器启动时需要带什么东西。
// 这是 ApplicationMaster 内部的逻辑
import org.apache.hadoop.yarn.api.records.*;
import org.apache.hadoop.fs.Path;
public ContainerLaunchContext createContainerContext() {
// 1. 设置启动命令
// 这里的命令是启动 Java 进程运行我们的 Task 类
String command = "${JAVA_HOME}/bin/java" +
" -Xmx512M" +
" com.example.MyMapTask " +
" 1>/stdout" +
" 2>/stderr";
// 2. 设置本地资源(需要从 HDFS 下载到本地的文件,如 jar 包、配置文件)
Map localResources = new HashMap
LocalResource jarRes = Records.newRecord(LocalResource.class);
Path jarPath = new Path("hdfs://namenode:8020/user/job.jar"); // 假设 jar 在 HDFS 上
// ... 设置 jarRes 的大小、时间戳、可见性 ...
localResources.put("job.jar", jarRes);
// 3. 设置环境变量
Map env = new HashMap();
env.put("CLASSPATH", ".:/opt/hadoop/*"); // 确保能找到库文件
// 4. 构建 Context
ContainerLaunchContext ctx = ContainerLaunchContext.newInstance(
localResources,
env,
Collections.singletonList(command),
null, // 服务数据
null // 安全令牌
);
return ctx;
}
实用见解:在生产环境中优化 YARN
仅仅理解理论是不够的,作为专业的技术人员,我们还需要关注以下实战经验:
1. 内存配置与 GC 调优
最常见的报错是 Container killed by YARN for exceeding memory limits(容器内存超限被杀)。
- 问题:你以为你分配了 4GB 内存给 Task,但 JVM 的堆外内存开销也被计算在内。当进程内存 > 4GB 时,NodeManager 会强制杀掉它。
- 解决方案:不要把容器内存设为 JVM Max Heap。在生产环境中,JVM 堆大小应设为容器内存的 80% 左右,留出 20% 给堆外内存、线程栈和 JVM 自身开销。
* 示例:如果 INLINECODE1e6fd645 (1GB),那么 MapReduce Map 任务的堆内存 INLINECODE03c100bc 设为 -Xmx800m 是比较安全的。
2. 数据本地性
在 YARN 的日志中,你经常能看到 data locality(数据本地性)这个词。
- 优化建议:YARN 会优先尝试在数据所在的节点上运行任务。这是最高效的(NODELOCAL)。如果不行,它尝试在同一个机架(RACKLOCAL)。最后才跨机架。
- 实践:如果你的集群经常出现跨机架任务,可能是节点资源太紧张,或者你的调度器等待本地数据的时间设置得太短。可以适当调大
yarn.scheduler.node-locality-delay参数。
3. 调度器的选择
- 单一集群共享:如果你的集群是一个公有云,服务于多个部门,请务必选择 Capacity Scheduler。通过配置队列(queue.root.dev, queue.root.prod),你可以限制开发部门最多占用 30% 的资源,防止他们意外阻塞生产环境。
常见错误与解决方案
作为开发者,我们在调试 YARN 应用时常会遇到以下问题:
- java.lang.OutOfMemoryError: Java heap space
* 原因:这是 Map 或 Reduce 任务本身处理的数据量过大,超过了 JVM 堆内存。
* 修正:增加 INLINECODE81f301a5 或 INLINECODEd6f880ce,同时相应增加 mapreduce.map.java.opts。
- Connection Refused: ResourceManager
* 原因:客户端拿不到集群配置,或者防火墙拦截了 8050(RM IPC 端口)。
* 修正:确保客户端机器的 INLINECODE6fd3ff3a 和 INLINECODEe8c354ab 与集群保持一致。
总结与后续步骤
通过这篇文章,我们不仅从宏观上理解了 Hadoop YARN 的架构设计,还深入到了组件间的交互细节,甚至探讨了如何编写客户端代码和优化性能。YARN 不仅仅是一个“资源分配器”,它已经成为了现代大数据生态的基石操作系统。
掌握 YARN 对于构建高性能、可扩展的大规模数据应用至关重要。
作为开发者,你接下来可以尝试:
- 编写一个自己的 YARN 应用:尝试不使用 MapReduce,而是通过 YARN API 提交一个简单的 Python 或 C++ 程序到集群运行。
- 深入了解 Spark on YARN:了解 Spark 是如何作为 YARN 的一个客户端运行的,对比 INLINECODEdf1dfb45 和 INLINECODE66e51b89 的区别。
希望这篇文章能帮助你更好地驾驭大数据这座庞大的矿场。如果你在实际操作中遇到任何问题,欢迎随时交流探讨!