LZ名約山炮

博客园 首页 新随笔 联系 订阅 管理

一、前言

1.学习内核的目的

  ①从宏观上了解Job(Spark on YARN)提交的流程。(画图)

  ②了解Job在提交之后,进行任务的划分,Stage的划分,任务的调度的过程!

    结合: 宽依赖,窄依赖,Stage,task , job

  ③了解整个Job在执行期间Driver和Executor之间的通信方式

  ④Shuffle (区别不同的shuffle)

    Spark是如何实现Shuffle!

    不同的Shuffle的效率影响!

  ⑤Spark的内存管理 (只有统一内存管理,用什么GC回收器)

  方式: 查看源码!

  最终要求: 用自己的语言表述上述的知识点!

2.指定App部署的模式

  在提交Job时,通过--deploy-mode:参数指定,在生产上使用cluster模式,cluster模式会在集群中选择一台机器启动Driver程序

  SparkOnYarnCluster提交流程源码分析(必须在idea中导入相关依赖):

<dependency>
  <groupId>org.apache.spark</groupId>
  <artifactId>spark-yarn_2.12</artifactId>
  <version>3.0.0</version>
</dependency>
<dependency>
  <groupId>org.apache.spark</groupId>
  <artifactId>spark-core_2.12</artifactId>
  <version>3.0.0</version>
</dependency>

  SparkOnYarnCluster提交流程架构图示:

  SparkOnYarnClient提交流程源码分析:

  SparkOnYarnClient提交流程:

二、通信

1.IO模型

  IO模型: 用什么样的方式进行数据的发送和接受!模型决定了程序通信的性能!

  Java支持3种IO模型: BIO,NIO,AIO

  三种模型的区别在于通道的数量(单/多),异步还是同步,阻塞还是非阻塞!

  Java BIO: 同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。JDK1.4之前的唯一选择!

  Java NIO : 同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器(Selector)轮询到连接有I/O请求就进行处理。JDK1.4引入!

  Java AIO(NIO.2) : 异步非阻塞,AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。 AIO还没有得到广泛的使用!JDK1.7支持!

2.RPC

  http,https是具体的协议,Tomcat,Jetty 是支持 http,https协议的产品。RPC(远程过程调用),是一种协议的类型,只要符合远程过程调用的特征,都可以称为RPC协议

  支持RPC协议的产品例如:

    AKKA , Netty ,Dubbo(Dubbo协议,底层还是Netty)

      AKKA:IO(阻塞式IO)通信

      Netty: NIO(非阻塞式IO)通信

  在大数据领域,在分布式系统之间,频繁发送请求,传输数据,NIO效率高(Driver和Executor之间使用RPC(远程过程调用)协议通信)

3.术语介绍

3.1 EndPoint

  Endpoint:通信端点,理解为一个要通信的终端进程或设备,一种抽象表达

private[spark] trait RpcEndpoint
private[spark] trait IsolatedRpcEndpoint extends RpcEndpointt

  Backend: 通信后台进程服务,具体的通信设备或终端, 例如:CoarseGrainedExecutorBackend。

//Executor的通信终端
private[spark] class CoarseGrainedExecutorBackend(
    override val rpcEnv: RpcEnv,
    driverUrl: String,
    executorId: String,
    bindAddress: String,
    hostname: String,
    cores: Int,
    userClassPath: Seq[URL],
    env: SparkEnv,
    resourcesFileOpt: Option[String],
    resourceProfile: ResourceProfile)
  extends IsolatedRpcEndpoint with ExecutorBackend with Logging
//Driver的通信终端
class DriverEndpoint extends IsolatedRpcEndpoint with Logging

  send:单方发送,不要求回复!要查询收件终端的recieve

  ask : 发送后要求收到回复,要查询收件终端的recieveAndReply

  每个RpcEnpoint都有自己的Inbox(收件箱),和outBox(发件箱),每个RpcEnpoint只有一个Inbox,有多个outBox,每个outBox对应要发送信息的对方的inbox

  RpcEnpointRef: 通信端点的引用,可以通过引用找到通信端点,类比为手机号或邮箱地址

  RpcAdress: 要发送信息的对方设备的IP地址

