Spark内存管理(3)—— 统一内存管理设计理念

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lemonZhaoTao/article/details/82079375

Spark内存管理系列文章:
Spark内存管理(1)—— 静态内存管理
Spark内存管理(2)—— 统一内存管理

在本文中,将会对各个内存的分布以及设计原理进行详细的阐述
相对于静态内存模型(即Storage和Execution相互隔离、彼此不可拆借),动态内存实现了存储和计算内存的动态拆借:

  • 也就是说,当计算内存超了,它会从空闲的存储内存中借一部分内存使用

  • 存储内存不够用的时候,也会向空闲的计算内存中拆借

值得注意的地方是:

  • 被借走用来执行运算的内存,在执行完任务之前是不会释放内存的

  • 通俗的讲,运行任务会借存储的内存,但是它直到执行完以后才能归还内存

和动态内存相关的参数

  • spark.memory.fraction
    Spark 1.6.1 默认0.75,Spark 2.2.0 默认0.6
    这个参数用来配置存储和计算内存占整个可用内存的比例
    这个参数设置的越低,也就是存储和计算内存占可用的比例越低,就越可能频繁的发生内存的释放(将内存中的数据写磁盘或者直接丢弃掉)
    反之,如果这个参数越高,发生释放内存的可能性就越小
    这个参数的目的是在jvm中留下一部分空间用来保存spark内部数据,用户数据结构,并且防止对数据的错误预估可能造成OOM的风险,这就是Other部分

  • spark.memory.storageFraction
    默认 0.5;在统一内存中存储内存所占的比例,默认是0.5,如果使用的存储内存超过了这个范围,缓存的数据会被驱赶

  • spark.memory.useLegacyMode
    默认false;设置是否使用saprk1.5及以前遗留的内存管理模型,即静态内存模型,前面的文章介绍过这个,主要是设置以下几个参数:

    • spark.storage.memoryFraction
    • spark.storage.safetyFraction
    • spark.storage.unrollFraction
    • spark.shuffle.memoryFraction
    • spark.shuffle.safetyFraction

动态内存设计中的取舍

因为内存可以被Execution和Storage拆借,我们必须明确在这种机制下,当内存压力上升的时候,该如何进行取舍?从三个角度进行分析:

  • 倾向于优先释放计算内存

  • 倾向于优先释放存储内存

  • 不偏不倚,平等竞争

1.释放内存的代价

释放存储内存的代价取决于Storage Level.:

  • 如果数据的存储level是MEMORY_ONLY的话代价最高,因为当你释放在内存中的数据的时候,你下次再复用的话只能重新计算了

  • 如果数据的存储level是MEMORY_AND_DIS_SER的时候,释放内存的代价最低
    因为这种方式,当内存不够的时候,它会将数据序列化后放在磁盘上,避免复用的时候再计算,唯一的开销在I/O

综述:
释放计算内存的代价不是很显而易见

  • 这里没有复用数据重计算的代价,因为计算内存中的任务数据会被移到硬盘,最后再归并起来(后面会有文章介绍到这点)

  • 最近的spark版本将计算的中间数据进行压缩使得序列化的代价降到了最低

值得注意的是:

  • 移到硬盘的数据总会再重新读回来

  • 从存储内存移除的数据也许不会被用到,所以当没有重新计算的风险时,释放计算的内存要比释放存储内存的代价更高(假使计算内存部分刚好用于计算任务的时候)

2.实现复杂度

  • 实现释放存储内存的策略很简单:我们只需要用目前的内存释放策略释放掉存储内存中的数据就好了

  • 实现释放计算内存却相对来说很复杂

这里有2个释放计算内存的思路:

  • 当运行任务要拆借存储内存的时候,给所有这些任务注册一个回调函数以便日后调这个函数来回收内存

  • 协同投票来进行内存的释放

