环境
- 本文使用的spark版本为2.4.8
spark运行流程
总体视图

- 上图来自spark官网,关于图上组件的详细说明请参见spark官网
分层视图
spark运行分资源请求和任务调度两条线,下图绿色线为资源请求线,而红色线为任务调度线。

- spark的计算层可以基于YARN、Apache Mesos、Kubernetes,也可以使用自身的standalone 模式,通过StandaloneAppClient实现与资源层的消息交互,通过DriverEndPoint实现与Executor的消息交互,具体如下:

- 资源请求时,资源的分配包括垂直分配和水平分配,为避免单台机器负载过大,默认采用水平分配模式
角色划分
资源层
- 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

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情况

- stage情况

- 运行日志

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

- 在整个Task的调度过程中,任务池Pool负责根据调度策略(FIFO\FAIR),对任务进行排队,而TaskSetManager根据位置(Process、Node、RACK)邻近原则确定任务运行的Executor。
任务启动
当Executor(实际启动类为CoarseGrainedExecutorBackend)程序启动后,向Driver注册自己,接收到注册成功的消息后,启动Executor,同时启动线程池,等待任务提交。当任务到达后,会被包装成TaskRunner,放入线程池队列,等待运行。

任务执行
任务执行流程
- 以word count为例,其执行流程如下:

内存管理
核心类图

- 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 jvm最大内存∗0.6∗0.9 ,执行内存= j v m 最 大 内 存 ∗ 0.2 ∗ 0.8 jvm最大内存*0.2*0.8 jvm最大内存∗0.2∗0.8
- 不支持堆外内存使用
UnifiedMemoryManager
UnifiedMemoryManager采用动态调整的方式,存储和执行默认各占安全内存的50%,可以相互借用对方的内存,支持堆外内存使用,系统默认采用该方式。下图为堆内内存分配情况:

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

- 以上结构不包括读写时数据的排序、聚合、溢写处理,这些功能将在具体的reader和writer中介绍。
- ShuffleBlockResolver负责block到物理文件的映射。
- 文件的实际读写由BlockManager类负责。
ShuffleWriter
ShuffleWriter根据是否需要在map端进行排序、聚合以及分区大小等不同情况进行了区别处理。下图为writer的条件:

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

- 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位为记录的内存地址

- 数据排序除了支持TimSort算法外,还支持RadixSort算法。RadixSort算法时间复杂度为O(nk),空间复杂度为O(n+k),n为元素个数,k为键的长度;TimSort算法时间复杂度为O(nlgn),空间复杂度为O(n)。排序后的数据为分区有序
- ShuffleExternalSorter负责内存请求、数据溢写,UnsafeShuffleWriter负责溢写合并
- IndexShuffleBlockResolver完成分区索引
SortShuffleWriter
1.核心类图

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

- 核心类图

- 同时拉取远程map输出数据最大默认为48M,为了避免只在一个节点上拉取,每次拉取的大小为1/5,最大可同时从5个节点拉取,远程请求也会被随机打散放入请求队列,从而避免单个节点压力过大
- shuffle读的核心类为ShuffleBlockFetcherIterator,负责根据相应的策略封装成多个并行请求放入队列,由ShuffleClient发起网络请求
任务结果收集

- 上图为任务结果收集流程,当task执行后,会根据结果的数据大小进行相应的处理
- 如果为ShuffleMapTask,则会把map结果先写入磁盘,返回MapStatus,其中包括Block信息
- 如果是ResultTask,会返回函数(RDD的action)结果,结果阈值(默认1G),则只返回Block信息,结果被丢弃;而结果大于1M且小于1G,则会写入内存和磁盘,并返回Block信息,结果通过BlockManager拉取;如果小于1M,则直接发送给driver端
BlockManager
- BlockManager主要负责Block存储管理,存储位置为内存或存磁盘,当内存空间不够时,会溢写到磁盘,下图为核心类图:

- 远程数据拉取主要由BlockTransferService来处理,其核心类结构如下:

- BlockTransferService底层通信框架为Netty,初始化的时候会启动服务监听,等待fetchBlocks数据拉取请求。如果本地任务需要拉取远程数据,则会创建客户端,向远程发起数据拉取请求,当block的大小超过阈值,则会把数据写入本地文件
- NettyBlockRpcServer处理远程数据请求
- OneForOneBlockFetcher发起远程数据请求
SparkContext、SparkEnv
- SparkContext为spark功能的入口点,封装了所有driver端的功能
- SparkEnv为工具类,封装了driver、executor执行过程中所有环境对象

底层通信(RPC)
RPC核心类图

- 图上浅蓝色和深蓝色为核心抽象类,而白色可视为实现类,存在混用抽象类与实现类,具有优化空间。
- RPCEndPoint为服务端,RPCEndPointRef为客户端在服务端的引用,底层通信层接收到消息进行解码后,通过Dispather路由到对应的EndPoint。
- 每个服务端和客服端会被封装成EndPointData,包含一个收件箱(inbox),负责存储未处理消息
- 发送给客服端的消息通过RPCEndPointRef的send/ask接口发出,如果为本地消息,则交给Dispather,远程消息则放入发件箱(outbox),通过TransportClient发出。
- 底层采用了netty作为通信框架。
本文深入探讨Spark的运行流程,从总体视图和分层视图解析其架构,详细介绍了RDD、任务调度、内存管理和Shuffle过程。特别关注DAGScheduler、TaskScheduler的工作原理,以及内存管理的StaticMemoryManager和UnifiedMemoryManager。此外,文章还阐述了ShuffleWriter的不同类型及其在数据读写的机制。

756

被折叠的 条评论
为什么被折叠?



