文章目录

  • Spark源码剖析——Master、Worker启动流程
  • 当前环境与版本
  • 1. 前言
  • 2. Master启动流程
  • 2.1 Master的伴生对象
  • 2.2 Master
  • 3. Worker启动流程
  • 3.1 Worker的伴生对象
  • 3.2 Worker
  • 4. Master与Worker的初步交互(注册)


Spark源码剖析——Master、Worker启动流程

当前环境与版本

环境

版本

JDK

java version “1.8.0_231” (HotSpot)

Scala

Scala-2.11.12

Spark

spark-2.4.4

1. 前言

  • Master与Worker是Spark在Standalone模式下的主要节点,维护起了整个分布式集群的管理、资源分配、应用运行等重要工作。
  • Master、Worker都是ThreadSafeRpcEndpoint的实现类,其启动流程部分较简单,查看此部分的代码,可以帮助我们快速上手,理解到集群中RpcEndpoint、RpcEnv的交互过程。这样在后续过程中查看其他代码将更加容易。
  • 在看此部分之前,建议先看Spark源码剖析——RpcEndpoint、RpcEnv

2. Master启动流程

2.1 Master的伴生对象

  • org.apache.spark.deploy.master.Master
  • 我们先看Master的伴生对象,此处是Java进程的入口(被start-master.sh启动)
private[deploy] object Master extends Logging {
  val SYSTEM_NAME = "sparkMaster"
  val ENDPOINT_NAME = "Master"

  def main(argStrings: Array[String]) {
    Thread.setDefaultUncaughtExceptionHandler(new SparkUncaughtExceptionHandler(
      exitOnUncaughtException = false))
    Utils.initDaemon(log)
    val conf = new SparkConf
    // 此处会解析外部传入的参数argStrings,由其内部的parse方法解析
    // 示例:--port 7077 --webui-port 8081
    val args = new MasterArguments(argStrings, conf)
    // 启动Master对应的RpcEndpoint、NettyRpcEnv
    val (rpcEnv, _, _) = startRpcEnvAndEndpoint(args.host, args.port, args.webUiPort, conf)
    
    // 此处调用的就是我们前面在NettyRpcEnv所讲的
    // 'ctrl + alt + 鼠标左键' 点击'awaitTermination',选择其实现类NettyRpcEnv,可以看到调用了dispatcher
    // 再继续点击,可以看到实际是调用了threadpool.awaitTermination(...),在此处进行了阻塞
    // 而该threadpool正是运行了MessageLoop(用于处理Inbox消息)的线程池
    rpcEnv.awaitTermination()
  }

  def startRpcEnvAndEndpoint(
      host: String,
      port: Int,
      webUiPort: Int,
      conf: SparkConf): (RpcEnv, Int, Option[Int]) = {
    // 安全管理,例如ACL、Sasl 
    val securityMgr = new SecurityManager(conf)
    // 创建NettyRpcEnv,由NettyRpcEnvFactory调用create(...)创建
    val rpcEnv = RpcEnv.create(SYSTEM_NAME, host, port, conf, securityMgr)
    // 创建Master这个RpcEndpoint,并将其注册到RpcEnv中
    val masterEndpoint = rpcEnv.setupEndpoint(ENDPOINT_NAME,
      new Master(rpcEnv, rpcEnv.address, webUiPort, securityMgr, conf))
     // 向Master发送了一个BoundPortsRequest,并同步返回一个BoundPortsResponse(包含了Master的端口信息)
    val portsResponse = masterEndpoint.askSync[BoundPortsResponse](BoundPortsRequest)
    (rpcEnv, portsResponse.webUIPort, portsResponse.restPort)
  }
}
  • 此处代码,相对来说还是比较简单的。Shell调用start-master.sh后,会启动一个Java进程。传入的参数则被MasterArguments进行了解析,最重要的参数是host、port、webUiPort。
  • 接着,就会调用startRpcEnvAndEndpoint(…),开始创建NettyRpcEnv与Master,并将Master注册进RpcEnv。
  • 创建NettyRpcEnv是利用的NettyRpcEnvFactory调用create(…)
  • Master则是直接被new实例化,此时该RpcEndpoint的构造器被调用
  • 注册Master则是调用了setupEndpoint(…),进而调用了dispatcher的registerRpcEndpoint(…)方法:
  • 为Master创建了一个EndpointData,包含一个Inbox。Inbox实例化时顺带将OnStart消息放入了队列。
  • 将EndpointData放入了receivers队列中,后续会被MessageLoop取出
  • 因此,我们可以看到,Master被实例化时,先调用了其构造器。接着,将其注册入RpcEnv时,其Inbox中放入了第一条消息OnStart。然后,该消息OnStart将被MessageLoop取出并处理,调用了Master这个Endpoint的onStart方法。也就是说Master的生命周期前面部分是:constructor -> onStart -> …

