spark源码解析-worker启动

spark版本: 2.0.0

1.概念

worker是执行任务的真正服务,它可以管理多个executors,并向master汇报任务的执行情况,现在让我们看看它的启动过程。
由于worker启动过程和master启动过程有一定相通之处,所以在阅读本文之前,请先阅读master的启动过程。

2.worker启动

在启动spark时,需要执行脚本sh start-slaves.sh,其最终是调用org.apache.spark.deploy.worker.Worker的main方法。

Worker.java
-----------------


  def main(argStrings: Array[String]) {
    Utils.initDaemon(log)
    val conf = new SparkConf
    // 启动worker时,必须提供masterurl ,格式:spark://hostname:port
    val args = new WorkerArguments(argStrings, conf)
    // 启动核心方法
    val rpcEnv = startRpcEnvAndEndpoint(args.host, args.port, args.webUiPort, args.cores,
      args.memory, args.masters, args.workDir, conf = conf)
    rpcEnv.awaitTermination()
  }

startRpcEnvAndEndpoint主要作用就是创建worker对象的rpcEnv对象,并注册worker endpoint。

Worker.scala
----------------------


  def startRpcEnvAndEndpoint(
      host: String,
      port: Int,
      webUiPort: Int,
      cores: Int,
      memory: Int,
      masterUrls: Array[String],
      workDir: String,
      workerNumber: Option[Int] = None,
      conf: SparkConf = new SparkConf): RpcEnv = {

    // The LocalSparkCluster runs multiple local sparkWorkerX RPC Environments
    // worker endpoint名称
    val systemName = SYSTEM_NAME + workerNumber.map(_.toString).getOrElse("")
    // 安全管理器
    val securityMgr = new SecurityManager(conf)
    // rpcEnv对象创建
    val rpcEnv = RpcEnv.create(systemName, host, port, conf, securityMgr)
    // 获取有效的master地址
    val masterAddresses = masterUrls.map(RpcAddress.fromSparkURL(_))
    // 启动并注册worker endpoint
    rpcEnv.setupEndpoint(ENDPOINT_NAME, new Worker(rpcEnv, webUiPort, cores, memory,
      masterAddresses, ENDPOINT_NAME, workDir, conf, securityMgr))
    rpcEnv
  }

和master启动一样,在执行setupEndpoint这个方法时会触发endpoint.onStart方法即worker.onStart方法。

Worker.scala
-----------------------


  override def onStart() {
    // 当前woker
    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)
    // 创建worker的工作目录,用于保存执行过程的文件
    createWorkDir()
    // 启动shuffle服务 【1】
    shuffleService.startIfEnabled()
    webUi = new WorkerWebUI(this, workDir, webUiPort)
    webUi.bind()

    val scheme = if (webUi.sslOptions.enabled) "https" else "http"
    workerWebUiUrl = s"$scheme://$publicAddress:${webUi.boundPort}"
    // 注册到master服务 【2】
    registerWithMaster()

    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)
  }

代码分析:

【1】
最终调用的是

  def start() {
    require(server == null, "Shuffle server already started")
    logInfo(s"Starting shuffle service on port $port with useSasl = $useSasl")
    val bootstraps: Seq[TransportServerBootstrap] =
      if (useSasl) {
        Seq(new SaslServerBootstrap(transportConf, securityManager))
      } else {
        Nil
      }
    // 启动一个shuffle服务
    server = transportContext.createServer(port, bootstraps.asJava)
  }

看到这个transportContext.createServer(port, bootstraps.asJava)在结合rpc原理这篇,可以知道会创建一个netty server,不过需要注意的是这里的handler对象是ExternalShuffleBlockHandler,方法比较简单,就不做分析。

【2】注册到master服务是worker启动的核心内容,现在来看一下:先主动触发注册到所有master的任务,如果没有注册成功,就使用定时任务注册(如果存在注册到某一个master,就会停止注册的定时器)

Worker.scala
-----------------------


 private def registerWithMaster() {
    // onDisconnected may be triggered multiple times, so don't attempt registration
    // if there are outstanding registration attempts scheduled.
    // 防止有多个注册线程
    registrationRetryTimer match {
      case None =>
        registered = false
        // 返回所有的注册到master任务(异步任务列表)
        registerMasterFutures = tryRegisterAllMasters()
        // 连接重试次数
        connectionAttemptCount = 0
        // 定时发送注册到master任务(注意:如果有注册成功的就会关闭这个定时器)
        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(_) =>
        logInfo("Not spawning another attempt to register with the master, since there is an" +
          " attempt scheduled already.")
    }
  }

tryRegisterAllMasters会遍历所有的master列表,尝试一一注册,知道成功为止。

Worker.scala
-----------------------

  /**
    * spark可以存在多个master,所以每个master都会尝试注册
    * @return
    */
  private def tryRegisterAllMasters(): Array[JFuture[_]] = {
    masterRpcAddresses.map { masterAddress =>
      registerMasterThreadPool.submit(new Runnable {
        override def run(): Unit = {
          try {
            logInfo("Connecting to master " + masterAddress + "...")
            // 获取master endpoint的引用
            val masterEndpoint = rpcEnv.setupEndpointRef(masterAddress, Master.ENDPOINT_NAME)
            registerWithMaster(masterEndpoint)
          } catch {
            case ie: InterruptedException => // Cancelled
            case NonFatal(e) => logWarning(s"Failed to connect to master $masterAddress", e)
          }
        }
      })
    }
  }

