Spark学习笔记

目录

Spark简介

Spark安装模式:Standalone Yarn  Mesos Local

Spark与MapReduce对比

Spark-Core 

RDD(Resilient Distrbuted Dataset)

分区规则

默认分区数 

数据分区

makeRDD

 textFile

算子

转换算子

 行动算子

RDD序列化

Kryo 

 依赖关系

RDD持久化

Cache&Persist

checkpoint

键值对RDD数据分区

Hash分区

Ranger分区

自定义分区

系统累加器

自定义累加器

广播变量

SparkSQL

DataFrame

DataSet

DSL

SparkStreaming

Spark内核

内核概述

部署模式

Yarn部署模式

Standalone 模式运行机制


目录

Spark简介

Spark-Core 

RDD(Resilient Distrbuted Dataset)

分区规则

默认分区数 

数据分区

makeRDD

 textFile

算子

RDD序列化

Kryo 

 依赖关系


学习通道: 尚硅谷大数据Spark教程从入门到精通_哔哩哔哩_bilibili

Spark简介

  • Spark安装模式:Standalone Yarn  Mesos Local

模式对比
模式Spark安装个数        需启动的进程所属者
Local       1        Spark
Yarn1Yarn及HDFSYarn
Standalone3Master及wokerSpark
  • Spark与MapReduce对比

Spark-Core 

RDD(Resilient Distrbuted Dataset)

特点:

  1. 弹性(存储、计算、分区、容错)    
  2. 分布式
  3. 数据集,不负责存储数据
  4. 数据抽象
  5. 不可变
  6. 可分区,并行计算

特性:

  1. 一组分区
  2. 一个计算分区的函数
  3. RDD之间的依赖关系
  4. 一个Partitioner,即分片函数;控制数据的分区流向
  5. 一个列表,存储存取每个patition的优先位置

分区规则

默认分区数 

makeRDD

conf.getInt("spark.default.parallelism", math.max(totalCoreCount.get(), 2))

textFile

math.min(defaultParallelism, 2)

totalCoreCount.get获取CPU核数

数据分区

makeRDD

def makeRDD[T: ClassTag](
      seq: Seq[T],
      numSlices: Int = defaultParallelism): RDD[T] = withScope {
    parallelize(seq, numSlices)
  }

def parallelize[T: ClassTag](
      seq: Seq[T],
      numSlices: Int = defaultParallelism): RDD[T] = withScope {
    assertNotStopped()
    new ParallelCollectionRDD[T](this, seq, numSlices, Map[Int, Seq[String]]())
  }

private object ParallelCollectionRDD{
......
.... def positions(length: Long, numSlices: Int): Iterator[(Int, Int)] = {
      (0 until numSlices).iterator.map { i =>
        val start = ((i * length) / numSlices).toInt
        val end = (((i + 1) * length) / numSlices).toInt
        (start, end)
      }
    }
......
}

 textFile

SparkContext
--textFile
    --hadoopFile
        --hadoopRDD
          --getPartitions
            --getInputFormat(jobConf).getSplits(jobConf, minPartitions)

算子

  • 转换算子

    • map() & mappartitions()
    • mapPartitonsWithIndex()
      def mapPartitionsWithIndex[U: ClassTag](
            f: (Int, Iterator[T]) => Iterator[U],
            preservesPartitioning: Boolean = false): RDD[U] 
    • flatMap()
    • glom()
    • groupBy()
      def groupBy[K](f: T => K)(implicit kt: ClassTag[K]): RDD[(K, Iterable[T])]
    • filter()
    • sample()
    • distinct()
    • coalesce()
      def coalesce(numPartitions: Int, shuffle: Boolean = false,
                     partitionCoalescer: Option[PartitionCoalescer] = Option.empty)
                    (implicit ord: Ordering[T] = null)
            : RDD[T]
    • repartition()

    • sortBy() 

    • pipe() 

      def pipe(command:String):RDD[String]
    • intersection()

    • union()

    • subtract()

    • zip()

    • patitionBy()

    • rebuceByKey()

    • groupByKey()

    • aggregateByKey()

      /*zeroValue:给每个分区的每个key分配初始值
      *seqOp:每个分区相同key的值与初始值进行计算
      *combOp:不同分区相同key值的计算
      */
      def aggregateByKey[U: ClassTag](zeroValue: U, partitioner: Partitioner)(seqOp: (U, V) => U,
            combOp: (U, U) => U): RDD[(K, U)]
    • flodByKey()

      /*zeroValue:给每个分区的每个key分配初始值
      *func:分区内与分区间计算函数相同
      *
      */
      def flodByKey(zeroValue V )(func :(V,V) => V) :RDD[(K,V)]
    • combineByKey()

      /*createCombine :遍历所有分区中的元素,给每一种Key分配一个初始值
      *mergeValue:
      *  mergeCombiners:
      */
      def combineByKey[C](createCombine : V => C,
                          mergeValue:(C,V) => C,
                          mergeCombiners:(C,C) =>C) :RDD[(K,C)]
      
       
    • sortByKey() 

 行动算子

  • reduce
  • collect()
  • count()

