Spark内存和shuffle

Spark的全排序
-------------------
    RDD.sortByKey()内部使用采样方式按照RangePartitioner进行分区。

Spark Shuffle
-------------------
    1.Bypass
        迂回shuffle
        可以使用零拷贝技术实现分区文件的合并。
        配置属性为:
        
            spark.file.transferTo=false

    2.SerializedShuffle
        串行shuffle,不安全shuffle

    3.SortShuffle
        基本shuffle.


ShuffleWriter
--------------------
    1.BypassMerge..Writer
    2.Unsafe..Writer
    3.SortShuffleWriter
        sorter = 
            if(mapCombine?){
                // agg + 排序
                new ExternalSorter[K, V, C](context, dep.aggregator, Some(dep.partitioner), dep.keyOrdering, dep.serializer)
            }
            else{
                //没有 agg , 没有排序
                new ExternalSorter[K, V, V](context, aggregator = None, Some(dep.partitioner), ordering = None, dep.serializer)
            }


        sorter.insertAll(records) ;
        
            --> if(agg.defined?){
                    
                }
                else{
                    //在缓冲区中插入数据
                    buffer.insert(getPartition(kv._1), kv._1, kv._2.asInstanceOf[C])

                    //写入kv到内存缓存区中,每个kv在缓存区中的存放形式为[(parid , key) , value]
                    //如果到达一定条件,溢出buffer数据到磁盘,溢出到磁盘后,对buffer进行重置。
                    //条件: (1)SparkEnv.get.conf.getLong("spark.shuffle.spill.numElementsForceSpillThreshold", Long.MaxValue)    //long最大值
                            (2)SparkEnv.get.conf.getLong("spark.shuffle.spill.initialMemoryThreshold", 5 * 1024 * 1024)            //5M
  
                    //溢出过程
                        累计溢出的次数。
                }

SortShuffleWriter
--------------------
    使用的最多的ShuffleWriter,迂回和UnsafeShuffleWriter不满足时,使用该类完成shuffle输出。
    分区id,k,v重新组合,(partid + k)形成新key,写入缓冲区或者map集合,究竟是
    缓冲区还是map集合看是否需要map合成,需要合成是map,否则是buf.
    缓存会一定条件下(内存超过阈值或者达到特定数据)溢出数据到磁盘,溢出磁盘前
    需要对kv进行排序,因此输出到文件中的数据是排了序的数据,注意可能产生多个溢出文件。
    最后还需要将内存数据和溢出的文件合并,产生一个shuffle文件。还有一个存放分区id的索引文件。

    本次磁盘文件写入完成后,生成mapStatus状态数据,内部主要包含数据块信息(在哪个节点运
    行的任务,端口,执行器id等)并将其发送给driver,在driver保存,以便供下一个阶段的task
    提取数据块使用。


ShuffleReader
------------------
    BlockStoreShuffleReader.
    从其他节点的块存储设备上请求数据,抓取并读出特定分区范围的数据。
    该对象由SortShuffleManger负责创建,reader内部含有四个属性,ShuffleHandle,
    int startPartition, int edn , ctx);

    该reader通过ShuffleBlockFetcherIterator抓取本地或者远程的块数据。返回(blockId->InputStream).
    本地块通过Local Block manager读取,远程块通过BlockTransferService来提取。

    //task一次性从远程节点抓取数据量的最大值,每次抓取请求是 1/5
    SparkEnv.get.conf.getSizeAsMb("spark.reducer.maxSizeInFlight", "48m")
    
    //一次请求抓取的量限制
    SparkEnv.get.conf.getInt("spark.reducer.maxReqsInFlight", Int.MaxValue))

    [概括]
    Task读取上一阶段的输出,包括两种方式读取,本地块和远程块,远程块需要向driver发送请求消息,获得
    mapstatus,拿到数据后再通过传输服务向对应的节点发送FetchRequest.