3.2 NettyRpcEnv

  NettyRpcEnv: Rpc通信环境,可以类比为 通信网络(联通,移动)

// 分发处理消息
private val dispatcher: Dispatcher = new Dispatcher(this, numUsableCores)

// 网络传输通信的服务端
private var server: TransportServer

// 当前环境中发向每个 远程设备的 发件箱   RpcAddress(远程设备) 
private val outboxes = new ConcurrentHashMap[RpcAddress, Outbox]()

/*
        调用dispatcher.regist()
         为每个EndPoint创建一个Inbox,在Inbox中放入onStart()
*/
rpcEnv.setupEndpoint

  每个Rpc的通信端点,必须加入到通信环境中,才能通信(必须插入联通的手机卡,连上联通的网络,才能使用联通的通信服务)

NettyRpcEnv.setUpEndPoint("设备名",设备)

  RpcEnpoint在加入到NettyRpcEnv后,都需要进行初始化 onStart

  Dispatcher: 一个通信网络中有一个Dispatcher(类似呼叫中心,或基站)负责将消息路由到指定的线路

  TransportClient: 每个NettyRpcEnv都有一个TransportClient,网络传输客户端

  TransportServer: 每个NettyRpcEnv都有一个TransportServer,网络传输服务端

3.3 Dispathcer

  每个RpcEnv中都有一个负责分发消息的Dispathcer(呼叫中心 / 基站):

