【SparkCore】RDD缓存优化、序列化、Spark内存管理、spark内存优化之GC调优、数据本地化

目录

一、RD缓存

  1、cache/persist缓存

  2、那么StorageLevel存储级别有哪些呢

二、内存优化之序列化

三、内存优化之Spark内存管理

  1、StaticMemoryManager管理器

  2、UnifiedMemoryManager 统一内存管理器

四、spark内存优化之GC调优

五、数据本地化


一、RD缓存

RDD Persistence

 

1、cache/persist缓存

背景:

1)对相同的RDD做action操作的时候,需要重新计算RDD:耗资源、耗时间。

2)RDD某分区数据丢失,需要分区重算。

所以可以使用cache/persist将RDD缓存进executor内存,可以很大程度的节省计算和程序运行时间。

 

执行:

rdd.cache()  或者使用  rdd.persist(StorageLevel.MEMORY_ONLY)

rdd.count()

1)cache是lazy的,需要触发一个action,才会产生job。

2)在Spark UI的storage页签中可以看到RDD占用的内存大小,但是默认比原来的文件大

3)用完记得要释放内存:rdd.unpersist(true)  这个是立刻发生的。

如何查看RDD占用executor内存大小?

两种方式:
     1)第一种:执行rdd.cache、rdd.count,查看Spark UI界面上的storage页签。
     2)第二种:
          import org.apache.spark.util.SizeEstimator
          SizeEstimator.estimate(filepath) 

cache进内存,为什么占用的内存变大了?(了解)

默认使用MEMORY_ONLY存储策略,存进executor内存,没有序列化。所以要考虑:
 1)一个java对象中包含头信息(16字节)、指向对象的指针信息。如果一个java对象是很小的数据,比如int类型,如果用一个对象来存储,由于有头+指针信息,就会造成一个小对象占用过多信息的情况。比如一个2字节的数据存储为一个java对象,那么占用的内存就成了16字节+指针,就占用多了。
2)对于String类型,默认会多出来40个字节,存储每一个字符的时候会占用两个字节,因为底层使用的是UTF-16进行编码的,
3)对于集合的类,例如HashMap、LikedList要看底层源码,面试会问,底层使用的是Linked的数据结构,这种是包装过后的对象,比如MashMap底层是使用了Map.Entry的。这种集合的对象不仅有头,还是指针信息(8个字节)指向下一个对象。所以使用集合也会多出来很多信息。
4)集合中存储的原生类型数据,在集合中还会进行box包装的,比如int age = 10 会转为Integer age = new Integer(10)。

 

persist()与cacehe()有什么区别呢?

1)cache源码:def cache(): this.type = persist() 底层调用persist

2)默认使用StorageLevel.MEMORY_ONLY存储策略:def persist(): this.type = persist(StorageLevel.MEMORY_ONLY) 

3)注意:RDD和Spark SQL默认使用的存储级别都是MEMORY_ONLY,但是Streaming默认使用的存储级别是MEMORY_ONLY_SER。

 

2、那么StorageLevel存储级别有哪些呢

StorageLevel存储级别来决定RDD存储在内存、磁盘、、是否开启堆外内存、是否开启序列化、副本等。

Class StorageLevel定义:

class StorageLevel private(
        private var _useDisk: Boolean,               # 是否使用磁盘
        private var _useMemory: Boolean,         #是否使用内存
        private var _useOffHeap: Boolean,        #是否使用堆外Heap(不受GC管理)
        private var _deserialized: Boolean,        #是否使用反序列化的方式进行存储
        private var _replication: Int = 1)              #副本因子
extends Externalizable {

Object StorageLevel定义的存储级别:

      val NONE = new StorageLevel(false, false, false, false)
      val DISK_ONLY = new StorageLevel(true, false, false, false)
      val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)
      val MEMORY_ONLY = new StorageLevel(false, true, false, true)
      val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)
      val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)
      val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)
      val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)
      val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)
      val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)
      val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)
      val OFF_HEAP = new StorageLevel(true, true, true, false, 1)

 

1、理解一下:

1)MEMORY_ONLY:val MEMORY_ONLY=new StorageLevel(false,true,false,true) 表示:使用内存、不序列化、1个副本。

                                 RDD默认的存储策略。如果内存不够,某些分区不会被缓存,RDD.action要重新计算未存储的部分分区。

                                在Spark UIstorage页签中也可以看到StorageLevel。

2)MEMORY_ONLY_2:这种带有尾巴的就是副本数不一样。     

3)MEMORY_AND_DISK:内存存不够的,存磁盘上,必定慢

