【源码剖析】- Spark 新旧内存管理方案(上)

Spark 作为一个以擅长内存计算为优势的计算引擎,内存管理方案是其非常重要的模块。作为使用者的我们,搞清楚 Spark 是如何管理内存的,对我们编码、调试及优化过程会有很大帮助。本文之所以取名为 "Spark 新旧内存管理方案剖析" 是因为在 Spark 1.6 中引入了新的内存管理方案,加之当前很多公司还在使用 1.6 以前的版本,所以本文会对这两种方案进行剖析。

刚刚提到自 1.6 版本引入了新的内存管理方案,但并不是说在 1.6 版本中不能使用旧的方案,而是默认使用新方案。我们可以通过设置 spark.memory.userLegacyMode 值来选择,该值为 false 表示使用新方案,true 表示使用旧方案,默认为 false。该值是如何发挥作用的呢?看了下面的代码就明白了:

val useLegacyMemoryManager = conf.getBoolean("spark.memory.useLegacyMode", false)
val memoryManager: MemoryManager =
  if (useLegacyMemoryManager) {
    new StaticMemoryManager(conf, numUsableCores)
  } else {
    UnifiedMemoryManager(conf, numUsableCores)
  }

根据 spark.memory.useLegacyMode 值的不同,会创建 MemoryManager 不同子类的实例:

  • 值为 false:创建 UnifiedMemoryManager 类实例,该类为新的内存管理模块的实现
  • 值为 true:创建 StaticMemoryManager类实例,该类为旧的内存管理模块的实现

MemoryManager 是用于管理内存的虚基类,声明了一些方法来管理用于 execution 、 storage 的内存和其他内存:

  • execution 内存:用于 shuffles,如joins、sorts 和 aggregations,避免频繁的 IO 而需要内存 buffer
  • storage 内存:用于 caching RDD,缓存 broadcast 数据及缓存 task results
  • 其他内存:在下文中说明

先来看看 MemoryManager 重要的成员和方法:

接下来,来看看 MemoryManager 的两种实现

StaticMemoryManager

spark.memory.userLegacyModetrue 时,在 SparkEnv 中是这样实例化 StaticMemoryManager:

new StaticMemoryManager(conf, numUsableCores)

调用的是 StaticMemoryManager 辅助构造函数,如下:

  def this(conf: SparkConf, numCores: Int) {
    this(
      conf,
      StaticMemoryManager.getMaxExecutionMemory(conf),
      StaticMemoryManager.getMaxStorageMemory(conf),
      numCores)
  }

继而调用主构造函数,如下:

private[spark] class StaticMemoryManager(
    conf: SparkConf,
    maxOnHeapExecutionMemory: Long,
    override val maxStorageMemory: Long,
    numCores: Int)
  extends MemoryManager(
    conf,
    numCores,
    maxStorageMemory,
    maxOnHeapExecutionMemory)

这样我们就可以推导出,对于 StaticMemoryManager,其用于 storage 的内存大小等于 StaticMemoryManager.getMaxStorageMemory(conf);用于 execution 的内存大小等于 StaticMemoryManager.getMaxExecutionMemory(conf),下面进一步看看这两个方法的实现

StaticMemoryManager.getMaxExecutionMemory(conf)

实现如下:

  private def getMaxExecutionMemory(conf: SparkConf): Long = {
    val systemMaxMemory = conf.getLong("spark.testing.memory", Runtime.getRuntime.maxMemory)
    val memoryFraction = conf.getDouble("spark.shuffle.memoryFraction", 0.2)
    val safetyFraction = conf.getDouble("spark.shuffle.safetyFraction", 0.8)
    (systemMaxMemory * memoryFraction * safetyFraction).toLong
  }

若设置了 spark.testing.memory 则以该配置的值作为 systemMaxMemory,否则使用 JVM 最大内存作为 systemMaxMemory。spark.testing.memory 仅用于测试,一般不设置,所以这里我们认为 systemMaxMemory 的值就是 executor 的最大可用内存。

spark.shuffle.memoryFraction:shuffle 期间用于 aggregation 和 cogroups 的内存占 executor 运行时内存的百分比,用小数表示。在任何时候,用于 shuffle 的内存总 size 不得超过这个限制,超出部分会 spill 到磁盘。如果经常 spill,考虑调大 spark.storage.memoryFraction

spark.shuffle.safetyFraction:为防止 OOM,不能把 systemMaxMemory * spark.shuffle.memoryFraction 全用了,需要有个安全百分比

所以最终用于 execution 的内存量为:executor 最大可用内存 * spark.shuffle.memoryFraction * spark.shuffle.safetyFraction,默认为 executor 最大可用内存 * 0.16

需要特别注意的是,即使用于 execution 的内存不够用了,但同时 executor 还有其他空余内存,也不能给 execution 用

StaticMemoryManager.getMaxStorageMemory(conf)

实现如下:

  private def getMaxStorageMemory(conf: SparkConf): Long = {
    val systemMaxMemory = conf.getLong("spark.testing.memory", Runtime.getRuntime.maxMemory)
    val memoryFraction = conf.getDouble("spark.storage.memoryFraction", 0.6)
    val safetyFraction = conf.getDouble("spark.storage.safetyFraction", 0.9)
    (systemMaxMemory * memoryFraction * safetyFraction).toLong
  }

分析过程与 getMaxExecutionMemory 一致,我们得出这样的结论,用于storage 的内存量为: executor 最大可用内存 * spark.storage.memoryFraction * spark.storage.safetyFraction,默认为 executor 最大可用内存 * 0.54

spark.storage.memoryFraction:用于做 memory cache 的内存占 executor 最大可用内存的百分比,该值不应大于老生代

spark.storage.safetyFraction:防止 OOM 的安全比例,由 spark.storage.safetyFraction 控制,默认为0.9。在 storage 中,有一部分内存是给 unroll 使用的,unroll 即反序列化 block,该部分占比由 spark.storage.unrollFraction 控制,默认为0.2

others

从上面的分析我们可以看到,storage 和 execution 总共使用了 80% 的内存,那剩余 20% 去哪了?这部分内存被系统保留了,用来存储运行中产生的对象

所以,各部分内存占比可由下图表示:

经过上面的描述,我们搞明白了旧的内存管理方案是如何划分内存的,也就可以根据我们实际的 app 来调整各个部分的比例。同时,我们可以明显的看到这种内存管理方式的缺陷,即 execution 和 storage 两部分内存固定死,不能共享,即使在一方内存不够用而另一方内存空闲的情况下。这样的方式经常会造成内存浪费,所以有必要引入支持共享,能更好利用内存的方案,UnifiedMemoryManager 就应运而生了


欢迎关注我的微信公众号:FunnyBigData

FunnyBigData
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容