Spark中的统一内存管理框架

138 阅读2分钟

背景概述

Spark中内存主要用于两种情况:一种是用于执行器(aggregation, join, shuffle, sorter)等,另一种是用于存储(主要是RDD中的cache)。早期的spark中执行器内存和存储的内存是分开的,分别会有一个固定的上限(配置好的比例),同样的每个task所能使用的内存的上限也是预定义好的。这样的做法会使得整体性能受配置的影响很大,需要工程师在运行程序之前要仔细了解许多参数,并且 要提前规划好各个组件的内存使用比例,相当繁琐。

为解决这一问题, 在SPARK-10000中提出了统一的内存管理框架,主要的设计如下:

  • 消除执行器内存和存储内存的静态限制,只使用一个参数spark.memory.fraction来控制使用的内存大小
  • 当执行器内存不足时,会尝试从存储的pool中分配内存,
  • 基于上述设计,需要增加一个参数spark.memory.storageFraction来控制storage的保留内存,但这个保留内存是动态的,即当storage没有使用时,execution仍然可以使用全部内存,但是当内存不足且storage的内存少于这个时,execution申请内存会失败

架构描述

Slide1.jpg

  • MemoryManager: 一个进程只有一个实例,包含execution memory pool(on heap, off heap)和storage memory pool(on heap, off heap)
  • StorageMemoryPool: 用于管理storage memory,主要是RDD的Cache
  • ExecutionMemoryPool:用于在不同的Task之间分配内存
  • MemoryConsumer: 需要分配内存的组件,比如HashMap, Sorter, Shffule等
  • TaskMemoryManager: 管理一个Task中的内存,包含一个Task中所有的Memory Consumer,并在适当的时候协调Memory Consumer进行spill来释放内存。

执行器的内存管理

由于Storage的内存管理比较简单,这里我们不去做讨论。Spark中的Task内存分配都是通过继承MemoryConsumer这个类来实现的,下图所示是acquireExecutionMemory的一个调用过程:

Spark_memory_management.png

  1. MemoryConsumer会像TaskMemoryManager发起acquireMemory的请求

  2. TaskMemoryManager通过MemoryManagerExecutionMemoryPool发起请求

  3. ExecutionMemoryPool会首先尝试让StorageMemoryPool释放所需要的内存,然后查看可以分配给当前task的内存(allocatedMemory),如果当前task所持有的内存(usedMemory)和allocatedMemory满足如下条件 则继续等待:

    (usedMemory+allocatedMemory)<(executionMemory/(2N))(usedMemory + allocatedMemory) \lt (executionMemory / (2*N))

    其中N是当前运行的task数量,为了保证公平,每个task所申请到的内存数量不会超过executionmemory的 1/N1/N,即:

    (usedMemory+allocatedMemory)<(executionMemory/N)(usedMemory + allocatedMemory) < (executionMemory/N)
  4. 如果从ExecutionMemoryPool中拿到的内存数量小于所申请的内存数量,则TaskMemoryManager会让当前的task中的其他MemoryConsumer进行spill操作, 尽可能地释放更多的内存

References