4)MEMORY_ONLY_SER:存内存,开启序列化存储。

                                              序列化必然省内存,但是耗CPU,所以要考虑CPU负载。

cache默认使用StorageLevel.MEMORY_ONLY存储策略,即内存+反序列化存储+1个副本。

 

2、这么多的缓存存储级别,使用的时候要怎么选择呢?

spark的存储级别旨在在内存使用率和CPU效率之间提供不同的权衡。

1)优先选择MEMORY_ONLY:但是不使用序列化且纯用内存,大数据场景使用有限,会OOM

2)次选MEMORY_ONLY_SER:自己去搭配一个快速的序列化library

                                                     纯使用内存,序列化存储更节省空间。

                                                     但是:序列化耗CPU、内存太大时有OOM,需要考虑调executor内存(Spark统一内存管理)。

3)注意:一般不用磁盘,太慢。

                一般存内存,资源有限,不用副本。

二、内存优化之序列化

Tuning Spark、Data Serialization

前面介绍了RDD存储进内存,默认使用MEMORY_ONLY机制,占用内存空间更大,所以最好是使用序列化的方式。

序列化在分布式系统中是必须的。因为设计到网络传输,必然用序列化的方式。

 

序列化有两种:
1)Java serializatio 不推荐
                     默认使用的序列化方式
                     使用:实现java.io.Serializable接口
                     缺点:慢、对象会变大。
2)Kryo serialization 推荐
                   优点:快、紧凑(小)
                   注意:需要注册类型才能实现上述效果。

 

使用Kryo序列化的几种方式:
1)第一种方式:代码中添加
sparkConf.set("spark.serializer","org.apache.spark.serializer.KryoSerializer")
sparkConf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2])) 

2)第二种方式:配置文件
vi $SPARK_HOME/conf/spark-defaults.conf
spark.serializer    org.apache.spark.serializer.KryoSerializer
代码中:conf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2])) 

3)第三种方式:作为提交参数
提交参数:./spark-submit --conf spark.serializer=org.apache.spark.serializer.KryoSerializer
代码:conf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2])) 

例子:

def main(args:Array[String]):Unit={
        val sparkConf = new SparkConf()
        sparkConf.setAppName("LogApp").setMaster("local[2]")

        sparkConf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
        sparkConf.registerKryoClasses(Array(classOf[Info]))
        val sc = new SparkContext(sparkConf)
        sc.stop() 
}
class Info{} 

 

准备一个151M的测试文件,测试序列化的效果:

1)情况1:
MEMORY_ONLY+java序列化
MEMORY_ONLY+Kryo序列化
MEMORY_ONLY+Kryo序列化+注册类
结果都是占用190M内存。因为使用MEMORY_ONLY存储策略,即反序列化存储,占用内存变大,所以这几种结果都是一样的

A。MEMORY_ONLY+java序列化:

package com.test
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.storage.StorageLevel
import scala.collection.mutable.ListBuffer
object SparkTest {
      def main(args:Array[String]):Unit = {
            val sparkConf = new SparkConf().setAppName("SparkTest").setMaster("local[2]")
            val sc = new SparkContext(sparkConf)
            val users = new ListBuffer[User]
            for(i <- 1 to 10000000){
              users.+=(new User(i,"name"+i,i.toString))
            }
            val usersRDD=sc.parallelize(users)
            usersRDD.persist(StorageLevel.MEMORY_ONLY)
            println(usersRDD.count)

            Thread.sleep(1000000)
            sc.stop()
      }
      class User(id:Int,username:String,age:String) extends Serializable {} //需要java序列化
}

B。MEMORY_ONLY+Kryo序列化:
加入:sparkConf.set("spark.serializer","org.apache.spark.serializer.KryoSerializer")
去掉:extends Serializable

C。MEMORY_ONLY+Kryo序列化+注册类:
加入:
sparkConf.set("spark.serializer","org.apache.spark.serializer.KryoSerializer")
sparkConf.registerKryoClasses(Array(classOf[User]))
去掉:extends Serializable

2)情况2:
MEMORY_ONLY_SER+java序列化      60.7M
MEMORY_ONLY_SER+Kryo序列化     257.5M
MEMORY_ONLY_SER+Kryo序列化+注册类  19.1M
传输的类必须使用java序列化或者Kryo序列化。不能什么序列化都不用。
另外可以知道使用Kryo序列化必须注册,否则占用内存会变大。

