|
|
|
|
移动端

还不收藏?Spark动态内存管理源码解析!

Spark有两种内存管理模式,静态内存管理(Static MemoryManager)和动态(统一)内存管理(Unified MemoryManager)。动态内存管理从Spark1.6开始引入,在SparkEnv.scala中的源码可以看到,Spark目前默认采用动态内存管理模式,若将spark.memory.useLegacyMode设置为true,则会改为采用静态内存管理。

作者:大黄鸭来源:若泽大数据|2018-06-06 08:28

技术沙龙 | 邀您于8月25日与国美/AWS/转转三位专家共同探讨小程序电商实战

一、Spark内存管理模式

Spark有两种内存管理模式,静态内存管理(Static MemoryManager)和动态(统一)内存管理(Unified MemoryManager)。动态内存管理从Spark1.6开始引入,在SparkEnv.scala中的源码可以看到,Spark目前默认采用动态内存管理模式,若将spark.memory.useLegacyMode设置为true,则会改为采用静态内存管理。

  1. // SparkEnv.scala 
  2.     val useLegacyMemoryManager = conf.getBoolean("spark.memory.useLegacyMode"false
  3.     val memoryManager: MemoryManager = 
  4.       if (useLegacyMemoryManager) { 
  5.         new StaticMemoryManager(conf, numUsableCores) 
  6.       } else { 
  7.         UnifiedMemoryManager(conf, numUsableCores) 
  8.       } 

二、Spark动态内存管理空间分配

相比于Static MemoryManager模式,Unified MemoryManager模型打破了存储内存和运行内存的界限,使每一个内存区能够动态伸缩,降低OOM的概率。由上图可知,executor JVM内存主要由以下几个区域组成:

(1)Reserved Memory(预留内存):这部分内存预留给系统使用,默认为300MB,可通过spark.testing.reservedMemory进行设置。

  1. // UnifiedMemoryManager.scala 
  2. private val RESERVED_SYSTEM_MEMORY_BYTES = 300 * 1024 * 1024 

另外,JVM内存的最小值也与reserved Memory有关,即minSystemMemory = reserved Memory*1.5,即默认情况下JVM内存最小值为300MB*1.5=450MB。

  1. // UnifiedMemoryManager.scala 
  2.     val minSystemMemory = (reservedMemory * 1.5).ceil.toLong 

(2)Spark Memeoy:分为execution Memory和storage Memory。去除掉reserved Memory,剩下usableMemory的一部分用于execution和storage这两类堆内存,默认是0.6,可通过spark.memory.fraction进行设置。例如:JVM内存是1G,那么用于execution和storage的默认内存为(1024-300)*0.6=434MB。

  1. // UnifiedMemoryManager.scala 
  2.     val usableMemory = systemMemory - reservedMemory 
  3.     val memoryFraction = conf.getDouble("spark.memory.fraction", 0.6) 
  4.     (usableMemory * memoryFraction).toLong 

他们的边界由spark.memory.storageFraction设定,默认为0.5。即默认状态下storage Memory和execution Memory为1:1.

  1. // UnifiedMemoryManager.scala 
  2.      onHeapStorageRegionSize = 
  3.         (maxMemory * conf.getDouble("spark.memory.storageFraction", 0.5)).toLong, 
  4.       numCores = numCores) 

(3)user Memory:剩余内存,用户根据需要使用,默认占usableMemory的(1-0.6)=0.4.

三、内存控制详解

首先我们先来了解一下Spark内存管理实现类之前的关系。

1.MemoryManager主要功能是:(1)记录用了多少StorageMemory和ExecutionMemory;(2)申请Storage、Execution和Unroll Memory;(3)释放Stroage和Execution Memory。

