spark深度剖析

spark深度剖析

  • 环境
  • spark运行流程
    • 总体视图
    • 分层视图
  • 角色划分
    • 资源层
    • 计算层
  • RDD
    • RDD依赖关系
    • RDD操作
  • 任务调度
    • job、stage、task及关系
      • 概念
      • 关系
    • DAGScheduler
      • 测试数据
      • 代码
      • 运行结果
    • TaskScheduler
      • Task调度流程
  • 任务启动
  • 任务执行
    • 任务执行流程
    • 内存管理
      • 核心类图
      • StaticMemoryManager
      • UnifiedMemoryManager
    • Shuffle读写核心类结构
      • ShuffleWriter
        • BypassMergeSortShuffleWriter
        • UnsafeShuffleWriter
        • SortShuffleWriter
      • ShuffleRead
  • 任务结果收集
  • BlockManager
  • SparkContext、SparkEnv
  • 底层通信(RPC)
    • RPC核心类图

环境

  • 本文使用的spark版本为2.4.8

spark运行流程

总体视图

spark深度剖析_第1张图片

  • 上图来自spark官网,关于图上组件的详细说明请参见spark官网

分层视图

spark运行分资源请求和任务调度两条线,下图绿色线为资源请求线,而红色线为任务调度线。
spark深度剖析_第2张图片

  • spark的计算层可以基于YARN、Apache Mesos、Kubernetes,也可以使用自身的standalone 模式,通过StandaloneAppClient实现与资源层的消息交互,通过DriverEndPoint实现与Executor的消息交互,具体如下:
    spark深度剖析_第3张图片
  • 资源请求时,资源的分配包括垂直分配和水平分配,为避免单台机器负载过大,默认采用水平分配模式

角色划分

资源层

  • master——负责集群资源的管理
  • worker——汇报资源情况、管理本节点的executor

计算层

  • driver——负责向集群申请资源、job拆分、任务调度、执行结果收集
  • executor——负责任务的执行以及执行情况的报告

RDD

RDD是spark中的核心概念,是不可变的、可进行并行计算的记录分区集。RDD主要包括如下属性:

  • 分区集
  • 作用于分区上的计算函数
  • RDD依赖关系
  • 分区函数(可选,默认hash分区)
  • 首选计算位置(可选,根据计算向数据移动原则,使计算作用于合适节点上的数据,比如hdfs中block的位置)

RDD依赖关系

  • NarrowDependency
    • OneToOneDependency
    • RangeDependency
  • ShuffleDependency

Narrow依赖不涉及数据移动,而Shuffle依赖需要Shuffle写和Shuffle读,Shuffle依赖是DAG Scheduler进行stage拆分的依据。

RDD操作

RDD操作分为两类:

  • transformations——从一个rdd生成新的rdd(一个dataset到新的dataset),比如filter、map
  • actions——在数据集上进行计算,输出结果,比如foreach、count

任务调度

job、stage、task及关系

概念

  • job——RDD上的每个actions类的操作都会生成一个job。
  • stage——在每个job中,DAGScheduler以RDD上transformations类操作生成的ShuffeRDD(对应RDD依赖关系为shuffle依赖)为边界,划分stage,每个stage会生成中间结果,供后续stage使用。stage分为ShuffleMapStage和ResultStage,前者输出中间结果,后者输出最终结果。
  • task——数据集的每个分区上的函数计算,对应ShuffleMapStage和ResultStage,最后会转换为对应的ShuffleMapTask和ResultTask集。

关系

一个job包括多个stage,一个stage包括多个task
spark深度剖析_第4张图片

DAGScheduler

DAGScheduler是顶级调度,面向stage,负责把job拆分为stage(依据就是RDD之间的shuffle依赖关系),根据分区情况,把stage转换成task集合,提交给TaskScheduler,然后跟踪每个stage的执行情况,下面以word count例子进行说明。

测试数据

hello world spark
good morning hawk
hello world hawk
hawk good morning
by by hawk

代码

object WorldCount {

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

    val sc = new SparkContext(conf)
    val wordfileRDD:RDD[String] = sc.textFile("data/wordcount.txt")
    val flatMapRDD = wordfileRDD.flatMap(_.split(" ")) 
    val tupleRDD = flatMapRDD.map((_,1))
    val resOverRDD = tupleRDD.reduceByKey(_+_) 
    resOverRDD.foreach(println) 
    Thread.sleep(Long.MaxValue)
  }

}
  • flatMap和Map操作会生成MapPartitionsRDD,而MapPartitionsRDD是Narrow依赖,reduceByKey会生成ShuffleRDD,产生shuffle依赖,reduceByKey会被拆分为单独的stage,因此当前job会有两个stage。
  • foreach为action类操作,会生成job,被提交。