上面代码中的setupEndpointRef有点复杂,这里简单分析一下,最终它会调用方法:

NettyRpcEnv.scala
------------------------------

  /**
    * 根据url同步获取endpoint引用
    * @param uri
    * @return
    */
  def asyncSetupEndpointRefByURI(uri: String): Future[RpcEndpointRef] = {
    val addr = RpcEndpointAddress(uri)
    // 构造endpoint引用
    val endpointRef = new NettyRpcEndpointRef(conf, addr, this)
    // 创建校验器的引用对象,在addr.rpcAddress这台服务器上可以通过RpcEndpointVerifier.NAME找到对应的endpoint
    val verifier = new NettyRpcEndpointRef(
      conf, RpcEndpointAddress(addr.rpcAddress, RpcEndpointVerifier.NAME), this)
    verifier.ask[Boolean](RpcEndpointVerifier.CheckExistence(endpointRef.name)).flatMap { find =>
      if (find) {
        Future.successful(endpointRef)
      } else {
        Future.failed(new RpcEndpointNotFoundException(uri))
      }
    }(ThreadUtils.sameThread)
  }

verifier.ask这段代码和master启动的val portsResponse = masterEndpoint.askWithRetry[BoundPortsResponse](BoundPortsRequest)思路差不多,不过它会将请求发送到outbox中,然后处理。不过后面会专门介绍inbox和outbox的整个请求和响应流程,现在只要知道verifier.ask请求master的host和port组成的地址对象的netty server, 找到endpoint名称为RpcEndpointVerifier.NAME(即校验endpoint,在前面的rpc原理说提到过,在NettyRpcEnv.startServer时就会注册校验endpoint),询问endpointRef.name(对应的是master的endpoint名称)这个是不是一个有效的endpint名称。

再次回归到tryRegisterAllMasters方法,它会执行方法registerWithMaster(masterEndpoint)

  /**
    * 注册到指定master endpoint引用对象中
    * @param masterEndpoint
    */
  private def registerWithMaster(masterEndpoint: RpcEndpointRef): Unit = {
    // 请求类型: RegisterWorker,响应类型: RegisterWorkerResponse
    masterEndpoint.ask[RegisterWorkerResponse](RegisterWorker(
      workerId, host, port, self, cores, memory, workerWebUiUrl))
      .onComplete {
        // This is a very fast action so we can use "ThreadUtils.sameThread"
        // 处理成功
        case Success(msg) =>
          Utils.tryLogNonFatalError {
            // 处理master的响应
            handleRegisterResponse(msg)
          }
          // 处理失败,worker进程退出
        case Failure(e) =>
          logError(s"Cannot register with master: ${masterEndpoint.address}", e)
          System.exit(1)
      }(ThreadUtils.sameThread)
  }

上面的处理过程是:请求服务端master endpoint,请求类型是RegisterWorker(注册Worker),如果响应成功就调用方法handleRegisterResponse。如果注册成功,worker服务将更新master的属性,定期向master发送心跳,周期性清理旧数据等。

Worker.scala
-------------------


 private def handleRegisterResponse(msg: RegisterWorkerResponse): Unit = synchronized {
    msg match {
      // 注册成功
      case RegisteredWorker(masterRef, masterWebUiUrl) =>
        logInfo("Successfully registered with master " + masterRef.address.toSparkURL)
        // 注册成功
        registered = true
        // 改变master相关属性,关闭其他注册相关服务等
        changeMaster(masterRef, masterWebUiUrl)
        // 定期发送心跳
        forwordMessageScheduler.scheduleAtFixedRate(new Runnable {
          override def run(): Unit = Utils.tryLogNonFatalError {
            // 发送到自己,通过receive方法处理
            self.send(SendHeartbeat)
          }
        }, 0, HEARTBEAT_MILLIS, TimeUnit.MILLISECONDS)
        // 周期性发送清理旧app数据
        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)
        }
        // executors信息
        val execs = executors.values.map { e =>
          new ExecutorDescription(e.appId, e.execId, e.cores, e.state)
        }
        // 发送最新的状态到master
        masterRef.send(WorkerLatestState(workerId, execs.toList, drivers.keys.toSeq))
      // 注册失败,需要判断worker是不是已经注册成功了,如果是也不用处理
      case RegisterWorkerFailed(message) =>
        if (!registered) {
          logError("Worker registration failed: " + message)
          System.exit(1)
        }
      // master不可用,不用处理
      case MasterInStandby =>
        // Ignore. Master not yet ready.
    }
  }

【说明】:前面我们简单介绍了ask方法,现在客户端请求服务端时又出现了一个新的方法:send。这个方法和ask有什么区别呢?主要是ask需要有返回值(双向请求),而send是一个oneWayMessage,不用接收返回值。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值