Spark编程入口 — SparkContext及其初始化

在之前我们分析了Spark的内核架构运行机制,其中有一个很重要的组件SparkContext,这里我们就分析SparkContext的相关源码。

我们知道所有的Spark程序,在运行之前都需要创建一个非常重要的组件,那就是SparkContext,它负责读取SparkConf中的相关配置信息,并且初始化一些Spark Application运行时需要用到的两个重要的组件DAGScheduler和TaskScheduler。

下面我们首先看一下整个代码的一个运行流程:

针对流程,我们具体看源码:首先是SparkContext的源码(版本1.6):

    // 创建和初始化TaskScheduler 和 DAGScheduler
    val (sched, ts) = SparkContext.createTaskScheduler(this, master)
    _schedulerBackend = sched
    _taskScheduler = ts
    _dagScheduler = new DAGScheduler(this)
    _heartbeatReceiver.ask[Boolean](TaskSchedulerIsSet)

    // start TaskScheduler after taskScheduler sets DAGScheduler reference in DAGScheduler's
    // constructor
    // TaskScheduler初始化,里面包含了注册Master
    _taskScheduler.start()

从上面的源码中可以看出,首先初始化化了两个组件TaskScheduler和DAGScheduler。它调用createTaskScheduler()方法,创建了TaskSchedulerImpl和SparkDepolySchedulerBackend。接着就启动TaskScheduler,调用了它的start()方法。

下面看一下createTaskScheduler()方法:

createTaskScheduler依据不同的提交模式创建对应的TaskScheduler和SparkDepolySchedulerBackend,我们这里看常用的standalone提交模式是怎么创建的:

private def createTaskScheduler(
      sc: SparkContext,
      master: String): (SchedulerBackend, TaskScheduler) = {
    // 省略代码
    ........
    // 这是我们常用的spark提交模式中的standalone方式
    case SPARK_REGEX(sparkUrl) =>
        // 创建TaskScheduler
        val scheduler = new TaskSchedulerImpl(sc)
        // 提取Master的URL地址
        val masterUrls = sparkUrl.split(",").map("spark://" + _)
        // 创建SparkDepolySchedulerBackend
        val backend = new SparkDeploySchedulerBackend(scheduler, sc, masterUrls)
        // 初始化
        scheduler.initialize(backend)
        // 将创建的两个组件返回
        (backend, scheduler)

    // 省略代码
    ........
 }

这里就是初始化两个组件,并将其返回。

接着我们看一下TaskScheduler的initialize()方法:它的主要功能就是创建Task的调度池,有两种调度方式:FIFO和FAIR,默认是FIFO的调度算法,也可以手动设置,只要设置参数:spark.scheduler.mode 为 FAIR即可。这两种算法的不同,这里不做具体分析了。大概就是,FIFO就是比较StageID大小,谁小(优先级高)谁就执行;FAIR就是,先比较task的数量,谁小谁先执行;不行的话,再比较task的权重,谁大谁先执行,假如都一样大,那么就按照名字比较谁大谁执行(感兴趣研究的FairSchedulingAlgorithm和FIFOSchedulingAlgorithm这两个方法)。

// 初始化Task的调度策略,默认是FIFO
def initialize(backend: SchedulerBackend) {
    this.backend = backend
    // temporarily set rootPool name to empty
    rootPool = new Pool("", schedulingMode, 0, 0)
    schedulableBuilder = {
      schedulingMode match {
        case SchedulingMode.FIFO =>
          new FIFOSchedulableBuilder(rootPool)
        case SchedulingMode.FAIR =>
          new FairSchedulableBuilder(rootPool, conf)
      }
    }
    // 创建调度池
    schedulableBuilder.buildPools()
  }

最后我们看一下taskScheduler的start()方法:

override def start() {
    // 调用的是SparkDeploySchedulerBackend的start()方法
    backend.start()

    if (!isLocal && conf.getBoolean("spark.speculation", false)) {
      logInfo("Starting speculative execution thread")
      speculationScheduler.scheduleAtFixedRate(new Runnable {
        override def run(): Unit = Utils.tryOrStopSparkContext(sc) {
          checkSpeculatableTasks()
        }
      }, SPECULATION_INTERVAL_MS, SPECULATION_INTERVAL_MS, TimeUnit.MILLISECONDS)
    }
  }

