Spark技术内幕:Executor分配详解

本文详细介绍了在Spark中,从创建SparkContext开始,如何通过Standalone模式的SchedulerBackend为集群分配executor的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

当用户应用new SparkContext后,集群就会为在Worker上分配executor,那么这个过程是什么呢?本文以Standalone的Cluster为例,详细的阐述这个过程。序列图如下:

1. SparkContext创建TaskScheduler和DAG Scheduler

SparkContext是用户应用和Spark集群的交换的主要接口,用户应用一般首先要创建它。如果你使用SparkShell,你不必自己显式去创建它,系统会自动创建一个名字为sc的SparkContext的实例。创建SparkContext的实例,主要的工作除了设置一些conf,比如executor使用到的memory的大小。如果系统的配置文件有,那么就读取该配置。否则则读取环境变量。如果都没有设置,那么取默认值为512M。当然了这个数值还是很保守的,特别是在内存已经那么昂贵的今天。

[java]  view plain  copy
  1. private[spark] val executorMemory = conf.getOption("spark.executor.memory")  
  2.     .orElse(Option(System.getenv("SPARK_EXECUTOR_MEMORY")))  
  3.     .orElse(Option(System.getenv("SPARK_MEM")).map(warnSparkMem))  
  4.     .map(Utils.memoryStringToMb)  
  5.     .getOrElse(512)  

除了加载这些集群的参数,它 完成了TaskScheduler和DAGScheduler的创建:

