Spark RPC 通信機(jī)制

相關(guān)概念

主要涉及RpcEnv,RpcEndpoint,RpcEndpointRef,其中RpcEnv是通信的基礎(chǔ),每個通信節(jié)點上都需要實現(xiàn)該類,其內(nèi)部實現(xiàn)了消息的傳輸處理機(jī)制,RpcEndpoint表示一個可以接收RPC消息的對象,遠(yuǎn)程節(jié)點通過RpcEndpointRef向相應(yīng)的RpcEndpoint發(fā)送消息

RpcEnv

RpcEnv 抽象類表示一個 RPC Environment,管理著整個RpcEndpoint的生命周期,目前唯一的實現(xiàn)類是NettyRpcEnv,具體功能是

  • 注冊RpcEndpoint
  • 將來自 RpcEndpointRef的消息發(fā)送給相應(yīng)的RpcEndpoint

RpcEnv會在所有的通信節(jié)點上創(chuàng)建,例如master,worker,driver,executor都會創(chuàng)建一個RpcEnv

Driver端上的RpcEnv在SparkEnv初始化時創(chuàng)建:

    val systemName = if (isDriver) driverSystemName else executorSystemName
    val rpcEnv = RpcEnv.create(systemName, bindAddress, advertiseAddress, port.getOrElse(-1), conf,
      securityManager, numUsableCores, !isDriver)

RpcEnv.create內(nèi)部通過工廠方法創(chuàng)建RpcEnv,具體是通過實現(xiàn)RpcEnvFactory接口的NettyRpcEnvFactory工廠類,創(chuàng)建RpcEnv的具體實現(xiàn)類NettyRpcEnv

  //指明該RpcEnv的名稱,監(jiān)聽地址和端口
  def create(
      name: String,
      bindAddress: String,
      advertiseAddress: String,
      port: Int,
      conf: SparkConf,
      securityManager: SecurityManager,
      numUsableCores: Int,
      clientMode: Boolean): RpcEnv = {
    val config = RpcEnvConfig(conf, name, bindAddress, advertiseAddress, port, securityManager,
      numUsableCores, clientMode)
    new NettyRpcEnvFactory().create(config)
  }

RpcEndpoint

RpcEndPoint 代表具體的通信節(jié)點,例如Master、Worker、CoarseGrainedSchedulerBackend中的DriverEndpoint、CoarseGrainedExecutorBackend等,都實現(xiàn)了該接口,在具體的函數(shù)中定義了消息傳遞來時的處理邏輯,整個生命周期是constructor -> onStart -> receive* -> onStop,即調(diào)用構(gòu)造函數(shù),然后向RpcEnv注冊,內(nèi)部調(diào)用onStart,之后如果收到消息,RpcEnv會調(diào)用receive*方法,結(jié)束時調(diào)用onStop方法

private[spark] trait RpcEndpoint {
  // 當(dāng)前RpcEndpoint注冊的RpcEnv
  val rpcEnv: RpcEnv
  // 獲取該RpcEndpoint對應(yīng)的RpcEndpointRef
  final def self: RpcEndpointRef

  // 處理RpcEndpointRef.send發(fā)送的消息
  def receive: PartialFunction[Any, Unit]
  // 處理RpcEndpointRef.ask發(fā)送的消息,通過RpcCallContext返回消息或異常
  def receiveAndReply(context: RpcCallContext): PartialFunction[Any, Unit]

  //一系列的回調(diào)函數(shù)
  def onError(cause: Throwable): Unit
  def onConnected(remoteAddress: RpcAddress): Unit
  def onDisconnected(remoteAddress: RpcAddress): Unit
  def onNetworkError(cause: Throwable, remoteAddress: RpcAddress): Unit
  def onStart(): Unit
  def onStop(): Unit

  // 停止RpcEndpoint
  final def stop(): Unit 
}

它的子類是ThreadSafeRpcEndpoint,Spark中實現(xiàn)的Endpoint大多是繼承這個類,應(yīng)該線程安全的處理消息,即RpcEnv中的Dispatcher在處理該Endpoint對應(yīng)的Inbox內(nèi)的消息時,只能單線程處理消息,不能進(jìn)行多線程同時處理多個消息

RpcEndpointRef

RpcEndPointRef 是對遠(yuǎn)程RpcEndpoint的一個引用,內(nèi)部記錄了RpcEndpoint的位置信息

