在理解OOM问题得先理解spark 的内存。
Spark 内存区域划分
spark的Executor的Container内存有两大部分组成:堆外内存和Executor内存。
堆外内存
spark.executor.memoryOverhead:spark.yarn.executor.memoryOverhead在2.3被废弃,使用前者代替,但是spark也提供了向后兼容。默认为0.01.
堆内内存
在一个Executor中,spark会把内存分为4个区域,Reserved Memory、User Memory、Execution Memory 和 Storage Memory。
接着对其具体说一下,
- Reserved Memory 固定为 300MB,不受开发者控制,它是 Spark 预留的、用来存储各种 Spark 内部对象的内存区域;
- User Memory 用于存储开发者自定义的数据结构,例如 RDD 算子中引用的数组、列表、映射等等。
- Execution Memory 用来执行分布式任务。分布式任务的计算,主要包括数据的转换、过滤、映射、排序、聚合、归并等环节,而这些计算环节的内存消耗,统统来自于 Execution Memory。
- Storage Memory 用于缓存分布式数据集,比如 RDD Cache、广播变量等等。
在上面的四个,我们可以知道,Execution Memory 和 Storage Memory 这两块内存区域,对于 Spark 作业的执行性能起着举足轻重的作用。在所有的内存区域中,Execution Memory 和 Storage Memory 是最重要的。
在 Spark 1.6 版本之前,Execution Memory 和 Storage Memory 的空间划分是静态的,一旦空间划分完毕,不同内存区域的用途与尺寸就固定了。也就是说,即便你没有缓存任何 RDD 或是广播变量,Storage Memory 区域的空闲内存也不能用来执行映射、排序或聚合等计算任务,宝贵的内存资源就这么白白地浪费掉了。
考虑到静态内存划分的弊端,在 1.6 版本之后,Spark 推出了统一内存管理模式,在这种模式下,Execution Memory 和 Storage Memory 之间可以相互转化。
Execution Memory 和 Storage Memory 之间的抢占规则,一共可以总结为 3 条:
- 如果对方的内存空间有空闲,双方可以互相抢占;
- 对于 Storage Memory 抢占的 Execution Memory 部分,当分布式任务有计算需要时,Storage Memory 必须立即归还抢占的内存,涉及的缓存数据要么落盘、要么清除;
- 对于 Execution Memory 抢占的 Storage Memory 部分,即便 Storage Memory 有收回内存的需要,也必须要等到分布式任务执行完毕才能释放。
Spark内存配置
总体来说,Executor JVM Heap 的划分,由图中的 3 个配置项来决定:
其中 spark.executor.memory 是绝对值,它指定了 Executor 进程的 JVM Heap 总大小。另外两个配置项,spark.memory.fraction 和 spark.memory.storageFraction 都是比例值,它们指定了划定不同区域的空间占比。
spark.memory.fraction 用于标记 Spark 处理分布式数据集的内存总大小,(M – 300)* mf,这部分内存包括 Execution Memory 和 Storage Memory 两部分,也就是图中绿色的矩形区域。(M – 300)* (1 – mf)刚好就是 User Memory 的区域大小,也就是图中蓝色区域的部分。
spark.memory.storageFraction 则用来进一步区分 Execution Memory 和 Storage Memory 的初始大小。Reserved Memory 固定为 300MB。(M – 300)* mf * sf 是 Storage Memory 的初始大小,相应地,(M – 300)* mf * (1 – sf)就是 Execution Memory 的初始大小。
熟悉了以上 3 个配置项,作为开发者,我们就能有的放矢地去调整不同的内存区域,从而提升内存的使用效率。
在了解了spark中的内存之后,我们可以很号的理解OOM问题。
spark的OOM问题
关于spark中出现OOM,我们可以大致分为三种情况:
- map执行内存溢出
- shuffle后内存溢出
- driver 内存溢出
- map执行中内存溢出代表了所有map类型的操作。包括:flatMap,map等。
- shuffle后内存溢出的shuffle操作包括join,reduceByKey,repartition等操作。
- driver内存溢出 1.用户在 Dirver 端口生成大对象,比如创建了一个大的集合数据结构。2.从 Executor 端收集数据(collect)回 Dirver 端。
解决方法
在上面我知道内存分为Reserved Memory、User Memory、Execution Memory 和 Storage Memory。
- Execution Memory,shuffle的数据也会先缓存在这个内存中,满了再写入磁盘中、排序、map的过程也是在这个内存中执行的、聚合、计算的内存。
- Storage Memory,用于集群中缓存RDD和传播内部数据的内存(cache、persist数据的地方、广播变量)
- 另外两个为其它,程序执行时预留给自己的内存,如spark程序的对象。
1、map过程产生大量对象导致内存溢出
这种溢出的原因:map 端过程产生大量对象导致内存溢出:这种溢出的原因是在单个map 中产生了大量的对象导致的。
解决方法:
- 增加堆内内存。
- 在不增加内存的情况下,可以减少每个 Task 处理数据量,使每个 Task产生大量的对象时,Executor 的内存也能够装得下。具体做法可以在会产生大量对象的 map 操作之前调用 repartition 方法,分区成更小的块传入 map。
2、数据不平衡导致内存溢出
数据不平衡除了可能导致内存溢出外,也可能导致性能的问题。
解决方法:
- 解决方法和上面的类似。
3、shuffle后内存溢出
shuffle 后内存溢出如 join,reduceByKey,repartition。shuffle内存溢出的情况可以说都是shuffle后,shuffle会产生数据倾斜,少数的key内存非常的大,它们都在同一个Executor中计算,导致运算量加大甚至会产生OOM。
解决方法:
- 在 shuffle 的使用,需要传入一个 partitioner,大部分 Spark 中的shuffle 操作,默认的 partitioner 都是 HashPatitioner,默认值是父RDD 中最大的分区数.这个参数 spark.default.parallelism 只对HashPartitioner 有效.如果是别的 partitioner 导致的 shuffle 内存溢出就需要重写 partitioner 代码了.
4、 driver 内存溢出
- 用户在 Dirver 端口生成大对象,比如创建了一个大的集合数据结构。
解决方法:
- 将大对象转换成 Executor 端加载,比如调用 sc.textfile 或 者评估大对象占用的内存,增加 dirver 端的内存
- 从 Executor 端收集数据(collect)回 Dirver 端。 解决方法:
- 建议将 driver 端对 collect 回来的数据所作的操作,转换成 executor 端 rdd 操作。