[java]  view plain  copy
  1. // Create and start the scheduler  
  2. private[spark] var taskScheduler = SparkContext.createTaskScheduler(this, master)  
  3. private val heartbeatReceiver = env.actorSystem.actorOf(  
  4.   Props(new HeartbeatReceiver(taskScheduler)), "HeartbeatReceiver")  
  5. @volatile private[spark] var dagScheduler: DAGScheduler = _  
  6. try {  
  7.   dagScheduler = new DAGScheduler(this)  
  8. catch {  
  9.   case e: Exception => throw  
  10.     new SparkException("DAGScheduler cannot be initialized due to %s".format(e.getMessage))  
  11. }  
  12.   
  13. // start TaskScheduler after taskScheduler sets DAGScheduler reference in DAGScheduler's  
  14. // constructor  
  15. taskScheduler.start()  

TaskScheduler是通过不同的SchedulerBackend来调度和管理任务。它包含资源分配和任务调度。它实现了FIFO调度和FAIR调度,基于此来决定不同jobs之间的调度顺序。并且管理任务,包括任务的提交和终止,为饥饿任务启动备份任务。

不同的Cluster,包括local模式,都是通过不同的SchedulerBackend的实现其不同的功能。这个模块的类图如下:



2. TaskScheduler通过SchedulerBackend创建AppClient

SparkDeploySchedulerBackend是Standalone模式的SchedulerBackend。通过创建AppClient,可以向Standalone的Master注册Application,然后Master会通过Application的信息为它分配Worker,包括每个worker上使用CPU core的数目等。

[java]  view plain  copy
  1. private[spark] class SparkDeploySchedulerBackend(  
  2.     scheduler: TaskSchedulerImpl,  
  3.     sc: SparkContext,  
  4.     masters: Array[String])  
  5.   extends CoarseGrainedSchedulerBackend(scheduler, sc.env.actorSystem)  
  6.   with AppClientListener  
  7.   with Logging {  
  8.   
  9.   var client: AppClient = null  <span style="font-family: Arial, Helvetica, sans-serif;">//注:Application与Master的接口</span>  
  10.   
  11.   val maxCores = conf.getOption("spark.cores.max").map(_.toInt) <span style="font-family: Arial, Helvetica, sans-serif;">//注:获得每个executor最多的CPU core数目</span>  
  12.   override def start() {  
  13.     super.start()  
  14.   
  15.     // The endpoint for executors to talk to us  
  16.     val driverUrl = "akka.tcp://%s@%s:%s/user/%s".format(  
  17.       SparkEnv.driverActorSystemName,  
  18.       conf.get("spark.driver.host"),  
  19.       conf.get("spark.driver.port"),  
  20.       CoarseGrainedSchedulerBackend.ACTOR_NAME)  
  21.     //注:现在executor还没有申请,因此关于executor的所有信息都是未知的。  
  22.     //这些参数将会在org.apache.spark.deploy.worker.ExecutorRunner启动ExecutorBackend的时候替换这些参数  
  23.     val args = Seq(driverUrl, "{{EXECUTOR_ID}}""{{HOSTNAME}}""{{CORES}}""{{WORKER_URL}}")  
  24.     //注:设置executor运行时需要的环境变量  
  25.     val extraJavaOpts = sc.conf.getOption("spark.executor.extraJavaOptions")  
  26.       .map(Utils.splitCommandString).getOrElse(Seq.empty)  
  27.     val classPathEntries = sc.conf.getOption("spark.executor.extraClassPath").toSeq.flatMap { cp =>  
  28.       cp.split(java.io.File.pathSeparator)  
  29.     }  
  30.     val libraryPathEntries =  
  31.       sc.conf.getOption("spark.executor.extraLibraryPath").toSeq.flatMap { cp =>  
  32.         cp.split(java.io.File.pathSeparator)  
  33.       }  
  34.   
  35.     // Start executors with a few necessary configs for registering with the scheduler  
  36.     val sparkJavaOpts = Utils.sparkJavaOpts(conf, SparkConf.isExecutorStartupConf)  
  37.     val javaOpts = sparkJavaOpts ++ extraJavaOpts  
  38.     //注:在Worker上通过org.apache.spark.deploy.worker.ExecutorRunner启动  
  39.     // org.apache.spark.executor.CoarseGrainedExecutorBackend,这里准备启动它需要的参数  
  40.     val command = Command("org.apache.spark.executor.CoarseGrainedExecutorBackend",  
  41.       args, sc.executorEnvs, classPathEntries, libraryPathEntries, javaOpts)  
  42.     //注:org.apache.spark.deploy.ApplicationDescription包含了所有注册这个Application的所有信息。  
  43.     val appDesc = new ApplicationDescription(sc.appName, maxCores, sc.executorMemory, command,  
  44.       sc.ui.appUIAddress, sc.eventLogger.map(_.logDir))  
  45.   
  46.     client = new AppClient(sc.env.actorSystem, masters, appDesc, this, conf)  
  47.     client.start()  
  48.     //注:在Master返回注册Application成功的消息后,AppClient会回调本class的connected,完成了Application的注册。  
  49.     waitForRegistration()  
  50.   }  

org.apache.spark.deploy.client.AppClientListener是一个trait,主要为了SchedulerBackend和AppClient之间的函数回调,在以下四种情况下,AppClient会回调相关函数以通知SchedulerBackend:
  1. 向Master成功注册Application,即成功链接到集群;
  2. 断开连接,如果当前SparkDeploySchedulerBackend::stop == false,那么可能原来的Master实效了,待新的Master ready后,会重新恢复原来的连接;
  3. Application由于不可恢复的错误停止了,这个时候需要重新提交出错的TaskSet;
  4. 添加一个Executor,在这里的实现仅仅是打印了log,并没有额外的逻辑;
  5. 删除一个Executor,可能有两个原因,一个是Executor退出了,这里可以得到Executor的退出码,或者由于Worker的退出导致了运行其上的Executor退出,这两种情况需要不同的逻辑来处理。
[java]  view plain  copy
  1. private[spark] trait AppClientListener {  
  2.   def connected(appId: String): Unit  
  3.   
  4.   /** Disconnection may be a temporary state, as we fail over to a new Master. */  
  5.   def disconnected(): Unit  
  6.   
  7.   /** An application death is an unrecoverable failure condition. */  
  8.   def dead(reason: String): Unit  
  9.   
  10.   def executorAdded(fullId: String, workerId: String, hostPort: String, cores: Int, memory: Int)  
  11.   
  12.   def executorRemoved(fullId: String, message: String, exitStatus: Option[Int]): Unit  
  13. }  


小结:SparkDeploySchedulerBackend装备好启动Executor的必要参数后,创建AppClient,并通过一些回调函数来得到Executor和连接等信息;通过org.apache.spark.scheduler.cluster.CoarseGrainedSchedulerBackend.DriverActor与ExecutorBackend来进行通信。

3. AppClient向Master提交Application


AppClient是Application和Master交互的接口。它的包含一个类型为org.apache.spark.deploy.client.AppClient.ClientActor的成员变量actor。它负责了所有的与Master的交互。actor首先向Master注册Application。如果超过20s没有接收到注册成功的消息,那么会重新注册;如果重试超过3次仍未成功,那么本次提交就以失败结束了。

[java]  view plain  copy
  1. def tryRegisterAllMasters() {  
  2.   for (masterUrl <- masterUrls) {  
  3.     logInfo("Connecting to master " + masterUrl + "...")  
  4.     val actor = context.actorSelection(Master.toAkkaUrl(masterUrl))  
  5.     actor ! RegisterApplication(appDescription) // 向Master注册  
  6.   }  
  7. }  
  8.   
  9. def registerWithMaster() {  
  10.   tryRegisterAllMasters()  
  11.   import context.dispatcher  
  12.   var retries = 0  
  13.   registrationRetryTimer = Some { // 如果注册20s内未收到成功的消息,那么再次重复注册  
  14.     context.system.scheduler.schedule(REGISTRATION_TIMEOUT, REGISTRATION_TIMEOUT) {  
  15.       Utils.tryOrExit {  
  16.         retries += 1  
  17.         if (registered) { // 注册成功,那么取消所有的重试  
  18.           registrationRetryTimer.foreach(_.cancel())  
  19.         } else if (retries >= REGISTRATION_RETRIES) { // 重试超过指定次数(3次),则认为当前Cluster不可用,退出  
  20.           markDead("All masters are unresponsive! Giving up.")  
  21.         } else { // 进行新一轮的重试<span style="margin: 0px; padding: 0px; border: non
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值