RDD序列化

Driver:执行RDD以外的代码。

Executor:执行RDD以内的代码。

  1. 继承Serializable
  2. 样例类

Kryo 

Java的序列化能够序列化任何的类。但是比较重,序列化后对象的体积也比较大。

Spark出于性能的考虑,Spark2.0开始支持另外一种Kryo序列化机制。Kryo速度是Serializable的10倍。当RDD在Shuffle数据的时候,简单数据类型、数组和字符串类型已经在Spark内部使用Kryo来序列化。

val conf: SparkConf = new SparkConf()
                .setAppName("SerDemo")
                .setMaster("local[*]")
                // 替换默认的序列化机制
                .set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
                // 注册需要使用kryo序列化的自定义类
                .registerKryoClasses(Array(classOf[Searche]))


Class Searche(){

............

}

 依赖关系

      RDD只支持粗粒度转换,即在大量记录上执行的单个操作。将创建RDD的一系列Lineage(血统)记录下来,以便恢复丢失的分区。RDD的Lineage会记录RDD的元数据信息和转换行为,当该RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区。

查看血缘 RDD.toDebugString

查看依赖 RDD.dependencies

宽依赖:有Shuffle(sortreduceByKeygroupByKeyjoin)

窄依赖:无Shuffle(map,filter,flatMap)

RDD持久化

Cache&Persist

       RDD通过Cache或者Persist方法将前面的计算结果缓存,默认情况下会把数据以序列化的形式缓存在JVM的堆内存中。但是并不是这两个方法被调用时立即缓存,而是触发后面的action时,该RDD将会被缓存在计算节点的内存中,并供后面重用。

Cache方法调用Persist,默认缓存在内存中。

mapRdd.cache()
def cache(): this.type = persist()
def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)
​
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)

checkpoint

检查点存储路径:Checkpoint的数据通常是存储在HDFS等容错、高可用的文件系统

检查点数据存储格式为:二进制的文件

检查点切断血缘:在Checkpoint的过程中,该RDD的所有依赖于父RDD中的信息将全部被移除。

检查点触发时间:对RDD进行Checkpoint操作并不会马上被执行,必须执行Action操作才能触发。但是检查点为了数据安全,会从血缘关系的最开始执行一遍。(建议在checkepoint前cache避免再次计算)

键值对RDD数据分区

      Spark 目前支持 Hash 分区和 Range 分区,和用户自定义分区 Hash 分区为当前的默认 分区。分区器直接决定了 RDD 中分区的个数、 RDD 中每条数据经过 Shuffle 后进入哪个分区,进而决定了 Reduce 的个数。

    只有 Key-Value 类型的 RDD 才有分区器,  Key-Value 类型的 RDD 分区的值是 None

    每个 RDD 的分区 ID 范围: 0 ~ (numPartitions - 1),决定这个值是属于那个分区的。

Hash分区

对于给定的 key,计算其 hashCode,并除以分区个数取余

class HashPartitioner(partitions: Int) extends Partitioner {
  require(partitions >= 0, s"Number of partitions ($partitions) cannot be negative.")

  def numPartitions: Int = partitions

  def getPartition(key: Any): Int = key match {
    case null => 0
    case _ => Utils.nonNegativeMod(key.hashCode, numPartitions)
  }

  override def equals(other: Any): Boolean = other match {
    case h: HashPartitioner =>
      h.numPartitions == numPartitions
    case _ =>
      false
  }

  override def hashCode: Int = numPartitions
}

Ranger分区

将一定范围内的数据映射到一个分区中, 尽量保证每个分区数据均匀, 且分区间有序。

自定义分区

系统累加器

声明累加器:val sum1: LongAccumulator = sc.longAccumulator("sum1")

累加器添加数据 sum.add(count)

累加器获取数据sum.value

自定义累加器

object accumulator03_define {

