相關(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)的EndpointData
,EndpointData
內(nèi)部包含了RpcEndpoint
、NettyRpcEndpointRef
信息,與一個Inbox
,收信箱Inbox
內(nèi)部有一個InboxMessage
鏈表,發(fā)送到該endpoint的消息,就是添加到該鏈表,同時將整個EndpointData
添加Dispatcher到阻塞隊列receivers
中,由Dispatcher線程異步處理
InboxMessage
是Inbox
內(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緩存 - 記錄
RpcEndpoint
和RpcEndpointRef
的映射關(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,子接口ChannelOutboundHandler
、ChannelInboundHandler
分別用于處理發(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
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)系記錄到TransportResponseHandler
的outstandingRpcs
字段中,當(dāng)收到返回消息時調(diào)用相應(yīng)的回調(diào),主要功能是通過Channel.writeAndFlush
將RPC請求發(fā)送出去 - 請求塊數(shù)據(jù)
fetchChunk
:首先使用流的標(biāo)記和塊的索引創(chuàng)建StreamChunkId
,向TransportResponseHandler
的outstandingFetches
添加索引與ChunkReceivedCallback
回調(diào)的映射關(guān)系,使用channel.writeAndFlush
發(fā)送ChunkFetchRequest
請求,接收到服務(wù)器端的相應(yīng)時,執(zhí)行相應(yīng)的回調(diào) - 請求流數(shù)據(jù)
stream
:在TransportResponseHandler
的streamCallbacks
添加streamId
與StreamCallback
回調(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)部會向NettyRpcEnv
的dispatcher
中注冊本身的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
有兩個子類,OneWayOutboxMessage
和RpcOutboxMessage
,表明不會有回復(fù)和存在回復(fù)兩種消息類型,分別對應(yīng)調(diào)用RpcEndpoint的receive
和receiveAndReply
方法,當(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)
實際流程分析
當(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)的
參考
- 這可能是目前最透徹的Netty原理架構(gòu)解析
- Netty 源碼分析之 一 揭開 Bootstrap 神秘的紅蓋頭 (客戶端)
- 深入解析Spark中的RPC
- Spark 底層網(wǎng)絡(luò)模塊
- Spark為何使用Netty通信框架替代Akka
其他Spark源碼分析,記錄在GitBook