// 分发消息
private[netty] class Dispatcher(nettyEnv: NettyRpcEnv, numUsableCores: Int) extends Logging {

    // 每个设备 及其  MessageLoop
  private val endpoints: ConcurrentMap[String, MessageLoop] =
    new ConcurrentHashMap[String, MessageLoop]
    
    // 记录在当前RpcEnv中注册的所有设备及其引用
  private val endpointRefs: ConcurrentMap[RpcEndpoint, RpcEndpointRef] =
    new ConcurrentHashMap[RpcEndpoint, RpcEndpointRef]

4.什么是Executor

  语境(上下文):

  ①从通信角度:

    Executor: 在源码中,从整个架构来说,一个CoarseGrainedExecutorBackend就是一个Executor,CoarseGrainedExecutorBackend是 YarnCoarseGrainedExecutorBackend的一个通信后台

    Executor就是Container容器中启动的YarnCoarseGrainedExecutorBackend进程,启动几个Container,就会启动几个Executor

  ②Job运算的角度讲,实际上: 每个CoarseGrainedExecutorBackend还有一个属性

// Executor  计算器,计算者,负责运行task,例如启动,杀死
var executor: Executor = null

三、Job的提交

1.提交流程图

 

  Job提交流程图源码解析:

//数据准备
def main(args: Array[String]): Unit = {
    //创建SparkConf并设置App名称
    val conf: SparkConf = new SparkConf().setAppName("TestSparkRDD").setMaster("local[*]")
    //创建SparkContext,该对象是提交Spark App的入口
    val sc: SparkContext = new SparkContext(conf)

    val rdd = sc.makeRDD(List(1,2,3,4))
    rdd.collect()

    //关闭连接
    sc.stop()
  }

2.常见API

2.1 Stage

  Stage是一组并行的Task,这些Task都用相同的shuffle依赖,并且都运行相同的函数,所有的Task都会被调度器调度运行,而它们的DAG运行图会由于shuffle的产生,而进行切分为多个阶段,然后 DAGScheduler按照拓扑结构的顺序运行这些阶段。每一个Stage都可以是一个ShuffleMap stage,或者是ResultStage。

  ShuffleMap stage中的task运算的结果会作为其他阶段的输入,而ResultStage会直接运行Spark的行动算子,对于ShuffleMap Stage,Spark还会追踪Map输出所在的节点。

  每一个Stage都有一个firstJobId,用于标识首先提交次Stage的Job,如果是FIFO调度器,那么将允许早期提交的Job先计算,或者先容错恢复。

  每个Stage都会在出现错误时,都会重新尝试计算多次

private[scheduler] abstract class Stage(
   val id: Int,    // stage的id,后续调度时,ID小的stage一般是靠前的,会优先执行
    val rdd: RDD[_], // stage的最后一个RDD
    val numTasks: Int,  //在 ShuffleMapStage 等于 当前Stage的最大分区数,在ResultStage,取决于使用的算子
    val parents: List[Stage],  // 父Stage
    val firstJobId: Int,     // 运行的第一个Job的Id
    val callSite: CallSite)
  extends Logging 

2.2 TaskManager

  负责一个TaskSet中任务的调度(将任务发给Executor执行),追踪当前TaskSet中的Task运行的状态,在失败时重试,通过延迟调度处理位置敏感的调度(某些Task需要在同一个节点上运行)

private[spark] class TaskSetManager(
    sched: TaskSchedulerImpl,
    val taskSet: TaskSet,
    val maxTaskFailures: Int,
    blacklistTracker: Option[BlacklistTracker] = None,
    clock: Clock = new SystemClock()) extends Schedulable with Logging

2.3 SchedulableBuilder

private[spark] trait SchedulableBuilder {
  def rootPool: Pool
  def buildPools(): Unit
  def addTaskSetManager(manager: Schedulable, properties: Properties): Unit
}

2.3.1 FIFOSchedulableBuilder(实现一)

  默认的调度器,按照Job中Stage提交的先后顺序,进行调度

  在TaskSchedulerImpl对象中被赋值:

def initialize(backend: SchedulerBackend): Unit = {
  this.backend = backend
  schedulableBuilder = {
    schedulingMode match {
        // 由调度模式决定 取决于spark.scheduler.mode,默认为FIFO
      case SchedulingMode.FIFO =>
        new FIFOSchedulableBuilder(rootPool)
      case SchedulingMode.FAIR =>
        new FairSchedulableBuilder(rootPool, conf)
      case _ =>
        throw new IllegalArgumentException(s"Unsupported $SCHEDULER_MODE_PROPERTY: " +
        s"$schedulingMode")
    }
  }
  schedulableBuilder.buildPools()
}

2.3.2 FairSchedulableBuilder(实现二)

  公平调度器,采取公平算法调度

private[spark] class FIFOSchedulingAlgorithm extends SchedulingAlgorithm {
    // TaskSetManager: Schedulable
  override def comparator(s1: Schedulable, s2: Schedulable): Boolean = {
    val priority1 = s1.priority
    val priority2 = s2.priority
      /*
      根据  (priority1 - priority2) 的结果,进行返回
              结果为负数,返回 -1
              结果为0,返回0
              结果为正数,返回 1
      */
    var res = math.signum(priority1 - priority2)
    if (res == 0) {
        // stageId 小的是靠前的阶段
      val stageId1 = s1.stageId
      val stageId2 = s2.stageId
      res = math.signum(stageId1 - stageId2)
    }
    res < 0
  }
}

四、Shuffle

1.shuffle的简介

  在Spark中,很多算子都会引起RDD中数据的重分区,新的分区被创建,旧的分区被合并或数据被重新分配,在重分区的过程中,如果数据发生了跨节点的移动,就称为shuffle,shuffle的作用就是为了在不同的task中交换数据

2.shuffle的实现方式

2.1 HashShuffle

  Spark1.6.3 之前,使用HashShuffle,HashShuffle使用Hash算法将RDD中的数据,进行分区,使用未优化的HashShuffle,将每个分区的数据单独存放在一个文件中

  生成的文件数量= MapTask数量 * 分区数(ReduceTask数量)

  弊端:

    ①随着MapTask数量的提高,生成大量的小文件,传输效率低!

    ②缓冲区占用内存空间大

  单个节点执行shuffle时缓冲区的消耗为: M * R * spark.shuffle.file.buffer(32K)

  优化后HashShuffle: 每个Core计算的所有的task,会将数据先写入到buffer中,在buffer中完成相同分区数据的合并,最终每个分区单独存放在一个文件中

   生成的文件数量= core数 * 分区数

2.2 SortShuffle(sort-based shuffle)

  随着Hadoop2.0的发布,Spark借鉴了Hadoop2.0中的shuffle过程(sort-based shuffle),sort-based shuffle核心要义:一个MapTask最终只生成一个数据文件,这个文件中有若干个分区,以及一个index文件,其中记录了分区的边界

  Hadoop2.0的shuffle:

    在MapTask上,先分区------>溢写前,进行排序----->溢写为一个片段

    所有片段全部溢写完成后------->merge------->合并为一个总的文件

  SortShuffle的结果就是一个Executor上的每个MapTask,只产生一个文件,这个文件有若干分区!在产生文件时,默认SortShuffleWriter会进行排序

  sort-based shuffle生成的文件数: 2 * MapTask的数量,并不是所有的sort-based shuffle都会对shuffle写出的数据进行排序

2.4 总结

  在老的版本中,可以通过spark.shuffle.manager进行配置,新的版本中已经去掉了此配置项,统一为Sort-based Shuffle。

3.shuffle的写出方式

  MapTask端需要将数据写出罗盘,在写出时可以采取不同的策略

3.1 ShuffleWriter

  ShuffleWriter负责将MapTask的数据写出

private[spark] abstract class ShuffleWriter[K, V] {
  /** Write a sequence of records to this task's output */
  @throws[IOException]
  def write(records: Iterator[Product2[K, V]]): Unit

  /** Close this writer, passing along whether the map completed */
  def stop(success: Boolean): Option[MapStatus]
}

3.2 BypassMergeSortShuffleWriter

  BypassMergeSortShuffle是HashShuffle和Sort-BaseShuffle的折中方案,本质是使用Hash Shuffle的方式处理数据,在最后将所有的文件拼接合并为1个文件,并生成索引文件! 可以理解为HashShuffle 的 Shuffle Fetch优化版,在过程中还是会产生大量的中间文件,BypassMergeSortShuffleWriter在分区过多时,不适合,效率低,因为需要持续打开全部分区的文件流和序列化器

  SortShuffleManager会在以下情况,使用BypassMergeSortShuffleWriter的:

    ①不能实现在map端聚合

    ②分区数必须 <= spark.shuffle.sort.bypassMergeThreshold(默认200)(自己配置)

  忽略Merge后的排序,因为是将分区进行拼接

3.3 UnsafeShuffleWriter(几乎不使用)

  Spark在1.5版本开始了Tungsten计划,在1.5.0-1.5.2版本推出了Tungsten-sort的选项,类似一种实验性质,本质上还是Sort-BaseShuffle,只是用 UnsafeShuffleWriter进行写出,采用了BytesToBytesMap的数据结构,将对数据的排序转换为对指针数组的排序,能够基于二进制数据直接操作,对GC有很大提升

  限制:

    ①不支持map端聚合

    ②序列化器必须支持重定向

    ③分区数 < 16777216

  可惜的是,1.6已经被取消

3.4 SortShuffleWriter(默认)

  特征:

    ①支持map端聚合

    ②支持排序(Map端),按照Key进行排序

    ③支持sort-based shuffle,最终生成一个结果文件和一个索引文件

override def write(records: Iterator[Product2[K, V]]): Unit = {
    
    // SortShuffleWriter 支持map端聚合,可选!
  sorter = if (dep.mapSideCombine) {
    new ExternalSorter[K, V, C](
      context, dep.aggregator, Some(dep.partitioner), dep.keyOrdering, dep.serializer)
  } else {
    // In this case we pass neither an aggregator nor an ordering to the sorter, because we don't
    // care whether the keys get sorted in each partition; that will be done on the reduce side
    // if the operation being run is sortByKey.
    new ExternalSorter[K, V, V](
      context, aggregator = None, Some(dep.partitioner), ordering = None, dep.serializer)
  }
    
    // 都创建了一个ExternalSorter
  sorter.insertAll(records)

  // Don't bother including the time to open the merged output file in the shuffle write time,
  // because it just opens a single file, so is typically too fast to measure accurately
  // (see SPARK-3570).
  val mapOutputWriter = shuffleExecutorComponents.createMapOutputWriter(
    dep.shuffleId, mapId, dep.partitioner.numPartitions)
  sorter.writePartitionedMapOutput(dep.shuffleId, mapId, mapOutputWriter)
    // 写出所有分区的数据  最终生成索引文件和数据文件
  val partitionLengths = mapOutputWriter.commitAllPartitions()
  mapStatus = MapStatus(blockManager.shuffleServerId, partitionLengths, mapId)
}
def insertAll(records: Iterator[Product2[K, V]]): Unit = {
  // TODO: stop combining if we find that the reduction factor isn't high
  val shouldCombine = aggregator.isDefined

  if (shouldCombine) {
    // Combine values in-memory first using our AppendOnlyMap
    val mergeValue = aggregator.get.mergeValue
    val createCombiner = aggregator.get.createCombiner
    var kv: Product2[K, V] = null
    val update = (hadValue: Boolean, oldValue: C) => {
      if (hadValue) mergeValue(oldValue, kv._2) else createCombiner(kv._2)
    }
    while (records.hasNext) {
      addElementsRead()
      kv = records.next()
        //  PartitionedAppendOnlyMap  将kv对聚合到map中
      map.changeValue((getPartition(kv._1), kv._1), update)
        // 判断是否该溢写
      maybeSpillCollection(usingMap = true)
    }
  } else {
    // Stick values into our buffer
    while (records.hasNext) {
      addElementsRead()
      val kv = records.next()
        // PartitionedPairBuffer 将kv对,放入buffer不聚合
      buffer.insert(getPartition(kv._1), kv._1, kv._2.asInstanceOf[C])
      maybeSpillCollection(usingMap = false)
    }
  }
}

3.5 ShuffleMapTask

  一旦一个Task需要将当前stage的结果写出到shuffle,此时这个Task使用ShuffleMapTask实现

override def runTask(context: TaskContext): MapStatus = {
 // 获取Shuffle的Writer
  dep.shuffleWriterProcessor.write(rdd, dep, mapId, context, partition)
}
def write(
    var writer: ShuffleWriter[Any, Any] = null
    try {
      val manager = SparkEnv.get.shuffleManager
        //获取writer
      writer = manager.getWriter[Any, Any](
        //写出
      writer.write(
        rdd.iterator(partition, context).asInstanceOf[Iterator[_ <: Product2[Any, Any]]])
      writer.stop(success = true).get
    } 
  }
}

4.Hadoop的shuffle和sparkshuffle之间的区别

  sort-based shuffle : 一个MapTask生成一个数据文件和一个索引文件

  区别:

    hadoop:

      MapTask: map--------->sort--------->merge

      ReduceTask: sort--------->reduce

    spark :

      MapTask: map--------->sort--------->merge

      ReduceTask: merge----->算子

  spark的shuffle在ReduceTask端不排序

四、内存管理

1.GC

  ①Java的垃圾回收线程是自动帮我们回收垃圾,收堆内存的垃圾

  ②只能使用System.gc() ,只能通过垃圾回收器来收垃圾

  ③当堆的内存不够的时候,一定会来收垃圾

2.内存的管理

  在Spark中有两种内存管理:

    ①静态内存管理(3.0之前): 指通过参数指定每个部分使用的内存的比例,是固定的

    ②统一(动态)内存管理(3.0只剩): 每个部分,即storage,execution,other使用的内存比例和数值可以动态变化

3.垃圾回收器

  jdk1.7 : 默认使用CMS

  jdk1.8: 默认使用 Parallel Scavenge

  jdk1.9 : 默认使用G1(presto)

  Spark使用G1

//Spark使用G1,可以在SparkEnv中配置
-XX:+UseG1GC
posted on 2021-06-07 09:00  LZ名約山炮  阅读(103)  评论(0编辑  收藏  举报