private[spark] abstract class RpcEndpointRef(conf: SparkConf)
  extends Serializable with Logging {
  // 最大重連次數(shù)(3),重新嘗試的等待事件(3s),默認(rèn)的超時事件(120s)
  private[this] val maxRetries = RpcUtils.numRetries(conf)
  private[this] val retryWaitMs = RpcUtils.retryWaitMs(conf)
  private[this] val defaultAskTimeout = RpcUtils.askRpcTimeout(conf)

  // 對應(yīng)RpcEndpoint的地址,名稱
  def address: RpcAddress
  def name: String

  // 發(fā)送一個消息
  def send(message: Any): Unit
  // 發(fā)送消息到相應(yīng)的`RpcEndpoint.receiveAndReply`,異步
  def ask[T: ClassTag](message: Any, timeout: RpcTimeout): Future[T]
  // 發(fā)送消息到相應(yīng)的`RpcEndpoint.receiveAndReply`,阻塞等待回復(fù)的結(jié)果
  def askSync[T: ClassTag](message: Any): T
  ....
}

地址表示

  • RpcAddress(host: String, port: Int):Rpc environment的地址
  • RpcEndpointAddress(rpcAddress: RpcAddress, name: String):RPC endpoint的地址,其中的rpcAddress指的是endpoint所在的RpcEnv地址,name指的是endpoint的名稱

NettyRpcEnv實現(xiàn)

NettyRpcEnvFactory

  • 通過RpcEnvConfig創(chuàng)建NettyRpcEnv
  • 如果非clientMode,在特定的地址和端口上啟動服務(wù)端startServer(bindAddress: String, port: Int)

NettyRpcEnv

內(nèi)部涉及的部分字段和函數(shù)如下:

private[netty] class NettyRpcEnv(
    val conf: SparkConf,
    //JavaSerializerInstance可以在多線性情況下運(yùn)行
    javaSerializerInstance: JavaSerializerInstance,
    host: String,
    securityManager: SecurityManager,
    numUsableCores: Int) extends RpcEnv(conf) with Logging {

  private[netty] val transportConf : TransportConf 傳輸上下文的配置信息,其中默認(rèn)的netty線程數(shù)目為8
  private val dispatcher: Dispatcher 消息分發(fā)器,負(fù)責(zé)將RPC消息發(fā)送到對應(yīng)的endpoint
  private val streamManager : NettyStreamManager 用于文件傳輸
  private val transportContext : TransportContext 傳輸?shù)暮诵模脕韯?chuàng)建TransportServer和TransportClientFactory
  private val clientFactory : TransportClientFactory 用來創(chuàng)建TransportClient
  @volatile private var fileDownloadFactory: TransportClientFactory 用來創(chuàng)建用于file下載的TransportClient,使得與主RPC傳輸分離
  val timeoutScheduler : ScheduledThreadPoolExecutor 線程池,超時控制相關(guān)
  private[netty] val clientConnectionExecutor:ThreadPoolExecutor 客戶端連接線程池,線程池默認(rèn)最大線程數(shù)目64
  @volatile private var server: TransportServer
  // 向遠(yuǎn)程RpcAddress發(fā)送消息時,將消息放到相應(yīng)的Outbox中即可
  private val outboxes = new ConcurrentHashMap[RpcAddress, Outbox]()

  // 在特定地址端口上啟動服務(wù) 即創(chuàng)建一個TransportServer
  def startServer(bindAddress: String, port: Int): Unit
  //該RpcEnv監(jiān)聽的地址 地址+端口
  def address: RpcAddress
  //注冊RpcEndpoint,返回RpcEndpointRef
  def setupEndpoint(name: String, endpoint: RpcEndpoint): RpcEndpointRef
  //檢索出對應(yīng)的RpcEndpointRef
  def setupEndpointRef(address: RpcAddress, endpointName: String): RpcEndpointRef
  // 獲取RpcEndpoint對應(yīng)的RpcEndpointRef
  private[rpc] def endpointRef(endpoint: RpcEndpoint): RpcEndpointRef
  // 等待RpcEnv退出
  def awaitTermination(): Unit
}

Dispatcher

進(jìn)行消息的異步處理,內(nèi)部有一個線程池,每個線程執(zhí)行MessageLoop任務(wù),不停將放置在阻塞隊列中receivers中的EndpointData消息取出分發(fā)到相應(yīng)的endpoint,如果為PoisonPill消息,關(guān)閉線程池

