Spark源碼剖析——RpcEndpoint、RpcEnv

Spark源碼剖析——RpcEndpoint、RpcEnv

當前環境與版本

環境 版本
JDK java version “1.8.0_231” (HotSpot)
Scala Scala-2.11.12
Spark spark-2.4.4

1. 前言

  • RpcEndpoint、RpcEnv可以說是Spark整個體系最核心的部分:
    • RpcEndpoint代表了一個終端
    • RpcEnv代表了通信環境
  • 一個終端可以通過通信環境與其他終端進行交互,由此構建出了Spark集羣的通信互聯,包括心跳、發送計算任務、數據塊管理、狀態響應、監控等。
  • 在Spark早期版本中,整個通信部分由Akka實現(底層是Netty),爲了獲得更好的性能,在Spark2後完全自主使用Netty實現了整個通信機制。而該通信機制仍然是模仿Akka的Actor模型,各個RpcEndpoint之間的通信依舊基於事件驅動,想要理解該部分的朋友可以先看看Akka的事件驅動模型示例
  • 先從RpcEndpoint、RpcEnv這部分看起,可以深刻的理解Spark整個計算框架的分佈式基礎,再去看其他部分將會事半功倍。
  • 我畫了一副RpcEndpoint與RpcEnv的總覽圖,如下:
    RpcEndpoint與RpcEnv的總覽圖
  • 想直接看總體結構的朋友,請直接跳到最後總結處!😃

2. RpcEndpoint

2.1 核心UML圖

  • UML圖
    核心UML圖
  • 描述
    • Master:在standalone模式下的主節點,負責管理集羣、分配應用資源
    • Worker:在standalone模式下的從節點,負責啓動Executor、運行具體的應用
    • ClientEndpoint:即我們常說的客戶端,用於在cluster模式下,向集羣申請Driver、提交配置、提交Jar包等(client模式下是在本地直接啓動Driver,不需要ClientEndpoint)
    • DriverEnpoint:我們常說的Driver中包含了它,由用戶代碼中new SparkContext()創建,負責資源申請、與Executor交互、啓動/關閉任務
    • CoarseGrainedExecutorBackend:是粗粒度的Executor後臺進程,由它與DriverEnpoint交互,負責Executor的註冊、啓動、關閉、任務啓動等
    • HeartbeatReceiver:負責心跳交互,確保RpcEndpoint相互知道對方是否存活
    • BlockManagerEndpoint:分Master(Driver處)和Slave(Executor處),由SparkEnv實例化,主要負責應用運行時的數據塊管理

2.2 RpcEndpoint源碼分析

  • org.apache.spark.rpc.RpcEndpoint
    /**
     * 可以在此處看到RpcEndpoint的生命週期的說明
     * The life-cycle of an endpoint is:
     *
     * {@code constructor -> onStart -> receive* -> onStop}
     * 
     */
    private[spark] trait RpcEndpoint {
    
      val rpcEnv: RpcEnv
    
      /**
       * 此處定義了對自身的引用,用於自己向自己發送消息
       */
      final def self: RpcEndpointRef = {
        require(rpcEnv != null, "rpcEnv has not been initialized")
        rpcEnv.endpointRef(this)
      }
    
      /**
       * scala偏函數
       * 用於處理RpcEndpoint調用`RpcEndpointRef.send`或 `RpcCallContext.reply`發送的消息,不需要回應發送方
       * 子類實現時,利用匹配模式進行處理
       */
      def receive: PartialFunction[Any, Unit] = {
        case _ => throw new SparkException(self + " does not implement 'receive'")
      }
    
      /**
       * scala偏函數
       * 用於處理RpcEndpoint調用`RpcEndpointRef.ask`發送的消息,需要回應發送方
       * 子類實現時,利用匹配模式進行處理
       */
      def receiveAndReply(context: RpcCallContext): PartialFunction[Any, Unit] = {
        case _ => context.sendFailure(new SparkException(self + " won't reply anything"))
      }
    
     // 省略部分代碼
    
      /**
       * 子類實現初始化工作
       */
      def onStart(): Unit = {
        // By default, do nothing.
      }
    
      def onStop(): Unit = {
        // By default, do nothing.
      }
      
      // 省略部分代碼
    }
    
  • org.apache.spark.rpc.ThreadSafeRpcEndpoint
    // RpcEndpoint的線程安全的實現
    private[spark] trait ThreadSafeRpcEndpoint extends RpcEndpoint
    
  • 首先,RpcEndpoint是一個trait,定義了一個抽象模板,需要由子類去實現各個功能。我們看到的大部分Endpoint都是其線程安全的實現ThreadSafeRpcEndpoint。
  • 其生命週期如最前面的文檔註釋:先是調用constructor,再調用onStart,接着隨時都有可能收到消息,收到消息則會調用receive或receiveAndReply,當其結束時會被調用onStop。(後面RpcEnv處我們將解釋爲什麼生命週期是這個順序)
  • 對於每個RpcEndpoint
    • 其中onStart是用於其初始化,例如Master中onStart會初始化WebUI、MetricsSystem、心跳檢測定時器等。
    • 其中receive、receiveAndReply是其與其他RpcEndpoint交互的核心方法,用於不同RpcEndpoint之間的消息解析、交互。其原理和Akka的Actor中的receive方法一樣,只需定義用作協議的樣例類(例如RegisterWorker、Heartbeat),進行匹配即可完成解析,再進行後續處理。(源碼查看的小技巧:使用Ctrl+鼠標左鍵點擊receive中case匹配的類,即可找到具體的協議Message,再次點擊,即可找到發送該Message的RpcEndpoint、接收該消息的RpcEndpoint,這樣可以快速的找到RpcEndpoint之間的消息傳遞邏輯)

