Spark 启动 & job Workflow

启动

什么叫做启动Spark Application?

首先,要想象为什么需要用Spark?

计算一个文件行数,直接启动一个jvm去读取文件,计算行数。

文件越来越来大,单机效果不理想,于是希望采取分布式的方式执行,让多台机子分别去统计一部分数据,之后汇总结果。分布式任务执行又涉及到资源的调度、failover、分布式信息的同步等等复杂问题,此时我们需要一个框架来帮我们屏蔽这些。对了,这样我们就用到了Spark框架来方便我们执行分布式的计算任务。 因此,启动一个Spark Application,可以简单的认为启动一个分布式的任务执行中间件,这个application可以将我提交的任务分布式得完成计算。

那回到问题,怎么才算启动了一个Spark Application?

要说明这一点,又需要引入两个概念:SparkSession, SparkContext.

SparkContext:A SparkContext represents the connection to a Spark cluster, and can be used to create RDDs, accumulators and broadcast variables on that cluster. we can only have one active SparkContext per JVM

SparkSession: Spark 2+, 引入DataSet、DataFrame的 API入口

查看两者源代码,可以看出SparkSession将SparkContext 封装在其里面,并额外添加了基于DataSet、DataFrame的API. SparkContext则更为具体,首先从API层面上,SparkContext直接提供的是RDD、broadcast、accumulator、job等Spark基础概念的底层API;其次从类成员变量上看,可以发现SparkContext的内容十分丰富且十分关键,比如_dagScheduler、_env、_taskScheduler、_applicationId.从变量和方法定义上就可以看出,SparkContext维护driver到executor之间的所有交互(任务分发、任务状态等),同时还包括与资源管理者(yarn)的交互(在物理资源上申请executor)等等。而_applicationId基本就在明说一个SparkContext=一个Spark Application.

== to-complete  ==

experiment: create two session connected to one application

复制代码

启动

local Application

一般用于本地调试用

spark2.2

val spark = SparkSession
      .builder()
      .appName("Spark SQL basic example")
      .master("local[4]")
      .getOrCreate()
复制代码
spark 1.6

val config = new SparkConf().setMaster("local[5]").setAppName("test")
val sc = SparkContext.getOrCreate(config)

复制代码
driver SparkContext 启动代码

如前文所说,SparkContext就对应一个application(spark 工作集群),SparkContext的create过程即为application的启动过程。( 只有driver有sparkContext, executor都会有SparkEnv, not SparkContext)

查看SparkContext.scala源文件,可以发现有SparkContext类以及其伴生对象

class SparkContext : SparkContext 封装类,当前开放设计上一个jvm只能有一个对象实例
object SparkContext : 提供一些SparkContext相关的静态方法, 比方说getOrCrea0te
复制代码

以SparkContext.getOrCreate(sparkConf) 为入口,通过加锁控制,获取当前active context 或新建一个SparkContext实例。

SparkContext.class

主要成员变量

其中几个主要的变量说明:

listenerBus: 用来传输spark事件,如webui通过监听一些启动、task完成等等事件来完成web上的数据刷新等。
_conf: SparkConf 配置信息,从外部传入数据拷贝生成
_env: Spark运行环境, 非常重要, 包括了该instance运行时所有需要需要的东西,如序列化、数据块管理、broadcast管理 、map output tracker
_jobProgressListener: JobProgressListener 
_ui: Spark UI


复制代码

