概述
本编文章的内容:
- 在executor端,内存的管理
- 在driver端,内存的管理
1. executor内存管理
1.1 堆内存布局
## UnifiedMemoryManager
private def getMaxMemory(conf: SparkConf): Long = {
val systemMemory = Runtime.getRuntime.maxMemory
val reservedMemory = 300 * 1024 * 1024
val usableMemory = systemMemory - reservedMemory
val memoryFraction = conf.getDouble("spark.memory.fraction", 0.6)
(usableMemory * memoryFraction).toLong
}
如代码所示,堆内存被分为3块,一块是预留内存,一块是额外使用内存,一块是spark用来管理的堆内存,值如下:
systemMemory
的值为eden区 + 一个surivior + old区
- 预留内存
reservedMemory
为300M- 默认情况下,额外使用内存为剩余内存的40%
- 默认情况下,通过UnifiedMemoryManager管理的内存为剩余内存的60%
里面也包含了一层含义,在整个executor的运行周期中,executor所有组件使用的内存通常不会超过300M,由于用户在rdd的transform或action中使用的内存不可预知,spark.memory.fraction
应该由用户自己来控制,如果用户在自己的程序中使用的内存(如没有使用Map、List等数据结构来存储数据)较少,可以相应的调大spark.memory.fraction的值,这对shuffle和cache都有好处;当然这也不是必须的,Reduce spark.memory.fraction default to avoid overrunning old gen in JVM default config描述了一种情况:当spark.memory.fraction
设置过大时,比如0.75,当需要缓存大量数据到堆内存中时,会导致长时间的full gc,原因如下:
- JVM的参数NewRatio默认是2,说明老年代占用的堆大小的66%
- 在使用堆内存缓存数据时,最大的内存占比为
spark.memory.fraction
,即75%- 此时数据不能完全放入到old区而导致full gc,影响程序性能
1.2 内存管理
Spark通过MemoryManager
来管理内存,它有两个实现类:StaticMemoryManager
、UnifiedMemoryManager
,前者基本没有用了,后者有如下特点:
- 内存分为2部分,一部分为存储内存,另一个部分为执行内存
- 默认情况下,存储内存占整个管理内存的50%,由
spark.memory.storageFraction
决定,其余的为执行内存- 存储内存与执行内存在对方有空闲空间时,可以互借;但是,执行内存不会主动释放,如果执行内存借了存储内存,此时存储内存不够需要执行内存归还,被借的内存由于执行内存占用不会归还。而当执行内存不够而存储内存借用的执行内存会被归还
UnifiedMemoryManager
除了使用上述的堆内存外,还可以使用堆外内存,由spark.memory.offHeap.enabled
开启,由spark.memory.offHeap.size
确定堆外内存的大小,堆外内存也分为存储内存和执行内存,存储内存占比也是由spark.memory.storageFraction
决定
1.3 执行内存的使用
如果需要使用
MemoryManager
管理的执行内存,都需要继承MemoryConsumer
,在学习代码的过程中,发现堆内存与堆外内存并没有混合使用,即堆外内存与堆内存不会互借(如果有,请读者不吝指出),可能实现比较麻烦,我总结了上图的内存使用场景。其实上图也比较好理解,因为ExternalSorter
、ExternalAppendOnlyMap
里面的数据都是反序列化的,所以需要使用堆内存;而ShuffleExternalSorter
的数据都是序列化的,可在堆内也可在堆外,可能因为取值实现起来麻烦所以堆外内存与堆内存不会互借。每个任务能使用的最大执行内存为该executor执行内存 / 该executor正在执行的任务数,最小执行内存为该executor执行内存 / 2 * 该executor正在执行的任务数,当任务需要的内存大于最小执行内存,但是实际能分配给它的内存又小于最小执行内存时,则会阻塞等待,直到其它的任务释放完内存后唤醒它。
1.4 存储内存的使用
在spark-core中,只有两种情形会是使用存储内存,一种是RDD需要缓存,二是广播变量,其消耗的内存类别如上图。
1.5 未被MemoryManager管理的内存使用
- 各个组件用来缓存的数据的数据结构,如
Map
,List
;这是走JVM管理的- 用户在自己的代码中使用的数据结构
- 在
NettyBlockTransferService
向外传输数据和接收数据,当数据小于spark.storage.memoryMapThreshold(默认2MB)
,分配传输的ByteBuffer
为HeapByteBuffer
,会分配在堆上;但是最终netty会使用堆外内存(默认)将数据传输出去