背景概述
Spark中内存主要用于两种情况:一种是用于执行器(aggregation, join, shuffle, sorter)等,另一种是用于存储(主要是RDD中的cache)。早期的spark中执行器内存和存储的内存是分开的,分别会有一个固定的上限(配置好的比例),同样的每个task所能使用的内存的上限也是预定义好的。这样的做法会使得整体性能受配置的影响很大,需要工程师在运行程序之前要仔细了解许多参数,并且 要提前规划好各个组件的内存使用比例,相当繁琐。
为解决这一问题, 在SPARK-10000中提出了统一的内存管理框架,主要的设计如下:
- 消除执行器内存和存储内存的静态限制,只使用一个参数spark.memory.fraction来控制使用的内存大小
- 当执行器内存不足时,会尝试从存储的pool中分配内存,
- 基于上述设计,需要增加一个参数spark.memory.storageFraction来控制storage的保留内存,但这个保留内存是动态的,即当storage没有使用时,execution仍然可以使用全部内存,但是当内存不足且storage的内存少于这个时,execution申请内存会失败
架构描述
- 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的一个调用过程:
-
MemoryConsumer会像TaskMemoryManager发起acquireMemory的请求 -
TaskMemoryManager通过MemoryManager向ExecutionMemoryPool发起请求 -
ExecutionMemoryPool会首先尝试让StorageMemoryPool释放所需要的内存,然后查看可以分配给当前task的内存(allocatedMemory),如果当前task所持有的内存(usedMemory)和allocatedMemory满足如下条件 则继续等待:其中
N是当前运行的task数量,为了保证公平,每个task所申请到的内存数量不会超过executionmemory的 ,即: -
如果从
ExecutionMemoryPool中拿到的内存数量小于所申请的内存数量,则TaskMemoryManager会让当前的task中的其他MemoryConsumer进行spill操作, 尽可能地释放更多的内存