文章目錄
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的總覽圖,如下:
- 想直接看總體結構的朋友,請直接跳到最後總結處!😃
2. RpcEndpoint
2.1 核心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圖
- 描述
- 此部分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,例如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封裝)。