前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >YARN 内存参数终极详解 转

YARN 内存参数终极详解 转

作者头像
stys35
发布2019-03-29 16:27:31
1.3K0
发布2019-03-29 16:27:31
举报
文章被收录于专栏:工作笔记精华工作笔记精华

Hadoop框架自身集成了很多第三方的JAR包库。Hadoop框架自身启动或者在运行用户的MapReduce等应用程序时,会优先查找Hadoop预置的JAR包。这样的话,当用户的应用程序使用的第三方库已经存在于Hadoop框架的预置目录,但是两者的版本不同时,Hadoop会优先为应用程序加载Hadoop自身预置的JAR包,这种情况的结果是往往会导致应用程序无法正常运行。

下面从我们在实践中遇到的一个实际问题出发,剖析Hadoop on YARN 环境下,MapReduce程序运行时JAR包查找的相关原理,并给出解决JAR包冲突的思路和方法。

一、一个JAR包冲突的实例

我的一个MR程序需要使用jackson库1.9.13版本的新接口:

图1:MR的pom.xml,依赖jackson的1.9.13

但是我的Hadoop集群(CDH版本的hadoop-2.3.0-cdh5.1.0)预置的jackson版本是1.8.8的,位于Hadoop安装目录下的share/hadoop/mapreduce2/lib/下。

使用如下命令运行我的MR程序时:

hadoop jar mypackage-0.0.1-jar-with-dependencies.jar com.umeng.dp.MainClass --input=../input.pb.lzo --output=/tmp/cuiyang/output/

由于MR程序中使用的JsonNode.asText()方法,是1.9.13版本新加入的,在1.8.8版本中没有,所以报错如下:

… 15/11/13 18:14:33 INFO mapreduce.Job:  map 0% reduce 0% 15/11/13 18:14:40 INFO mapreduce.Job: Task Id : attempt_1444449356029_0022_m_000000_0, Status : FAILED Error: org.codehaus.jackson.JsonNode.asText()Ljava/lang/String; …

二、搞清YARN框架执行应用程序的过程

在继续分析如何解决JAR包冲突问题前,我们需要先搞明白一个很重要的问题,就是用户的MR程序是如何在NodeManager上运行起来的?这是我们找出JAR包冲突问题的解决方法的关键。

本篇文章不是一篇介绍YARN框架的文章,一些基本的YARN的知识假定大家都已经知道,如ResourceManager(下面简称RM),NodeManager(下面简称NM),AppMaster(下面简称AM),Client,Container这5个最核心组件的功能及职责,以及它们之间的相互关系等等。

图2:YARN架构图

如果你对YARN的原理不是很了解也没有关系,不会影响下面文章的理解。我对后面的文章会用到的几个关键点知识做一个扼要的总结,明白这些关键点就可以了:

  1. 从逻辑角度来说,Container可以简单地理解为是一个运行Map Task或者Reduce Task的进程(当然了,AM其实也是一个Container,是由RM命令NM运行的),YARN为了抽象化不同的框架应用,设计了Container这个通用的概念;
  2. Container是由AM向NM发送命令进行启动的;
  3. Container其实是一个由Shell脚本启动的进程,脚本里面会执行Java程序,来运行Map Task或者Reduce Task。

好了,让我们开始讲解MR程序在NM上运行的过程。

上面说到,Map Task或者Reduce Task是由AM发送到指定NM上,并命令NM运行的。NM收到AM的命令后,会为每个Container建立一个本地目录,将程序文件及资源文件下载到NM的这个目录中,然后准备运行Task,其实就是准备启动一个Container。NM会为这个Container动态生成一个名字为launch_container.sh的脚本文件,然后执行这个脚本文件。这个文件就是让我们看清Container到底是如何运行的关键所在!

脚本内容中和本次问题相关的两行如下:

export CLASSPATH="$HADOOP_CONF_DIR:$HADOOP_COMMON_HOME/share/hadoop/common/*:(...省略…):$PWD/*" exec /bin/bash -c "$JAVA_HOME/bin/java -D(各种Java参数) org.apache.hadoop.mapred.YarnChild 127.0.0.1 58888 (其他应用参数)"

先看第2行。原来,在YARN运行MapReduce时,每个Container就是一个普通的Java程序,Main程序入口类是:org.apache.hadoop.mapred.YarnChild。

