资源分配是Spark任务中需要深入理解的,如果相关的参数设置不合理,将会造成资源分配不均的情况,可能导致某些任务大量的资源消耗或者某些任务因等待资源而饥饿。本文主要梳理Spark应用基本的过程,然后会介绍如何设置Spark应用执行过程中executor的数量、每个executor上memory大小的以及cores数量的设置。以上三者的优化需要考虑如下几个因素:
- 数据量的大小
- 一个Spark任务需要在多少时间内完成
- 静态或者动态分配资源
- 是否有上游应用或者下游应用
简介
首先梳理一下Spark应用的一些基本术语。
Partitions: Partition是分布式数据中的一小块,在partition的帮助下Spark可以更好的并行处理数据,同时减少数据在executors之间的shuffle过程.
Task: Task是可以在partition上的对数据进行处理的一系列操作的集合,并且可以在一个executor上执行。在同一个stage的所有tasks可以并行执行。
Executor: Executor是运行在worker node上的一个JVM进程,executor执行task,并将数据保存在内存或溢写到磁盘中。每一个Spark应用都有自己的executors,一个节点可以运行多个executors,而同一个Spark应用的executors可能分布在多个worker节点上。Executors将持续在整个Spark应用的生命周期中,同时并行的跑多个任务。Executors的数量可以在Spark程序内通过SparkConf设置,也可以在启动Spark程序时通过`-num-executors`提交。
Cluster Manager: 是一个可以获取集群各个节点资源使用情况的服务,也是平时所说的Spark运行在哪种集群管理模式(例如Standalone,Mesos、YARN)下。这些模式都是master(Resource Manager)和slave(Node Manager)组成,master服务决定哪些application可以运行,什么时候去哪里运行,slave服务接收master指令执行executor进程。只要Spark程序运行正常,资源管理器就会和各个节点相互通信,监听运行状况。本文重点将放在YARN作为Cluster Manager的情况,Spark可以运行在Yarn on cluster或者Yarn on client两种模式下。Yarn-client mode: Driver运行在client中,Application Master仅仅向YARN请求executors,client会和请求的container通信来调度他们工作。该模式一般用于交互调试。Yarn-cluster mode: Driver运行在Application Master中,它负责向YARN申请资源,并监督作业的运行状况。当用户提交了作业之后,就可以关掉Client,作业会继续在YARN上运行。该模式一般用于跑自动任务。
Cores: Core(核)是CPU的基本组成单元,一个CPU可以包含一个或多个Core,在Spark中,核数决定了executor中有多少任务可以并行执行。
在Cluster模式下Spark任务的运行过程
1. 构建Spark Application的运行环境(启动SparkContext),SparkContext向资源管理器(可以是Standalone、Mesos或YARN)注册并申请运行Executor资源;
2. 资源管理器分配Executor资源,并启动监听程序,Executor运行情况将随着心跳发送到资源管理器上;
3. SparkContext构建成DAG图,将DAG图分解成Stage,并把Task set发送给Task Scheduler。Executors向SparkContext申请Tasks;
4. Task Scheduler将Task发放给Executor运行同时SparkContext将应用程序代码发放给Executor;
5. Task在Executor上运行,运行完毕释放所有资源。
由此可知,executor和其memory的设置在Spark程序中有着举足轻重的作用,如果分配过多的memory给executor,会导致GC的延迟。下面我们将介绍如何设置Spark任务的executor和core的数量:
1. 静态分配: 在提交任务的时候设置
2. 动态分配: 根据数据的大小、以及运算中产生的数据大小动态设置,便于以后类似任务的重用
静态分配
例子1: 硬件(6个节点,每个节点16个cores和64GB RAM)
首先,假设每个节点将有1个core和1GB内存用于操作系统和Hadoop的守护进程,因此每个节点还剩下15个cores和63GB内存;
先从core的数量选择开始,core的数量意味着一个executor能同时执行的tasks数量,也许我们认为高并行能够带来好的性能,但是研究表明超过5个并行任务会使性能下降,所以我们选择将core的数量设置为5个。无论一个node有多少个core,我们都将他设置成5,因为这取决于并行任务的性能而不是core的数量。
接着,如果将core数设置成5,那么每个节点可以跑3(15/5)个executors,在有6个节点的情况下,将会有18(3*6)个executors,其中一个将会被YARN的Application Master占用,所以最终可用于Spark任务的有17个executors。
由上可推出每个executor将会有21(63/3)GB,但是这其中包含了overhead memory,overhead memory等于384MB与0.07*`spark.executor.memory`,在此例中则为`max(384MB, 0.07*21GB)=1.47GB`,因此最终每个executor的内存约等于19(21-1.47)GB。
所以,在该例中,共有17个executors,每个executor有5个cores和19GB的内存。
例子2: 硬件(6个节点,每个节点32个cores和64GB RAM)
core的数量同上,还是5个,则每个节点有exectuors为6(32/5)个,共有35(6*6-1)个exectuors,每个executor占内存10GB(63/6),overhead memory为约为1GB(0.07*10GB),所以最终每个executor的内存为9GB。
例子3: 当executor不需要过大的内存,大致能根据数据量和计算过程需要用到的内存算出每个executor所需的内存
上面第一个例子事先设定好了core的数量为5,得到每个executor有19GB,但如果数据量的大小和计算过程中的数据量仅仅需要10GB就足够,那么将core的数量有5设置为3(任何小于等于5的正整数),每个节点将会有5个executors,总共29(5*6-1)个executors,内存约为12(63/5)GB,算上overhead memory,最终将会得到29个executors,3个cores,内存约为11(12-12*0.07)GB。
动态分配
注意:如果是动态分配,executor的数量没有上限,所以Spark应用可能使用掉所有的资源,如果集群中还运行了其他的程序,我们应该注意资源的分配问题。因此应该为不同的用户设置不同的队列资源上限和下限,以保证不同的应用可以在YARN上正常运行。
当`spark.dynamicAllocation.enabled=true`时,在提交Spark任务时就不需要明确executors的数量了,Spark将通过以下参数进行动态设置:
1. `spark.dynamicAllocation.initialExecutors`用于设置初始化executor的数量;
2. 随后,根据任务的执行等待情况,executor的数量将在`spark.dynamicAllocation.minExecutors`和`spark.dynamicAllocation.maxExecutors`范围内变化;
3. 当有任务超过`spark.dynamicAllocation.schedulerBacklogTimeout`设置的等待时间时会申请新的executor资源,且申请的资源将成指数增长,直到达到最大值;
4. 当一个executor的执行时间达到`spark.dynamicAllocation.executorIdleTimeout`时将释放资源。
总结
如果我们想更好的控制Spark任务的执行时间,监控任务的执行情况,应该选择静态分配模式。如果选择动态分配模式,资源分配将不再透明,且有可能影响集群上其他任务的执行。