3. RpcEndpointRef

3.1 RpcEndpointRef

  • org.apache.spark.rpc.RpcEndpointRef
  • 代表了本節點對於另一個RpcEndpoint的引用封裝,利用它既可以實現對於另一個RpcEndpoint的訪問。它主要定義了以下幾個方法:
    • send - 用於發送單向的異步消息
    • ask - 用於發送雙向的消息,並返回Future,由使用者決定什麼時候阻塞等待響應的消息
    • askSync - 內部調用ask,並將Future阻塞,等待響應的消息

3.2 NettyRpcEndpointRef

  • org.apache.spark.rpc.netty.NettyRpcEndpointRef
  • RpcEndpointRef其唯一實現是NettyRpcEndpointRef,由Netty實現其通信機制。
  • 其幾個核心方法send、ask通通都調用了NettyRpcEnv的send、ask來實現。而NettyRpcEnv發送消息時需要封裝一個RequestMessage,主要由發送方、接收方、內容組成。
  • 源碼較簡單,此處就不做展示了。需要注意的是NettyRpcEndpointRef源代碼中的this,它指的是NettyRpcEndpointRef,對應某一個RpcEndpoint,不是調用它的RpcEndpoint(當然也有自己調自己的情況)。

4. RpcEnv

4.1 核心UML圖

  • UML圖
    RpcEnv核心UML圖
  • 描述
    • 此部分UML圖主要展示了RpcEnv創建、功能的核心部分,我們查看源碼時應該首先關注該部分代碼
    • Spark會利用RpcEnvFactory創建RpcEnv,目前其唯一實現是NettyRpcEnvFactory。封裝一個RpcEnvConfig,即可利用create方法創建NettyRpcEnv。
    • RpcEnv目前其唯一實現是NettyRpcEnv,利用它可與其他RpcEndpoint進行交互,上圖展示了NettyRpcEnv的幾個核心屬性與功能方法:
      • dispatcher - 負責註冊本節點的RpcEndpoint、處理RpcEndpoint的收件箱(Inbox)收到的消息
      • outboxes - 維護的是對於遠程RpcEndpoint的發件箱(Outbox),利用它可以向對應的RpcEndpoint發送消息
      • transportContext - 實際創建TransportServer的對象,實例化時,還會傳入一個實例化的NettyRpcHandler,用於處理Netty的消息(收到消息後調用該handler的receive,利用dispatcher將消息發送到Inbox)
      • startServer(…) - 由NettyRpcEnvFactory調用create(…)時一起調用,它再調用transportContext,創建並啓動TransportServer(內部是Netty)
      • postToOutbox(…) - 該類內部私有方法,調用該方法,即可利用對應的Outbox或message發送消息,最終由TransportClient向連接的channel發送消息
      • send(…) - 提供給外部調用的方法,用於發送消息,最終調用dispatcher或postToOutbox(…)
      • ask(…) - 同send(…),不同之處在於它會返回一個Future,由調用者來控制如何處理