由于它调用的是SparkDeploySchedulerBackend的start方法,下面我们接着看:

override def start() {
    
    // 创建DriverEndPoint,用于与Executor进行通信,用于接收executor的状态信息等
    super.start()
    // launcher服务
    launcherBackend.connect()

    // 省略代码,主要是一些参数配置
    .............................    

    // Start executors with a few necessary configs for registering with the scheduler
    val sparkJavaOpts = Utils.sparkJavaOpts(conf, SparkConf.isExecutorStartupConf)
    val javaOpts = sparkJavaOpts ++ extraJavaOpts
    val command = Command("org.apache.spark.executor.CoarseGrainedExecutorBackend",
      args, sc.executorEnvs, classPathEntries ++ testingClassPath, libraryPathEntries, javaOpts)
    val appUIAddress = sc.ui.map(_.appUIAddress).getOrElse("")
    val coresPerExecutor = conf.getOption("spark.executor.cores").map(_.toInt)

    // 它代表了当前执行的这个application的一些参数
    // 包括 application最大需要多少cpu core,每个worker上需要多少内存
    // 对应spark-submit的脚本中的
    // --num-executors  --executor-memory  --executor-cores
    val appDesc = new ApplicationDescription(sc.appName, maxCores, sc.executorMemory,
      command, appUIAddress, sc.eventLogDir, sc.eventLogCodec, coresPerExecutor)

    // 创建了AppClient  -- ClientEndPoint
    client = new AppClient(sc.env.rpcEnv, masters, appDesc, this, conf)
    // 连接Master,注册Application
    client.start()
    // 注册状态更新
    launcherBackend.setState(SparkAppHandle.State.SUBMITTED)
    // 等待executor注册返回
    waitForRegistration()
    // 注册成功
    launcherBackend.setState(SparkAppHandle.State.RUNNING)
  }

这里就创建了一个重要的组件AppClient,接着调用它的start方法,里面创建了一个ClientEndPoint,调用它的onStart()方法,onStart方法中接着调用了registerWithMaster方法,它里面接着调用了tryRegisterAllMasters()方法。我们看一下这个方法:

def start() {
    // Just launch an rpcEndpoint; it will call back into the listener.
    // 调用RpcEndPoint,里面创建了ClientEndPoint,并调用它的start方法
    endpoint.set(rpcEnv.setupEndpoint("AppClient", new ClientEndpoint(rpcEnv)))
  }
override def onStart(): Unit = {
      try {
        // 调用了这个方法,传入的参数是,尝试次数
        registerWithMaster(1)
      } catch {
        case e: Exception =>
          logWarning("Failed to connect to master", e)
          markDisconnected()
          stop()
      }
    }
// 尝试注册所有的master
private def tryRegisterAllMasters(): Array[JFuture[_]] = {
      // 遍历Master
      for (masterAddress <- masterRpcAddresses) yield {
        registerMasterThreadPool.submit(new Runnable {
          override def run(): Unit = try {
            if (registered.get) {
              return
            }
            logInfo("Connecting to master " + masterAddress.toSparkURL + "...")
            // 连接Master
            val masterRef =
              rpcEnv.setupEndpointRef(Master.SYSTEM_NAME, masterAddress, Master.ENDPOINT_NAME)
            // 向Master注册Application的信息
            masterRef.send(RegisterApplication(appDescription, self))
          } catch {
            case ie: InterruptedException => // Cancelled
            case NonFatal(e) => logWarning(s"Failed to connect to master $masterAddress", e)
          }
        })
      }
    }

通过上面代码可以看出,通过tryRegisterAllMasters来注册所有的Master,为什么是所有的Master,下篇文章再进行分组,这里它就向master发送Application的注册信息。

当所有的Executor都反向注册到SparkDeploySchedulerBackend上时,整个SparkContext就初始化完成了,就会接着执行我们的Application作业。

上述源码的分析,就对应着我们一开始的流程图。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值