spark源碼解析-master流程分析

spark版本: 2.0.0

1.概念

master管理着spark的主要元數據,用於管理集羣,資源調度等。

2.master啓動過程

2.1 Master.main方法

在start-master.sh腳本中可以看出最終調用的是org.apache.spark.deploy.master.Master的main方法。現在來分析一下這個方法:

  def main(argStrings: Array[String]) {
  // 日誌
    Utils.initDaemon(log)
    // spark 配置對象
    val conf = new SparkConf
    // master參數對象,用於解析傳遞參數,比如:--host ,--webui-port等
    val args = new MasterArguments(argStrings, conf)
    val (rpcEnv, _, _) =
    // 啓動master通信端(核心方法)
    startRpcEnvAndEndpoint(args.host, args.port, args.webUiPort, conf)
    rpcEnv.awaitTermination()
  }

2.2 Master.startRpcEnvAndEndpoint方法

 def startRpcEnvAndEndpoint(
      host: String,
      port: Int,
      webUiPort: Int,
      conf: SparkConf): (RpcEnv, Int, Option[Int]) = {
    // 安全管理器
    val securityMgr = new SecurityManager(conf)
    // 創建rpc環境對象,現在是基於netty
    val rpcEnv = RpcEnv.create(SYSTEM_NAME, host, port, conf, securityMgr)
    // 註冊master通信端,並返回其通信引用 【1】   
    val masterEndpoint = rpcEnv.setupEndpoint(ENDPOINT_NAME,
      new Master(rpcEnv, rpcEnv.address, webUiPort, securityMgr, conf))
    // 向Master的通信終端發送請求,獲取綁定的端口號 【2】
    val portsResponse = masterEndpoint.askWithRetry[BoundPortsResponse](BoundPortsRequest)

    (rpcEnv, portsResponse.webUIPort, portsResponse.restPort)
  }

核心位置分析:

【1】

Dispatcher.scala
----------------------------
  /**
    * 註冊rpc通信端
    * @param name
    * @param endpoint
    * @return
    */
  def registerRpcEndpoint(name: String, endpoint: RpcEndpoint): NettyRpcEndpointRef = {
    val addr = RpcEndpointAddress(nettyEnv.address, name)
    // 獲取rpc通信端的引用,可以進行通信
    val endpointRef = new NettyRpcEndpointRef(nettyEnv.conf, addr, nettyEnv)
    synchronized {
      if (stopped) {
        throw new IllegalStateException("RpcEnv has been stopped")
      }
      // 添加endpoint名稱和對應的數據封裝映射
      if (endpoints.putIfAbsent(name, new EndpointData(name, endpoint, endpointRef)) != null) {
        throw new IllegalArgumentException(s"There is already an RpcEndpoint called $name")
      }
      val data = endpoints.get(name)
      // 添加endpoint引用
      endpointRefs.put(data.endpoint, data.ref)
      // 添加到消息處理隊列中,等待定時任務處理 
      receivers.offer(data)  // for the OnStart message
    }
    endpointRef
  }

上面有一段最核心的代碼是:

receivers.offer(data)

看似只是將請求的數據放入receivers隊列中,但是它將觸發定時任務處理請求,詳情如下:

Dispatcher.scala
-------------------

 /** 線程池一直在處理MessageLoop的run方法 */
  private val threadpool: ThreadPoolExecutor = {
    val numThreads = nettyEnv.conf.getInt("spark.rpc.netty.dispatcher.numThreads",
      math.max(2, Runtime.getRuntime.availableProcessors()))
    // 守護線程不停監聽消息
    val pool = ThreadUtils.newDaemonFixedThreadPool(numThreads, "dispatcher-event-loop")
    for (i <- 0 until numThreads) {
      pool.execute(new MessageLoop)
    }
    pool
  }

  /** Message loop used for dispatching messages. */
  private class MessageLoop extends Runnable {
    override def run(): Unit = {
      try {
      // 不斷循環
        while (true) {
          try {
            val data = receivers.take()
            // 特殊請求
            if (data == PoisonPill) {
              // Put PoisonPill back so that other MessageLoops can see it.
              receivers.offer(PoisonPill)
              return
            }
            // 接收方處理收信箱
            data.inbox.process(Dispatcher.this)
          } catch {
            case NonFatal(e) => logError(e.getMessage, e)
          }
        }
      } catch {
        case ie: InterruptedException => // exit
      }
    }
  }
  