4.2 NettyRpcEnv源碼分析

  • org.apache.spark.rpc.netty.NettyRpcEnv
    private[netty] class NettyRpcEnv(
        val conf: SparkConf,
        javaSerializerInstance: JavaSerializerInstance,
        host: String,
        securityManager: SecurityManager,
        numUsableCores: Int) extends RpcEnv(conf) with Logging {
    
       // 省略部分代碼
    
      private val dispatcher: Dispatcher = new Dispatcher(this, numUsableCores)
    
      private val transportContext = new TransportContext(transportConf, 
        new NettyRpcHandler(dispatcher, this, streamManager))
    
       // 省略部分代碼
    
      /**
       * Map包含了遠端RpcAddress與Outbox的映射關係
       */
      private val outboxes = new ConcurrentHashMap[RpcAddress, Outbox]()
    
       // 省略部分代碼
    
      /**
       * 創建TransportServer,由NettyRpcEnvFactory.create(...)時調用該方法
       */
      def startServer(bindAddress: String, port: Int): Unit = {
        // 是否開啓Spark認證,默認不會開啓
        val bootstraps: java.util.List[TransportServerBootstrap] =
          if (securityManager.isAuthenticationEnabled()) {
            java.util.Arrays.asList(new AuthServerBootstrap(transportConf, securityManager))
          } else {
            java.util.Collections.emptyList()
          }
        // 創建TransportServer,內部最終會利用Netty的ServerBootstrap進行創建
        server = transportContext.createServer(bindAddress, port, bootstraps)
        // 註冊一個RpcEndpoint,用於方便遠程RpcEnv來查詢是否存在RpcEndpoint
        dispatcher.registerRpcEndpoint(
          RpcEndpointVerifier.NAME, new RpcEndpointVerifier(this, dispatcher))
      }
    
       // 省略部分代碼
      /**
       * 註冊一個RpcEndpoint,RpcEndpoint被創建時,一般都會調用該方法進行註冊
       * 例如Master、Worker、CoarseGrainedExecutorBackend等
       */
      override def setupEndpoint(name: String, endpoint: RpcEndpoint): RpcEndpointRef = {
        dispatcher.registerRpcEndpoint(name, endpoint)
      }
    
       // 省略部分代碼
    
      /**
       * 該類內部私有方法,由send、ask調用,利用Outbox向對應的節點發送消息
       */
      private def postToOutbox(receiver: NettyRpcEndpointRef, message: OutboxMessage): Unit = {
        if (receiver.client != null) {
          // client不爲null時,會直接利用OutboxMessage發送消息
          // 實際調用的是receiver.client的send*方法發送消息
          // client內部擁有與對應節點連接的channel,利用它才實現了最終的消息發送
          message.sendWith(receiver.client)
        } else {
          require(receiver.address != null,
            "Cannot send message to client endpoint with no listen address.")
          // 獲取發件箱
          val targetOutbox = {
            // 先從outboxes中找
            val outbox = outboxes.get(receiver.address)
            if (outbox == null) {
              // outboxes中找不到,那就新建一個
              val newOutbox = new Outbox(this, receiver.address)
              val oldOutbox = outboxes.putIfAbsent(receiver.address, newOutbox)
              if (oldOutbox == null) {
                newOutbox
              } else {
                oldOutbox
              }
            } else {
              outbox
            }
          }
          // 是否已經被停用
          if (stopped.get) {
            outboxes.remove(receiver.address)
            targetOutbox.stop()
          } else {
            // 如果沒停,那就將message存入Outbox
            // 並調用drainOutbox()將Outbox中所有的message發出
            // 最終還是同上面client不爲null一樣,調用的message.sendWith(...)
            targetOutbox.send(message)
          }
        }
      }
    
      /**
       * 向對應節點發送單向消息
       */
      private[netty] def send(message: RequestMessage): Unit = {
        val remoteAddr = message.receiver.address
        if (remoteAddr == address) {
          // 如果接收方是本地,那麼直接利用dispatcher向本節點的Inbox發送消息
          try {
            dispatcher.postOneWayMessage(message)
          } catch {
            case e: RpcEnvStoppedException => logDebug(e.getMessage)
          }
        } else {
          // 否則調用postToOutbox,向遠程節點發送消息
          postToOutbox(message.receiver, OneWayOutboxMessage(message.serialize(this)))
        }
      }
    
       // 省略部分代碼
    
      /**
       * 向對應節點發送消息,並返回一個Future
       */
      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)) {
            e match {
              case e : RpcEnvStoppedException => logDebug (s"Ignored failure: $e")
              case _ => 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) {
            // 如果接收方是本地,那麼直接利用dispatcher向本節點的Inbox發送消息
            val p = Promise[Any]()
            p.future.onComplete {
              case Success(response) => onSuccess(response)
              case Failure(e) => onFailure(e)
            }(ThreadUtils.sameThread)
            dispatcher.postLocalMessage(message, p)
          } else {
            // 將回調onFailure、onSuccess封裝入消息內
            val rpcMessage = RpcOutboxMessage(message.serialize(this),
              onFailure,
              (client, response) => onSuccess(deserialize[Any](client, response)))
            // 否則調用postToOutbox,向遠程節點發送消息
            postToOutbox(message.receiver, rpcMessage)
            promise.future.failed.foreach {
              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 from ${remoteAddr} " +
                s"in ${timeout.duration}"))
            }
          }, timeout.duration.toNanos, TimeUnit.NANOSECONDS)
          promise.future.onComplete { v =>
            timeoutCancelable.cancel(true)
          }(ThreadUtils.sameThread)
        } catch {
          case NonFatal(e) =>
            onFailure(e)
        }
        promise.future.mapTo[T].recover(timeout.addMessageIfTimeout)(ThreadUtils.sameThread)
      }
    
       // 省略部分代碼
    
      /**
       * 獲取RpcEndpoint對應的RpcEndpointRef
       */
      override def endpointRef(endpoint: RpcEndpoint): RpcEndpointRef = {
        dispatcher.getRpcEndpointRef(endpoint)
      }
    
       // 省略部分代碼
    
      /**
       * 一個節點創建完RpcEnv、RpcEndpoint後,會調用該方法進行阻塞
       * 例如Master、Worker、CoarseGrainedExecutorBackend等
       */
      override def awaitTermination(): Unit = {
        // dispatcher內部將進一步調用其內的線程池的awaitTermination,進行阻塞
        dispatcher.awaitTermination()
      }
    
      // 省略部分代碼
    
    }
    
  • 我們可以看到NettyRpcEnv主要由幾個部分組成:
    • 維護RpcEndpoint,並處理其收件箱(Inbox)的Dispatcher(後面會解釋它是如何處理的)
    • 存儲待發送的消息的發件箱(Outbox)
    • 用於發送消息的postToOutbox、send、ask