Execution内存用来执行shuffle、joins、sorts和aggegations操作,Storage内存用于缓存和广播数据,每一个JVM中都存在着一个MemoryManager。构造MemoryManager需要指定onHeapStorageMemory和onHeapExecutionMemory参数。

  1. // MemoryManager.scala 
  2. private[spark] abstract class MemoryManager( 
  3.     conf: SparkConf, 
  4.     numCores: Int
  5.     onHeapStorageMemory: Long, 
  6.     onHeapExecutionMemory: Long) extends Logging { 

创建StorageMemoryPool和ExecutionMemoryPool对象,用来创建堆内或堆外的Storage和Execution内存池,管理Storage和Execution的内存分配。

  1. // MemoryManager.scala 
  2.   @GuardedBy("this"
  3.   protected val onHeapStorageMemoryPool = new StorageMemoryPool(this, MemoryMode.ON_HEAP) 
  4.   @GuardedBy("this"
  5.   protected val offHeapStorageMemoryPool = new StorageMemoryPool(this, MemoryMode.OFF_HEAP) 
  6.   @GuardedBy("this"
  7.   protected val onHeapExecutionMemoryPool = new ExecutionMemoryPool(this, MemoryMode.ON_HEAP) 
  8.   @GuardedBy("this"
  9.   protected val offHeapExecutionMemoryPool = new ExecutionMemoryPool(this, MemoryMode.OFF_HEAP) 

默认情况下,不使用堆外内存,可通过saprk.memory.offHeap.enabled设置,默认堆外内存为0,可使用spark.memory.offHeap.size参数设置。

  1. // All the code you will ever need 
  2.  final val tungstenMemoryMode: MemoryMode = { 
  3.     if (conf.getBoolean("spark.memory.offHeap.enabled"false)) { 
  4.       require(conf.getSizeAsBytes("spark.memory.offHeap.size", 0) > 0, 
  5.         "spark.memory.offHeap.size must be > 0 when spark.memory.offHeap.enabled == true"
  6.       require(Platform.unaligned(), 
  7.         "No support for unaligned Unsafe. Set spark.memory.offHeap.enabled to false."
  8.       MemoryMode.OFF_HEAP 
  9.     } else { 
  10.       MemoryMode.ON_HEAP 
  11.     } 
  12.   } 
  1. // MemoryManager.scala  
  2.  protected[this] val maxOffHeapMemory = conf.getSizeAsBytes("spark.memory.offHeap.size", 0) 

释放numBytes字节的Execution内存方法

  1. // MemoryManager.scala 
  2. def releaseExecutionMemory( 
  3.       numBytes: Long, 
  4.       taskAttemptId: Long, 
  5.       memoryMode: MemoryMode): Unit = synchronized { 
  6.     memoryMode match { 
  7.       case MemoryMode.ON_HEAP => onHeapExecutionMemoryPool.releaseMemory(numBytes, taskAttemptId) 
  8.       case MemoryMode.OFF_HEAP => offHeapExecutionMemoryPool.releaseMemory(numBytes, taskAttemptId) 
  9.     } 
  10.   } 

释放指定task的所有Execution内存并将该task标记为inactive。

  1. // MemoryManager.scala 
  2.  private[memory] def releaseAllExecutionMemoryForTask(taskAttemptId: Long): Long = synchronized { 
  3.     onHeapExecutionMemoryPool.releaseAllMemoryForTask(taskAttemptId) + 
  4.       offHeapExecutionMemoryPool.releaseAllMemoryForTask(taskAttemptId) 
  5.   } 

释放numBytes字节的Stoarge内存方法

  1. // MemoryManager.scala 
  2. def releaseStorageMemory(numBytes: Long, memoryMode: MemoryMode): Unit = synchronized { 
  3.     memoryMode match { 
  4.       case MemoryMode.ON_HEAP => onHeapStorageMemoryPool.releaseMemory(numBytes) 
  5.       case MemoryMode.OFF_HEAP => offHeapStorageMemoryPool.releaseMemory(numBytes) 
  6.     } 
  7.   } 

释放所有Storage内存方法

  1. // MemoryManager.scala 
  2. final def releaseAllStorageMemory(): Unit = synchronized { 
  3.     onHeapStorageMemoryPool.releaseAllMemory() 
  4.     offHeapStorageMemoryPool.releaseAllMemory() 
  5.   } 

2.接下来我们了解一下,UnifiedMemoryManager是如何对内存进行控制的?动态内存是如何实现的呢?

UnifiedMemoryManage继承了MemoryManager

  1. // UnifiedMemoryManage.scala 
  2. private[spark] class UnifiedMemoryManager private[memory] ( 
  3.     conf: SparkConf, 
  4.     val maxHeapMemory: Long, 
  5.     onHeapStorageRegionSize: Long, 
  6.     numCores: Int
  7.   extends MemoryManager( 
  8.     conf, 
  9.     numCores, 
  10.     onHeapStorageRegionSize, 
  11.     maxHeapMemory - onHeapStorageRegionSize) { 

重写了maxOnHeapStorageMemory方法,最大Storage内存=最大内存-最大Execution内存。

  1. // UnifiedMemoryManage.scala 
  2.  override def maxOnHeapStorageMemory: Long = synchronized { 
  3.     maxHeapMemory - onHeapExecutionMemoryPool.memoryUsed 
  4.   } 

核心方法acquireStorageMemory:申请Storage内存。

  1. // UnifiedMemoryManage.scala 
  2. override def acquireStorageMemory( 
  3.       blockId: BlockId, 
  4.       numBytes: Long, 
  5.       memoryMode: MemoryMode): Boolean = synchronized { 
  6.     assertInvariants() 
  7.     assert(numBytes >= 0) 
  8.     val (executionPool, storagePool, maxMemory) = memoryMode match { 
  9.       //根据不同的内存模式去创建StorageMemoryPool和ExecutionMemoryPool 
  10.       case MemoryMode.ON_HEAP => ( 
  11.         onHeapExecutionMemoryPool, 
  12.         onHeapStorageMemoryPool, 
  13.         maxOnHeapStorageMemory) 
  14.       case MemoryMode.OFF_HEAP => ( 
  15.         offHeapExecutionMemoryPool, 
  16.         offHeapStorageMemoryPool, 
  17.         maxOffHeapMemory) 
  18.     } 
  19.     if (numBytes > maxMemory) { 
  20.       // 若申请内存大于最大内存,则申请失败 
  21.       logInfo(s"Will not store $blockId as the required space ($numBytes bytes) exceeds our " + 
  22.         s"memory limit ($maxMemory bytes)"
  23.       return false 
  24.     } 
  25.     if (numBytes > storagePool.memoryFree) { 
  26.       // 如果Storage内存池没有足够的内存,则向Execution内存池借用 
  27.       val memoryBorrowedFromExecution = Math.min(executionPool.memoryFree, numBytes)//当Execution内存有空闲时,Storage才能借到内存 
  28.       executionPool.decrementPoolSize(memoryBorrowedFromExecution)//缩小Execution内存 
  29.       storagePool.incrementPoolSize(memoryBorrowedFromExecution)//增加Storage内存 
  30.     } 
  31.     storagePool.acquireMemory(blockId, numBytes) 
  32.   } 

核心方法acquireExecutionMemory:申请Execution内存。

  1. // UnifiedMemoryManage.scala 
  2. override private[memory] def acquireExecutionMemory( 
  3.       numBytes: Long, 
  4.       taskAttemptId: Long, 
  5.       memoryMode: MemoryMode): Long = synchronized {//使用了synchronized关键字,调用acquireExecutionMemory方法可能会阻塞,直到Execution内存池有足够的内存。 
  6.    ... 
  7.     executionPool.acquireMemory( 
  8.       numBytes, taskAttemptId, maybeGrowExecutionPool, computeMaxExecutionPoolSize) 
  9.   } 

方法最后调用了ExecutionMemoryPool的acquireMemory方法,该方法的参数需要两个函数:maybeGrowExecutionPool()和computeMaxExecutionPoolSize()。

每个Task能够使用的内存被限制在pooSize / (2 * numActiveTask) ~ maxPoolSize / numActiveTasks。其中maxPoolSize代表了execution pool的最大内存,poolSize表示当前这个pool的大小。

  1. // ExecutionMemoryPool.scala 
  2.       val maxPoolSize = computeMaxPoolSize() 
  3.       val maxMemoryPerTask = maxPoolSize / numActiveTasks 
  4.       val minMemoryPerTask = poolSize / (2 * numActiveTasks) 

maybeGrowExecutionPool()方法实现了如何动态增加Execution内存区的大小。在每次申请execution内存的同时,execution内存池会进行多次尝试,每次尝试都可能会回收一些存储内存。

  1. // UnifiedMemoryManage.scala  
  2.      def maybeGrowExecutionPool(extraMemoryNeeded: Long): Unit = {  
  3.       if (extraMemoryNeeded > 0) {//如果申请的内存大于0  
  4.         //计算execution可借到的storage内存,是storage剩余内存和可借出内存的最大值  
  5.         val memoryReclaimableFromStorage = math.max(  
  6.           storagePool.memoryFree,  
  7.           storagePool.poolSize - storageRegionSize)  
  8.         if (memoryReclaimableFromStorage > 0) {//如果可以申请到内存  
  9.           val spaceToReclaim = storagePool.freeSpaceToShrinkPool(  
  10.             math.min(extraMemoryNeeded, memoryReclaimableFromStorage))//实际需要的内存,取实际需要的内存和storage内存区域全部可用内存大小的最小值  
  11.           storagePool.decrementPoolSize(spaceToReclaim)//storage内存区域减少  
  12.           executionPool.incrementPoolSize(spaceToReclaim)//execution内存区域增加  
  13.         }  
  14.       }  
  15.     }  

【编辑推荐】

  1. 企业数据资产管理中容灾备份的重要性
  2. Accordion:HBase的 “呼吸式”内存压缩算法
  3. Spark调优的关键—RDD Cache缓存使用详解
  4. 如何在万亿级别规模的数据量上使用 Spark?
  5. 存储管理人员需要保持的技能
【责任编辑:武晓燕 TEL:(010)68476606】


点赞 0
分享:
大家都在看
猜你喜欢
24H热文
一周话题
本月最赞

热门职位+更多

读 书 +更多

计算机网络技术

本书是为北大燕工教育研究院编写的计算机网络技术的学习教材。它以实际教学大纲为依据,全面系统的介绍了计算机网络技术知识,对于一个...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