Spark内存管理
---------------------
    不是通过物理或者硬件底层API实现对内存资源探测,只是通过对内存操作过程期间
    的字节量的变化不断更新维护的数字,通过该方式跟踪内存使用情况。spark对每个task都关联了
    内存的使用量,存放在了map<Long,Long>中。严格意义上讲,spark内存的管理是估算量,不是
    精确量。

    [JVM]
        [堆角度]
            1.堆内
                年轻代            //伊甸区 + 幸存区()
                年老代            //对象
                

            2.非堆
                jvm - heap
                方法区
            
            3.离堆
                os - jvm

        [全局角度]
            1.方法区
            2.堆区
            3.java方法栈
            4.本地方法栈
            5.程序计数器

    指定spark内存。        //针对堆的管理
    JVM                    //全面

    [MemoryManager]
    强制在storage和execute之间共享内存的数量。
    执行内存 : shuffle中执行计算的内存,join,aggregate,sort
    存储内存 : 用于跨集群,缓存或传播数据的内存。

    实现有两种:
    1.StaticMemoryManager
        将堆空间划分成不相交的两个区域,此两个区域完全独立,不存在相互借用的问题。
        spark.memory.useLegacyMode=true,默认false,

        [spark.shuffle.memoryFraction]
        shuffle期间,用户cogroup和聚合的内存比例,默认0.2

        [spark.storage.memoryFraction]
        是spark缓存的内存比例,针对jvm的堆空间的比例,默认0.62.UnifiedMemoryManager
        在execution和storage之间存在交叉区间,可以相互借用。

         1.spark.memory.fraction
            用于spark的执行和存储的内存比例,针对(heap - 300m)而言的,默认值是 0.6
            (heap - 300M) * 0.6

         2.spark.memory.storageFraction
            在spark内存中,控制存储内存的比例。默认0.5
            存储内存 = spark内存 * 0.5 = (heap - 300M) * 0.6 * 0.5

        存储内存可以借用执行内存的空闲空间,执行内存不足时可以收回借出的内存,但最多
        也只能收回借出的那部分内存,执行内存不能抢存储内存的内存。
        
        执行内存也可以借用存储内存空间,不还内存。

    3.内存结构
        3.0)driver内存配置和executor内存配置。
            --driver-memory                //driver内存
            spark.executor.memory        //执行器内存,等价于--executor-memory

        3.1)保留内存
            默认是300M,程序硬编码实现。
            //RESERVED_SYSTEM_MEMORY_BYTES = 300M
            conf.getLong("spark.testing.reservedMemory",if (conf.contains("spark.testing")) 0 else RESERVED_SYSTEM_MEMORY_BYTES)
            可以通过spark.testing.reservedMemory设置特定的保留内置的大小。
            spark.testing属性可以控制是否关闭保留内存。
            //设置保留内存的大小
            spark-shell --master spark://s101:7077 --executor-memory 20m --conf spark.testing.reservedMemory=80000000

            //关闭保留内存
            spark-shell --master spark://s101:7077 --executor-memory 20m --conf spark.testing=1

        3.2)系统内存
            最小 = 保留内存 * 1.5
            spark.testing.memory=配置系统内存
            --executor-memory=配置系统内存
            
        3.3)内存管理实现原理
            内存池实现。
            class MemoryPool{
                //容量
                var _poolSize: Long = 0
                //已使用
                def memoryUsed: Long
            }
            
            //执行内存池
            class ExecutionMemoryPool extends MemoryPool{
                //维护内存
                memoryForTask = new mutable.HashMap[Long, Long]()
            }
            
            不是通过物理或者硬件底层API实现对内存资源探测,只是通过对内存操作过程期间
            的字节量的变化不断更新维护的数字,通过该方式跟踪内存使用情况。spark对每个task都关联了
            内存的使用量,存放在了map<Long,Long>中。严格意义上讲,spark内存的管理是估算量,不是
            精确量。
            
            
        
        3.4)统一内存管理
            系统内存            //堆内存,通过--executor-memory | spark.executor.memory 
            堆内存                //系统内存,至少是保留的1.5倍。
            保留内存            //300M定量,关闭通过--conf spark.testing=1 , 可用spark.testing.reservedMemory设置保留内存大小.
            可用内存            //(系统内存 - 保留内存)
            spark内存            //可用内存 *  spark.memory.fraction(0.6)
            用户内存            //可用内存 - spark内存
            存储内存            //spark内存 * spark.memory.storageFraction(0.5)
            执行内存            //spark内存 *  (1 - spark.memory.storageFraction)
        
        3.5)静态内存
            spark.memory.useLegacyMode=true            //启用传统模式
            spark.shuffle.memoryFraction            //0.2,
            spark.storage.memoryFraction            //0.6
            spark.storage.unrollFraction            //

        
        3.6)练习
            调整保留内存,用户内存,执行内存,存储内存均为10m
            spark-shell --master spark://s101:7077 --executor-memory 40m --conf spark.testing.reservedMemory=10000000 --conf spark.memory.fraction=0.67 --conf spark.memory.storageFraction=0.5