其內(nèi)部記錄了該節(jié)點上所有的RpcEndpoint

  private val endpoints: ConcurrentMap[String, EndpointData]
  private val endpointRefs: ConcurrentMap[RpcEndpoint, RpcEndpointRef]
  // 存儲EndpointData數(shù)據(jù)的阻塞隊列
  private val receivers = new LinkedBlockingQueue[EndpointData]
  // 創(chuàng)建一個線程名前綴名稱為dispatcher-event-loop的線程池,默認(rèn)線程數(shù)目是JVM可獲取的核數(shù)與2的最大值,用來處理消息InboxMessage
  private val threadpool: ThreadPoolExecutor

  // 注冊RpcEndpoint 將相關(guān)信息添加到endpoints和endpointRefs,receivers集合中
  def registerRpcEndpoint(name: String, endpoint: RpcEndpoint)
  // 是否存在該endpoint
  def verify(name: String): Boolean

  // 內(nèi)部的線程池,如果沒有指定線程數(shù)目,使用核數(shù)作為線程數(shù)目
  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) {
      pool.execute(new MessageLoop)
    }
    pool
  }

EndpointData

每個endpoint都有一個對應(yīng)的EndpointDataEndpointData內(nèi)部包含了RpcEndpointNettyRpcEndpointRef信息,與一個Inbox,收信箱Inbox內(nèi)部有一個InboxMessage鏈表,發(fā)送到該endpoint的消息,就是添加到該鏈表,同時將整個EndpointData添加Dispatcher到阻塞隊列receivers中,由Dispatcher線程異步處理

InboxMessageInbox內(nèi)的消息,所有的RPC消息都繼承自InboxMessage

  • OneWayMessage:不需要Endpoint回復(fù)的消息
  • RpcMessage:需要Endpoint回復(fù)的消息
  • OnStart:Inbox實例化后自動添加一個OnStart,用于通知對應(yīng)的RpcEndpoint啟動
  • OnStop:用于關(guān)閉對應(yīng)的RpcEndpoint
  • RemoteProcessConnected、RemoteProcessDisconnected、RemoteProcessConnectionError:告訴所有的endpoints,遠(yuǎn)程連接狀態(tài)相關(guān)的信息

注冊RpcEndpoint

NettyRpcEnv內(nèi)注冊RpcEndpoint

  override def setupEndpoint(name: String, endpoint: RpcEndpoint): RpcEndpointRef = {
    dispatcher.registerRpcEndpoint(name, endpoint)
  }

Dispatcher.registerRpcEndpoint調(diào)用:

  • 創(chuàng)建RpcEndpointAddress,記錄endpoint的地址,端口,名稱
  • 創(chuàng)建一個對應(yīng)的RpcEndpointRef
  • 創(chuàng)建一個EndpointData(內(nèi)部的Inbox內(nèi)在構(gòu)造時會放入OnStart消息),放入endpoints緩存
  • 記錄RpcEndpointRpcEndpointRef的映射關(guān)系到endpointRefs緩存
  • EndpointData放入阻塞隊列receives,分發(fā)器異步調(diào)用對應(yīng)的OnStart函數(shù)
  • 返回RpcEndpointRef

RpcCallContext

當(dāng)發(fā)送的消息類型是RpcMessage時,需要回復(fù)消息,需要在其中封裝NettyRpcCallContext,用來向客戶端發(fā)送消息

private[spark] trait RpcCallContext {
  // 回復(fù)消息給發(fā)送方
  def reply(response: Any): Unit
  // 回復(fù)失敗給發(fā)送方
  def sendFailure(e: Throwable): Unit
  // 獲取發(fā)送方地址
  def senderAddress: RpcAddress
}

NettyRpcCallContext為實現(xiàn)RpcCallContext接口的抽象類,有兩個具體的實現(xiàn)類

  • LocalNettyRpcCallContext : 接收方和發(fā)送方在地址相同,即同一進(jìn)程,直接通過Promise進(jìn)行回調(diào)
  • RemoteNettyRpcCallContext : 不在一起的時候,使用遠(yuǎn)程連接式的回調(diào)

TransportContext

傳輸上下文TransportContext,內(nèi)部包含傳輸配置信息TransportConf,以及對收到的RPC消息進(jìn)行處理的RpcHandler,用來創(chuàng)建TransportServer和TransportClientFactory,底層依賴Netty實現(xiàn)

