文章目录
概述
以Shuffle的临时数据存储为例,介绍执行内存的使用。
在Spark内存管理之堆内/堆外内存原理详解一文中,我们可以知道,无论是on-heap还是off-heap,都存在Execution内存,主要用于存放 Shuffle、Join、Sort、Aggregation 等计算过程中的临时数据
本文将主要从Shuffle的临时数据存储过程来说明Execution Memory如何发挥效用。
1. 多任务间的分配
Executor内运行的任务同样共享执行内存,Spark用一个HashMap结构保存了任务到内存耗费的映射。
- 每个任务可占用的执行内存大小的范围为
1/2N ~ 1/N
,其中N为当前Executor内正在运行的任务的个数。 - 每个任务在启动之时,要向MemoryManager请求申请最少为1/2N的执行内存,如果不能被满足要求则该任务被阻塞,直到有其他任务释放了足够的执行内存,该任务才可以被唤醒。
2. Shuffle的内存占用
Shuffle存在Map端的Shufle与Reduce端的Shuffle:
Map端的Shufle
:Map的输出结果结果首先被写入缓存(执行内存),当缓存满时,就启动溢写操作,把缓存中的数据写入磁盘文件,并清空缓存。当启动溢写操作时,首先需要吧缓存中的数据分区,然后对每个分区的数据进行排序(sort)和合并(combine),之后在写入磁盘文件。每次溢写操作会生成一个新的磁盘文件,随着Map任务的执行,磁盘中就会生成多个溢写文件。在Map任务全部完成之前,这些溢写文件会被归并(merge)成一个大的磁盘文件,然后通知相应的Reduce任务来获取属于自己处理的数据。Reduce端的Shuffle
:Reduce任务从Map端的不同Map机器领回属于自己处理的那部分数据,这些数据首先被放入缓存中,如果缓存被占满,会向Map端那样被溢写到磁盘中。多个经过排序、归并的溢写文件形成一个大文件(磁盘),然后将这若干个大文件,会交给Reduce处理。
从上面可以看出,执行内存需要用来存储任务在执行Shuffle时,shuffle的临时数据占用的内存。它不会被持久化在执行内存中。之所以说是临时,是因为实际上,最终数据都是写到磁盘的。Shuffle是按照一定规则对RDD数据重新分区的过程,我们来看Shuffle的Write和Read两阶段对执行内存的使用。
2.1 Shuffle Write对内存的使用
若在map端选择普通的排序方式
,那么map的输出结果会采用ExternalSorter进行外排,并写入内存缓存,主要占用堆内执行空间。若在map端选择Tungsten的排序方式
,则采用ShuffleExternalSorter直接对以序列化形式存储的数据排序,在内存中存储数据时可以占用堆外或堆内执行空间,取决于用户是否开启了堆外内存以及堆外执行内存是否足够。
2.2 Shuffle Read对内存的使用
在对reduce端的数据进行聚合时
,会将从map端(磁盘/缓存)领回的数据交给Aggregator处理,同样写入内存缓存中,占用堆内执行空间。如果需要进行最终结果排序
,则要再次将数据交给ExternalSorter处理,占用堆内执行空间。
2.3 溢写:处理shuffle执行内存不足
在ExternalSorter
和Aggregator
中,Spark会使用一种叫AppendOnlyMap
的哈希表在堆内执行内存中存储数据。但在Shuffle过程中所有数据并不能都保存到该哈希表中,对这个哈希表占用的内存会进行周期性地采样估算,当其大到一定程度,无法再从MemoryManager申请到新的执行内存时,Spark就会将其全部内容存储到磁盘文件中,这个过程被称为溢写(Spill)
,溢存到磁盘的文件最后会被归并(Merge)成一个大的磁盘文件。
2.4 Tungsten介绍
Shuffle Write阶段中用到的Tungsten是Databricks公司提出的对Spark优化内存和CPU使用的计划,解决了一些JVM在性能上的限制和弊端。Spark会根据Shuffle的情况来自动选择是否采用Tungsten排序。Tungsten采用的页式内存管理机制
建立在MemoryManager之上,即Tungsten对执行内存的使用进行了一步的抽象,这样在Shuffle过程中无需关心数据具体存储在堆内还是堆外。每个内存页用一个MemoryBlock来定义,并用Object obj
和long offset
这两个变量统一标识一个内存页在系统内存中的地址。堆内的MemoryBlock是以long型数组的形式分配的内存,其obj的值为是这个数组的对象引用,offset是long型数组的在JVM中的初始偏移地址,两者配合使用可以定位这个数组在堆内的绝对地址;堆外的MemoryBlock是直接申请到的内存块,其obj为null,offset是这个内存块在系统内存中的64位绝对地址。Spark用MemoryBlock
巧妙地将堆内和堆外内存页统一抽象封装,并用页表(pageTable)
管理每个Task申请到的内存页。
Tungsten页式管理下的所有内存用64位的逻辑地址表示,由页号和页内偏移量组成:
1. 页号:占13位,唯一标识一个内存页,Spark在申请内存页之前要先申请空闲页号。
2. 页内偏移量:占51位,是在使用内存页存储数据时,数据在页内的偏移地址。
有了统一的寻址方式,Spark可以用64位逻辑地址的指针定位到堆内或堆外的内存,整个Shuffle Write排序的过程只需要对指针进行排序,并且无需反序列化,整个过程非常高效,对于内存访问效率和CPU使用效率带来了明显的提升(参见Spark Tungsten-sort Based Shuffle 分析和探索Spark Tungsten的秘密。
总结
主要通过shuffle临时数据的缓存来讲解了执行内存的作用。
注意这和Shuffle数据的持久化不同,shuffle数据持久化是结果数据持久化,并且最终是写入到磁盘中。