4.3 Outbox源碼分析

  • org.apache.spark.rpc.netty.Outbox
    private[netty] class Outbox(nettyEnv: NettyRpcEnv, val address: RpcAddress) {
    
      outbox => // 給this一個別名叫outbox,方便使用
    
      // 省略部分代碼
    
      /**
       * 發消息,由外部調用
       */
      def send(message: OutboxMessage): Unit = {
        val dropped = synchronized {
          if (stopped) {
            true
          } else {
            // 將消息添加入隊列中
            messages.add(message)
            false
          }
        }
        if (dropped) {
          message.onFailure(new SparkException("Message is dropped because Outbox is stopped"))
        } else {
          // 發送並清空Outbox內的消息
          drainOutbox()
        }
      }
    
      /**
       * 發送並清空Outbox內的消息
       */
      private def drainOutbox(): Unit = {
        var message: OutboxMessage = null
        synchronized {
          if (stopped) {
            return
          }
          if (connectFuture != null) {
            // We are connecting to the remote address, so just exit
            return
          }
          if (client == null) {
            // 此處會利用nettyEnv創建一個client,方便後面發消息
            launchConnectTask()
            return
          }
          if (draining) {
            // There is some thread draining, so just exit
            return
          }
          // 取出消息
          message = messages.poll()
          if (message == null) {
            return
          }
          draining = true
        }
        while (true) {
          try {
           // 獲取到client
            val _client = synchronized { client }
            if (_client != null) {
              // 根據不同的message類型
              // 利用client的send*方法通過channel向對應節點發送消息
              message.sendWith(_client)
            } else {
              assert(stopped == true)
            }
          } catch {
            case NonFatal(e) =>
              handleNetworkFailure(e)
              return
          }
          synchronized {
            if (stopped) {
              return
            }
            message = messages.poll()
            if (message == null) {
              draining = false
              return
            }
          }
        }
      }
    
      // 省略部分代碼
      
    }
    
  • Oubox中核心的就是send(…)與drainOutbox(),利用這兩個方法來發送消息。
  • 需要注意的是OutboxMessage有兩種實現:
    • OneWayOutboxMessage - 單向消息,不需要回應
    • RpcOutboxMessage - Rpc請求,需要回應

