文章目录
基本配置
静态内存管理
spark.storage.memoryFraction 默认0.6 storage内存区域
spark.shuffle.memoryFraction 默认0.2 executor内存区域
统一内存管理
spark.memory.fraction 默认0.6 storage和executor可用内存
spark.memory.storageFraction 默认0.5 不会被清除的最小storage内存(a fraction of spark.memory.fraction)
RDD缓存对GC的影响
Spark 缓存的RDD对象会储存在老年代,如果缓存的RDD数量太多,占用了很大的老年代空间,则会造成经常的full gc。所以要降低缓存的对象的数量,可以降低spark.memory.fraction参数,这样用于缓存RDD的内存空间就会减少,过多的需要缓存的RDD对象会缓存到磁盘,或者被淘汰。(也可降低年轻代空间,增加老年代空间)
参考博客:https://blog.csdn.net/rlnLo2pNEfx9c/article/details/94682732
Spark内存管理的详细详解
Spark对堆内存的管理
Spark 内存管理对于JVM堆内存是一种逻辑上的规划管理。因为对于堆上内存的使用和释放完全是由JVM控制的,Spark只能在申请后和释放前记录这些内存。
Spark对堆内存的管理存在的问题:
对于 Spark 中序列化的对象,由于是字节流的形式,其占用的内存大小可直接计算,而对于非序列化的对象,其占用的内存是通过周期性地采样近似估算而得,即并不是每次新增的数据项都会计算一次占用的内存大小,这种方法降低了时间开销但是有可能误差较大,导致某一时刻的实际内存有可能远远超出预期。此外,在被 Spark 标记为释放的对象实例,很有可能在实际上并没有被JVM 回收,导致实际可用的内存小于
Spark 记录的可用内存。所以 Spark 并不能准确记录实际可用的堆内内存,从而也就无法完全避免内存溢出(OOM,Out of Memory)的异常。
Spark对堆内存的管理的必要性:
虽然不能精准控制堆内内存的申请和释放,但 Spark 通过对存储内存和执行内存各自独立的规划管理,可以决定是否要在存储内存里缓存新的RDD,以及是否为新的任务分配执行内存,在一定程度上可以提升内存的利用率,减少异常的出现。
Spark对堆内存的管理原理:
对于储存内存,使用LinkedHashMap来集中管理所有缓存的 Block,Block 由需要缓存的 RDD 的 Partition 转化而成;如果缓存的RDD过多的话,会对缓存数据进行淘汰或者写入磁盘,保证缓存数据所占用的空间不会大于储存内存。
对于执行内存,使用ExternalSorter(AppendOnlyMap)、ShuffleExternalSorter(MemoryBlock)、ExternalAppendOnlyMap等来存储 Shuffle 过程中的数据,当内存中储存数据过多时,会将数据溢写到磁盘中,最后再对磁盘文件进行merge,保证shuffle过程中使用的内存不会超过执行内存。
内存管理源码
TaskMemoryManager
TaskMemoryManager :Manages the memory allocated by an individual task.
TaskMemoryManager包含成员MemoryBlock[] pageTable和MemoryManager memoryManager。
内存有关方法:
- TaskMemoryManager 的acquireExecutionMemory用来申请内存,调用memoryManager.acquireExecutionMemory(required, taskAttemptId, mode)以申请内存。释放内存同理。
- TaskMemoryManager 的(返回类型MemoryBlock) allocatePage()用来申请内存页for Tungsten memory。其首先调用acquireExecutionMemory用来申请内存。然后调用memoryManager.tungstenMemoryAllocator().allocate(acquired),申请获得内存页,并添加到pageTable数组中。释放同理。
MemoryManager
MemoryManager有两个子类StaticMemoryManager(静态内存管理),UnifiedMemoryManager(统一内存管理,Spark1.6之后默认)。
MemoryManager包含四个内存池对象(内存池MemoryPool通过Long型变量,控制内存池的使用情况)和tungstenMemoryAllocator(Allocates memory for use by Unsafe/Tungsten code)。
内存有关方法:
- MemoryManager的acquireXXXMemory调用memeoryPool.acquireMemory的方法申请内存。释放内存同理。
- tungstenMemoryAllocator用于分配内存给Unsafe/Tungsten code使用。见下文。
MemoryPool
MemoryPool有两个子类StorageMemoryPool(储存内存池), ExecutionMemoryPool(执行内存池)。其根据内部的memoryMode,又可分为on-heap和off-heap类型。
内存池对象记录可用的全部内存,已使用内存,空闲内存(Long型变量)。**获取内存和释放内存都是对Long型变量的增减,其作用只是记录内存使用情况,从而在逻辑上对内存进行管理。**但是对于非序列的堆上对象,只能估算其大小,所以无法准确控制内存使用。而且堆上对象的释放也是不受控制的。非堆内存可以精确的使用和释放,所以可以准确控制。
- StorageMemoryPool包含对象MemoryStore,其可以看作是Spark储存内存的接口。MemoryStore含有entries = new LinkedHashMap储存缓存的block。当StorageMemoryPool的内存使用完时,会向ExecutionMemoryPool借内存(同样是通过Long型变量处理,降低ExecutionMemoryPool的poolSize,提高StorageMemoryPool的poolSize),如果内存还是不够的话,即memoryFree<0,调用MemoryStore的evictBlocksToFreeSpace方法,淘汰LinkedHashMap中缓存的block对象。
- ExecutionMemoryPool直接通过Long型变量控制整个内存池的使用。其包含emoryForTask = new mutable.HashMapLong, Long,记录每个Task所使用的内存量。当ExecutionMemoryPool内存用尽时,也会向StorageMemoryPool借内存,但是不会使Storage的内存低于spark.memory.storageFraction。
Tungsten 内存管理
MemoryAllocator
Tungsten内存分配管理的基础是 MemoryAllocator 接口,定义 了 allocate和 free 两个方各分别来申请内存和释放内存。 MemoryAllocator 类的具体实现包括 HeapMemoryAllocator (堆内内存分配)和 UnsafeMemoryAllocator (堆外内存分配)两种。
在MemoryManager中:
两种 MemoryAllocator 的实现对比如图所示。 首先是 HeapMemory Allocator,从 allocate 方法的 实现可以看到,以 8 字节对齐的方式申请长度为(size+7) /8 的长整型数组。然后构造 MemoryBlock 对象,其 obj 成员即为 array, offset 成员为 Platform 类中得到的 LONG_ARRAY _OFFSET 指标。 堆内内存的释放一般不做任何操作,直接交给 JVM 的垃圾回收。
依赖于 Unsafe 包, Spark 将相关的一系列 Unsafe 操作作为静态方法统一封装在 Platform 类 中。 因此, UnsafeMemoryAllocator 中直接使用 Platform 的 allocateMemory 方法分配内存,这个 方法是本地化(Native)的实现,即通过 JNI 最终调用 C 和 C++ 实现,类似在 C 语言中新建一 个数组,得到的是绝对内存地址。 堆外内存的回收也需要 Spark 来完成, UnsafeMemoryAllocator 使用的是 Platform 的 freeMemory 方法。
MemoryLocation和MemoryBlock
Spark 中定义 MemoryLocation 来统一记录和追踪堆内、堆外的内存地址:在堆内模式中,内存由该对象的引用(obj)和 64bit 的偏移量 (offset)去寻址;在堆外模式中,内存可以直接通过 64bit 长度的地址进行寻址(将 obj设置为 null) 。
MemoryBlock 用来表示一段连续的内存块,在 MemoryLocation 的基础上增加了内存页编号(pageNumber)和对应数据的大小 (length) 。
在TaskMemoryManager类中,Tungsten 按照内存页表 (pageTable)的方式来管理内存, pageTable 本质上是一个 MemoryBlock 的数组,这些内存会被 Task 内部的内存消费者(MemoryConsumer)使用。
每个 Task 的内存空间被划分为多个内存页(page) ,每个内存页本质上都是一个内存块 (MemoryBlock) 。 为统一堆内和堆外的内存访问方式, TaskMemoryManager 引入了类似操作系统中虚拟内存逻辑地址的概念,并将逻辑地址映射到实际的物理地址。 逻辑地址由一个 64bits 的长整型表示,其中处于高位的 13bits 用来表示页编号(page Number),处于低位的 5lbits 用来 表示在该内存页内部的偏移(offsetlnPage)。 这样内存映射的过程,实际上就是先根据内存页编号查询页表(pageTable),得到对应的内存页,然后得到该页的物理地址,最后在物理地址上加 上偏移,得到实际内存物理地址。因此,所有内存地址都可由 pageNumber 和 offsetinPage 决定。
参考:
《SparkSQL内核剖析》