A。MEMORY_ONLY_SER+java序列化:
package com.test
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.storage.StorageLevel
import scala.collection.mutable.ListBuffer
object SparkTest {
      def main(args:Array[String]):Unit = {
            val sparkConf = new SparkConf().setAppName("SparkTest").setMaster("local[2]")
            val sc = new SparkContext(sparkConf)
            val users = new ListBuffer[User]
            for(i <- 1 to 10000000){
              users.+=(new User(i,"name"+i,i.toString))
            }
            val usersRDD=sc.parallelize(users)
            usersRDD.persist(StorageLevel.MEMORY_ONLY_SER)
            println(usersRDD.count)
            Thread.sleep(1000000)
            sc.stop()
      }
      //传输的类必须使用java序列化或者Kryo序列化。不能什么序列化都不用。
      class User(id:Int,username:String,age:String) extends Serializable {}
}

B。MEMORY_ONLY_SER+Kryo序列化:
加入:sparkConf.set("spark.serializer","org.apache.spark.serializer.KryoSerializer")
去掉:extends Serializable

C。MEMORY_ONLY_SER+Kryo序列化+注册类:
加入:
sparkConf.set("spark.serializer","org.apache.spark.serializer.KryoSerializer")
sparkConf.registerKryoClasses(Array(classOf[User]))
去掉:extends Serializable

总结:
最优:MEMORY_ONLY_SER+Kryo序列化+注册类:节省7倍多的内存。
次优:MEMORY_ONLY_SER+java序列化 :             节省2倍多的内存。
次次优:MEMORY_ONLY 不序列化:                        会增加内存。
次次次优:MEMORY_ONLY_SER+Kryo序列化:      会更增加内存。

注意:
1)使用序列化的缺点:耗费CPU,如果CPU负载很高了,建议还是不要再用序列化了。
2)如果对象很大,可能还需要增加spark.kryoserializer.buffer配置。此值必须足够大,以容纳要序列化的最大对象。
3)自从Spark 2.0.0以来,RDD中简单类型、简单类型数组或字符串类型的简单类型,在内部已经使用了Kryo序列化器,在sc初始化的时候被注册进去了:
private[spark] class SerializerManager(
    defaultSerializer: Serializer,
    conf: SparkConf,
    encryptionKey: Option[Array[Byte]]) {
  private[this] val kryoSerializer = new KryoSerializer(conf)
  private[this] val primitiveAndPrimitiveArrayClassTags: Set[ClassTag[_]] = {
    val primitiveClassTags = Set[ClassTag[_]](
      ClassTag.Boolean,
      ClassTag.Byte,
      ClassTag.Char,
      ClassTag.Double,
      ClassTag.Float,
      ClassTag.Int,
      ClassTag.Long,
      ClassTag.Null,
      ClassTag.Short
    )


三、内存优化之Spark内存管理

也就是调节executor的内存。

spark中的内存使用主要分为两类:
1)执行内存:做计算:比如shuffle、join、sorts、aggregations。
2)存储内存:做存储:比如cache数据、广播数据。

 

SparkEnv确定了MemoryManager抽象类的具体实现:

val useLegacyMemoryManager = conf.getBoolean("spark.memory.useLegacyMode", false)
val memoryManager: MemoryManager = 
    if (useLegacyMemoryManager) {
        new StaticMemoryManager(conf, numUsableCores)   静态内存管理器Spark1.6之前的
    } else {
        UnifiedMemoryManager(conf, numUsableCores)        统一内存管理器Spark1.6+开始的
    }
}

 

1、StaticMemoryManager管理器

StaticMemoryManager.getMaxExecutionMemory(conf)   拿到最大存储内存
StaticMemoryManager.getMaxStorageMemory(conf)      拿到最大执行内存

1)getMaxExecutionMemory源码:
private def getMaxExecutionMemory(conf: SparkConf): Long = {

        val systemMaxMemory = conf.getLong("spark.testing.memory", Runtime.getRuntime.maxMemory)
        if (systemMaxMemory < MIN_MEMORY_BYTES) {//MIN_MEMORY_BYTES=32M
          throw new IllegalArgumentException(s"System memory $systemMaxMemory must " +
            s"be at least $MIN_MEMORY_BYTES. Please increase heap size using the --driver-memory " +
            s"option or spark.driver.memory in Spark configuration.")
        }
      
        if (conf.contains("spark.executor.memory")) {
          val executorMemory = conf.getSizeAsBytes("spark.executor.memory")
          if (executorMemory < MIN_MEMORY_BYTES) {
            throw new IllegalArgumentException(s"Executor memory $executorMemory must be at least " +
              s"$MIN_MEMORY_BYTES. Please increase executor memory using the " +
              s"--executor-memory option or spark.executor.memory in Spark configuration.")
          }
        }

        val memoryFraction = conf.getDouble("spark.shuffle.memoryFraction", 0.2) 
        val safetyFraction = conf.getDouble("spark.shuffle.safetyFraction", 0.8) 

        //(假设系统内存是10G)10G * 0.2 * 0.8 = 1.6G
        (systemMaxMemory * memoryFraction * safetyFraction).toLong
}