Netty中的相關(guān)概念:
每個Channel都有一個 ChannelPipeline,在Channel創(chuàng)建時會被自動創(chuàng)建

  • ChannelPipeline:內(nèi)部有一個由ChannelHandlerContext組成的雙向鏈表,每個ChannelHandlerContext對應(yīng)一個ChannelHandler
  • ChannelHandler:處理I/O事件或攔截I/O操作,并將其轉(zhuǎn)發(fā)到ChannelPipeline中的下一個ChannelHandler,子接口ChannelOutboundHandlerChannelInboundHandler分別用于處理發(fā)送和接收的I/O

TransportConf

傳輸上下文的配置信息,使用SparkTransportConf.fromSparkConf方法來構(gòu)造

內(nèi)部實際使用的是一份克隆的SparkConf存儲配置屬性,默認(rèn)分配給網(wǎng)絡(luò)傳輸?shù)腎O線程數(shù)是系統(tǒng)可用處理器的數(shù)量,但線程數(shù)目最多為8,最終確定的線程數(shù)將被用于設(shè)置客戶端傳輸線程數(shù)(spark.$module.io.clientThreads)和服務(wù)端傳輸線程數(shù)(spark.$module.io.serverThreads),此外還將spark.rpc.io.numConnectionsPerPeer屬性設(shè)置為1

內(nèi)部包含大量io相關(guān)的配置屬性,及其默認(rèn)值,例如IO模式,緩存大小,線程數(shù)目等,屬性名稱為"spark." + 模塊名稱 + "." + 后綴,其中模塊名稱為rpc,在NettyRpcEnv中創(chuàng)建TransportConf時指定

TransportClientFactory

TransportContext.createClientFactory方法創(chuàng)建,是用來創(chuàng)建TransportClient的工廠類,內(nèi)部包含一個連接池ConcurrentHashMap<SocketAddress, ClientPool> connectionPool,進(jìn)行緩存,方便重復(fù)使用

連接池中每個SocketAddress對應(yīng)一個客戶端池ClientPool,其內(nèi)有一個TransportClient數(shù)組,數(shù)組大大小由spark.rpc.io.numConnectionsPerPeer指定,即本節(jié)點和遠(yuǎn)程節(jié)點建立的連接數(shù)目,默認(rèn)為1

TransportClientFactory構(gòu)造函數(shù)中包含內(nèi)部Netty客戶端相關(guān)的配置,具體類型取決于ioMode:NIO或者EPOLL(Java NIO 在Linux下默認(rèn)使用的就是epoll)

  • 事件處理:NioEventLoopGroup/EpollEventLoopGroup
  • 通道:NioSocketChannel/EpollSocketChannel
  • 緩存分配器:PooledByteBufAllocator

每個TransportClient和一個遠(yuǎn)程地址通信,由TransportClientFactory創(chuàng)建,流程如下

  • 通過host和port構(gòu)造未解析的遠(yuǎn)程連接地址InetSocketAddress
  • 從連接池connectionPool中獲取該地址對應(yīng)的ClientPool,為空則初始化一個客戶端池
  • ClientPool中隨機(jī)選取一個TransportClient
  • 如果TransportClient不為空并且處于活躍狀態(tài),更新該客戶端的最后一次請求事件,直接返回該TransportClient
  • 否則創(chuàng)建一個新的TransportClient
image

Channel中處理 I/O 事件的ChanelHandler核心是TransportChannelHandler,此外還有編解碼相關(guān)和空閑狀態(tài)檢查相關(guān)的handler

TransportClientBootstrap

TransportClientFactory創(chuàng)建Client成功連接到遠(yuǎn)程服務(wù)端以后,先執(zhí)行引導(dǎo)程序,主要用來進(jìn)行初始信息交互,例如SaslClientBootstrap進(jìn)行SASL認(rèn)證,完成后才會返回該新建的TransportClient

TransportClient

TransportClient內(nèi)部包含一個通道Channel,以及一個TransportResponseHandler,此類用于向服務(wù)器發(fā)出請求,而TransportResponseHandler負(fù)責(zé)處理來自服務(wù)器的響應(yīng),是線程安全的,可以從多個線程調(diào)用