运行结果

  • job情况
    job
  • stage情况
    spark深度剖析_第5张图片
  • 运行日志
    运行日志

TaskScheduler

TaskScheduler是低级调度,面向task。TaskScheduler会把DAGScheduler提交的TaskSet放入Pool,当首次提交任务或者有新的Executor加入时,根据调度策略(FIFO、Fair)依次提交队列任务给Executor执行。

Task调度流程

spark深度剖析_第6张图片

  • 在整个Task的调度过程中,任务池Pool负责根据调度策略(FIFO\FAIR),对任务进行排队,而TaskSetManager根据位置(Process、Node、RACK)邻近原则确定任务运行的Executor。

任务启动

当Executor(实际启动类为CoarseGrainedExecutorBackend)程序启动后,向Driver注册自己,接收到注册成功的消息后,启动Executor,同时启动线程池,等待任务提交。当任务到达后,会被包装成TaskRunner,放入线程池队列,等待运行。
spark深度剖析_第7张图片

任务执行

任务执行流程

  • 以word count为例,其执行流程如下:
    spark深度剖析_第8张图片

内存管理

核心类图

spark深度剖析_第9张图片

  • spark把内存划分为执行和存储两个区域,包括静态管理(StaticMemoryManager)和动态管理(UnifiedMemoryManager)两种方式,每个 MemoryManager负责一个Executor(一个jvm)的内存管理
  • 内存的分配和回收包括对堆内和堆外两种内存的管理,都是直接使用Unsafe的native接口操作内存地
  • TaskMemoryManger负责跟踪Task的内存使用情况,其主要功能为Unsafe模式下的页管理(MemoryBlock),MemoryBlock就是一页,包括页号、地址和长度。

StaticMemoryManager

  • StaticMemoryManager采用静态分配方式,存储内存= j v m 最 大 内 存 ∗ 0.6 ∗ 0.9 jvm最大内存*0.6*0.9 jvm0.60.9执行内存= j v m 最 大 内 存 ∗ 0.2 ∗ 0.8 jvm最大内存*0.2*0.8 jvm0.20.8
  • 不支持堆外内存使用

UnifiedMemoryManager

UnifiedMemoryManager采用动态调整的方式,存储和执行默认各占安全内存的50%,可以相互借用对方的内存,支持堆外内存使用,系统默认采用该方式。下图为堆内内存分配情况:

spark深度剖析_第10张图片

  • 总体分为保留内存和可用内存,保留内存默认300M
  • 可用内存只有60%可分配,在这60%中一半分配给存储,一半分配给执行,执行和存储可相互借用
  • 最小内存必须是保留内存的1.5倍
  • 每个任务执行内存最低要求为 内 存 池 大 小 ∗ 1 2 ∗ 活 动 任 务 数 量 内存池大小*\frac{1}{2*活动任务数量} 21

Shuffle读写核心类结构

spark深度剖析_第11张图片

  • 以上结构不包括读写时数据的排序、聚合、溢写处理,这些功能将在具体的reader和writer中介绍。
  • ShuffleBlockResolver负责block到物理文件的映射。
  • 文件的实际读写由BlockManager类负责。

ShuffleWriter

ShuffleWriter根据是否需要在map端进行排序、聚合以及分区大小等不同情况进行了区别处理。下图为writer的条件:
spark深度剖析_第12张图片

BypassMergeSortShuffleWriter

该writer是针对分区数小于200,不需要map端进行combine处理时使用。其包括三步:
1.每个分区数据key、value的形式直接输出到文件
2.合并分区文件
3.按分区建立数据索引

UnsafeShuffleWriter