2)getMaxStorageMemory源码:
StaticMemoryManager.getMaxStorageMemory(conf) {
        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)

        // (假设系统内存是10G)10G * 0.6 * 0.9 = 5.4G
        (systemMaxMemory * memoryFraction * safetyFraction).toLong  
}

总结:

1)执行内存:
val memoryFraction = conf.getDouble("spark.shuffle.memoryFraction", 0.2) 
val safetyFraction = conf.getDouble("spark.shuffle.safetyFraction", 0.8) 
val systemMaxMemory =  "spark.driver.memory"值
(systemMaxMemory * memoryFraction * safetyFraction).toLong

2)存储内存:
val memoryFraction = conf.getDouble("spark.storage.memoryFraction", 0.6)
val safetyFraction = conf.getDouble("spark.storage.safetyFraction", 0.9)
val systemMaxMemory = "spark.testing.memory"
(systemMaxMemory * memoryFraction * safetyFraction).toLong

出现OOM时,调整参数。

 

2、UnifiedMemoryManager 统一内存管理器

Memory Management

UnifiedMemoryManager(conf,numUsableCores)没有new,底层是Objetc的apply方法,源码

def apply(conf: SparkConf, numCores: Int): UnifiedMemoryManager = {
        val maxMemory = getMaxMemory(conf)   执行内存+存储内存
        new UnifiedMemoryManager(
          conf,
          maxHeapMemory = maxMemory,
          onHeapStorageRegionSize =                   存储内存
        (maxMemory * conf.getDouble("spark.memory.storageFraction", 0.5)).toLong,
          numCores = numCores)
}

 

1)getMaxMemory源码:
private def getMaxMemory(conf: SparkConf): Long = {

        val systemMemory = conf.getLong("spark.testing.memory", Runtime.getRuntime.maxMemory) 

       //预留内存300M
        val reservedMemory = conf.getLong("spark.testing.reservedMemory",
          if (conf.contains("spark.testing")) 0 else RESERVED_SYSTEM_MEMORY_BYTES)

        val minSystemMemory = (reservedMemory * 1.5).ceil.toLong
        if (systemMemory < minSystemMemory) {
          throw new IllegalArgumentException(s"System memory $systemMemory must " +
        s"be at least $minSystemMemory. Please increase heap size using the --driver-memory " +
        s"option or spark.driver.memory in Spark configuration.")
        }
        if (conf.contains("spark.executor.memory")) {
          val executorMemory = conf.getSizeAsBytes("spark.executor.memory")
          if (executorMemory < minSystemMemory) {
        throw new IllegalArgumentException(s"Executor memory $executorMemory must be at least " +
          s"$minSystemMemory. Please increase executor memory using the " +
          s"--executor-memory option or spark.executor.memory in Spark configuration.")
          }
        }

        // (比如系统内存是10G)10G - 300M
        val usableMemory = systemMemory - reservedMemory
        val memoryFraction = conf.getDouble("spark.memory.fraction", 0.6)

        // (假设系统内存是10G)  (10G - 300M) * 60%
        (usableMemory * memoryFraction).toLong
}

2)存储内存:
(maxMemory * conf.getDouble("spark.memory.storageFraction", 0.5)).toLong

 

总结一下统一内存管理器:
1)总的可用内存maxMemory=执行内存+存储内存:
val usableMemory = systemMemory-reservedMemory=系统堆内存heap space-300M预留内存
val memoryFraction = conf.getDouble("spark.memory.fraction", 0.6)
(usableMemory * memoryFraction).toLong

2)存储内存:
(总内存maxMemory * conf.getDouble("spark.memory.storageFraction", 0.5)).toLong
所以默认统一内存管理中,执行内存和存储内存各占用50%。

画图理解:

【SparkCore】RDD缓存优化、序列化、Spark内存管理、spark内存优化之GC调优、数据本地化_第1张图片

总的系统内存是所有的框。
Reserved Memory是预留内存。系统内存-预留内存=可用内存
可用内存的60%是SparkMemory,可用内存的40%是User Memory剩余内存。
SparkMemory分为两部分,各占50%,就是执行内存+存储内存。

 

注意:

1)默认执行内存和存储内存各占用50%,共享一个统一内存空间,当执行内存没有被使用时,存储可以获取所有spark内存,反之亦然,内存可以动态伸缩
2)执行内存和存储内存一共占用了100%,如果执行内存不够,可以向存储内存借,强制的借,但是存储内存不能强制的去借执行内存中的内存。

 