client用來發(fā)送五種RequestMessage:ChunkFetchRequest、OneWayMessage、RpcRequest、StreamRequest、UploadStream

  • 發(fā)送RPC請求sendRpc:每個RpcRequest對應(yīng)一個使用UUID生成的requestId,將requestId與對應(yīng)的RpcResponseCallback映射關(guān)系記錄到TransportResponseHandleroutstandingRpcs字段中,當(dāng)收到返回消息時調(diào)用相應(yīng)的回調(diào),主要功能是通過Channel.writeAndFlush將RPC請求發(fā)送出去
  • 請求塊數(shù)據(jù)fetchChunk:首先使用流的標(biāo)記和塊的索引創(chuàng)建StreamChunkId,向TransportResponseHandleroutstandingFetches添加索引與ChunkReceivedCallback回調(diào)的映射關(guān)系,使用channel.writeAndFlush發(fā)送ChunkFetchRequest請求,接收到服務(wù)器端的相應(yīng)時,執(zhí)行相應(yīng)的回調(diào)
  • 請求流數(shù)據(jù)stream:在TransportResponseHandlerstreamCallbacks添加streamIdStreamCallback回調(diào)的映射關(guān)系,發(fā)送StreamRequest請求,收到響應(yīng)時執(zhí)行對應(yīng)的回調(diào)
  • 發(fā)送不需要回復(fù)的數(shù)據(jù)send,直接通過Channel.writeAndFlush發(fā)送OneWayMessage
  • 發(fā)送流數(shù)據(jù)uploadStream:發(fā)送UploadStream數(shù)據(jù)流到遠(yuǎn)端

TransportServerBootstrap

當(dāng)客戶端和服務(wù)器建立連接后,在服務(wù)端對應(yīng)的管道上運(yùn)行的引導(dǎo)程序

TransportServer

RPC框架的服務(wù)端,只要RpcEnvConfig.clientMode不為ture,都會啟動服務(wù)

調(diào)用TransportServer.startServer啟動服務(wù),通過TransportContext.createServer創(chuàng)建服務(wù)端,然后內(nèi)部會向NettyRpcEnvdispatcher中注冊本身的RpcEndpoint,名稱為endpoint-verifier,類型為RpcEndpointVerifier

它的作用是,當(dāng)遠(yuǎn)程節(jié)點需要創(chuàng)建該RpcEnv上的Endpoint的一個引用時(setupEndpointRef方法),因為每個RpcEnv上都有RpcEndpointVerifier,所以遠(yuǎn)端可以直接創(chuàng)建一個RpcEndpointVerifier對應(yīng)的ref,通過它發(fā)送CheckExistence(name: String)消息,查詢該dispatcher內(nèi)部的endpoints緩存中是否存在的名稱為name的endpoint,從而確定是否可以創(chuàng)建該RpcEndpointRef

TransportServer內(nèi)部包含的變量如下:

  • context:傳輸上下文TransportContext,用來配置channelHandler
  • conf:傳輸配置TransportContext
  • appRpcHandler:RPC請求處理器RpcHandler
  • bootstraps:TransportServerBootstrap類型,當(dāng)客戶端連接到服務(wù)器端時,在通道創(chuàng)建時執(zhí)行的引導(dǎo)程序

其創(chuàng)建過程就是標(biāo)準(zhǔn)的netty服務(wù)端創(chuàng)建方式,對于已經(jīng)鏈接進(jìn)來的client Chanel,ChanelHandler的配置和客戶端配置類似

RpcHandler

TransportClient.sendRpc發(fā)送的RPC消息進(jìn)行處理,內(nèi)部通過Dispatcher將收到的RPC分發(fā)到對應(yīng)的Endpoint

// RpcHandler部分定義
public abstract class RpcHandler {
  // 接收一個RPC消息,具體邏輯執(zhí)行由子類實現(xiàn),處理完成后通過RpcResponseCallback回調(diào),如果不需要回調(diào)返回消息的,傳入?yún)?shù)為OneWayRpcCallback,只打印日志
  public abstract void receive(
      TransportClient client,
      ByteBuffer message,
      RpcResponseCallback callback);

  // 獲取StreamManager
  public abstract StreamManager getStreamManager();
  // Channel處于活躍狀態(tài)時調(diào)用
  public void channelActive(TransportClient client) { }
  // 非活躍狀態(tài)時調(diào)用
  public void channelInactive(TransportClient client) { }
  // 產(chǎn)生異常時調(diào)用
  public void exceptionCaught(Throwable cause, TransportClient client) { }
  ...
}

NettyStreamManager用于提供NettyRpcEnv的文件流服務(wù),可以將文件,目錄和jar包注冊到其中,然后根據(jù)請求,將相應(yīng)文件的信息封裝為FileSegmentManagedBuffer,可以用來處理StreamRequest類型的消息