1.核心类图
spark深度剖析_第13张图片

  • UnsafeShuffleWriter使用Unsafe的native接口进行内存操作
  • 分为堆内和堆外,堆内采用Long数组作为基础数据结构,相对于堆外,内存存在浪费
  • 数据按页存储(MemoryBlock),默认大小不大于64M,每页包含n条记录
 val pageSizeBytes: Long = {
   val minPageSize = 1L * 1024 * 1024   // 1MB
   val maxPageSize = 64L * minPageSize  // 64MB
   val cores = if (numCores > 0) numCores else Runtime.getRuntime.availableProcessors()
   // Because of rounding to next power of 2, we may have safetyFactor as 8 in worst case
   val safetyFactor = 16
   val maxTungstenMemory: Long = tungstenMemoryMode match {
     case MemoryMode.ON_HEAP => onHeapExecutionMemoryPool.poolSize
     case MemoryMode.OFF_HEAP => offHeapExecutionMemoryPool.poolSize
   }
   val size = ByteArrayMethods.nextPowerOf2(maxTungstenMemory / cores / safetyFactor)
   val default = math.min(maxPageSize, math.max(minPageSize, size))
   conf.getSizeAsBytes("spark.buffer.pageSize", default)
 }
  • 记录的分区及内存地址使用Long存储,高24位为分区Id(对应为分区的低24位),低40位为记录的内存地址
    spark深度剖析_第14张图片
  • 数据排序除了支持TimSort算法外,还支持RadixSort算法。RadixSort算法时间复杂度为O(nk),空间复杂度为O(n+k),n为元素个数,k为键的长度;TimSort算法时间复杂度为O(nlgn),空间复杂度为O(n)。排序后的数据为分区有序
  • ShuffleExternalSorter负责内存请求、数据溢写,UnsafeShuffleWriter负责溢写合并
  • IndexShuffleBlockResolver完成分区索引
SortShuffleWriter

1.核心类图
spark深度剖析_第15张图片

  • SortShuffleWriter 核心的处理任务是由ExternalSorter完成的
  • ExternalSorter会根据mapSideCombine选择采用buffer还是map数据结构
  • ExternalSorter负责内存的请求、数据的溢写处理、溢写合并
  • 排序算法采用TimSort,该算法对部分有序的数据比较友好,时间复杂度为O(nlgn),空间复杂度为O(n),排序是以(partition,key)作为排序的键,因此,最后的数据是分区有序,分区内有序
  • IndexShuffleBlockResolver完成分区索引

ShuffleRead

  1. 数据读取流程
    spark深度剖析_第16张图片
  2. 核心类图
    spark深度剖析_第17张图片
  • 同时拉取远程map输出数据最大默认为48M,为了避免只在一个节点上拉取,每次拉取的大小为1/5,最大可同时从5个节点拉取,远程请求也会被随机打散放入请求队列,从而避免单个节点压力过大
  • shuffle读的核心类为ShuffleBlockFetcherIterator,负责根据相应的策略封装成多个并行请求放入队列,由ShuffleClient发起网络请求

任务结果收集

spark深度剖析_第18张图片

  • 上图为任务结果收集流程,当task执行后,会根据结果的数据大小进行相应的处理
  • 如果为ShuffleMapTask,则会把map结果先写入磁盘,返回MapStatus,其中包括Block信息
  • 如果是ResultTask,会返回函数(RDD的action)结果,结果阈值(默认1G),则只返回Block信息,结果被丢弃;而结果大于1M且小于1G,则会写入内存和磁盘,并返回Block信息,结果通过BlockManager拉取;如果小于1M,则直接发送给driver端

BlockManager

  1. BlockManager主要负责Block存储管理,存储位置为内存或存磁盘,当内存空间不够时,会溢写到磁盘,下图为核心类图:
    spark深度剖析_第19张图片
  2. 远程数据拉取主要由BlockTransferService来处理,其核心类结构如下:
    spark深度剖析_第20张图片
  • BlockTransferService底层通信框架为Netty,初始化的时候会启动服务监听,等待fetchBlocks数据拉取请求。如果本地任务需要拉取远程数据,则会创建客户端,向远程发起数据拉取请求,当block的大小超过阈值,则会把数据写入本地文件
  • NettyBlockRpcServer处理远程数据请求
  • OneForOneBlockFetcher发起远程数据请求

SparkContext、SparkEnv

  • SparkContext为spark功能的入口点,封装了所有driver端的功能
  • SparkEnv为工具类,封装了driver、executor执行过程中所有环境对象
    spark深度剖析_第21张图片

底层通信(RPC)

RPC核心类图

spark深度剖析_第22张图片

  • 图上浅蓝色和深蓝色为核心抽象类,而白色可视为实现类,存在混用抽象类与实现类,具有优化空间。
  • RPCEndPoint为服务端,RPCEndPointRef为客户端在服务端的引用,底层通信层接收到消息进行解码后,通过Dispather路由到对应的EndPoint。
  • 每个服务端和客服端会被封装成EndPointData,包含一个收件箱(inbox),负责存储未处理消息
  • 发送给客服端的消息通过RPCEndPointRef的send/ask接口发出,如果为本地消息,则交给Dispather,远程消息则放入发件箱(outbox),通过TransportClient发出。
  • 底层采用了netty作为通信框架。

你可能感兴趣的:(大数据,spark,大数据,hadoop)