我们知道,JVM加载类的时候,会依据CLASSPATH中路径的声明顺序,依次寻找指定的类路径,直到找到第一个目标类即会返回,而不会再继续查找下去。也就是说,如果两个JAR包都有相同的类,那么谁声明在CLASSPATH前面,就会加载谁。这就是我们解决JAR包冲突的关键!

再看第1行,正好是定义JVM运行时需要的CLASSPATH变量。可以看到,YARN将Hadoop预置JAR包的目录都写在了CLASSPATH的最前面。这样,只要是Hadoop预置的JAR包中包含的类,就都会优先于应用的JAR包中具有相同类路径的类进行加载!

那对于应用中独有的类(即Hadoop没有预置的类),JVM是如何加载到的呢?看CLASSPATH变量定义的结尾部分:"/*:$PWD/*"。也就是说,如果Java类在其他地方都找不到的话,最后会在当前目录查找。

那当前目录究竟是什么目录呢?上面提到过,NM在运行Container前,会为Container建立一个单独的目录,然后会将所需要的资源放入这个目录,然后运行程序。这个目录就是存放Container所有相关资源、程序文件的目录,也就是launch_container.sh脚本运行时的当前目录。如果你执行程序的时候,传入了-libjars参数,那么指定的JAR文件,也会被拷贝到这个目录下。这样,JVM就可以通过CLASSPATH变量,查找当前目录下的所有JAR包,于是就可以加载用户自引用的JAR包了。

在我的电脑中运行一次应用时,该目录位于/Users/umeng/worktools/hadoop-2.3.0-cdh5.1.0/ops/tmp/hadoop-umeng/nm-local-dir/usercache/umeng/appcache/application_1444449356029_0023,内容如下(可以通过配置文件进行配置,从略):

图3:NM中Job运行时的目录 

好了,我们现在已经知道了为何YARN总是加载Hadoop预置的class及JAR包,那我们如何解决这个问题呢?方法就是:看源码!找到动态生成launch_container.sh的地方,看是否可以调整CLASSPATH变量的生成顺序,将Job运行时的当前目录,调整到CLASSPATH的最前面。

三、阅读源码, 解决问题

追溯源码,让我们深入其中,透彻一切。

首先想到,虽然launch_container.sh脚本文件是由NM生成的,但是NM只是运行Task的载体,而真正精确控制Container如何运行的,应该是程序的大脑:AppMaster。查看源码,果然验证了我们的想法:Container的CLASSPATH,是由MRApps(MapReduce的AM)传给NodeManager的,NodeManager再写到sh脚本中。

MRApps中的TaskAttemptImpl::createCommonContainerLaunchContext()方法会创建一个Container,之后这个Container会被序列化后直接传递给NM;这个方法的实现中,调用关系为:createContainerLaunchContext() -> getInitialClasspath()-> MRApps.setClasspath(env, conf)。首先,我们来看setClasspath():

首先,会判断userClassesTakesPrecedence,如果设置了这个Flag,那么就不会去调用MRApps.setMRFrameworkClasspath(environment, conf)这个方法。也就是说,如果设置了这个Flag的话,需要用户设置所有的JAR包的CLASSPATH。

下面看setMRFrameworkClasspath()方法:

其中,DEFAULT_YARN_APPLICATION_CLASSPATH里放入了所有Hadoop预置JAR包的目录。能够看到,框架会先用YarnConfiguration.YARN_APPLICATION_CLASSPATH设置的CLASSPATH,如果没有设置,则会使用DEFAULT_YARN_APPLICATION_CLASSPATH。

然后由conf.getStrings()把配置字符串按逗号分隔转化为一个字符串数组;Hadoop遍历该数组,依次调用MRApps.addToEnvironment(environment, Environment.CLASSPATH.name(), c.trim(), conf)设置CLASSPATH。

看到这里,我们看到了一线曙光:默认情况下,MRApps会使用DEFAULT_YARN_APPLICATION_CLASSPATH作为Task的默认CLASSPATH。如果我们想改变CLASSPATH,那么看来我们就需要修改YARN_APPLICATION_CLASSPATH,让这个变量不为空。

于是,我们在应用程序中加入了如下语句:

代码语言:javascript
复制
String[] classpathArray = config.getStrings(YarnConfiguration.YARN_APPLICATION_CLASSPATH, YarnConfiguration.DEFAULT_YARN_APPLICATION_CLASSPATH);
String cp = "$PWD/*:" +  StringUtils.join(":", classpathArray);
config.set(YarnConfiguration.YARN_APPLICATION_CLASSPATH, cp);

上面的语句意思是:先获得YARN默认的设置DEFAULT_YARN_APPLICATION_CLASSPATH,然后在开头加上Task程序运行的当前目录,然后一起设置给YARN_APPLICATION_CLASSPATH变量。这样,MRApps在创建Container时,就会将我们修改过的、程序当前目录优先的CLASSPATH,作为Container运行时的CLASSPATH。

最后一步,我们需要将我们的应用依赖的JAR包,放入到Task运行的目录中,这样加载类的时候,才能加载到我们真正需要的类。那如何做到呢?对,就是使用-libjars这个参数,这个前面也已经解释过了。这样,运行程序的命令就改为如下:

hadoop jar ./target/mypackage-0.0.1-SNAPSHOT-jar-with-dependencies.jar com.umeng.dp.MainClass-libjars jackson-mapper-asl-1.9.13.jar,jackson-core-asl-1.9.13.jar --input=../input.pb.lzo --output=/tmp/cuiyang/output/

四、结语

本文中,我们通过分析Hadoop的源代码,解决了我们遇到的一个JAR包冲突问题。

即使再成熟再完善的文档手册,也不可能涵盖其产品所有的细节以解答用户所有的问题,更何况是Hadoop这种非以盈利为目的的开源框架。而开源的好处就是,在你困惑的时候,你可以求助源码,自己找到问题的答案。这正如侯捷老师所说的: “源码面前,了无秘密”。

YARN 内存参数终极详解

很多朋友在刚开始搭建和使用 YARN 集群的时候,很容易就被纷繁复杂的配置参数搞晕了:参数名称相近、新老命名掺杂、文档说明模糊 。特别是那几个关于内存的配置参数,即使看好几遍文档也不能完全弄懂含义不说,配置时一不小心就会张冠李戴,犯错误。

如果你同样遇到了上面的问题,没有关系,在这篇文章中,我就为大家梳理一下 YARN 的几个不易理解的内存配置参数,并结合源码阐述它们的作用和原理,让大家彻底清楚这些参数的含义。

一、YARN 的基本架构

介绍 YARN 框架的介绍文章网上随处都可以找到,我这里就不做详细阐述了。之前我的文章“YARN环境中应用程序JAR包冲突问题的分析及解决”中也对 YARN 的一些知识点做了总结,大家可以在TheFortyTwo 后台回复编号 0x0002 获得这篇文章的推送。下面附上一张 YARN 框架图,方便引入我们的后续内容:

图 1: YARN 架构图

二、内存相关参数梳理

YARN 中关于内存配置的参数呢,乍一看有很多,其实主要也就是那么几个(如果你感觉实际接触到的比这更多更混乱,是因为大部分的配置参数都有新命名和旧命名,我后面会分别解释),我已经整理出来列在了下表中。大家先看一下,对于表中各列的意义,我会在本节后面详细说明;而对于每个参数的意义,我会放在下节进行详细解释。

图 2: 内存参数整理图

下面我们解释一下表中的各列:

配置对象:指参数是针对何种组件起作用;

参数名称:这个不用解释,大家都明白;

旧参数名称:大家都知道,MapReduce 在大版本上,经历了 MR1 和 MR on YARN;而小版本则迭代了不计其数次。版本的演进过程中,开发人员发现很多参数的命名不够标准,就对参数名称做了修改;但是为了保证程序的前后兼容,仍然保留了旧参数名称的功能。这样等于是实现同一个功能的参数,就有了新旧两种不同的名称。比如 mapreduce.map.java.opts 和 mapred.map.child.java.opts 两个参数,其实是等价的。那如果新旧两个参数都设置了情况下,哪个参数会实际生效呢?Hadoop 的规则是,新参数设置了的话,会使用新参数,否则才会使用旧参数设置的值,而与你设置参数的顺序无关;

缺省值:如果没有设置参数的话,Hadoop 使用的默认值。需要注意的是,并非所有参数的默认值都是写在配置文件(如 mapred-default.xml)中的,比如 mapreduce.map.java.opts 这个参数,它的取值是在创建 Map Task 前,通过下面代码获得的:

if (isMapTask) { userClasspath = jobConf.get(“mapreduce.map.java.opts”, jobConf.get( “mapred.child.java.opts”, “-Xmx200m")); … }

可以看到,这个参数的取值优先级是:

mapreduce.map.java.opts > mapred.child.java.opts > -Xmx200m

所在配置文件:指明了如果你想静态配置这个参数(而非在程序中调用 API 动态设置参数),应该在哪个配置文件中进行设置比较合适;

三、各参数终极解释

下面我们分别来讲解每个参数的功能和意义。

mapreduce.map.java.opts 和 mapreduce.map.memory.mb

我反复斟酌了一下,觉得这两个参数还是要放在一起讲才容易让大家理解,否则割裂开会让大家困惑更大。这两个参数的功能如下:

  1. mapreduce.map.java.opts: 运行 Map 任务的 JVM 参数,例如 -Xmx 指定最大内存大小;
  2. mapreduce.map.memory.mb: Container 这个进程的最大可用内存大小。

这两个参数是怎样一种联系呢?首先大家要了解 Container 是一个什么样的进程(想详细了解的话,就真的需要大家去看我的另一篇文章“YARN环境中应用程序JAR包冲突问题的分析及解决”,回复编号0x0002)。简单地说,Container 其实就是在执行一个脚本文件(launch_container.sh),而脚本文件中,会执行一个 Java 的子进程,这个子进程就是真正的 Map Task。

图 3: Container 和 Map Task 的关系图

理解了这一点大家就明白了,mapreduce.map.java.opts 其实就是启动 JVM 虚拟机时,传递给虚拟机的启动参数,而默认值 -Xmx200m 表示这个 Java 程序可以使用的最大堆内存数,一旦超过这个大小,JVM 就会抛出 Out of Memory 异常,并终止进程。而 mapreduce.map.memory.mb 设置的是 Container 的内存上限,这个参数由 NodeManager 读取并进行控制,当 Container 的内存大小超过了这个参数值,NodeManager 会负责 kill 掉 Container。在后面分析 yarn.nodemanager.vmem-pmem-ratio 这个参数的时候,会讲解 NodeManager 监控 Container 内存(包括虚拟内存和物理内存)及 kill 掉 Container 的过程。

紧接着,一些深入思考的读者可能就会提出这些问题了:

Q: 上面说过,Container 只是一个简单的脚本程序,且里面仅运行了一个 JVM 程序,那么为何还需要分别设置这两个参数,而不能简单的设置 JVM 的内存大小就是 Container的大小?

A: YARN 作为一个通用的计算平台,设计之初就考虑了各种语言的程序运行于这个平台之上,而非仅适用 Java 及 JVM。所以 Container 被设计成一个抽象的计算单元,于是它就有了自己的内存配置参数。

Q: JVM 是作为 Container 的独立子进程运行的,与 Container 是两个不同的进程。那么 JVM 使用的内存大小是否受限于 Container 的内存大小限制?也就是说,mapreduce.map.java.opts 参数值是否可以大于 mapreduce.map.memory.mb 的参数值?

A: 这就需要了解 NodeManager 是如何管理 Container 内存的了。NodeManager 专门有一个 monitor 线程,时刻监控所有 Container 的物理内存和虚拟内存的使用情况,看每个 Container 是否超过了其预设的内存大小。而计算 Container 内存大小的方式,是计算 Container 的所有子进程所用内存的和。上面说过了,JVM 是 Container 的子进程,那么 JVM 进程使用的内存大小,当然就算到了 Container 的使用内存量之中。一旦某个 Container 使用的内存量超过了其预设的内存量,则 NodeManager 就会无情地 kill 掉它。

mapreduce.reduce.java.opts 和 mapred.job.reduce.memory.mb

和上面介绍的参数类似,区别就是这两个参数是针对 Reducer 的。

mapred.child.java.opts

这个参数也已经是一个旧的参数了。在老版本的 MR 中,Map Task 和 Reduce Task 的 JVM 内存配置参数不是分开的,由这个参数统一指定。也就是说,这个参数其实已经分成了 mapreduce.map.java.opts 和 mapreduce.reduce.java.opts 两个,分别控制 Map Task 和 Reduce Task。但是为了前后兼容,这个参数在 Hadoop 源代码中仍然被使用,使用的地方上面章节已经讲述过了,这里再把优先级列一下:

mapreduce.map.java.opts > mapred.child.java.opts > -Xmx200m

yarn.nodemanager.resource.memory-mb

从这个参数开始,我们来看 NodeManager 的配置项。

这个参数其实是设置 NodeManager 预备从本机申请多少内存量的,用于所有 Container 的分配及计算。这个参数相当于一个阈值,限制了 NodeManager 能够使用的服务器的最大内存量,以防止 NodeManager 过度消耗系统内存,导致最终服务器宕机。这个值可以根据实际服务器的配置及使用,适度调整大小。例如我们的服务器是 96GB 的内存配置,上面部署了 NodeManager 和 HBase,我们为 NodeManager 分配了 52GB 的内存。

yarn.nodemanager.vmem-pmem-ratio 和 yarn.nodemanager.vmem-check-enabled

yarn.nodemanager.vmem-pmem-ratio 这个参数估计是最让人困惑的了。网上搜出的资料大都出自官方文档的解释,不够清晰明彻。下面我结合源代码和大家解释一下这个参数到底在控制什么。

首先,NodeManager 接收到 AppMaster 传递过来的 Container 后,会用 Container 的物理内存大小 (pmem) * yarn.nodemanager.vmem-pmem-ratio 得到 Container 的虚拟内存大小的限制,即为 vmemLimit:

long pmemBytes = container.getResource().getMemory() * 1024 * 1024L; float pmemRatio = container.daemonConf.getFloat(YarnConfiguration.NM_VMEM_PMEM_RATIO, YarnConfiguration.DEFAULT_NM_VMEM_PMEM_RATIO); long vmemBytes = (long) (pmemRatio * pmemBytes);

然后,NodeManager 在 monitor 线程中监控 Container 的 pmem(物理内存)和 vmem(虚拟内存)的使用情况。如果当前 vmem 大于 vmemLimit 的限制,或者 olderThanAge(与 JVM 内存分代相关)的内存大于限制,则 kill 掉进程:

if (currentMemUsage > (2 * vmemLimit)) { isOverLimit = true; } else if (curMemUsageOfAgedProcesses > vmemLimit) { isOverLimit = true; }

kill 进程的代码如下:

if (isMemoryOverLimit) { // kill the container eventDispatcher.getEventHandler().handle(new ContainerKillEvent(containerId, msg)); }

上述控制是针对虚拟内存的,针对物理内存的使用 YARN 也有类似的监控,读者可以自行从源码中进行探索。yarn.nodemanager.vmem-check-enabled 参数则十分简单,就是上述监控的开关。

上面的介绍提到了 vmemLimit,也许大家会有个疑问:这里的 vmem 究竟是否是 OS 层面的虚拟内存概念呢?我们来看一下源码是怎么做的。

ContainerMontor 就是上述所说的 NodeManager 中监控每个 Container 内存使用情况的 monitor,它是一个独立线程。ContainerMonitor 获得单个 Container 内存(包括物理内存和虚拟内存)使用情况的逻辑如下:

Monitor 每隔 3 秒钟就更新一次每个 Container 的使用情况;更新的方式是:

  1. 查看 /proc/pid/stat 目录下的所有文件,从中获得每个进程的所有信息;
  2. 根据当前 Container 的 pid 找出其所有的子进程,并返回这个 Container 为根节点,子进程为叶节点的进程树;在 Linux 系统下,这个进程树保存在 ProcfsBasedProcessTree 类对象中;
  3. 然后从 ProcfsBasedProcessTree 类对象中获得当前进程 (Container) 总虚拟内存量和物理内存量。

由此大家应该立马知道了,内存量是通过 /proc/pid/stat 文件获得的,且获得的是该进程及其所有子进程的内存量。所以,这里的 vmem 就是 OS 层面的虚拟内存概念。

图 4: 内存参数的组合示意图

四、结语

本文带大家深入剖析了 YARN 中几个容易混淆的内存参数,大家可以见微知著,从文章分析问题的角度找出同类问题的分析方法,文档与源码相结合,更深入了解隐藏在框架之下的秘密。

(adsbygoogle = window.adsbygoogle || []).push({});

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • YARN 内存参数终极详解
相关产品与服务
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档