管道初始化

管道初始化過程中都使用了TransportContext.initializePipeline創(chuàng)建的TransportChannelHandler

TransportChannelHandler是單個傳輸層的通道handler,用于將請求委派給TransportRequestHandler并響應(yīng)TransportResponseHandler。在傳輸層中創(chuàng)建的所有通道都是雙向的。當(dāng)客戶端使用RequestMessage發(fā)送給Netty通道(由服務(wù)器的RequestHandler處理)時,服務(wù)器將生成ResponseMessage(由客戶端的ResponseHandler處理)。但是,服務(wù)器也會在同一個Channel上獲取句柄,因此它可能會向客戶端發(fā)送RequestMessages。這意味著客戶端還需要一個RequestHandler,而Server需要一個ResponseHandler,用于客戶端對服務(wù)器請求的響應(yīng)進(jìn)行響應(yīng)。

此類還處理來自io.netty.handler.timeout.IdleStateHandler的超時信息。如果存在未完成的提取fetch或RPC請求但是在“requestTimeoutMs”時間內(nèi)通道上沒有的流量,我們認(rèn)為連接超時。注意這是雙工通道;如果客戶端不斷發(fā)送但是沒有響應(yīng),為簡單起見不認(rèn)為是超時

TransportChannelHandler內(nèi)部使用MessageHandler處理Message,其中MessageHandler有兩種類型,分別用來處理客戶端請求/處理服務(wù)端的響應(yīng),Message共有10種類型

TransportRequestHandler

處理客戶端的五種請求信息RequestMessage,內(nèi)部包含RpcHandler處理RPC信息,TransportClient用來和請求方通信

  • RpcRequest:通過RpcHandler接收請求,實際類型為NettyRpcHandler,這里將ByteBuffer類型的RPC請求轉(zhuǎn)換為RequestMessage,加入到對應(yīng)endpoint的inbox內(nèi),由Dispatcher負(fù)責(zé)處理,通過RpcResponseCallback回復(fù)RpcResponse/RpcFailure
  • OneWayMessage:類似前一種情況,最后不對客戶端進(jìn)行回復(fù)
  • ChunkFetchRequest:通過StreamManager.getChunk處理要請求的數(shù)據(jù)塊,同時返回結(jié)果描述ChunkFetchSuccess/ChunkFetchFailure
  • StreamRequest:通過StreamManager.openStream獲取請求的流數(shù)據(jù),最后返回結(jié)果StreamResponse/StreamFailure給客戶端
  • UploadStream:處理客戶端上傳的流

SaslServerBootstrap類型的引導(dǎo)首先會對服務(wù)器端的RPCHandler進(jìn)行代理,與客戶端進(jìn)行認(rèn)證交互,認(rèn)證成功后,將加解密的Handler添加到通道的Pipeline中,后續(xù)的消息交給被代理的RPCHandler進(jìn)行代理

TransportResponseHandler

處理服務(wù)端對請求的響應(yīng),內(nèi)部會記錄需要回復(fù)的請求的ID,以及對應(yīng)的callback函數(shù),一共由六種ResponseMessage,根據(jù)消息類型和ID,執(zhí)行響應(yīng)的回調(diào)

  • ChunkFetchFailure
  • ChunkFetchSuccess
  • RpcFailure
  • RpcResponse
  • StreamFailure
  • StreamResponse

其他ChannelHandler

  • MessageEncoder:對Message進(jìn)行編碼,frame格式如下
length(long類型,消息長度,8字節(jié))|message type(消息類型,1個字節(jié))|message meta(消息元數(shù)據(jù),例如RpcRequest消息的元數(shù)據(jù)長度為12,包括requestId和消息體長度bodysize)|message body(消息體,具體的信息,例如ChunkFetchSuccess中的塊數(shù)據(jù))
  • MessageDecoder:解碼,從中獲取消息類型,和消息內(nèi)容,創(chuàng)建對應(yīng)的消息
  • TransportFrameDecoder:處理TCP中的粘包拆包,得到一個完整的消息,內(nèi)部主要依靠的就是每個frame前8個字節(jié),表示整個frame的長度
  • IdleStateHandler:實現(xiàn)心跳功能,觸發(fā)IdleStateEvent事件,當(dāng)通道內(nèi)沒有執(zhí)行讀取,寫入操作時,底層通過向線程任務(wù)隊列中添加定時任務(wù),如果空閑超時,則會觸發(fā)TransportChannelHandler.userEventTriggered方法。在這個方法如果確認(rèn)超時,會關(guān)閉通道