值得我们注意的一个地方是,以上无论哪种方式,都需要考虑一种特殊情况:

  • 即如果我要释放正在运行的计算任务的内存,同时我们想要cache到存储内存的一部分数据恰巧是由这个计算任务产生的

  • 此时,如果我们现在释放掉正在运行的任务的计算内存,就需要考虑在这种环境下会造成的饥饿情况:即生成cache的数据的计算任务没有足够的内存空间来跑出cache的数据,而一直处于饥饿状态(因为计算内存已经不够了,再释放计算内存更加不可取)

  • 此外,我们还需要考虑:一旦我们释放掉计算内存,那么那些需要cache的数据应该怎么办?有2种方案:

    • 最简单的方式就是等待,直到计算内存有足够的空闲,但是这样就可能会造成死锁,尤其是当新的数据块依赖于之前的计算内存中的数据块的时候
    • 另一个可选的操作就是丢掉那些最新的正准备写入到磁盘中的块并且一旦当计算内存够了又马上加载回来。为了避免总是丢掉那些等待中的块,我们可以设置一个小的内存空间(比如堆内存的5%)去确保内存中至少有一定的比例的的数据块

综述:
所给的两种方法都会增加额外的复杂度,这两种方式在第一次的实现中都被排除了
综上目前看来,释放掉存储内存中的计算任务在实现上比较繁琐,目前暂不考虑
即计算内存借了存储内存用来计算任务,然后释放,这种不考虑;计算内存借来内存之后,是可以不还的

结论:
我们倾向于优先释放掉存储内存
即如果存储内存拆借了计算内存,当计算内存需要进行计算并且内存空间不足的时候,优先把计算内存中这部分被用来存储的内存释放掉

可选设计

1.设计方案

结合我们前面的描述,针对在内存压力下释放存储内存有以下几个可选设计:

  • 释放存储内存数据块,完全平滑
    计算和存储内存共享一片统一的区域,没有进行统一的划分:

    • 内存压力上升,优先释放掉存储内存部分中的数据
    • 如果压力没有缓解,开始将计算内存中运行的任务数据进行溢写磁盘
  • 释放存储内存数据块,静态存储空间预留,存储空间的大小是定死的
    这种设计和1设计很像,不同的是会专门划分一个预留存储内存区域:在这个内存区域内,存储内存不会被释放,只有当存储内存超出这个预留区域,才会被释放(即超过50%了就被释放,当然50%为默认值)。这个参数由spark.memory.storageFraction(默认值为0.5,即计算和存储内存的分割线) 配置

  • 释放存储内存数据块,动态存储空间预留
    这种设计于设计2很相似,但是存储空间的那一部分区域不再是静态设置的了,而是动态分配;这样设置带来的不同是计算内存可以尽可能借走存储内存中可用的部分,因为存储内存是动态分配的

结论:最终采用的的是设计3

2.各个方案的优劣

  • 设计1被拒绝的原因
    设计1不适合那些对cache内存重度依赖的saprk任务,因为设计1中只要内存压力上升就释放存储内存

  • 设计2被拒绝的原因
    设计2在很多情况下需要用户去设置存储内存中那部分最小的区域
    另外无论我们设置一个具体值,只要它非0,那么计算内存最终也会达到一个上限,比如,如果我们将存储内存设置为0.6,那么有效的执行内存就是:

    • Spark 1.6.1 可用内存*0.4*0.75
    • Spark 2.2.0 可用内存*0.4*0.6
      那么如果用户没有cache数据,或是cache的数据达不到设置的0.6,那么这种情况就又回到了静态内存模型那种情况,并没有改善什么
  • 最终选择设计3的原因
    设计3就避免了2中的问题只要存储内存有空余的情况,那么计算内存就可以借用
    需要关注的问题是:

    • 当计算内存已经使用了存储内存中的所有可用内存但是又需要cache数据的时候应该怎么处理
    • 最早的版本中直接释放最新的block来避免引入执行驱赶策略(eviction策略,上述章节中有介绍)的复杂性

同时设计3是唯一一个同时满足下列条件的:
1. 存储内存没有上限
2. 计算内存没有上限
3. 保障了存储空间有一个小的保留区域

下一篇文章中,将带来对Spark统一内存管理源码的剖析

猜你喜欢

转载自blog.csdn.net/lemonZhaoTao/article/details/82079375
今日推荐