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

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

当前环境与版本

环境 版本
JDK java version “1.8.0_231” (HotSpot)
Sacla 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的代码之间跳转,方便了对其交互流程的查看。(消息通信的具体实现,请看RpcEndpoint、RpcEnv
  • 此时,我们来到了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的通信流程示意图如下
    Worker注册到Master的通信流程示意图
发布了146 篇原创文章 · 获赞 54 · 访问量 17万+

猜你喜欢

转载自blog.csdn.net/alionsss/article/details/104575137