本篇是云上大数据系列第二篇文章,主要介绍
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
模式:
在 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
的分配,也存在同样的策略,因为比较简单,这里就不再赘述,读者对照上述关于内存的介绍,看下配置文件中相应的配置的名称便能理解。
为了便于理解,笔者画了一张示意图总结一下上述的分配策略:
为 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
模式
在 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
控制非堆内存;分配策略使用下面这张示意图可以比较清楚的表示出来:
在实际的配置中,需要注意:由于 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
下图比较清晰得展示了统一内存管理模型下内存空间的划分:
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
性能调优的同学有所帮助。