Outbox

可以理解為發(fā)出消息的盒子,每個地址對應(yīng)個盒子

NettyRpcEnv中outboxes : ConcurrentHashMap[RpcAddress, Outbox]字段,每個遠(yuǎn)程RpcAddress對應(yīng)一個Outbox,OutBox其內(nèi)部包含一個OutboxMessage的鏈表,所有向遠(yuǎn)端發(fā)送的消息都要封裝為OutboxMessage

調(diào)用Outbox.send方法發(fā)送消息時,將消息添加到OutboxMessage鏈表中,如果遠(yuǎn)程連接還未建立,會先通過NettyRpcEnv中的clientConnectionExecutor線程池執(zhí)行建立連接的任務(wù),即創(chuàng)建特定RpcAddress上的TransportClient,然后發(fā)送消息

OutboxMessage有兩個子類,OneWayOutboxMessageRpcOutboxMessage,表明不會有回復(fù)和存在回復(fù)兩種消息類型,分別對應(yīng)調(diào)用RpcEndpoint的receivereceiveAndReply方法,當(dāng)TransportClient發(fā)送消息時,如果Message是RpcOutboxMessage,先會創(chuàng)建一個UUID,底層TransportResponseHandler維護(hù)一個發(fā)送消息ID與其Callback的HashMap,當(dāng)Netty收到完整的遠(yuǎn)程RpcResponse時候,做反序列化,回調(diào)相應(yīng)的Callback,進(jìn)而執(zhí)行Spark中的業(yè)務(wù)邏輯,即Promise/Future的響應(yīng)

Rpc底層通信框架

實際流程分析

當(dāng)通過RpcEndpointRef發(fā)送需要回復(fù)的消息時:

  • RpcEndpointRef.ask
  • NettyRpcEndpointRef.ask 構(gòu)建RequestMessage(senderAddress,receiver,message content)

如果遠(yuǎn)程地址與當(dāng)前NettyRpcEnv相同:

  • NettyRpcEnv.ask,創(chuàng)建一個Promise對象,設(shè)定Future完成后的回調(diào)
  • Dispatcher.postLocalMessage,構(gòu)建RpcMessage和回調(diào)上下文LocalNettyRpcCallContext
  • Dispatcher.postMessage,將RequestMessage消息重構(gòu)成RpcMessage,放置到對應(yīng)endpoint的inbox內(nèi),dispatcher內(nèi)部的線程池會取出消息,然后根據(jù)消息類型,執(zhí)行不同的操作,調(diào)用endpoint的receiveAndReply,內(nèi)部回調(diào)RpcCallContext.reply返回結(jié)果

如果需要連接遠(yuǎn)程地址時:

  • NettyRpcEnv.ask,將RequestMessage序列化,構(gòu)造一個RpcOutboxMessage,設(shè)定消息成功和失敗時對應(yīng)的的回調(diào)
  • NettyRpcEnv.postToOutbox,如果尚未創(chuàng)建對應(yīng)的TransportClient,將消息放入目的地址相應(yīng)的Outbox,必要時新建Outbox以及對應(yīng)的TransportClient
  • 通過message.sendWith(client)調(diào)用TransportClient.sendRpc發(fā)送RpcRequest消息,記錄該requestId對應(yīng)的回調(diào)RpcResponseCallback,用來收到回復(fù)后調(diào)用

上述步驟都是異步執(zhí)行的,當(dāng)將消息放置到相應(yīng)位置后,就會返回,然后:

  • 使用timeoutScheduler設(shè)定一個計時器,用于超時處理,超時拋出TimeoutException異常

在Spark 1.6之前,底層使用的Akka進(jìn)行PRC,它是基于Actor的RPC通信系統(tǒng),但是無法適用大的package/stream的數(shù)據(jù)傳輸,所以還有Netty通信框架,所以將兩套通信框架合并統(tǒng)一使用netty,并且akka使用時版本必須保證一致,否則會出現(xiàn)很多問題。但是RpcEnv參照了Akka的思路,內(nèi)部原理基本一致,都是按照MailBox的設(shè)計思路來實現(xiàn)的

image

參考

其他Spark源碼分析,記錄在GitBook

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,958評論 2 373