4.4 Dispatcher、Inbox源碼分析

  • org.apache.spark.rpc.netty.Dispatcher
    private[netty] class Dispatcher(nettyEnv: NettyRpcEnv, numUsableCores: Int) extends Logging {
    
      private class EndpointData(
          val name: String,
          val endpoint: RpcEndpoint,
          val ref: NettyRpcEndpointRef) {
        // 實例化EndpointData,同時會實例化該RpcEndpoint對應的Inbox
        val inbox = new Inbox(ref, endpoint)
      }
    
      // Endpoint於EndpointData的映射關係,EndpointData內擁有該Endpoint的Inbox
      // 向Endpoint發送消息都要調用它
      private val endpoints: ConcurrentMap[String, EndpointData] =
        new ConcurrentHashMap[String, EndpointData]
        
      // RpcEndpoint與自身Ref的映射,方便後續快速獲取自己的引用,不用重複創建RpcEndpointRef
      // 由RpcEndpoint中的self方法從中獲取RpcEndpointRef
      private val endpointRefs: ConcurrentMap[RpcEndpoint, RpcEndpointRef] =
        new ConcurrentHashMap[RpcEndpoint, RpcEndpointRef]
    
      // 維護了EndpointData,後面的MessageLoop會從該隊列取數據,進行處理(也就是處理Inbox中的消息)
      // 標識了哪些EndpointData的Inbox中可能有消息
      private val receivers = new LinkedBlockingQueue[EndpointData]
    
      /**
       * 註冊RpcEndpoint
       */
      def registerRpcEndpoint(name: String, endpoint: RpcEndpoint): NettyRpcEndpointRef = {
        val addr = RpcEndpointAddress(nettyEnv.address, name)
        // 構建了一個對應地址的NettyRpcEndpointRef
        val endpointRef = new NettyRpcEndpointRef(nettyEnv.conf, addr, nettyEnv)
        synchronized {
          if (stopped) {
            throw new IllegalStateException("RpcEnv has been stopped")
          }
          // 如果沒有該名稱的EndpointData,就新建一個EndpointData,並放進去
          // 需要注意此處new EndpointData,內部同時還會實例化一個Inbox
          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)
          endpointRefs.put(data.endpoint, data.ref)
          // 注意此行代碼,將EndpointData放入了receivers隊列中
          // 後面循環取出EndpointData,進行處理時,將第一個處理該EndpointData(因此會先調用RpcEndpoint的onStart)
          receivers.offer(data)
        }
        endpointRef
      }
    
      // 省略部分代碼
    
      /**
       * 遍歷所有的RpcEndpoint,併發送消息
       */
      def postToAll(message: InboxMessage): Unit = {
        val iter = endpoints.keySet().iterator()
        while (iter.hasNext) {
          val name = iter.next
            postMessage(name, message, (e) => { e match {
              case e: RpcEnvStoppedException => logDebug (s"Message $message dropped. ${e.getMessage}")
              case e: Throwable => logWarning(s"Message $message dropped. ${e.getMessage}")
            }}
          )}
      }
    
      /** 
       * 遠程endpoint發送消息
       * 由Netty的Server收到消息,調用NettyRpcHandler的receive,再調用至此
       */
      def postRemoteMessage(message: RequestMessage, callback: RpcResponseCallback): Unit = {
        val rpcCallContext =
          new RemoteNettyRpcCallContext(nettyEnv, callback, message.senderAddress)
        val rpcMessage = RpcMessage(message.senderAddress, message.content, rpcCallContext)
        postMessage(message.receiver.name, rpcMessage, (e) => callback.onFailure(e))
      }
    
      /** 本地endpoint發送消息 */
      def postLocalMessage(message: RequestMessage, p: Promise[Any]): Unit = {
        val rpcCallContext =
          new LocalNettyRpcCallContext(message.senderAddress, p)
        val rpcMessage = RpcMessage(message.senderAddress, message.content, rpcCallContext)
        postMessage(message.receiver.name, rpcMessage, (e) => p.tryFailure(e))
      }
    
      /** 發送單向消息,遠程或本地調用*/
      def postOneWayMessage(message: RequestMessage): Unit = {
        postMessage(message.receiver.name, OneWayMessage(message.senderAddress, message.content),
          (e) => throw e)
      }
    
      /**
       * 向指定的endpoint發送消息
       * 實際只是將消息放入Inbox,後續會由內部MessageLoop輪詢處理Inbox中的消息
       */
      private def postMessage(
          endpointName: String,
          message: InboxMessage,
          callbackIfStopped: (Exception) => Unit): Unit = {
        val error = synchronized {
          // 獲取到該endpoint對應的EndpointData
          val data = endpoints.get(endpointName)
          if (stopped) {
            Some(new RpcEnvStoppedException())
          } else if (data == null) {
            Some(new SparkException(s"Could not find $endpointName."))
          } else {
            // 將消息存入inbox
            data.inbox.post(message)
            receivers.offer(data)
            None
          }
        }
        // We don't need to call `onStop` in the `synchronized` block
        error.foreach(callbackIfStopped)
      }
    
      // 省略部分代碼
    
      def awaitTermination(): Unit = {
        threadpool.awaitTermination(Long.MaxValue, TimeUnit.MILLISECONDS)
      }
    
      // 省略部分代碼
    
      /** 伴隨着Dispatcher實例化被調用*/
      private val threadpool: ThreadPoolExecutor = {
        val availableCores =
          if (numUsableCores > 0) numUsableCores else Runtime.getRuntime.availableProcessors()
        val numThreads = nettyEnv.conf.getInt("spark.rpc.netty.dispatcher.numThreads",
          math.max(2, availableCores))
        val pool = ThreadUtils.newDaemonFixedThreadPool(numThreads, "dispatcher-event-loop")
        for (i <- 0 until numThreads) {
          // 線程池開始處理MessageLoop,調用其run方法
          pool.execute(new MessageLoop)
        }
        pool
      }
    
      private class MessageLoop extends Runnable {
        override def run(): Unit = {
          try {
            // 該方法,將會持續循環
            while (true) {
              try {
                // 先要從已標識爲可能有消息的EndpointData的隊列中取出EndpointData
                val data = receivers.take()
                // PoisonPill是毒藥片的意思,用來指明當前Thread是否應該退出循環(即毒死它 ^_^)
                if (data == PoisonPill) {
                  // 不僅自己要死,還要拖其他Thread陪葬,哈哈
                  receivers.offer(PoisonPill)
                  return
                }
                // 調用inbox的process來處理消息
                data.inbox.process(Dispatcher.this)
              } catch {
                case NonFatal(e) => logError(e.getMessage, e)
              }
            }
          } catch {
            // 省略部分代碼
          }
        }
      }
    
    }
    
    
  • org.apache.spark.rpc.netty.Inbox
    private[netty] class Inbox(
        val endpointRef: NettyRpcEndpointRef,
        val endpoint: RpcEndpoint)
      extends Logging {
    
      inbox =>  // 給this一個別名叫inbox,方便使用
    
      // 實例化Inbox時,此處會被調用
      // 將OnStart放入了messages隊列的第一位,因此RpcEndpoint生命週期中在調用了構造器後,會調用onStart
      inbox.synchronized {
        messages.add(OnStart)
      }
    
      /**
       * 處理收件箱中的消息,由dispatcher中的MessageLoop調用
       */
      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此處是scala中的柯里化
          safelyCall(endpoint) {
            // 匹配消息類型,根據不同類型的消息,進行不同的處理
            message match {
              case RpcMessage(_sender, content, context) =>
                try {
                  // 收到遠程endpoint的RpcMessage(意味着發送消息並等待對方迴應)
                  // 因此,需要調用本endpoint的receiveAndReply,處理並進行迴應
                  endpoint.receiveAndReply(context).applyOrElse[Any, Unit](content, { msg =>
                    throw new SparkException(s"Unsupported message $message from ${_sender}")
                  })
                } catch {
                  case e: Throwable =>
                    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
                }
    
              case OneWayMessage(_sender, content) =>
                // 收到的是單向消息,可能是本地或遠程
                // 因此,只需要調用本endpoint的receive方法進行處理
                endpoint.receive.applyOrElse[Any, Unit](content, { msg =>
                  throw new SparkException(s"Unsupported message $message from ${_sender}")
                })
    
              case OnStart =>
                // OnStart由實例化Inbox時放入消息隊列
                // 用於調用endpoint生命週期的onStart
                endpoint.onStart()
                if (!endpoint.isInstanceOf[ThreadSafeRpcEndpoint]) {
                  inbox.synchronized {
                    if (!stopped) {
                      enableConcurrent = true
                    }
                  }
                }
    
              // 省略部分代碼
            }
          }
    
          // 省略部分代碼
        }
      }
    
      /**
       * 用於外部將消息投遞入Inbox的消息隊列
       */
      def post(message: InboxMessage): Unit = inbox.synchronized {
        if (stopped) {
          // We already put "OnStop" into "messages", so we should drop further messages
          onDrop(message)
        } else {
          // 將消息加入消息隊列
          messages.add(message)
          false
        }
      }
    
      // 省略部分代碼
    
    }
    
  • 由於Dispatcher與Inbox的業務是合在一起處理的,因此我們在此處一同進行分析。
  • 可以看到Dispatcher提供了registerRpcEndpoint,用於將RpcEndpoint註冊進來,這樣才能利用其Inbox向其發送消息。而發送消息分別提供了postToAll、postRemoteMessage、postLocalMessage、postOneWayMessage幾個公共方法,其核心都是調用了postMessage,將消息放入了收件箱(Inbox)的消息隊列中。最後由伴隨着Dispatcher實例化時就被啓動的MessageLoop循環調用inbox.process(…),處理了收件箱(Inbox)中的消息。
  • 而Inbox的核心則是:
    • post - 將消息放入消息隊列中
    • process - 根據不同類型的消息,進行不同的處理,最終調用對應Endpoint的receiveAndReply或receive
  • 另外,Inbox在實例化時順帶着將OnStart消息放入了消息隊列之首,由MessageLoop調用inbox.process(…),進而調用了Endpoint的onStart方法。
  • 由此,我們可以理解到最開始對於RpcEndpoint生命週期所作出的描述:
    • constructor -> onStart -> receive* -> onStop
  • 喜歡刨根問底的朋友,可能覺得此處的順序還是不夠清晰,因爲沒看到實際調用的代碼,這部分我們將在後續對於RpcEndpoint具體的實現類的調用中進行分析。