    def main(args: Array[String]): Unit = {

        //1.创建SparkConf并设置App名称
        val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")

        //2.创建SparkContext,该对象是提交Spark App的入口
        val sc: SparkContext = new SparkContext(conf)

        //3. 创建RDD
        val rdd: RDD[String] = sc.makeRDD(List("Hello", "Hello", "Hello", "Hello", "Spark", "Spark"), 2)

        //3.1 创建累加器
        val acc: MyAccumulator = new MyAccumulator()

        //3.2 注册累加器
        sc.register(acc,"wordcount")

        //3.3 使用累加器
        rdd.foreach(
            word =>{
                acc.add(word)
            }
        )

        //3.4 获取累加器的累加结果
        println(acc.value)

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

// 声明累加器
// 1.继承AccumulatorV2,设定输入、输出泛型
// 2.重新方法
class MyAccumulator extends AccumulatorV2[String, mutable.Map[String, Long]] {

    // 定义输出数据集合
    var map = mutable.Map[String, Long]()

    // 是否为初始化状态,如果集合数据为空,即为初始化状态
    override def isZero: Boolean = map.isEmpty

    // 复制累加器
    override def copy(): AccumulatorV2[String, mutable.Map[String, Long]] = {
        new MyAccumulator()
    }

    // 重置累加器
    override def reset(): Unit = map.clear()

    // 增加数据
    override def add(v: String): Unit = {
        // 业务逻辑
        if (v.startsWith("H")) {
            map(v) = map.getOrElse(v, 0L) + 1L
        }
    }

    // 合并累加器
    override def merge(other: AccumulatorV2[String, mutable.Map[String, Long]]): Unit = {

        other.value.foreach{
            case (word, count) =>{
                map(word) = map.getOrElse(word, 0L) + count
            }
        }
    }

    // 累加器的值,其实就是累加器的返回结果
    override def value: mutable.Map[String, Long] = map
}

广播变量

广播变量:分布式共享只读变量。

广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一个或多个Spark操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询表,广播变量用起来都很顺手。在多个并行操作中使用同一个变量,但是 Spark会为每个任务分别发送。

  • 调用SparkContext.broadcast(广播变量)创建出一个广播对象,任何可序列化的类型都可以这么实现。
  • 通过广播变量.value,访问该对象的值。
  • 变量只会被发到各个节点一次,作为只读值处理(修改这个值不会影响到别的节点)。

SparkSQL

DataFrame

  • DataFrame是一种以RDD为基础的分布式数据集,类似于传统数据库中的二维表格。

  • DataFrame与RDD的主要区别在于,前者带有schema元信息,即DataFrame所表示的二维表数据集的每一列都带有名称和类型。

DataSet

  • DataSet是具有强类型的数据集合,需要提供对应的类型信息。

DSL

SparkStreaming

Spark内核

内核概述

Spark 内核泛指 Spark 的核心运行机制, 包括 Spark 核心组件的运行机制、 Spark 任务调 度机制、 Spark 内存管理机制、 Spark 核心功能的运行原理等, 熟练掌握 Spark 内核原理, 能够帮助我们更好地完成 Spark代码设计, 并能够帮助我们准确锁定项目运行过程中出现的问 题的症结所在。

Driver

    Spark驱动器节点,用于执行Spark任务中的main方法,负责实际代码的执行工作。Driver在Spark作业执行时主要负责:

  1. 将用户程序转化为作业(Job);
  2. 在Executor之间调度任务(Task);
  3. 跟踪Executor的执行情况;
  4. 通过UI展示查询运行情况;

 Executor

        Spark Executor对象是负责在Spark作业中运行具体任务,任务彼此之间相互独立。Spark 应用启动时,ExecutorBackend节点被同时启动,并且始终伴随着整个Spark应用的生命周期而存在。如果有ExecutorBackend节点发生了故障或崩溃,Spark应用也可以继续执行,会将出错节点上的任务调度到其他Executor节点上继续运行。

Executor有两个核心功能:

  1. 负责运行组成Spark应用的任务,并将结果返回给驱动器(Driver);
  2. 它们通过自身的块管理器(Block Manager)为用户程序中要求缓存的 RDD 提供内存式存储。RDD是直接缓存在Executor进程内的,因此任务可以在运行时充分利用缓存数据加速运算。

部署模式

  • Standalone:独立模式, Spark 原生的简单集群管理器, 自带完整的服务, 可单独部署到一个集群中, 无需依赖任何其他资源管理系统, 使用 Standalone 可以很方便地搭建一个 集群;

  • Hadoop Yarn:统一的资源管理机制,在上面可以运行多套计算框架, 如 MR 、Storm 等。根据 Driver 在集群中的位置不同,分为 yarn client(集群外) 和 yarn cluster(集群内部)

  • Apache Mesos:一个强大的分布式资源管理框架, 它允许多种不同的框架部署在其上, 包括 Yarn。

  • K8S : 容器式部署环境。

    实际上, 除了上述这些通用的集群管理器外, Spark 内部也提供了方便用户测试和学习 的本地集群部署模式和 Windows 环境。由于在实际工厂环境下使用的绝大多数的集群管理 器是 Hadoop YARN,因此我们关注的重点是 Hadoop YARN模式下的 Spark 集群部署。

Yarn部署模式

YarnCluster模式

  1. 执行脚本提交任务, 实际是启动一个 SparkSubmit 的 JVM 进程;

  2. SparkSubmit 类中的 main 方法反射调用 YarnClusterApplication 的 main 方法;

  3. YarnClusterApplication 创建 Yarn 客户端, 然后向 Yarn 服务器发送执行指令: bin/javaApplicationMaster;

  4. Yarn 框架收到指令后会在指定的 NM 中启动ApplicationMaster;

  5. ApplicationMaster 启动 Driver 线程,执行用户的作业;

  6. AM 向 RM 注册, 申请资源;

  7. 获取资源后 AM 向 NM 发送指令: bin/java YarnCoarseGrainedExecutorBackend;

  8. CoarseGrainedExecutorBackend 进程会接收消息, 跟 Driver 通信, 注册已经启动的 Executor;然后启动计算对象 Executor 等待接收任务

  9. Driver 线程继续执行完成作业的调度和任务的执行。

  10. Driver 分配任务并监控任务的执行。

YarnClient

  1. 执行脚本提交任务, 实际是启动一个 SparkSubmit 的 JVM 进程;

  2. SparkSubmit 类中的 main 方法反射调用用户代码的 main 方法;

  3. 启动 Driver 线程, 执行用户的作业, 并创建 ScheduleBackend;

  4. YarnClientSchedulerBackend 向 RM 发送指令: bin/java ExecutorLauncher;

  5. Yarn 框架收到指令后会在指定的 NM 中启动 ExecutorLauncher (实际上还是调用 ApplicationMaster 的 main 方法);

    object ExecutorLauncher {
    def main(args: Array[String]): Unit = {
    ApplicationMaster.main(args)
    	}
    }

  6. AM 向 RM 注册, 申请资源;

  7. 获取资源后 AM 向 NM 发送指令: bin/java CoarseGrainedExecutorBackend;

  8. CoarseGrainedExecutorBackend 进程会接收消息, 跟 Driver 通信, 注册已经启动的 Executor;然后启动计算对象 Executor 等待接收任务

  9. Driver 分配任务并监控任务的执行。

Standalone 模式运行机制

Standalone 集群有 2 个重要组成部分, 分别是:

  1. Master(RM):是一个进程,主要负责资源的调度和分配, 并进行集群的监控等职责;

  2. Worker(NM):是一个进程, 一个 Worker 运行在集群中的一台服务器上, 主要负责两个 职责, 一个是用自己的内存存储 RDD 的某个或某些 partition;另一个是启动其他进程 和线程(Executor),对 RDD 上的 partition 进行并行的处理和计算。

Cluster

        在 Standalone Cluster 模式下,任务提交后, Master 会找到一个 Worker 启动 Driver。

       Driver 启动后向 Master 注册应用程序, Master 根据 submit 脚本的资源需求找到内部资源至 少可以启动一个 Executor 的所有 Worker,然后在这些 Worker 之间分配 Executor,Worker 上 的 Executor 启动后会向 Driver 反向注册, 所有的 Executor 注册完成后, Driver 开始执行main 函数,之后执行到 Action 算子时,开始划分 Stage,每个 Stage 生成对应的 taskSet,之后将Task 分发到各个 Executor 上执行。

Client

        在 Standalone Client 模式下, Driver 在任务提交的本地机器上运行。 Driver 启动后向 Master 注册应用程序, Master 根据 submit 脚本的资源需求找到内部资源至少可以启动一个 Executor 的所有 Worker,然后在这些 Worker 之间分配 Executor,Worker 上的 Executor 启动 后会向 Driver 反向注册, 所有的 Executor 注册完成后, Driver 开始执行 main 函数, 之后执 行到 Action 算子时, 开始划分 Stage,每个 Stage 生成对应的 TaskSet,之后将 Task 分发到 各个 Executor 上执行。

YarnCluter提交步骤

parkSubmit

	-- main
	
		-- doSubmit
		
			// 解析参数
			-- parseArguments
				--new SparkSubmitArguments(args)
					SparkSubmitArguments
						--parse(args.asJava)
						//action=>SUBMIT
						--loadEnvironmentArguments()
			
				// master => --master => yarn
				// mainClass => --class => SparkPi(WordCount)
				-- parse
				
			-- submit
			
				-- doRunMain
				
				-- runMain
				
					// (childArgs, childClasspath, sparkConf, childMainClass)
					// 【Cluster】childMainClass => org.apache.spark.deploy.yarn.YarnClusterApplication
					// 【Client 】childMainClass => SparkPi(WordCount)
					-- prepareSubmitEnvironment
					
					// Class.forName("xxxxxxx")
					-- mainClass : Class = Utils.classForName(childMainClass)
					
					// classOf[SparkApplication].isAssignableFrom(mainClass)
					-- 【Cluster】a). mainClass.getConstructor().newInstance().asInstanceOf[SparkApplication]
					-- 【Client 】b). new JavaMainApplication(mainClass)
					
					-- app.start
					
