程一舰
数据技术处
大数据平台是我行“一个智慧大脑、两大技术平台、三项服务能力”金融科技框架中的一个重要支撑平台,大数据平台的批量主集群采用Spark、Hadoop及Impala等计算引擎,为大批量数据下的客户行为分析、日志分析、数据挖掘和BI分析提供技术支撑。
Spark是基于内存的大数据计算引擎,大家在编写Spark程序或者提交Spark任务的时候,不可避免的要进行内存等资源的优化和调优。Spark官方也提供了很多配置参数用来进行内存或CPU的资源使用,但是为什么我们要进行这些参数的配置,这些参数是怎么影响到任务执行的,本篇文章将从Spark内存管理的原理方面进行分析。
01
JVM内存
1.JVM内存区域划分
因为Spark任务最终是运行在java虚拟机里面的,所以这里先分析一下JVM的内存区域划分。JVM的运行时内存划分主要包括以下几类:
程序计数器:程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。
Java栈:同计数器也为线程私有,生命周期与相同,就是我们平时说的栈,栈描述的是Java方法执行的内存模型。
本地方法栈:本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。
方法区:方法区同堆一样,是所有线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。
堆:对于大多数应用来说,堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。
JAVA堆内存管理是影响性能主要因素之一。堆内存溢出是JAVA项目非常常见的故障,因此,必须先了解下JAVA堆内存是怎么工作的。
2.JVM内存模型
从JVM内存模型的角度来看堆内内存:
对于堆内内存,又分为工作内存和主内存,工作内存属于线程私有,主内存数据线程公有,例如可以存储一些全局变量等数据。所以当线程公有的时候,如果线程1和线程2同时进行某个共享变量的读写的时候,就会出现数据异常,因此内存模型会涉及到三个概念:可见性、原子性和指令重排序(非本文重点可自行学习)。
02
Spark内存
了解了JVM的内存区域划分和内存模型,就不难理解Spark的内存管理了。Spark程序最是运行在JVM里面的,因此,就需要通过管理好JVM里的内存从来提升Job的运行效率。如上所述,运行时效率主要与堆内存有关,堆内存的概念涉及堆内内存和堆外内存:
- 堆内内存是JVM中的堆内存,例如Spark程序的Driver和Executor都运行在堆内内存;
- 堆外内存是Jvm运行时所在的server节点的操作系统的一部分内存。
1.Spark内存管理策略
那么Spark是如何对这两种内存进行管理的呢?Spark提供了两种内存管理的策略:一种是静态内存管理策略,另一种是统一内存管理策略。(可以通过Spark源码的MemoryManager来找到它的两种实现策略)。
这两种内存管理策略本质上两套不同的(对(堆内内存和堆外内存的)执行内存和存储内存的)划分方案。
那么执行内存和存储内存又是什么呢?这就要看Spark的堆内内存和堆外内存的划分方案了。不管是堆内内存还是堆外内存,都至少包含两个重要的内存区域:存储内存和执行内存。因此,Spark的两套内存管理策略就是针对这两种内存划分展开的。
2.静态内存管理策略
静态内存管理机制是,对于存储内存和执行内存及其他的小部分内存的大小,在Spark的任务运行期间是固定的,用户在进行任务提交前进行配置即可。
静态内存管理-堆内内存划分策略
静态内存管理-堆外内存划分策略
3.统一内存管理模型
在Spark版本1.6之前,Spark的内存划分策略都是静态内存管理。随着硬件技术的快速发展,内存容量的提升,静态内存管理的方式已经不太适应新的硬件水平,Spark又推出了统一内存管理的策略。
统一内存管理与静态内存管理大致是一样的,只是在静态内存管理的基础上,加入了“动态占用机制”。对于运行期间,存储内存和执行内存不够的情况下,可以临时占用对方的内存,这就使得内存使用的灵活度大大提高,内存使用率也大大提高。
统一内存管理-堆内内存划分策略
统一内存管理-堆外内存划分策略
内存一旦可以临时占用,又会出现很多问题,例如如果某个时刻,存储内存和执行内存都想占用对方的内存怎么办,或者执行内存首先占用了存储内存,导致存储内存在需要的时候不够用,这个时候是否可以让执行内存强制释放占用的内存?
鉴于以上问题,Spark根据两种内存对于任务的影响程度划分了占用优先级。由于执行内存会有一些中间数据或shuffle数据,这部分数据如果丢失对于整个任务的正确性都是至关重要的,而存储内存中存的大多是一些RDD缓存数据,丢失了顶多会对任务的性能有影响,因此执行内存的优先级要高于存储内存。也就是如果执行内存占用了存储内存,存储内存就得乖乖等着。
4.Spark任务与堆内存
通过以上,你可能已经了解了JVM内存中最重要的堆内存相关的两个概念,以及Spark是如何进行这两种内存管理的。那Spark的一个具体的任务跟这两种内存有什么关系呢?
上图展示了一个worker节点上的任务与内存的关系。可以看到一个worker节点有两个Executor,每个Executor都是一个进程,每个task是一个线程。Executor的内部会有一块堆内内存,也就是主内存,这块内存是每个task共享的;两个Executor进程之外还有一块堆外内存,是供两个进程共享的。那么,堆外内存是什么时候使用呢?
我们在进行Spark任务提交的时候,可能会给每个Executor分配内存,例如每个Executor分配了1G的内存。但是如果100个Executor中,有99个都够用这1G内存,只有1个Executor不够用,这时候如果通过配置,将所有的Executor的内存统一调高,会造成大量的内存浪费。因此,这个时候就是堆外内存派上用场的时候了,可以允许内存不够用的Executor借用堆外内存来完成自己的任务处理。
5.常用的Spark调优参数
通过上面的内容,我们从JVM内存到spark使用的堆内存进行了剖析,下面列举在实际的任务提交过程中常用的配置参数,以及给出每个参数应该如何配置的一般建议。
num-executors:该参数是指spark运行的总executor数,executor数量的计算公式为:
executor数量=spark.core.max/executor-cores
executor-memory:每个executor分配的内存,该参数如果设置不当,容易出现GC时间过长甚至JVM的OOM异常,该参数一般设置4-8G比较合适;
executor-cores:每个executor进程分配的CPU核数,该参数代表了每个Executor并行执行的能力,每个Executor一般设置2-4个比较合适,如果该值设置为2,executor-memory设置为4G,那么可以计算出每个task线程的工作内存约为2G;在共享队列中,总的cores不要超过队列总数的1/3-1/2,避免影响别的作业运行,总的cores计算方式如下:
total-executor-cores = num-executors * executor-cores
Spark.default.parallelism:指定并行的task数量,spark作业task数量在500-1000较为合适,如果不设置,根据hdfs block去自动设置,task过少可能造成executors*cores资源浪费。一般设置为num-executors*executor-core的2-3倍较为合适,这样可以在某个core运行完及时补充上task任务,充分利用资源。
03
总 结
以上主要从内存原理的角度介绍了为什么要进行内存调优以及内存调优从本质上是影响了什么。全文围绕堆内内存和堆外内存以及执行内存和存储内存几个概念进行分析,最后通过Spark任务来的参数配置来分析这几个概念在实际实例中的关联。
通过本文希望你下次配置内存参数的时候,能够知道你配置的参数到底是到了内存趋于的哪一块,这样才能更好地做好调优工作。同时,我们也将在大数据平台层面对集群进行配置参数的优化,更好地为大家服务,为我行金融科技发展打造坚实的大数据平台支撑。