4.5 NettyRpcEnv是如何接收外部消息的?

  • 此部分的關鍵在於NettyRpcEnv中的transportContext,它啓動了Netty的Server、並提供了處理消息的NettyRpcHandler,是整個NettyRpcEnv通信的關鍵。
  • 首先,看到NettyRpcEnv中的transportContext
    private val transportContext = new TransportContext(transportConf,
    new NettyRpcHandler(dispatcher, this, streamManager))
    
  • 此處,伴隨着NettyRpcEnv的實例化,創建了TransportContext,並實例化了NettyRpcHandler
  • 再看NettyRpcEnv中的startServer(…)方法
    def startServer(bindAddress: String, port: Int): Unit = {
      val bootstraps: java.util.List[TransportServerBootstrap] =
          if (securityManager.isAuthenticationEnabled()) {
          java.util.Arrays.asList(new AuthServerBootstrap(transportConf, securityManager))
          } else {
          java.util.Collections.emptyList()
          }
      server = transportContext.createServer(bindAddress, port, bootstraps)
      dispatcher.registerRpcEndpoint(
          RpcEndpointVerifier.NAME, new RpcEndpointVerifier(this, dispatcher))
    }
    
  • 此處,調用了transportContext的createServer方法,創建了TransportServer
  • 接着再看TransportContext的createServer(…)做了什麼
    public TransportServer createServer(
        String host, int port, List<TransportServerBootstrap> bootstraps) {
      return new TransportServer(this, host, port, rpcHandler, bootstraps);
    }
    
  • 此處,實例化了一個TransportServer,並將rpcHandler傳了進去(也就是前面的NettyRpcHandler)
  • 我們再看TransportServer中做了什麼(在這裏,附上Netty使用示例
    public TransportServer(
        TransportContext context,
        String hostToBind,
        int portToBind,
        RpcHandler appRpcHandler,
        List<TransportServerBootstrap> bootstraps) {
      this.context = context;
      this.conf = context.getConf();
      this.appRpcHandler = appRpcHandler;
      this.bootstraps = Lists.newArrayList(Preconditions.checkNotNull(bootstraps));
    
      boolean shouldClose = true;
      try {
        // 傳入host、port,進行初始化
        init(hostToBind, portToBind);
        shouldClose = false;
      } finally {
        if (shouldClose) {
          JavaUtils.closeQuietly(this);
        }
      }
    }
    
    private void init(String hostToBind, int portToBind) {
    
      IOMode ioMode = IOMode.valueOf(conf.ioMode());
      // bossGroup處理其他節點的channel連接
      EventLoopGroup bossGroup =
        NettyUtils.createEventLoop(ioMode, conf.serverThreads(), conf.getModuleName() + "-server");
      // workerGroup處理channel接收、發出的數據
      EventLoopGroup workerGroup = bossGroup;
    
      PooledByteBufAllocator allocator = NettyUtils.createPooledByteBufAllocator(
        conf.preferDirectBufs(), true /* allowCache */, conf.serverThreads());
      
      // 構建ServerBootstrap,用於啓動Netty的Server
      bootstrap = new ServerBootstrap()
        .group(bossGroup, workerGroup)
        .channel(NettyUtils.getServerChannelClass(ioMode))
        .option(ChannelOption.ALLOCATOR, allocator)
        .option(ChannelOption.SO_REUSEADDR, !SystemUtils.IS_OS_WINDOWS)
        .childOption(ChannelOption.ALLOCATOR, allocator);
    
      this.metrics = new NettyMemoryMetrics(
        allocator, conf.getModuleName() + "-server", conf);
    
      if (conf.backLog() > 0) {
        bootstrap.option(ChannelOption.SO_BACKLOG, conf.backLog());
      }
    
      if (conf.receiveBuf() > 0) {
        bootstrap.childOption(ChannelOption.SO_RCVBUF, conf.receiveBuf());
      }
    
      if (conf.sendBuf() > 0) {
        bootstrap.childOption(ChannelOption.SO_SNDBUF, conf.sendBuf());
      }
    
      // 初始化負責處理channel數據的Pipeline
      bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
        @Override
        protected void initChannel(SocketChannel ch) {
          logger.debug("New connection accepted for remote address {}.", ch.remoteAddress());
    	
          RpcHandler rpcHandler = appRpcHandler;
          for (TransportServerBootstrap bootstrap : bootstraps) {
            rpcHandler = bootstrap.doBootstrap(ch, rpcHandler);
          }
          // 傳入rpcHandler,並將其進行封裝入TransportRequestHandler
          // 再將TransportRequestHandler封裝入TransportChannelHandler
          // TransportChannelHandler則是繼承了Netty的ChannelInboundHandlerAdapter
          // (請查看TransportChannelHandler中的channelRead方法,不懂的朋友建議先學學Netty)
          // 最後將TransportChannelHandler添加至Pipeline的最後
          context.initializePipeline(ch, rpcHandler);
        }
      });
    
      // 利用bootstrap啓動Server
      InetSocketAddress address = hostToBind == null ?
          new InetSocketAddress(portToBind): new InetSocketAddress(hostToBind, portToBind);
      channelFuture = bootstrap.bind(address);
      channelFuture.syncUninterruptibly();
    
      port = ((InetSocketAddress) channelFuture.channel().localAddress()).getPort();
      logger.debug("Shuffle server started on port: {}", port);
    }
    
  • 我們可以看到構造器中調用了init(…)方法,而init(…)則真正的利用Netty構建了ServerBootstrap,並啓動。
  • 此部分的關鍵之處在於bootstrap.childHandler(…)中調用context.initializePipeline(…),將我們前面傳進來的RpcHandler封裝入TransportChannelHandler,並添加到了Pipeline的最後。這樣,當遠程發過來消息時,每條消息最終會由RpcHandler處理。
  • 接着由Netty構建的Server收到消息後,傳至Pipeline,最後RpcHandler的方法(receive最關鍵)會被調用。
  • org.apache.spark.rpc.netty.NettyRpcHandler代碼如下
    private[netty] class NettyRpcHandler(
        dispatcher: Dispatcher,
        nettyEnv: NettyRpcEnv,
        streamManager: StreamManager) extends RpcHandler with Logging {
    
      private val remoteAddresses = new ConcurrentHashMap[RpcAddress, RpcAddress]()
    
      override def receive(
          client: TransportClient,
          message: ByteBuffer,
          callback: RpcResponseCallback): Unit = {
        val messageToDispatch = internalReceive(client, message)
        // 利用dispatcher發送消息
        dispatcher.postRemoteMessage(messageToDispatch, callback)
      }
    
      override def receive(
          client: TransportClient,
          message: ByteBuffer): Unit = {
        val messageToDispatch = internalReceive(client, message)
        // 利用dispatcher發送消息
        dispatcher.postOneWayMessage(messageToDispatch)
      }
    
      // 省略部分代碼,其他代碼也會用到dispatcher發送消息
    }
    
  • 我們可以看到,收到的消息都由dispatcher進行了處理,最終消息到達了Endpoint的收件箱(Inbox),這樣就完成了我們的消息接收!!!^_^