RDD持久化
---------------
    不要和task结果溢出到磁盘搞混淆,RDD缓存是对某个rdd的结算结果(中间结果)
    进行缓存,避免该过程重复执行。
    存储级别,默认是None.

    一旦指定级别后,不能修改,除非先取消持久化。
    
    rdd.cache()是persist()的默认形式。

    class StorageLevel private(
        private var _useDisk: Boolean,
        private var _useMemory: Boolean,
        private var _useOffHeap: Boolean,
        private var _deserialized: Boolean,
        private var _replication: Int = 1)

    spark在内存中跨操作,在内存(磁盘)中,为了便于重用。
    1.MEMORY_ONLY
        存储原生对象在内中,如果内存不足,有些分区不存储。需要的重新计算,默认级别。
    2.MEMORY_AND_DISK
        原生态存储rdd在内存中,不足的分区存到disk.

    3.MEMORY_ONLY_SER 
        在内存中存储串行化rdd.在具有快速串行化器的时,更加高效。
        cpu增加压力。
    4.MEMORY_AND_DISK_SER
        串行化态存储rdd在内存中,不足的分区存到disk.

    5.DISK_ONLY
        只在磁盘存储。

    6.MEMORY_ONLY_2
        等价于memory_only,2是2个节点存放分区。
    
    7.OFF_HEAP
        离堆存储
    
    8.方法
        1.rdd.persist(newLevel: StorageLevel)
            持久化rdd

        2.rdd.unpersist()
            撤销持久化。

    9.选择策略
        默认是合理的。
        如果内存不足,使用MEMORY_ONLY_SER.
        轻易不要选择spill到磁盘。
        如果想要更快容错,可以采用副本策略。


广播变量******************
-------------------
     允许开发人员在每个节点上缓存一份数据,不是每个任务都发送一次拷贝,对于每个节点都采用的大型数据集
     适合使用该方式发送。

     采用类似于BT算法实现的,实现机制如下:
     driver切割串行对象成chunk,并存在driver端的blockManager。
     在executor中也有blockmanager,需要的话首先从自己的blockmanger中fetch数据,
     如果没有,尝试从driver或其他的executor中fetch,fetch到的data存放到自己blockmanger中,
     以便供其他executor fetch,有效防止driver因发送多个副本给execcute造成的瓶颈。

     广播变量实现的原理通过scala的lazy手段实现。********************************

     BT:bit torrent.
     我为人人,
     RDD以广播变量方式传输的。

    //代码
    def host() = java.net.InetAddress.getLocalHost().getHostName
    def pid() = java.lang.management.ManagementFactory.getRuntimeMXBean().getName().split("@")(0)
    def tid() = Thread.currentThread().getName()
    def oid(obj: AnyRef, str: String) = obj.toString() + " : " + str
    def doSend(str: String) = {
    val sock = new java.net.Socket("s101", 8888)
    val out = sock.getOutputStream()
    out.write((str + "\r\n").getBytes())
    out.flush();
    out.close();
    }
    def sendInfo(obj: AnyRef, str: String) = {
       val info = host() + "/" + pid() + "/" + tid() + "/" + oid(obj, str); doSend(info)
    }

    //300M
    //val list = db.find() ;
    /**********************************/
    /************    广播变量***********/
    /**********************************/
    class Dog(val name: String, age: Int) extends Serializable
    val d = new Dog("dahuang", 3)
    //作成广播对象
    val dd = sc.broadcast(d)

    val rdd1 = sc.makeRDD(1 to 10 , 4)
    val rdd2 = rdd1.map(e=>{
        /********************/
        /***** 访问广播变量   */
        /********************/
        val d0 = dd.value
        val hash = d0.hashCode()
        val name = d0.name ;
        sendInfo(this,hash + " : " + e + " : " + name)
    })
    rdd2.collect()


def sendInfo(msg: String,objStr:String) = {
//获取ip
val ip = java.net.InetAddress.getLocalHost.getHostAddress
//得到pid
val rr = java.lang.management.ManagementFactory.getRuntimeMXBean();
val pid = rr.getName().split("@")(0);//pid
//线程
val tname = Thread.currentThread().getName
//对象id
val sock = new java.net.Socket("s101", 8888)
val out = sock.getOutputStream
val m = ip + "\t:" + pid + "\t:" + tname + "\t:" + msg + "\t:" +  objStr+ "\r\n"
out.write(m.getBytes)
out.flush()
out.close()
}



//准备
4个执行器 , 6个线程
spark-shell --master spark://s101:7077 --total-executor-cores 4 --executor-cores 1 

192.168.231.102 :5907   :Executor task launch worker-2  :map    :2122466653
192.168.231.102 :5907   :Executor task launch worker-2  :map    :211241535

192.168.231.103 :3329   :Executor task launch worker-2  :map    :1508566448
192.168.231.103 :3329   :Executor task launch worker-2  :map    :923167588

192.168.231.104 :3206   :Executor task launch worker-2  :map    :2004855724
192.168.231.104 :3207   :Executor task launch worker-2  :map    :1386748001


192.168.231.102 :5907   :Executor task launch worker-2  :map    :1769298538
192.168.231.102 :5907   :Executor task launch worker-2  :map    :1769298538
192.168.231.102 :5907   :Executor task launch worker-2  :map    :1769298538

192.168.231.103 :3329   :Executor task launch worker-2  :map    :1496579174

192.168.231.104 :3207   :Executor task launch worker-2  :map    :2016831251
192.168.231.104 :3206   :Executor task launch worker-2  :map    :975295493

 

posted on 2018-06-25 17:32  飞机耳朵  阅读(348)  评论(0编辑  收藏  举报

导航