查看SparkContext的初始化代码,关键步骤说明如下

  1. SparkEnv初始化

    // Create the Spark execution environment (cache, map output tracker, etc)
     _env = createSparkEnv(_conf, isLocal, listenerBus)
     SparkEnv.set(_env)
    复制代码

    SparkEnv中封装了该spark instance运行时需要的所有东西,如序列化、数据块管理、broadcast管理 、map output tracker。

    初始化步骤

    1.1 生成一个RpcEnv,RpcEnv中包含streamingManager,用来管理jars以及files,若当前instance为driver,则会按照RpcEnv里的端口设定等,启动netty server,作为RPC server;若为executor,==todo==

    1.2 生成序列化相关,serializerManager(用户可指定序列户类) 、closureSerializer(使用JavaSerializer)

    1.3 生成boradcastManager, 用来管理boradcast变量

    1.4 生成mapOutputTracker,有driver / executor之分

    1.5 shuffleManager

    1.6 memoryManager

    1.7 blockManager

    1.8 metricsSystem

    1.9 outputCommitCoordinator

  2. _ui SparkUI spark 提供的web UI,可查看spark 集群中任务的运行情况、内存、task分布等等,十分有用。实际上就是基于jetty实现的http server.

     _ui =
      if (conf.getBoolean("spark.ui.enabled", true)) {
        Some(SparkUI.createLiveUI(this, _conf, listenerBus, _jobProgressListener,
          _env.securityManager, appName, startTime = startTime))
      } else {
        // For tests, do not enable the UI
        None
      }
    // Bind the UI before starting the task scheduler to communicate
    // the bound port to the cluster manager properly
    _ui.foreach(_.bind())
    复制代码
  3. 添加jars,files

    添加jars、files到env.fileserver里,后续通过filerserver提供访问这些文件的接口,目前看来是通过NettryStreamMananger实现

  4. 任务执行相关

    4.1 读取executor设置参数,driver的rpc server添加hearbeat相关处理。

    4.2 生成调度相关

     _dagScheduler,最上层的调度层,以stage为单位进行调度。负责将每一个job按照shuffle分为stage,形成DAG图,优化stage的调度顺序。 一个stage里包含多个task,每个task内容一样,task数目=partition数目,dagScheduler将task提交给 _taskScheduler
    
     _taskScheduler, 负责接受DAG提交的stage,也就是提交的任务集(taskSet),放入task Pool中。根据调度策略(FIFO/FAIR)向下层(物理资源层)提交任务,屏蔽了底层具体的物理资源调度,物理层上的一层抽象。根据 集群资源管理设置(local, yarn, mesos)会有不同的实现,但差异 应该不大(继承自TaskSchedulerImpl)
    
     _schedulerBackend, 根据 集群资源管理设置(local, yarn, mesos) 来实际完成与资源管理器的交互, 从_schedulerBackend中获取applicaitonId, 并更新给其他
    复制代码

    4.3 启动_taskScheduler,(此时应该启动driver的executor? local模式下此时启动了driver-executor)

    4.3 启动_executorAllocationManager

至此,Spark Application已经成功启动,local 模式下,_schedulerBackend实现为LocalSchedulerBackend,local模式下,所有进程均在同一jvm中,此时driver executor已经启动,executor里自带线程池,用来接受提交的任务。

job 启动

示例:

val dataRdd = sc.parallelize(data, 3)
dataRdd.count()
复制代码
dataframe在RDD上封装一层,直接从dataframe作为入口查看任务执行,太复杂了 
复制代码

count函数实现为:

def count(): Long = sc.runJob(this, Utils.getIteratorSize _).sum
复制代码

即通过sparkContext提交任务,将每个partition计算的Utils.getIteratorSize( job 的运行结果Array[T])进行求和返回。 主题核心为runJob方法

 def runJob[T, U: ClassTag](
      rdd: RDD[T],
      func: (TaskContext, Iterator[T]) => U,
      partitions: Seq[Int]): Array[U] = {
    
    // job 返回的result 为 Array[U] 大小为partition个数
    val results = new Array[U](partitions.size)
    // runJob 参数: 数据源rdd, partition, resultHandler:此处为将各个子结果放入result数组中
    runJob[T, U](rdd, func, partitions, (index, res) => results(index) = res)
    results
  }
复制代码