YarnClusterApplication

	-- start
	
		// new ClientArguments
		// --class => userClass => SparkPI(WordCount)
		-- new Client
		
		-- client.run
		
			-- submitApplication
			
				// 【Cluster】 org.apache.spark.deploy.yarn.ApplicationMaster
				// 【Client 】 org.apache.spark.deploy.yarn.ExecutorLauncher
				--createContainerLaunchContext
				--createApplicationSubmissionContext

通讯架构

  • Spark早期版本中采用Akka作为内部通讯部件
  • Spark1.3中引入了Netty通讯架构,为了解决Shuffle的大数据传输问题
  • Spark1.6中Akka和Netty可以配置使用。Netty完全实现了Akka在Spark中的功能。
  • Spark2.x系列中抛弃Spark,使用Netty。

三种通讯方式

  1. BIO
  2. NIO(Spark采用)
  3. AIO

RpcEndpoint:RPC 通信终端。 Spark 针对每个节点(Client/Master/Worker)都称之为一 个 RPC 终端,且都实现 RpcEndpoint 接口,内部根据不同端点的需求, 设计不同的消 息和不同的业务处理,如果需要发送(询问) 则调用 Dispatcher 。在 Spark 中,所有的 终端都存在生命周期

        Constructor =》onStart =》receive* =》onStop