Spark程序如果出现OOM,选判断是哪个端:

1)driver端:调driver内存

2)executor端:调executor统一内存管理。

 

四、spark内存优化之GC调优

Garbage Collection Tuning

JVM内存管理:
1)堆(GC管理):
      年轻代:[Eden, Survivor1, Survivor2]
      老年代:
2)metaspace:元数据

堆的GC:
1)如果Eden区满了的时候,就会有一个minor gc。清理Eden区,将Edenh和Survivor1中活着的对象拷贝到Survivor2区,同时把Edenh和Survivor1清空。
2)Survivor1, Survivor2可以交换:
A.Eden+Survivor1 ===>cpoy活的对象进Survivor2。Eden+Survivor1 清空
B.下次的时候Eden+Survivor2 ===>cpoy活的对象进Survivor1。Eden+Survivor2 清空
对象GC次数达到15,就会进入老年代。
或者是cpoy活的进Survivor2,如果Survivor2满了,或者对象真的很老,就把活的移动到Old区域。
3)年轻代满了之后的GC是:minor GC  大多数
     老年代满了之后的GC是:full GC       耗费性能

GC调优信息查看:
第一步是:收集一些统计信息
添加-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps to the Java options
下一步:Spark运行的时候,要看打印在worker中的日志(通过UI可以看到)

Spark中GC调优的目的是:
年轻代放在Young,long-lived的RDDs移动到Old区域。
因为80%的对象都是Young的,所以尽可能的存储这些对象在Young,尽量减少full GC。
因为在task执行阶段如果发生了full GC,如果选择的GC不好的话,会直接停止用户程序。

一些可能有用的操作.............:

1)通过收集GC统计信息来检查垃圾收集是否太多。如果在任务完成之前多次调用full GC,则意味着没有足够的内存用于执行任务。GC的时间在UI上是有的:

2)如果有很多minor collections却没有很多major GCs,就需要去调整Eden的大小。可以为每个task设置超过所需内存的Eden大小。如果Eden大小是E,那么可以用-Xmn=4/3*E来设置Young的大小。设置4/3也是为了说明活着的对象使用的空间。
3)如果OldGen满了,请通过降低spark.memory.fraction来减少用于缓存的内存量。最好是减少缓存的对象为不是减慢任务的执行。或者考虑减少Young的规模。这意味着降低-Xmn。

如果没有降低-Xmn,请尝试更改JVM的newRatio参数的值。许多JVM将此值默认为2,这意味着Old占用堆的2/3。它应该足够大,以至于这个分数超过了spark.memory.fraction。

4)可以通过设置-XX:+UseG1GC来使用G1GC垃圾收集,在垃圾收集成为瓶颈的情况下,它可以提高性能。

请注意,对于较大的执行器堆大小,使用-xx:g1HeapRegionSize增加g1区域大小可能很重要。

5)例如,如果您的任务是从HDFS读取数据,则可以使用从HDFS读取的数据块大小来估计任务使用的内存量。请注意,解压块的大小通常是块大小的2到3倍。因此,如果我们希望有3或4个任务的工作空间,并且HDFS块大小为128MB,我们可以估计Eden的大小为4*3*128MB。

可以通过在作业配置中设置spark.executor.extraJavaOptions来指定执行器的GC优化标志。也就是设置使用什么GC,调优参数的配置。

更多的GC调优,参考:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/index.html

 

五、数据本地化

希望运行的代码和数据在一起,但是一般是很难保证的。

数据本地化的几种机制(由优到差):
1)PROECSS_LOCAL:在同一个JVM里面,也就是代码和数据在同一个进程里面的,是最好的。可以理解为数据已经cacahe到你的本机器了。
2)NODE_LOCAL:在同一个节点上的(也就是executor代码与需要的数据都在一个节点上),
   数据在两个进程中传输。因为一个在executor也就是yarn进程中,另一个在datanode也就是HDFS进程中
3)NO_PREF
4)RACK_LOCAL: 同一个机架 代码和数据在同一个机架但是不同的节点上
5)ANY:不同机架

Spark优先使用最好的本地化机制,但是一般都实现不了,Spark会从优到次递进使用。Spark在超过了等待的时间之后就会选择其他的机制,不同机制等待的时候参考spark.locality的配置:

【SparkCore】RDD缓存优化、序列化、Spark内存管理、spark内存优化之GC调优、数据本地化_第2张图片

一般设置wait=1s

你可能感兴趣的:(【SparkCore】RDD缓存优化、序列化、Spark内存管理、spark内存优化之GC调优、数据本地化)