2.2 Master

  • org.apache.spark.deploy.master.Master
  • Master的class代码相对来说还是比较多的,我们主要看起启动流程部分代码
  • 首先,我们来看其onStart()方法做了什么
override def onStart(): Unit = {
  logInfo("Starting Spark master at " + masterUrl)
  logInfo(s"Running Spark version ${org.apache.spark.SPARK_VERSION}")
  // 启动Master的WebUI界面
  webUi = new MasterWebUI(this, webUiPort)
  webUi.bind()
  masterWebUiUrl = "http://" + masterPublicAddress + ":" + webUi.boundPort
  // 是否启用反向代理,默认为false
  if (reverseProxy) {
    masterWebUiUrl = conf.get("spark.ui.reverseProxyUrl", masterWebUiUrl)
    webUi.addProxy()
    logInfo(s"Spark Master is acting as a reverse proxy. Master, Workers and " +
     s"Applications UIs are available at $masterWebUiUrl")
  }
  // 启用定时任务,心跳机制,向自己发送CheckForWorkerTimeOut消息,用于检测Worker是否超时
  // 跟踪代码可知,最终会调用timeOutDeadWorkers(),用于检测超时的Worker,并移除
  checkForWorkerTimeOutTask = forwardMessageThread.scheduleAtFixedRate(new Runnable {
    override def run(): Unit = Utils.tryLogNonFatalError {
      self.send(CheckForWorkerTimeOut)
    }
  }, 0, WORKER_TIMEOUT_MS, TimeUnit.MILLISECONDS)

  // 是否启用了RESTServer,默认为false
  if (restServerEnabled) {
    val port = conf.getInt("spark.master.rest.port", 6066)
    restServer = Some(new StandaloneRestServer(address.host, port, conf, self, masterUrl))
  }
  restServerBoundPort = restServer.map(_.start())

 // MetricsSystem是Spark的监控度量系统
  masterMetricsSystem.registerSource(masterSource)
  masterMetricsSystem.start()
  applicationMetricsSystem.start()
  // Attach the master and app metrics servlet handler to the web ui after the metrics systems are
  // started.
  masterMetricsSystem.getServletHandlers.foreach(webUi.attachHandler)
  applicationMetricsSystem.getServletHandlers.foreach(webUi.attachHandler)

  // Spark的恢复模式,暂时可以不管
  // 省略部分代码
}
  • Master的onStart()方法主要做了以下几件事:
  • 启动了Master的WebUI界面
  • 开启了Worker的心跳检测定时任务
  • 启动了监控度量系统MetricsSystem
  • 至此,Master的启动就算结束了。后面会等待着接收消息,消息进入Inbox,再传给Master这个RpcEndpoint,调用Master的receive、receiveAndReply。
  • 另外,在Master的伴生对象的startRpcEnvAndEndpoint(…)中,完成Endpoint的注册后,还会向Master发送一条同步消息BoundPortsRequest,并获得回应的消息BoundPortsResponse。

3. Worker启动流程

3.1 Worker的伴生对象

  • org.apache.spark.deploy.worker.Worker
  • Worker被start-slave.sh启动
  • 此伴生对象和Master的伴生对象代码逻辑几乎一样,就不再做展示,自行看代码即可。
  • 需要注意的是
  • main入口中一定要传入master的地址,传参示例--webui-port 8081 spark://192.168.0.101:7077 --cores 2 --memory 2G
  • 实例化Worker时,同时也传入了masterAddresses,用于后续获取Master的RpcEndpointRef,向其发送消息

3.2 Worker

  • org.apache.spark.deploy.worker.Worker
  • Worker启动时,同Master一样,将调用其构造器,接着onStart方法被调用。我们来看Worker的onStart()方法做了什么。
override def onStart() {
  assert(!registered)
  logInfo("Starting Spark worker %s:%d with %d cores, %s RAM".format(
    host, port, cores, Utils.megabytesToString(memory)))
  logInfo(s"Running Spark version ${org.apache.spark.SPARK_VERSION}")
  logInfo("Spark home: " + sparkHome)
  // 创建工作目录
  createWorkDir()
  // ExternalShuffleService是一个单独的进程服务,默认不开启
  // 用于帮助Executor处理shuffle,降低Executor的压力
  startExternalShuffleService()

  // 启动Worker的WebUI
  webUi = new WorkerWebUI(this, workDir, webUiPort)
  webUi.bind()

  workerWebUiUrl = s"http://$publicAddress:${webUi.boundPort}"
  // 注册到Master,下一部分来说
  registerWithMaster()

 // 启动metricsSystem,用于度量各种指标
  metricsSystem.registerSource(workerSource)
  metricsSystem.start()
  // Attach the worker metrics servlet handler to the web ui after the metrics system is started.
  metricsSystem.getServletHandlers.foreach(webUi.attachHandler)
}
  • 可以看到,Worker的onStart()方法主要做了以下几件事:
  • 创建工作目录
  • 启动ExternalShuffleService(默认不启动)
  • 启动Worker的WebUI
  • 注册到Master(下一节详细来看)
  • 启动metricsSystem,用于度量各种指标
  • 至此,Worker启动结束。
  • 后续Master与Worker只需等待新的应用提交上来,并运行。