RpcEnv:RPC 上下文环境,每个 RPC 终端运行时依赖的上下文环境称为 RpcEnv;在

把当前 Spark 版本中使用的NettyRpcEnv

Dispatcher:消息调度(分发) 器, 针对于 RPC 终端需要发送远程消息或者从远程 RPC

接收到的消息,分发至对应的指令收件箱(发件箱)。如果指令接收方是自己则存入收 件箱, 如果指令接收方不是自己,则放入发件箱;

Inbox:指令消息收件箱。 一个本地 RpcEndpoint 对应一个收件箱, Dispatcher 在每次向

Inbox 存入消息时, 都将对应 EndpointData 加入内部 ReceiverQueue 中, 另外 Dispatcher

创建时会启动一个单独线程进行轮询ReceiverQueue,进行收件箱消息消费;

RpcEndpointRef:RpcEndpointRef 是对远程 RpcEndpoint 的一个引用。 当我们需要向一 个具体的 RpcEndpoint 发送消息时,一般我们需要获取到该RpcEndpoint 的引用,然后 通过该应用发送消息。

OutBox:指令消息发件箱。 对于当前 RpcEndpoint 来说, 一个目标 RpcEndpoint 对应一 个发件箱,如果向多个目标RpcEndpoint 发送信息,则有多个OutBox。当消息放入Outbox 后, 紧接着通过 TransportClient 将消息发送出去。消息放入发件箱以及发送过程是在同 一个线程中进行;

RpcAddress:表示远程的 RpcEndpointRef 的地址, Host + Port。

TransportClient:Netty 通信客户端,一个 OutBox 对应一个 TransportClient,TransportClient 不断轮询 OutBox,根据 OutBox 消息的 receiver 信息,请求对应的远程 TransportServer;

TransportServer:Netty 通信服务端, 一个 RpcEndpoint 对应一个 TransportServer,接受 远程消息后调用Dispatcher 分发消息至对应收发件箱;

Driver

class DriverEndpoint extends IsolatedRpcEndpoint

Executor

