Driver和Executor都是Jvm进程,运行于yarn/k8s中,因此Spark内存管理会涉及Driver端和Executor这两种进程中内存的申请和回收操作。
Driver端和Executor端都有自己的内存空间,内存管理统一由MemoryManager统一管理。
统一内存管理
在Spark1.6之前,采用的是静态内存管理(StaticMemoryManager), 从1.6开始默认采用统一内存管理(UnifiedMemoryManager). 相比静态内存管理方式,统一内存管理方式中存储和计算内存能够共享同一空间,并且可以动态使用彼此的空闲区域。一下我们以统一内存管理来说。 Memory将内存模式分为堆内(ON_HEAP) 和堆外(OFF_HEAP)。
堆内内存
因为Executor的运行于Jvm之上,对象实例占用内存的申请和释放都由Jvm完成,只能在申请和释放后记录这些内存。因此 Spark对堆内内存的管 理只是在逻辑上进行记录和规划。
在 JVM 中,对象可以以序列化的方式存储, 在访问时需要进行序 列化的逆过程一一反序列化 , Spark 中序列化的对象是字节流的形式,其占用的内存大小可直接计 算,而对于反序列化的对象,其占用的内存是通过周期性地采样近似估算而得的,并不是每次 新增的数据项都会计算一次占用的内存大小,这种方法降低了时间开销,但是有可能误差较大,因此可能导致某一时刻的实际内存可能远远超出预期。
堆内内存依赖于 JVM,无法完全摆脱 GC 带来的开销, 在被Spark标记为释放的对象实例,很有可能在实际上并没有被JVM回收,导致实际可用的内存小于Spark记录的可用内存。所以Spark并不能准确记录实际可用的堆内内存,从而也就无法完全避免内存溢出(OOM)的异常。
Spark通过对存储内存和执行内存各自独立的规划管理,可以决定是否要在存储内存里缓存新的RDD,以及是否为新的任务分配执行内存,在一定程度上可以提升内存的利用率,减少异常的出现。
堆外内存
不在 JVM 内申请内存,而是调用 Java 的 unsafe 相关 API 进行申请,可以避免频繁的GC, 堆外内存可以被精确地申请和释放,而且序列化的数据占用的空间可以被精确计算,所以相比堆内内存来说降低了管理的难度。** 为了进一步优化内存的使用以及提高Shuffle时排序的效率,Spark引入了堆外(Off-heap)内存。使之可以直接在工作节点的系统内存中开辟空间,存储经过序列化的二进制数据。
spark官方建议谨慎使用堆外内存,因为堆内和堆外不互通,tungsten对堆内的优化足以应付。
统一内存模型
堆内内存模型
在堆内内存中,MemoryManager将内存分为存储内存,执行内存,其他,预留内存。
堆外内存模型
在堆外内存中,MemoryManager将内存分为存储内存,执行内存。而堆外内存的开启方式由spark.unsafe.offHeap=true, spark.memory.offHeap.size=2g,来管理。 Executor 中的 Execution 内存是堆内的 Execution 内存和堆外的 Execution 内存之和,同理,Storage 内存也一样。
-
spark.memory.offHeap.enabled 等价于 spark.unsafe.offHeap -
spark.executor.memoryOverhead/ spark.driver.memoryOverhead作用于yarn/k8s上的堆外内存,是executor/driver所需要的额外的内存开销, 用来保证稳定性,主要存储nio buffer, 这部分内存在堆内和堆外都用不到,spark也用不到所以不用关系。 -
spark.memory.offHeap.size 想用堆外让spark管理数据,只有这个配置, 作用于executor的堆外内存,在spark3之前spark.executor.memoryOverhead和spark.memory.offHeap.size并没有进行区分,在spark-3.0上面又有一些差异,spark.memory.offHeap.size和spark.executor.memoryOverhead单独进行区分。 具体可以参考:https://stackoverflow.com/questions/58666517/difference-between-spark-yarn-executor-memoryoverhead-and-spark-memory-offhea -
SystemReserved(预留内存) 系统会预留300MB内存,留出充足空间,防止OOM, -
UnifiedMemory(统一内存):在剩余的可用内存( (JVM heap space - 300M) )中, 有spark.memory.fraction(默认0.6)会被分配到storage和Execution区域, 用于存储和计算, 其中,storage和Execution各占50%, 即默认为总堆上内存的0.6*0.5=0.3。 -
Other(其他内存): 在 (JVM heap space - 300MiB) * (1-spark.memory.fraction,默认0.4)就是Other区域, 用于进行spark内部元数据, 用户数据结构、以及在大数据情况下防止OOM错误的使用。
内存区域存储对象
堆内和堆外的execution内存和storage内存使用场景几乎一直,堆内内存因为依赖JVM的GC所以不能保证内存空间已经被回收,堆外内存因为手动进行内存的分配和释放,所以Spark能准确预估内存使用量。
堆内和堆外的内存不能互相借用。比如一个task在使用了堆外内存之后,发现堆外内存不够了,但是堆内是足够的,但是是不会借用堆内的,只能OOM. Tungsten统一了内存管理,使用page来管理内存,这样做的目的主要是统一内存对象。对于堆内来讲page就是大对象,对于堆外来讲page是os的内存寻址,因此两块内存是不能同时使用的。
execution内存
主要用于shuffle,join, sort, aggregation计算的临时数据。
storage内存
主要存储spark的缓存数据,rdd缓存,广播变量等。
统一内存的计算逻辑
storage存储内存
jvm堆内内存 * spark.memory.fraction * spark.storage.memoryFraction
- jvm堆内内存 = spark.executor.memory,一般而言会稍微小一点,和jvm分配有关系。
- spark.memory.fraction 默认是0.6。 统一内存executor,storage占用的空间。其他0.4包括预留内存和其他内存,主要是为了防止OOM和用户定义的数据结构存储。
- spark.storage.memoryFraction 默认是0.5。 存储内存占用的空间。
在线上的场景下,可以根据任务的状态设置,比如执行比例较高,就把spark.storage.memoryFraction调低, 如果gc较少,spark.memory.fraction调高,以目前各大厂商提供的机器配置,大部分场景下,限制性能主要是CPU,而不是内存,所以可以根据情况调高spark.memory.Fraction.降低spark.memory.storageFraction。
execution执行内存
jvm堆内内存 * spark.memory.fraction * (1-spark.storage.memoryFraction)
动态占用机制
统一 内存管理最大的特点在于动态占用机制,Execution 内存和 Storage 内存可以互相共享的。也就是说,如果 Execution 内存不足,而 Storage 内存有空闲,那么 Execution 可以从 Storage 中申请空间;反之亦然。其中的规则如下:
- **双方的空间都不足时,则存储到硬盘,若己方空间不足而对方空间空余时,可借用对方的空间。
- 执行内存的空间被存储占用后,可让存储将占用的部分转存到硬盘,然后“归还”借用的空间。
- 存储内存的空间被执行占用后,无法让执行占用内存“归还”,因为需要考虑 Shuffle过程中的很多因素**
执行内存管理
Executor 内运行的任务同样共 享执行内存 。 假设当前 Executor 中正在执行的任务数目为 n,那么每个任务可占用的执行内存大小的范围为[ l/缸, 1/叫。 每个任务在启动时,要向 MemoryManager 申请最少 l/2n 的执行内存,如果不能满足要求, 则该任务被阻塞,直到有其 他任务释放了足够的执行内 存 , 该任务才能被唤醒 。 在执行期间, Executor 中活跃的任务数目 是不断变化的, Spark采用 wait和 notifyAll机制同步状态并重新计算 n 的值。
内存常见报错
OOM的问题通常出现在execution这块内存中,因为storage这块内存在存放数据满了之后,会直接丢弃内存中旧的数据,对性能有影响但是不会有OOM的问题
内存溢出
- java.lang.OutOfMemoryError: GC overhead limit execeeded
- java.lang.OutOfMemoryError: Java heap space
- Container killed by YARN for exceeding memory limits. 1*.4 GB of 1* GB physical memory used.
- shuffle file cannot find,
- executor lost、
- task lost
该类错误一般是由于Heap已达上限,Task需要更多的内存,而又得不到足够的内存而导致。因此,解决方案要从增加每个Task的内存使用量,满足任务需求 或 降低单个Task的内存消耗量。
- 增加单个task的内存使用量
- 降低单个Task的内存消耗量(增加executor的并发度,降低每个task任务处理数据量)
参考
https://blog.csdn.net/pre_tender/article/details/101517789 https://zhuanlan.zhihu.com/p/354816279
|