5. 總結

  • 最後,我們來對前面所說的做個總結。爲了方便,再把最前面的圖挪出來一道看 ^_^
    RpcEndpoint與RpcEnv的總覽圖
  • 這是單個節點(進程)內的結構圖,其他節點同理。
  • 首先,單個節點內可能存在多個RpcEndpoint,例如Driver節點中包含DriverEndpoint、HeartbeatReceiver等RpcEndpoint。
  • 每一個RpcEndpoint在RpcEnv中註冊後,會在其中擁有自己的Inbox,用於接收消息,統一由Dispatcher維護。
  • 在Dispatcher處理之前,遠程scoket的連接由使用Netty封裝的TransportServer處理,最後通過Pipeline調用至NettyRpcHandler,利用Dispatcher將消息發送給對應的RpcEndpoint,完成了消息接收。
  • 當RpcEndpoint需要發送消息時,先要獲取到接收方的RpcEndpointRef,再調用RpcEnv中的ask/send方法,將消息發入本地的Inbox、或是利用Outbox發送、或是直接發送:
    • 如果接收方是本地節點,那麼利用Dispatcher發入本地的Inbox
    • 如果接收方是遠程節點,那麼調用postToOutbox,將消息直接發出或是利用Outbox發出
  • 最終對外消息的發出是由TranportClient中維護的channel完成(TranportClient由TransportClientFactory中的createClient創建,依舊是Netty封裝)。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章