class CoarseGrainedExecutorBackend extends IsolatedRpcEndpoint
//Executor
CoarseGrainedExecutorBackend
	--run
		--RpcEnv.create
			--new NettyRpcEnvFactory().create(config)					
				--new NttyRpcEnv
					//outboxs
					--new ConcurrentHashMap[RpcAddress, Outbox]()							
				nettyEnv.startServer
					--transportContext.createServer
							--new TransportServer
								--init
									--NettyUtils.getServerChannelClass
										//NIO EPOLL
										--getServerChannelClass																		
				//NettyRpcEnv
				--create//启动 nettyEnv.startServer
					--Utils.startServiceOnPort					
						--startService
		//获取env								
		--SparkEnv.createExecutorEnv
		
		--env.rpcEnv.setupEndpoint
			--NttyRpcEnv.setupEndpoint
				--dispatcher.registerRpcEndpoint
						--new NettyRpcEndpointRef
						//TransClinet
						
						--new DedicatedMessageLoop
						
							//messages.add(OnStart)
							--new Inbox => onStart	

	--onStart
	--receive
		//RegisteredExecutor
		--new Executor
		--driver.get.send(LaunchedExecutor(executorId))
		
//Driver	
SparkContext
	--_env = createSparkEnv(_conf, isLocal, listenerBus)
		--SparkEnv.createDriverEnv
			--create
				--new NettyRpcEnvFactory().create(config)

任务调度机制

        在生产环境下, Spark 集群的部署方式一般为 YARN-Cluster 模式, 之后的内核分析内容 中我们默认集群的部署方式为 YARN-Cluster 模式。在上一章中我们讲解了 Spark YARN- Cluster 模式下的任务提交流程, 但是我们并没有具体说明 Driver 的工作流程, Driver 线程 主 要 是 初 始 化 SparkContext 对 象 , 准 备 运 行 所 需 的 上 下 文 , 然 后 一 方 面 保 持 与 ApplicationMaster 的 RPC 连接, 通过 ApplicationMaster 申请资源,另一方面根据用户业务

        逻辑开始调度任务, 将任务下发到已有的空闲 Executor 上。

        当 ResourceManager 向 ApplicationMaster 返回 Container 资源时, ApplicationMaster 就尝 试在对应的 Container 上启动 Executor 进程, Executor 进程起来后,会向 Driver 反向注册, 注册成功后保持与 Driver 的心跳,同时等待 Driver 分发任务, 当分发的任务执行完毕后, 将任务状态上报给 Driver。

Spark任务调度概述

        当Driver起来后,Driver则会根据用户程序逻辑准备任务,并根据Executor资源情况逐步分发任务。在详细阐述任务调度前,首先说明下Spark里的几个概念。一个Spark应用程序包括Job、Stage以及Task三个概念:

  1. Job是以Action方法为界,遇到一个Action方法则触发一个Job;
  2. Stage是Job的子集,以RDD宽依赖(即Shuffle)为界,遇到Shuffle做一次划分;
  3. Task是Stage的子集,以并行度(分区数)来衡量,分区数是多少,则有多少个task。

Spark的任务调度总体来说分两路进行,一路是Stage级的调度,一路是Task级的调度,总体调度流程如下图所示:

        Spark RDD通过其Transactions操作,形成了RDD血缘(依赖)关系图,即DAG,最后通过Action的调用,触发Job并调度执行,执行过程中会创建两个调度器:DAGScheduler和TaskScheduler。

  • DAGScheduler负责Stage级的调度,主要是将job切分成若干Stages,并将每个Stage打包成TaskSet交给TaskScheduler调度。
  • TaskScheduler负责Task级的调度,将DAGScheduler给过来的TaskSet按照指定的调度策略分发到Executor上执行,调度过程中SchedulerBackend负责提供可用资源,其中SchedulerBackend有多种实现,分别对接不同的资源管理系统。

 

     Driver初始化SparkContext过程中,会分别初始化DAGScheduler、TaskScheduler、 SchedulerBackend以及HeartbeatReceiver,并启动SchedulerBackend以及HeartbeatReceiver。SchedulerBackend通过ApplicationMaster申请资源,并不断从TaskScheduler中拿到合适的Task分发到Executor执行。HeartbeatReceiver负责接收Executor的心跳信息,监控Executor的存活状况,并通知到TaskScheduler。

Spark Stage级调度

