近期在调实验,发现了一个Java.lang.nullPointerException异常, 导致部分task挂掉,最终通过调节参数内存管理参数成功解决。下面总结一下Spark中的内存管理机制。
从Spark1.6.0开始,Spark的内存管理采用了和之前不同的方式,采用了一种新的内存管理模式叫做统一内存管理,UnifiedMemoryManager。而1.6.0版本之前的Spark的内存管理是静态内存管理,通过实现StaticMemoryManager类(legacy)来实现内存管理。在1.6.0模式下,legacy被默认设置为假。在使用1.6.0以及以后的版本时,如果想使用之前的管理模式,可以通过把参数spark.memory.useLegacyMode为true来实现。
./spark-submit --master spark://master:7077 --conf spark.memory.useLegacyMode=true --conf spark.storage.memoryFraction=0.4 --conf spark.shuffle.memoryFraction=0.4 ...
首先看一下在1.6.0版本之前,对于一个Executor来说,内存中有哪几个组成部分。
1.ExecutorMemory。这片区域主要是供shuffle使用。常见的会产生shuffle的spark算子有reduceByKey,groupByKey,join,sort等等,通过spark.shuffle.memoryFraction来设置(默认为0.2)。如果代码中有大量的shuffle操作,可以调大这一部分所占比重。
2.StorageMemory。这片内存区域只要是供持久化使用,解决block cache(cache,presisit),通过参数spark.storage.memory来设置(默认为0.6),如果有大量的持久化操作,建议调大这一部分的比重。
3.otherMemory。给系统预留的,默认为0.2。
在1.6.0版本之后,采用了统一内存管理模式,首先通过一张图来看一下内存中的三个主要区域(图片来源网络,侵删):
主要分为三部分:Reserved Memory, User Memory和Spark Memory。
1.Reserved Memory。这一部分是为系统预留的,大小也是被硬编码的,在spark1.6.0版本中,它的大小为300M。这一部分是不可能被改变的,除非重新编译源码或者设置spark.testing.reservedMemory来重新改变它的大小(没有这么做的必要),这一部分不会被spark用来干任何事情。
2.User Memory。这一部分是分配Spark Memory内存之后的部分,这一部分用来干什么完全取决于用户,用户可以用它来存储一些数据结构或者保存hash表等。这一部分spark不会管你用来干嘛,也不会管这一部分是否内存溢出。
3.Spark Memory。这一部分是被spark管理的。大小的计算方式为: (“Java Heap” – 300MB) * 0.75。可以通过设置参数spark.memory.fraction=0.8来改变这一部分的比例。例如:如果堆内存大小有4G,将有2847MB的Spark Memory,Spark Memory=(4*1024MB-300)*0.75=2847MB。这部分内存会被分成两部分:Storage Memory和Execution Memory,而且这两部分的边界由spark.memory.storageFraction参数设定,默认是0.5即50%。新的内存管理模型中的优点是,这个边界不是固定的,在内存压力下这个边界是可以移动的。如一个区域内存不够用时可以从另一区域借用内存。
1.Storage Memory。和之前版本的差不多,这一部分用来存储持久化数据,但是他同时也可以用来缓存spark数据也可以用来unroll序列化数据的临时空间。广播变量以block的形式也存储在这里。
2.Execution Memory。这部分内存用存储执行task过程中的一些对象。例如shuffle和map端的中间缓存,也可以用来存储hash aggregation过程的hash table,内存不足的时候溢写到磁盘。
下面介绍一下边界移动的具体方式。从Execution Memory的本质来看,这一部分的数据是不能被挤出去的,因为这一部分存储的是计算的中间结果,如果被挤占,那么将存在数据的丢失问题。但是Storage Memory不一样,如果被挤占,将会重新从RDD中去读取。所以总计起来,记住一点就是:
Execution Memory只要不够用了就可以向Storage Memory挤占空间不管Storage Memory有没有空余,而Storage Memory只能当Execution Memory有空余时才可以借用但不能抢占。
我们同样可以设置参数来控制这两部分的大小。