4. Master与Worker的初步交互(注册)

  • Worker在启动时,是需要注册到Master的,我们来详细看看此部分代码。
  • Worker的onStart()中调用的registerWithMaster()方法如下
private def registerWithMaster() {
    registrationRetryTimer match {
      case None => // 第一次进来时,registrationRetryTimer为None
        registered = false
        // 此处,是向所有Master发起注册请求
        // 因为高可用模式下会存在多个Master
        registerMasterFutures = tryRegisterAllMasters()
        connectionAttemptCount = 0
        // 由于网络等问题,可能注册失败,因此需要一个能够重试的定时器,去注册
        registrationRetryTimer = Some(forwordMessageScheduler.scheduleAtFixedRate(
          new Runnable {
            override def run(): Unit = Utils.tryLogNonFatalError {
              Option(self).foreach(_.send(ReregisterWithMaster))
            }
          },
          INITIAL_REGISTRATION_RETRY_INTERVAL_SECONDS,
          INITIAL_REGISTRATION_RETRY_INTERVAL_SECONDS,
          TimeUnit.SECONDS))
      case Some(_) =>
        // registrationRetryTimer已存在,不需要再创建了
        logInfo("Not spawning another attempt to register with the master, since there is an" +
          " attempt scheduled already.")
    }
  }
  • 接着,再看tryRegisterAllMasters()的代码
private def tryRegisterAllMasters(): Array[JFuture[_]] = {
  masterRpcAddresses.map { masterAddress =>
    // 线程池提交,返回一个JFuture
    registerMasterThreadPool.submit(new Runnable {
      override def run(): Unit = {
        try {
          logInfo("Connecting to master " + masterAddress + "...")
          // 获取到Master对应的RpcEndpointRef
          val masterEndpoint = rpcEnv.setupEndpointRef(masterAddress, Master.ENDPOINT_NAME)
          // 向Master发送注册消息
          sendRegisterMessageToMaster(masterEndpoint)
        } catch {
          case ie: InterruptedException => // Cancelled
          case NonFatal(e) => logWarning(s"Failed to connect to master $masterAddress", e)
        }
      }
    })
  }
}
  • 再看sendRegisterMessageToMaster(…)方法