Spark的任务调度是从DAG切割开始,主要是由DAGScheduler来完成。当遇到一个Action操作后就会触发一个Job的计算,并交给DAGScheduler来提交,下图是涉及到Job提交的相关方法调用流程图。

  1. Job由最终的RDD和Action方法封装而成;
  2. SparkContext将Job交给DAGScheduler提交,它会根据RDD的血缘关系构成的DAG进行切分,将一个Job划分为若干Stages,具体划分策略是,由最终的RDD不断通过依赖回溯判断父依赖是否是宽依赖,即以Shuffle为界,划分Stage,窄依赖的RDD之间被划分到同一个Stage中,可以进行pipeline式的计算。划分的Stages分两类,一类叫做ResultStage,为DAG最下游的Stage,由Action方法决定,另一类叫做ShuffleMapStage,为下游Stage准备数据,下面看一个简单的例子WordCount。

Job由saveAsTextFile触发,该Job由RDD-3和saveAsTextFile方法组成,根据RDD之间的依赖关系从RDD-3开始回溯搜索,直到没有依赖的RDD-0,在回溯搜索过程中,RDD-3依赖RDD-2,并且是宽依赖,所以在RDD-2和RDD-3之间划分Stage,RDD-3被划到最后一个Stage,即ResultStage中,RDD-2依赖RDD-1,RDD-1依赖RDD-0,这些依赖都是窄依赖,所以将RDD-0、RDD-1和RDD-2划分到同一个Stage,形成pipeline操作,。即ShuffleMapStage中,实际执行的时候,数据记录会一气呵成地执行RDD-0到RDD-2的转化。不难看出,其本质上是一个深度优先搜索(Depth First Search)算法。

一个Stage是否被提交,需要判断它的父Stage是否执行,只有在父Stage执行完毕才能提交当前Stage,如果一个Stage没有父Stage,那么从该Stage开始提交。Stage提交时会将Task信息(分区信息以及方法等)序列化并被打包成TaskSet交给TaskScheduler,一个Partition对应一个Task,另一方面TaskScheduler会监控Stage的运行状态,只有Executor丢失或者Task由于Fetch失败才需要重新提交失败的Stage以调度运行失败的任务,其他类型的Task失败会在TaskScheduler的调度过程中重试。

相对来说DAGScheduler做的事情较为简单,仅仅是在Stage层面上划分DAG,提交Stage并监控相关状态信息。TaskScheduler则相对较为复杂,下面详细阐述其细节。

Spark Task级调度

Spark Task的调度是由TaskScheduler来完成,由前文可知,DAGScheduler将Stage打包到交给TaskScheTaskSetduler,TaskScheduler会将TaskSet封装为TaskSetManager加入到调度队列中,TaskSetManager结构如下图所示。

TaskSetManager负责监控管理同一个Stage中的Tasks,TaskScheduler就是以TaskSetManager为单元来调度任务。

前面也提到,TaskScheduler初始化后会启动SchedulerBackend,它负责跟外界打交道,接收Executor的注册信息,并维护Executor的状态,所以说SchedulerBackend是管“粮食”的,同时它在启动后会定期地去“询问”TaskScheduler有没有任务要运行,也就是说,它会定期地“问”TaskScheduler“我有这么余粮,你要不要啊”,TaskScheduler在SchedulerBackend“问”它的时候,会从调度队列中按照指定的调度策略选择TaskSetManager去调度运行,大致方法调用流程如下图所示:

上图中,将TaskSetManager加入rootPool调度池中之后,调用SchedulerBackend的riviveOffers方法给driverEndpoint发送ReviveOffer消息;driverEndpoint收到ReviveOffer消息后调用makeOffers方法,过滤出活跃状态的Executor(这些Executor都是任务启动时反向注册到Driver的Executor),然后将Executor封装成WorkerOffer对象;准备好计算资源(WorkerOffer)后,taskScheduler基于这些资源调用resourceOffer在Executor上分配task。

调度策略

TaskScheduler支持两种调度策略,一种是FIFO,也是默认的调度策略,另一种是FAIR。在TaskScheduler初始化过程中会实例化rootPool,表示树的根节点,是Pool类型。

  1. FIFO调度策略

如果是采用FIFO调度策略,则直接简单地将TaskSetManager按照先来先到的方式入队,出队时直接拿出最先进队的TaskSetManager,其树结构如下图所示,TaskSetManager保存在一个FIFO队列中。

  1. FAIR调度策略

