Spark 内存管理与调优
Spark 是基于内存的大数据计算引擎,因此,在编写 Spark 程序或者提交 Spark 任务的时候,要特别注意内存方面的优化和调优。Spark 官方也提供了很多配置参数用来进行内存或 CPU 的资源使用,但是为什么我们要进行这些参数的配置,这些参数是怎么影响到任务执行的,本篇文章将从 Spark 内存管理的原理方面进行分析。
一、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 同时进行某个共享变量的读写的时候,就会出现数据异常,因此内存模型会涉及到三个概念:可见性、原子性和指令重排序(非本文重点可自行学习)。
二、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 借用堆外内存来完成自己的任务处理。
三、总结
以上主要从内存原理的角度介绍了为什么要进行内存调优以及内存调优从本质上是影响了什么。全文围绕堆内内存和堆外内存以及执行内存和存储内存几个概念进行分析,最后通过简单地 Spark 任务来分析这几个概念在实际实例中的关联。
通过本文希望你下次配置内存参数的时候,能够知道你配置的参数到底是到了内存趋于的哪一块,这样才能更好地做好调优工作。
版权声明: 本文为 InfoQ 作者【小舰】的原创文章。
原文链接:【http://xie.infoq.cn/article/8ef5565358fa4d7f90eb1b6e4】。文章转载请联系作者。
评论