private def sendRegisterMessageToMaster(masterEndpoint: RpcEndpointRef): Unit = {
    masterEndpoint.send(RegisterWorker(
      workerId,
      host,
      port,
      self,
      cores,
      memory,
      workerWebUiUrl,
      masterEndpoint.address))
  }
  • 此处,正式向Master发送了消息RegisterWorker,进行注册
  • 快速查看技巧:利用’ctrl+鼠标左键’点击RegisterWorker,看到case class RegisterWorker。再次利用’ctrl+鼠标左键’点击RegisterWorker,IDEA会为我们展示出什么地方使用了它。可以看到IDEA展示的部分:
  • Worker.scala <- masterEndpoint.send(RegisterWorker(,此处是Worker发送该消息的代码处
  • Master.scala <- case RegisterWorker(,此处既是Master接收到该消息的地方
  • 利用上面的技巧,我们可以快速地在RpcEndpoint的代码之间跳转,方便了对其交互流程的查看。此时,我们来到了Master的receive方法,代码如下
override def receive: PartialFunction[Any, Unit] = {
    // 省略部分代码
    case RegisterWorker(
      id, workerHost, workerPort, workerRef, cores, memory, workerWebUiUrl, masterAddress) =>
      // Master收到了Worker发来的RegisterWorker消息,开始进行处理
      logInfo("Registering worker %s:%d with %d cores, %s RAM".format(
        workerHost, workerPort, cores, Utils.megabytesToString(memory)))
      
      if (state == RecoveryState.STANDBY) {
        // 高可用模式下,该Master可能是STANDBY的,因此回复一个MasterInStandby
        workerRef.send(MasterInStandby)
      } else if (idToWorker.contains(id)) {
        // 如果该Worker已经注册了,回一个RegisterWorkerFailed
        workerRef.send(RegisterWorkerFailed("Duplicate worker ID"))
      } else {
        // 开始注册Worker
        val worker = new WorkerInfo(id, workerHost, workerPort, cores, memory,
          workerRef, workerWebUiUrl)
         // 调用registerWorker(...),将worker添加到本节点
        if (registerWorker(worker)) {
          persistenceEngine.addWorker(worker)
          // 如果过成功,那么就向Worker回复消息RegisteredWorker
          workerRef.send(RegisteredWorker(self, masterWebUiUrl, masterAddress))
          schedule()
        } else {
          // 注册失败,回复RegisterWorkerFailed
          val workerAddress = worker.endpoint.address
          logWarning("Worker registration failed. Attempted to re-register worker at same " +
            "address: " + workerAddress)
          workerRef.send(RegisterWorkerFailed("Attempted to re-register worker at same address: "
            + workerAddress))
        }
      }

      // 省略部分代码
  }
  • Master收到消息后,需要检测本节点的状态是否是STANDBY、是否已经注册该Worker,如果没问题,那么调用registerWorker(…),将worker添加到本节点,最后会回复Worker一个消息RegisteredWorker
  • 跟随着RegisteredWorker消息,我们来到Worker接收消息处。Worker中先是receive被调用,再匹配到RegisterWorkerResponse,接着调用了handleRegisterResponse(…)方法,代码如下
private def handleRegisterResponse(msg: RegisterWorkerResponse): Unit = synchronized {
  msg match {
    case RegisteredWorker(masterRef, masterWebUiUrl, masterAddress) =>
      // 如果在Master注册成功,则会收到RegisteredWorker
      
      if (preferConfiguredMasterAddress) {
        logInfo("Successfully registered with master " + masterAddress.toSparkURL)
      } else {
        logInfo("Successfully registered with master " + masterRef.address.toSparkURL)
      }
      registered = true
      // 修改本Worker节点对应的Master
      changeMaster(masterRef, masterWebUiUrl, masterAddress)
      // 启用定时器,发送心跳
      // 追踪SendHeartbeat可知,定时器先发送给自己,Worker在receive处收到后,再调用sendToMaster(...)发送给Master
      forwordMessageScheduler.scheduleAtFixedRate(new Runnable {
        override def run(): Unit = Utils.tryLogNonFatalError {
          self.send(SendHeartbeat)
        }
      }, 0, HEARTBEAT_MILLIS, TimeUnit.MILLISECONDS)
      // 是否删除之前应用的工作目录,默认false
      if (CLEANUP_ENABLED) {
        logInfo(
          s"Worker cleanup enabled; old application directories will be deleted in: $workDir")
        forwordMessageScheduler.scheduleAtFixedRate(new Runnable {
          override def run(): Unit = Utils.tryLogNonFatalError {
            self.send(WorkDirCleanup)
          }
        }, CLEANUP_INTERVAL_MILLIS, CLEANUP_INTERVAL_MILLIS, TimeUnit.MILLISECONDS)
      }

	  // 准备本Worker的Executor信息,并将最新状态消息WorkerLatestState发送给Master
	  // 不过,显然第一次启动时,本节点是没有启动Excutor的
      val execs = executors.values.map { e =>
        new ExecutorDescription(e.appId, e.execId, e.cores, e.state)
      }
      masterRef.send(WorkerLatestState(workerId, execs.toList, drivers.keys.toSeq))

    case RegisterWorkerFailed(message) =>
      // 注册失败,回复此消息RegisterWorkerFailed
      if (!registered) {
        logError("Worker registration failed: " + message)
        System.exit(1)
      }

    case MasterInStandby =>
      // Ignore. Master not yet ready.
  }
}
  • 最后,Master将会收到WorkerLatestState消息,代码如下
override def receive: PartialFunction[Any, Unit] = {
  // 省略部分代码
  case WorkerLatestState(workerId, executors, driverIds) =>
    idToWorker.get(workerId) match {
      case Some(worker) =>
        // 因为是第一次,因此该executors是空的,for中代码不执行
        for (exec <- executors) {
          val executorMatches = worker.executors.exists {
            case (_, e) => e.application.id == exec.appId && e.id == exec.execId
          }
          if (!executorMatches) {
            // master doesn't recognize this executor. So just tell worker to kill it.
            worker.endpoint.send(KillExecutor(masterUrl, exec.appId, exec.execId))
          }
        }
		// 因为是第一次,因此该driverIds是空的,for中代码不执行
        for (driverId <- driverIds) {
          val driverMatches = worker.drivers.exists { case (id, _) => id == driverId }
          if (!driverMatches) {
            // master doesn't recognize this driver. So just tell worker to kill it.
            worker.endpoint.send(KillDriver(driverId))
          }
        }
      case None =>
        logWarning("Worker state from unknown worker: " + workerId)
    }

    // 省略部分代码
}
  • 至此,Worker注册到Master通信流程,完全结束。^_^
  • 后面整个集群会持续以下模式:由Worker定时向Master发送心跳包,而Master也会在本节点定时检测Worker的心跳,移除超时的Worker。
  • Worker注册到Master的通信流程示意图如下

spark怎么启动master spark单独启动worker_Master