继续

 def runJob[T, U: ClassTag](
      rdd: RDD[T],
      func: (TaskContext, Iterator[T]) => U,
      partitions: Seq[Int],
      resultHandler: (Int, U) => Unit): Unit = {
    if (stopped.get()) {
      throw new IllegalStateException("SparkContext has been shutdown")
    }
    val callSite = getCallSite
    // Clean a closure to make it ready to serialized and send to tasks (removes unreferenced variables in $outer's, updates REPL variables)
    val cleanedFunc = clean(func)
    logInfo("Starting job: " + callSite.shortForm)
    if (conf.getBoolean("spark.logLineage", false)) {
      logInfo("RDD's recursive dependencies:\n" + rdd.toDebugString)
    }
    //向dag调度器提交该job,
    // 数据源rdd, 生成的闭包(任务执行需要的fun以及需要access 的field),partiton, driver code 中该方法的触发调用地址, resultHandler函数, properties
    dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get)
    progressBar.foreach(_.finishAll())
    rdd.doCheckpoint()
  }

复制代码

DAGScheduler.scala

def runJob[T, U](
      rdd: RDD[T],
      func: (TaskContext, Iterator[T]) => U,
      partitions: Seq[Int],
      callSite: CallSite,
      resultHandler: (Int, U) => Unit,
      properties: Properties): Unit = {
    val start = System.nanoTime
    //提交job, 生成job waiter(相当与futer), 这个方法向eventProcessLoop里提交任务信息,由DAGSchedulerEventProcessLoop来负责处理。DAGSchedulerEventProcessLoop实际即为生产-消费,blockingQueue存放各种DAGSchedulerEvent,另一个线程负责持续消费,最后都是交由dagScheduler来对event进行各种异步处理。
    val waiter = submitJob(rdd, func, partitions, callSite, resultHandler, properties)
    
    //job 要经过各个executor执行,最后当所有 task都完成,对job waiter进行唤醒, driver的程序唤醒后对Array[T]进行处理,获得最终结果。
    ThreadUtils.awaitReady(waiter.completionFuture, Duration.Inf)
    waiter.completionFuture.value.get match {
      case scala.util.Success(_) =>
        logInfo("Job %d finished: %s, took %f s".format
          (waiter.jobId, callSite.shortForm, (System.nanoTime - start) / 1e9))
      case scala.util.Failure(exception) =>
        logInfo("Job %d failed: %s, took %f s".format
          (waiter.jobId, callSite.shortForm, (System.nanoTime - start) / 1e9))
        // SPARK-8644: Include user stack trace in exceptions coming from DAGScheduler.
        val callerStackTrace = Thread.currentThread().getStackTrace.tail
        exception.setStackTrace(exception.getStackTrace ++ callerStackTrace)
        throw exception
    }
  }
复制代码

至此,Spark最外层的job执行流程走完,总结下即

  1. driver将job的提交生成一个JobSubmitted event, 等待job complete,接受partition上计算出来的结果
  2. 对结果进行汇总

要点:

  1. DAG eventloop 是driver进行分布式调度消息分发的核心,executor的所有事件(包括executor事件、Job事件、task事件)均投递至此,并由DAG进行相应的处理
  2. JobWaiter的阻塞和唤醒
  3. resultHandler partition结果汇总至此

通过代码可以看出,任务执行的核心包括任务提交的处理,分布式下任务状态的维护等等,核心逻辑均在于DAGScheduler如何处理其相对应的事件。

private def doOnReceive(event: DAGSchedulerEvent): Unit = event match {
    case JobSubmitted(jobId, rdd, func, partitions, callSite, listener, properties) =>
      dagScheduler.handleJobSubmitted(jobId, rdd, func, partitions, callSite, listener, properties)

    case MapStageSubmitted(jobId, dependency, callSite, listener, properties) =>
      dagScheduler.handleMapStageSubmitted(jobId, dependency, callSite, listener, properties)
    ......
复制代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值