转载链接:https://blog.csdn.net/wypblog/article/details/104935712/
https://segmentfault.com/a/1190000019157848
为什么会出现OOM?
1,当有多个 Task 同时在 Executor 上执行时, 将会有多个 TaskMemoryManager 共享 MemoryManager 管理的内存。那么 MemoryManager 是怎么分配的呢?答案是每个任务可以分配到的内存范围是 [1 / (2 * n), 1 / n],其中 n 是正在运行的 Task 个数。因此,多个并发运行的 Task 会使得每个 Task 可以获得的内存变小。
2,前面提到,在 MemoryConsumer 中有 Spill 方法,当 MemoryConsumer 申请不到足够的内存时,可以 Spill 当前内存到磁盘,从而避免无节制的使用内存。但是,对于堆内内存的申请和释放实际是由 JVM 来管理的。因此,在统计堆内内存具体使用量时,考虑性能等各方面原因,Spark 目前采用的是抽样统计的方式来计算 MemoryConsumer 已经使用的内存,从而造成堆内内存的实际使用量不是特别准确。从而有可能因为不能及时 Spill 而导致 OOM。
3、Executor 中任务以线程的方式执行,各线程共享JVM的资源(即 Execution 内存),任务之间的内存资源没有强隔离(任务没有专用的Heap区域)。因此,可能会出现这样的情况:先到达的任务可能占用较大的内存,而后到的任务因得不到足够的内存而挂起。
在 Spark 任务内存管理中,使用 HashMap 存储任务与其消耗内存的映射关系。每个任务可占用的内存大小为潜在可使用计算内存( 潜在可使用计算内存为: 初始计算内存 + 可抢占存储内存)的 1/2n ~ 1/n,当剩余内存为小于 1/2n 时,任务将被挂起,直至有其他任务释放执行内存,而满足内存下限 1/2n,任务被唤醒。其中 n 为当前 Executor 中活跃的任务树。
比如如果 Execution 内存大小为 10GB,当前 Executor 内正在运行的 Task 个数为5,则该 Task 可以申请的内存范围为 10 / (2 * 5) ~ 10 / 5,也就是 1GB ~ 2GB 的范围。
任务执行过程中,如果需要更多的内存,则会进行申请,如果存在空闲内存,则自动扩容成功,否则,将抛出 OutOffMemroyError。
解决:
Executor OOM类错误 (错误代码 137、143等)
该类错误一般是由于 Heap(M2)已达上限,Task 需要更多的内存,而又得不到足够的内存而导致。因此,解决方案要从增加每个 Task 的内存使用量,满足任务需求 或 降低单个 Task 的内存消耗量,从而使现有内存可以满足任务运行需求两个角度出发。因此有如下解决方案:
法一:增加单个task的内存使用量
增加最大 Heap值,即上图中 M2 的值,使每个 Task 可使用内存增加。
降低 Executor 的可用 Core 的数量 N , 使 Executor 中同时运行的任务数减少,在总资源不变的情况下,使每个 Task 获得的内存相对增加。当然,这会使得 Executor 的并行度下降。可以通过调高 spark.executor.instances 参数来申请更多的 executor 实例(或者通过
spark.dynamicAllocation.enabled 启动动态分配),提高job的总并行度。
法二:降低单个Task的内存消耗量
降低单个Task的内存消耗量可从配置方式和调整应用逻辑两个层面进行优化:
-
配置方式
减少每个 Task 处理的数据量,可降低 Task 的内存开销,在 Spark 中,每个 partition 对应一个处理任务 Task,因此,在数据总量一定的前提下,可以通过增加 partition 数量的方式来减少每个 Task 处理的数据量,从而降低 Task 的内存开销。针对不同的 Spark 应用类型,存在不同的 partition 配置参数 :
-
P = spark.default.parallism (非SQL应用)
-
P = spark.sql.shuffle.partition (SQL 应用)
(滑动可查看)
通过增加 P 的值,可在一定程度上使 Task 现有内存满足任务运行。注: 当调整一个参数不能解决问题时,上述方案应进行协同调整。
a.调整应用逻辑
Executor OOM 一般发生 Shuffle 阶段,该阶段需求计算内存较大,且应用逻辑对内存需求有较大影响,下面举例就行说明:
选择合适的算子,如 groupByKey 转换为 reduceByKey。
一般情况下,groupByKey 能实现的功能使用 reduceByKey 均可实现,而 ReduceByKey 存在 Map 端的合并,可以有效减少传输带宽占用及 Reduce 端内存消耗。
b.避免数据倾斜 (data skew)
Data Skew 是指任务间处理的数据量存大较大的差异。
如左图所示,key 为 010 的数据较多,当发生 shuffle 时,010 所在分区存在大量数据,不仅拖慢 Job 执行(Job 的执行时间由最后完成的任务决定)。而且导致 010 对应 Task 内存消耗过多,可能导致 OOM。
而右图,经过预处理(加盐,此处仅为举例说明问题,解决方法不限于此)可以有效减少 Data Skew 导致的问题。
NOTE
上述举例仅为说明调整应用逻辑可以在一定程序上解决OOM问题,解决方法不限于次。
(4)Execution Memory Spill Heuristic
-
现象:在 stage 3 发现执行内存溢出。Shuffle read bytes 和 spill 分布均匀。这个 stage 有 200 个 tasks。
-
原因:执行内存溢出,意味着执行内存不足。跟上面的 OOM 错误一样,只是执行内存不足的情况下不会报 OOM 而是会将数据溢出到磁盘。但是整个性能很难接受。
-
解决方案:同 3。
(5) Executor GC Heuristic
-
现象:Executor 花费很多时间在 GC。
-
原因:可以通过-verbose:gc
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps 查看 GC 情况
-
解决方案: Garbage Collection Tuning
(6)Beyond … memory, killed by yarn.
出现该问题原因是由于实际使用内存上限超过申请的内存上限而被 Yarn 终止掉了, 首先说明 Yarn 中 Container 的内存监控机制:
-
Container 进程的内存使用量 : 以 Container 进程为根的进程树中所有进程的内存使用总量。
-
Container 被杀死的判断依据 : 进程树总内存(物理内存或虚拟内存)使用量超过向 Yarn 申请的内存上限值,则认为该 Container 使用内存超量,可以被“杀死”。
因此,对该异常的分析要从是否存在子进程两个角度出发。
a. 不存在子进程
Overhead) 不足,依据 Yarn 内存使用情况有如下两种方案:
法一:如果,M (spark.executor.memory) 未达到 Yarn 单个 Container 允许的上限时,可仅增加 M1(spark.yarn.executor.memoryOverhead),从而增加 M;如果,M 达到 Yarn 单个 Container 允许的上限时,增加 M1,降低 M2。
注意二者之各要小于 Container 监控内存量,否则伸请资源将被 yarn 拒绝。
法二:减少可用的 Core 的数量 N,使并行任务数减少,从而减少 Overhead 开销
b. 存在子进程
Spark 应用中 Container 以 Executor(JVM进程)的形式存在,因此根进程为 Executor 对应的进程,而 Spark 应用向Yarn申请的总资源 M = M1 + M2,都是以 Executor(JVM) 进程(非进程树)可用资源的名义申请的。
申请的资源并非一次性全量分配给 JVM 使用,而是先为 JVM 分配初始值,随后内存不足时再按比率不断进行扩容,直致达到 Container 监控的最大内存使用量 M。当 Executor 中启动了子进程(如调用 shell 等)时,子进程占用的内存(记为 S)就被加入 Container 进程树,此时就会影响 Executor 实际可使用内存资源(Executor 进程实际可使用资源变为: M - S),然而启动 JVM 时设置的可用最大资源为 M,且 JVM 进程并不会感知 Container 中留给自己的使用量已被子进程占用。因此,当 JVM 使用量达到 M - S,还会继续开劈内存空间,这就会导致 Executor 进程树使用的总内存量大于 M 而被 Yarn 杀死。
典形场景有:
-
PySpark(Spark已做内存限制,一般不会占用过大内存)
-
自定义Shell调用
其解决方案分别为:
a. PySpark场景:
-
如果,M 未达到 Yarn 单个 Container 允许的上限时,可仅增加 M1 ,从而增加 M;如果,M 达到 Yarn 单个 Container 允许的上限时,增加 M1,降低 M2。
-
减少可用的 Core 的数量 N,使并行任务数减少,从而减少 Overhead 开销
b. 自定义 Shell 场景:(OverHead 不足为假象)