FAIR调度策略的树结构如下图所示:

   FAIR模式中有一个rootPool和多个子Pool,各个子Pool中存储着所有待分配的TaskSetMagager。

     在FAIR模式中,需要先对子Pool进行排序,再对子Pool里面的TaskSetMagager进行排序,因为Pool和TaskSetMagager都继承了Schedulable特质,因此使用相同的排序算法。

    排序过程的比较是基于Fair-share来比较的,每个要排序的对象包含三个属性: runningTasks值(正在运行的Task数)、minShare值、weight值,比较时会综合考量runningTasks值,minShare值以及weight值。

     注意,minShare、weight的值均在公平调度配置文件fairscheduler.xml中被指定,调度池在构建阶段会读取此文件的相关配置。

  1. 如果A对象的runningTasks大于它的minShare,B对象的runningTasks小于它的minShare,那么B排在A前面;(runningTasks比minShare小的先执行)
  2. 如果A、B对象的runningTasks都小于它们的minShare,那么就比较runningTasks与minShare的比值(minShare使用率),谁小谁排前面;(minShare使用率低的先执行)
  3. 如果A、B对象的runningTasks都大于它们的minShare,那么就比较runningTasks与weight的比值(权重使用率),谁小谁排前面。(权重使用率低的先执行)
  4. 如果上述比较均相等,则比较名字。

       整体上来说就是通过minShare和weight这两个参数控制比较过程,可以做到让minShare使用率和权重使用率少(实际运行task比例较少)的先运行。

        FAIR模式排序完成后,所有的TaskSetManager被放入一个ArrayBuffer里,之后依次被取出并发送给Executor执行。

        从调度队列中拿到TaskSetManager后,由于TaskSetManager封装了一个Stage的所有Task,并负责管理调度这些Task,那么接下来的工作就是TaskSetManager按照一定的规则一个个取出Task给TaskScheduler,TaskScheduler再交给SchedulerBackend去发到Executor上执行。

 本地化调度

        DAGScheduler切割Job,划分Stage, 通过调用submitStage来提交一个Stage对应的tasks,submitStage会调用submitMissingTasks,submitMissingTasks 确定每个需要计算的 task 的preferredLocations,通过调用getPreferrdeLocations()得到partition 的优先位置,由于一个partition对应一个Task,此partition的优先位置就是task的优先位置,对于要提交到TaskScheduler的TaskSet中的每一个Task,该task优先位置与其对应的partition对应的优先位置一致。

        从调度队列中拿到TaskSetManager后,那么接下来的工作就是TaskSetManager按照一定的规则一个个取出task给TaskScheduler,TaskScheduler再交给SchedulerBackend去发到Executor上执行。前面也提到,TaskSetManager封装了一个Stage的所有Task,并负责管理调度这些Task。

        根据每个Task的优先位置,确定Task的Locality级别,Locality一共有五种,优先级由高到低顺序:

名称

解析

PROCESS_LOCAL

进程本地化,task和数据在同一个Executor中,性能最好。

NODE_LOCAL

节点本地化,task和数据在同一个节点中,但是task和数据不在同一个Executor中,数据需要在进程间进行传输。

RACK_LOCAL

机架本地化,task和数据在同一个机架的两个节点上,数据需要通过网络在节点之间进行传输。

NO_PREF

对于task来说,从哪里获取都一样,没有好坏之分。

ANY

task和数据可以在集群的任何地方,而且不在一个机架中,性能最差。

        在调度执行时,Spark调度总是会尽量让每个task以最高的本地性级别来启动,当一个task以X本地性级别启动,但是该本地性级别对应的所有节点都没有空闲资源而启动失败,此时并不会马上降低本地性级别启动而是在某个时间长度内再次以X本地性级别来启动该task,若超过限时时间则降级启动,去尝试下一个本地性级别,依次类推。

        可以通过调大每个类别的最大容忍延迟时间,在等待阶段对应的Executor可能就会有相应的资源去执行此task,这就在在一定程度上提到了运行性能。

 失败重试与黑名单机制

        除了选择合适的Task调度运行外,还需要监控Task的执行状态,前面也提到,与外部打交道的是SchedulerBackend,Task被提交到Executor启动执行后,Executor会将执行状态上报给SchedulerBackend,SchedulerBackend则告诉TaskScheduler,TaskScheduler找到该Task对应的TaskSetManager,并通知到该TaskSetManager,这样TaskSetManager就知道Task的失败与成功状态,对于失败的Task,会记录它失败的次数,如果失败次数还没有超过最大重试次数,那么就把它放回待调度的Task池子中,否则整个Application失败。

        在记录Task失败次数过程中,会记录它上一次失败所在的Executor Id和Host,这样下次再调度这个Task时,会使用黑名单机制,避免它被调度到上一次失败的节点上,起到一定的容错作用。黑名单记录Task上一次失败所在的Executor Id和Host,以及其对应的“拉黑”时间,“拉黑”时间是指这段时间内不要再往这个节点上调度这个Task了。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值