爲了解釋上面的data.inbox.process(Dispatcher.this),重點介紹一下data.inbox屬性

Dispatcher.scala
-------------------


  private class EndpointData(
      val name: String,
      val endpoint: RpcEndpoint,
      val ref: NettyRpcEndpointRef) {
      // 每次創建一個新對象時,同時創建一個Inbox對象
    val inbox = new Inbox(ref, endpoint)
  }
  

private[netty] class Inbox(
    val endpointRef: NettyRpcEndpointRef,
    val endpoint: RpcEndpoint)
  extends Logging {

  inbox =>  // Give this an alias so we can use it more clearly in closures.

  // 消息集合,放入這裏的消息並不會馬上處理,而是要加入到Dispatcher.receivers中,利用線程池併發處理
  @GuardedBy("this")
  protected val messages = new java.util.LinkedList[InboxMessage]()

  /** True if the inbox (and its associated endpoint) is stopped. */
  // 是否已經停止接收
  @GuardedBy("this")
  private var stopped = false

  /** Allow multiple threads to process messages at the same time. */
  // 是否允許併發
  @GuardedBy("this")
  private var enableConcurrent = false

  /** The number of threads processing messages for this inbox. */
  // inbox中活躍線程數
  @GuardedBy("this")
  private var numActiveThreads = 0

  // OnStart should be the first message to process
  // 每次創建Inbox對象時,都會先添加一個OnStart消息
  inbox.synchronized {
    messages.add(OnStart)
  }

根據上面分析可知,每次創建EndpointData對象時,就會添加OnStart消息到inbox對象中。所以在註冊時receivers.offer(data)就會添加一個OnStart消息等待處理,現在來看一下真正的處理消息方法(即解釋:data.inbox.process(Dispatcher.this)):

  def process(dispatcher: Dispatcher): Unit = {
    var message: InboxMessage = null
    inbox.synchronized {
      // 存在線程處理
      if (!enableConcurrent && numActiveThreads != 0) {
        return
      }
      // 讀取消息
      message = messages.poll()
      if (message != null) {
        numActiveThreads += 1
      } else {
        return
      }
    }
    while (true) {
      safelyCall(endpoint) {
        /**
          * 處理各種類型的消息
          */
        message match {
           .......
           // 只保留引用到的OnStart消息處理
          case OnStart =>
            // 這裏的endpoint指Master對象,所以就是調用Master.onStart方法 
            endpoint.onStart()
            if (!endpoint.isInstanceOf[ThreadSafeRpcEndpoint]) {
              inbox.synchronized {
                if (!stopped) {
                  enableConcurrent = true
                }
              }
            }
           .......
        }
      }
 .......
   
  }

接着上面分析的節奏,來分析一下Master.onStart方法

Master.scala
----------------------


 override def onStart(): Unit = {
    logInfo("Starting Spark master at " + masterUrl)
    logInfo(s"Running Spark version ${org.apache.spark.SPARK_VERSION}")
    // 使用jetty創建web ui請求服務
    webUi = new MasterWebUI(this, webUiPort)
    webUi.bind()
    masterWebUiUrl = "http://" + masterPublicAddress + ":" + webUi.boundPort
    // 檢查超時
    checkForWorkerTimeOutTask = forwardMessageThread.scheduleAtFixedRate(new Runnable {
      override def run(): Unit = Utils.tryLogNonFatalError {
        self.send(CheckForWorkerTimeOut)
      }
    }, 0, WORKER_TIMEOUT_MS, TimeUnit.MILLISECONDS)
    // 如果啓用了rest server,那麼啓動rest服務,可以通過該服務向master提交各種請求
    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())
    // 指標監控(不是重點,建議直接跳過)
    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.
    // 監控的指標也放在web ui中
    masterMetricsSystem.getServletHandlers.foreach(webUi.attachHandler)
    applicationMetricsSystem.getServletHandlers.foreach(webUi.attachHandler)


// ------------這段屬於master HA部分,以後單獨介紹---------------
    // 指定是java序列化方式,可以修改爲工廠模式
    val serializer = new JavaSerializer(conf)
    // 根據恢復模式選擇,持久化引擎和leader選舉
    val (persistenceEngine_, leaderElectionAgent_) = RECOVERY_MODE match {
      // 如果恢復模式是ZOOKEEPER,那麼通過zookeeper來持久化恢復狀態
      case "ZOOKEEPER" =>
        logInfo("Persisting recovery state to ZooKeeper")
        val zkFactory =
          new ZooKeeperRecoveryModeFactory(conf, serializer)
        (zkFactory.createPersistenceEngine(), zkFactory.createLeaderElectionAgent(this))
      // 如果恢復模式是文件系統,那麼通過文件系統來持久化恢復狀態
      case "FILESYSTEM" =>
        val fsFactory =
          new FileSystemRecoveryModeFactory(conf, serializer)
        (fsFactory.createPersistenceEngine(), fsFactory.createLeaderElectionAgent(this))
      // 如果恢復模式是定製的,那麼指定你定製的全路徑類名,然後產生相關操作來持久化恢復狀態
      case "CUSTOM" =>
        val clazz = Utils.classForName(conf.get("spark.deploy.recoveryMode.factory"))
        val factory = clazz.getConstructor(classOf[SparkConf], classOf[Serializer])
          .newInstance(conf, serializer)
          .asInstanceOf[StandaloneRecoveryModeFactory]
        (factory.createPersistenceEngine(), factory.createLeaderElectionAgent(this))
      // 其他處理方式
      case _ =>
        (new BlackHolePersistenceEngine(), new MonarchyLeaderAgent(this))
    }
    persistenceEngine = persistenceEngine_
    leaderElectionAgent = leaderElectionAgent_
  }

其中master.onStart非常簡單,就是創建監聽服務,訪問ui端口,確定master HA恢復模式
上面介紹了這麼多,其實只是介紹了startRpcEnvAndEndpoint方法中的核心代碼之一的val masterEndpoint = rpcEnv.setupEndpoint(ENDPOINT_NAME, new Master(rpcEnv, rpcEnv.address, webUiPort, securityMgr, conf)),現在來介紹一下:val portsResponse = masterEndpoint.askWithRetry[BoundPortsResponse](BoundPortsRequest)

【2】:

RpcEndpointRef.scala
-----------------------



  /**
   *         多次重試請求
   */
  def askWithRetry[T: ClassTag](message: Any, timeout: RpcTimeout): T = {
    // TODO: Consider removing multiple attempts
    var attempts = 0
    var lastException: Exception = null
    // 如果沒有達到最大重試次數
    while (attempts < maxRetries) {
      attempts += 1
      try {
        // 處理請求(核心)
        val future = ask[T](message, timeout)
        // 等待處理結果
        val result = timeout.awaitResult(future)
        if (result == null) {
          throw new SparkException("RpcEndpoint returned null")
        }
        return result
      } catch {
        case ie: InterruptedException => throw ie
        case e: Exception =>
          lastException = e
          logWarning(s"Error sending message [message = $message] in $attempts attempts", e)
      }
      // 休眠等待下一次重試機會
      if (attempts < maxRetries) {
        Thread.sleep(retryWaitMs)
      }
    }

    throw new SparkException(
      s"Error sending message [message = $message]", lastException)
  }

處理請求代碼ask[T](message, timeout) (message=BoundPortsRequest)來分析一下,

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


  private[netty] def ask[T: ClassTag](message: RequestMessage, timeout: RpcTimeout): Future[T] = {
    val promise = Promise[Any]()
    // 目標地址
    val remoteAddr = message.receiver.address

    def onFailure(e: Throwable): Unit = {
      if (!promise.tryFailure(e)) {
        logWarning(s"Ignored failure: $e")
      }
    }

    def onSuccess(reply: Any): Unit = reply match {
      case RpcFailure(e) => onFailure(e)
      case rpcReply =>
        if (!promise.trySuccess(rpcReply)) {
          logWarning(s"Ignored message: $reply")
        }
    }

    try {
      // 如果請求的目標地址是本機
      if (remoteAddr == address) {
        val p = Promise[Any]()
        // 異步處理消息
        p.future.onComplete {
          // 如果成功,會調用onSuccess方法,promise.future對象可以獲取到數據
          case Success(response) => onSuccess(response)
          case Failure(e) => onFailure(e)
        }(ThreadUtils.sameThread)
        // 發送本地消息
        dispatcher.postLocalMessage(message, p)
      } else {
        // 封裝rpc請求對象
        val rpcMessage = RpcOutboxMessage(serialize(message),
          onFailure,
          (client, response) => onSuccess(deserialize[Any](client, response)))
        //
        postToOutbox(message.receiver, rpcMessage)
        promise.future.onFailure {
          case _: TimeoutException => rpcMessage.onTimeout()
          case _ =>
        }(ThreadUtils.sameThread)
      }
      // 超時檢查
      val timeoutCancelable = timeoutScheduler.schedule(new Runnable {
        override def run(): Unit = {
          onFailure(new TimeoutException(s"Cannot receive any reply in ${timeout.duration}"))
        }
      }, timeout.duration.toNanos, TimeUnit.NANOSECONDS)
      promise.future.onComplete { v =>
        timeoutCancelable.cancel(true)
      }(ThreadUtils.sameThread)
    } catch {
      case NonFatal(e) =>
        onFailure(e)
    }
    // 如果獲取到返回結果,直接轉換爲T類型對象;出現異常使用超時處理
    promise.future.mapTo[T].recover(timeout.addMessageIfTimeout)(ThreadUtils.sameThread)
  }

雖然上面的代碼很長,但是主要是區分兩種請求接收方:

(1) remoteAddr == address,請求和接收方是一臺服務器

核心代碼是:dispatcher.postLocalMessage(message, p)

(2) remoteAddr != address,不同服務器

核心代碼是:postToOutbox(message.receiver, rpcMessage)不過由於master啓動,一般在本機執行,所以這裏先之分析remoteAddr == address的請況,在以後會介紹outbox處理。

接下來,我將依次分析這句代碼,想看一下:dispatcher.postLocalMessage(message, p),它表示通過消息分發器將message發送到本機:

Dispatcher.scala 
-------------------


  def postLocalMessage(message: RequestMessage, p: Promise[Any]): Unit = {
    val rpcCallContext =
      new LocalNettyRpcCallContext(message.senderAddress, p)
    // 拼裝rpc消息對象
    val rpcMessage = RpcMessage(message.senderAddress, message.content, rpcCallContext)
    // 核心代碼**
    postMessage(message.receiver.name, rpcMessage, (e) => p.tryFailure(e))
  }


  private def postMessage(
      endpointName: String,
      message: InboxMessage,
      callbackIfStopped: (Exception) => Unit): Unit = {
    val error = synchronized {
      val data = endpoints.get(endpointName)
      if (stopped) {
        Some(new RpcEnvStoppedException())
      } else if (data == null) {
        Some(new SparkException(s"Could not find $endpointName."))
      } else {
        // 往需要發送的通信端inbox中添加一條消息,並添加到receivers從而觸發消息處理
        data.inbox.post(message)
        receivers.offer(data)
        None
      }
    }
    // We don't need to call `onStop` in the `synchronized` block
    error.foreach(callbackIfStopped)
  }

這段代碼是不是很熟悉,其實就是將message發送到endpoint的inbox,然後通過定時處理請求。
根據前面的分析,可以知道最終相當於調用inbox.process方法,請求類型是RpcMessage
即:

 def process(dispatcher: Dispatcher): Unit = {
 ..... 爲了突出重點,這裏是提出這段代碼
  message match {
          case RpcMessage(_sender, content, context) =>
            try {
            // 這裏endpoint = master 即調用master.receiveAndReply方法
              endpoint.receiveAndReply(context).applyOrElse[Any, Unit](content, { msg =>
                throw new SparkException(s"Unsupported message $message from ${_sender}")
              })
            } catch {
              case NonFatal(e) =>
                context.sendFailure(e)
                // Throw the exception -- this exception will be caught by the safelyCall function.
                // The endpoint's onError function will be called.
                throw e
            }
......



Master.scala  -> 

override def receiveAndReply(context: RpcCallContext): PartialFunction[Any, Unit] = {
.......
    case BoundPortsRequest =>
      context.reply(BoundPortsResponse(address.port, webUi.boundPort, restServerBoundPort))
......

Master endpoint對BoundPortsRequest請求處理邏輯非常簡單,不做多說明

至此,master啓動涉及的核心對象和方法就介紹完了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章