本篇是云上大数据系列第二篇文章,主要介绍Hadoop系统的基础调优,让Hadoop集群的资源能够被充分利用起来。在后续的文章中,我们还将会分享更多关于云上大数据系统的性能分析和调优经验,敬请期待。
大数据系统对资源的占用较大,如果不进行合适的基础调优,很容易造成资源的浪费。尤其是在云端部署大数据系统,按量计费却没有最大化利用购买的资源,往往导致投入产出比较低。本篇我们介绍如何对Hadoop系统进行基础优化,让 Hadoop 系统的资源能够被充分利用起来。
- 资源环境:
ecs.d1.6xlarge × 5
- 软件系统:
CDH 5.14.2 (Spark 1.6)
- 操作系统:
CentOS 7.3
我们以 CDH 5.14.2 为例,介绍 Spark-on-YARN 的基础调优方法,在这一版本的 CDH 中,Spark 版本是 1.6 。值得注意的是,Spark 1.6 以后(含),其内存管理方式发生了变化,本文论述的方法不一定适用于之前的版本。阅读本文前,你需要有一定的 Hadoop 使用或开发经验。
1. Spark-on-YARN 的资源分配
提交 Spark 任务的时候,YARN 在做什么?
YARN 将两类主要资源(CPU、memory)打包为 container ,Spark 作为运行在 YARN 上的应用程序,每次使用 spark-submit(或其他方式)提交新的任务,都会先向YARN 申请资源。简要过程可以归为:
-
ResourceManager选择一个结点启动一个准备运行 ApplicationMaster 的 container;
- 如果是
cluster 模式部署,则进一步启动 SparkContext;
- 接着,
ApplicationMaster 向 ResourceManager 申请运行该任务所需要的资源;
- 在得到资源以后通知相应的
NodeManager 启动运行 Spark Executor 的 container;
-
Spark Driver 为 Spark Executor 分配 task,Executor 将 task 的执行情况汇报给 Driver。
下文中的两幅示意图简要地展示了整个启动的流程。
如何合理地为 Spark 配置集群资源
Spark 资源的分配牵扯到 Spark 在 YARN 上的两种不同的部署模式:
-
client 模式:Spark Driver 运行在提交任务的客户端。
-
cluster 模式:Spark Driver 运行在 ApplicationMaster 中;
下面就每一种模式下如何配置资源,做详细介绍:
1) client 模式:
![spark_on_yarn_client spark_on_yarn_client]()
在 client 模式下,Spark Drivr 运行在提交任务的客户端,不需要单独为其配置资源,只需要为 ApplicationMaster 和 Spark Executor 分配合适的资源即可。
为 ApplicationMaster 分配资源
client 模式下,ApplicationMaster 的作用仅限于资源的申请和分配,可以为其分配少量的资源即可,例如按照默认值,分配 1 个 vcore 和 512M 内存:
spark.yarn.am.cores=1
spark.yarn.am.memory=512m
如果我们仅配置这两项,YARN实际申请的资源有可能比这个配置大的多,原因在于 spark.yarn.am.memory 仅限制了ApplicationMaster(JVM进程)的堆内存空间,对于非堆内存空间,也需要做配置:
spark.yarn.am.memoryOverhead=128m
默认情况下,该值是通过下面这个公式计算得来:
spark.yarn.am.memoryOverhead=max(spark.yarn.am.memory*0.1, 384)m
此时,我们通过 ResourceManager 界面观察 YARN 的资源分配情况,有可能发现实际分配的资源比 896m (512m+384m) 还要大。这是由于 YARN 对于资源的分配存在最小粒度,总是按照最小粒度的整数倍来分配资源。
如果你用的是 Capacity Scheduler(默认调度器),这个粒度通过下面变量来控制:
yarn.scheduler.minimum-allocation-mb
如果你用的是 Fair Scheduler,则通过下面变量来控制:
yarn.scheduler.increment-allocation-mb
这两个变量控制了 YARN 为每个 container 分配内存空间的最小尺寸。当用户申请的内存空间小于该尺寸的时候,YARN 会按照这个最小尺寸来分配;当用户申请的内存空间大于该尺寸的时候,YARN 会按照该尺寸的整数倍来分配空间。默认值都是 1024m。
例如,上例中,YARN 实际为 ApplicationMaster 分配的空间应该是 1024m。
切换这两种调度器,你可以:
yarn.resourcemanager.scheduler.class=org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FairScheduler
当然,YARN 为 container 分配内存也存在上线,可以通过
yarn.scheduler.maximum-allocation-mb
来控制。用户申请的内存空间超过该值,YARN 会拒绝分配并报出相应的错误。
对于 CPU 的分配,也存在同样的策略,因为比较简单,这里就不再赘述,读者对照上述关于内存的介绍,看下配置文件中相应的配置的名称便能理解。
为了便于理解,笔者画了一张示意图总结一下上述的分配策略:
![yarn_am_mem_allocation_strategy_client yarn_am_mem_allocation_strategy_client]()
为 Spark Executor 分配资源
Spark Executor 被分配到 Node Manager 结点。在每个结点上,需要为操作系统和 Hadoop 的其他进程分配一点资源。因此,总体可分配的资源比结点固有资源要少一些。Executor 的资源分配从以下几个方面来入手:
每个 Executor 分配多少 CPU 资源?
我们先来讨论这个问题,因为这决定了最终需要分配多少 Executor。
ecs.d1.6xlarge的每个结点有 24 Core和 96G内存。理论上每个 Executor 最多可以分配到 24Core。但如上所述,我们需要为操作系统和其他进程预留一部分资源,因此实际可分配的资源少于 24Core 和 96G 内存。
Core 的数量决定了任务运行的并发度,是不是并发度越高越好呢?实验表明,HDFS 的一次读/写并发超过 5 以后,性能就会急剧下降。因此,这里推荐将每个 Executor 的 Core 数设为 5:
spark.executor.cores=5
此时,每个结点可以起 4 个 Executor。我们预留了 4 个 Core 给其他进程。
每个结点分配多少个 Executor?
每个结点起 4 个 Executor,4 个几点总共可以起 16 个 Executor。
spark.executor.instances=16
值得注意的是, ApplicationMaster 可能被调度到任意结点,我们预留的 4 个 Core 已经足够。
每个 Executor 分配多少内存资源?
每个结点总共 96G 内存空间,我们为其他进程预留 4G 内存,剩余 92G 内存可以为 4 个 Executor 平均分配 23G 内存。考虑到 Executor 也存在非堆内存:
spark.yarn.executor.memoryOverhead=max(spark.executor.memory*0.1, 384)m
而
23 * 1024 * 0.1 > 384
因此,23G 内存中需要预留 10% 的空间给非堆内存,堆内存实际分配到的空间为:
spark.executor.memory=23*1024*0.9=21196m
此时,每个 Executor 申请的内存空间为:
21196+21196*0.1=23315.6m ~ 22.7G
我们知道 YARN 对于内存资源的分配存在最小粒度,如果此时最小粒度是 1024m,那么实际 YARN 为每个 Executor Container 分配到的内存空间是 23G。
值得注意的是,如果你为 Application Master 分配的内存过大,超过了预留的 4G,那么上述的资源分配策略将会失效, YARN 会因为无法按照上述策略分配资源而报错。
另外需要注意的是,Executor 内存不宜过大,否则 Java 虚拟机对于内存的管理将存在很大的负担,往往容易造成 GC 非常严重的后果。
如果你按照上述配置来启动集群,并且成功提交了一个 Spark 任务,你大概率会发现一个问题:一个 Executor 也没起来。是哪里出问题了呢?上面我们一直在论述 Spark 资源的申请分式,却忽略了资源是由 YARN 来分配的事实。YARN 对于每个 NodeManager 的资源都设定了一个上线例如:每个 NodeManager 可以分配的最大内存是(默认 8G ):
yarn.nodemanager.resource.memory-mb
我们申请的 23G 内存远远超过了 YARN 的允许,因此无法为 Executor 分配 Container。
同样的,对于 CPU,YARN 也有规定:
yarn.nodemanager.resource.cpu-vcores
在实际的配置中,我们要格外注意这一点。
2) cluster 模式
![spark_on_yarn_cluster spark_on_yarn_cluster]()
在 cluster 模式下,Spark Driver 运行在 ApplicationMaster 进程中,该进程又被调度在集群的某个结点上,因此,需要通过 Spark 的相关配置来决定 ApplicationMaster 实际需要申请多少资源。ApplicationMaster 占用了集群中某个结点的资源,那么该结点上可以分配给 Spark Executor 的资源就相应的减少了,可分配的 Executor 数量也会相应的减少。
为 ApplicationMaster 分配资源
默认情况下,Spark会为Driver申请如下资源(spark-defaults.conf):
spark.driver.cores=1
spark.driver.memory=512m
现在读者已经明白了 YARN 的资源分配策略,知道实际分配的资源可能远大于上述的配置,使用
spark.yarn.driver.memoryOverhead=max(spark.driver.memory*0.1, 384)m
控制非堆内存;分配策略使用下面这张示意图可以比较清楚的表示出来:
![yarn_am_mem_allocation_strategy_cluster yarn_am_mem_allocation_strategy_cluster]()
在实际的配置中,需要注意:由于 Spark Driver 运行在 Application Master 进程中,需要适当地调高 Application Master 的内存大小。
为 Spark Executor 分配资源
Spark Executor在 cluster模式下的分配策略和在 client模式下相同,这里不再赘述。需要注意的一点是:调高后的 Application Master 内存有可能超过预留的内存大小,此时,如果有必要,适当调低每个 Executor 的内存大小。
2. Spark 1.6 的内存模型
1.6 以后,Spark 采用一种叫统一内存管理模型的方式管理内存空间,如果你想要切换回静态内存分配方式,可以:
spark.memory.useLegacyMode=true
下图比较清晰得展示了统一内存管理模型下内存空间的划分:
![spark_unified_mem_model spark_unified_mem_model]()
Reserved Memory 是系统预留的空间,默认是 300M,存储了 Spark 的一些内部对象,不能用来做数据缓存或存储计算的中间结果。在生产环境中,是不能改变的。在测试环境下可以通过下面的参数修改:
spark.testing.reservedMemory
如果为 Executor 申请的堆内存空间小于预留空间的 1.5 倍,Spark 会报错,提示你申请更大的内存空间。
User Memory 是完全由用户掌控的内存空间,默认大小为:
User_Memory_Size=(JVM_Heap_Size - Reserved_Memory_Size)*(1.0 - spark.memory.fraction)
需要注意的是,如果这块内存使用不当,是可以造成内存溢出的。Spark 不会保证这块内存的安全。
Spark Memory 顾名思义是 Spark 管理的内存空间,分为存储和计算两部分,可以通过
spark.memory.storageFraction
来改变两部分的比例。 Storage Memory 主要用来缓存从 HDFS 读取的数据或者计算的中间结果。 Execution Memory 则存储执行 task 过程中的一些对象或者 shuffle 过程中的临时数据(例如准备排序的数据)。统一内存模型以后,这两部分的内存空间可以相互借用。具体的借用方式有:
- 一方空闲,一方内存不足情况下,内存不足一方可以向空闲一方借用内存;
-
Execution Memory 可以强制拿回 Storage Memory 在 Execution Memory 空闲时,借用的 Execution Memory 的部分内存(强制取回,而 Storage Memory 数据丢失,重新计算即可);
-
Storage Memory 只能等待 Execution Memory 主动释放占用的 StorageMemory 空闲时的内存(不强制取回,因为如果 task 执行,数据丢失就会导致 task 失败)。
这就意味着,当我们在配置 Storage Memory 的时候,最好不要小于其初始值大小。因为其内存空间总是会被 Execution Memory 占用而不能强制释放,这就造成如果 Storage Memory 很小,那么其实际可用的空间将更小,缓存数据的功能将被大大减弱。
希望这篇文章能对进行 Spark-on-YARN 性能调优的同学有所帮助。