Spark源碼分析之Rpc框架

概述

在Spark中很多地方都涉及網絡通信,比如 Spark各個組件間的消息互通用戶文件與Jar包的上傳節點間的Shuffle過程Block數據的複製與備份等。Spark1.6之前,Spark的Rpc是基於Akka來實現的,Akka是一個基於scala語言的異步的消息框架,但由於Akka不適合大文件的傳輸,在Spark1.6之前RPC通過Akka來實現,而大文件是基於Jetty實現的HttpFileServer。但在Spark1.6中移除了Akka(https://issues.apache.org/jira/plugins/servlet/mobile#issue/SPARK-5293),原因概括爲:

  • 很多Spark用戶也使用Akka,但是由於Akka不同版本之間無法互相通信,這就要求用戶必須使用跟Spark完全一樣的Akka版本,導致用戶無法升級Akka。
  • Spark的Akka配置是針對Spark自身來調優的,可能跟用戶自己代碼中的Akka配置衝突。
  • Spark用的Akka特性很少,這部分特性很容易自己實現。同時,這部分代碼量相比Akka來說少很多,debug比較容易。如果遇到什麼bug,也可以自己馬上fix,不需要等Akka上游發佈新版本。而且,Spark升級Akka本身又因爲第一點會強制要求用戶升級他們使用的Akka,對於某些用戶來說是不現實的。

在Spark2.0.0中也移除了Jetty,在Spark2.0.0版本借鑑了Akka的設計,重構了基於Netty的Rpc框架體系,其中Rpc和大文件傳輸都是使用Netty。

注:

  • [1] Akka是基於Actor併發編程模型實現的併發的分佈式的框架。Akka是用Scala語言編寫的,它提供了Java和Scala兩種語言的API,減少開發人員對併發的細節處理,並保證分佈式調用的最終一致性。
  • [2] Jetty 是一個開源的Servlet容器,它爲基於Java的Web容器,例如JSP和Servlet提供運行環境。Jetty是使用Java語言編寫的,它的API以一組JAR包的形式發佈。開發人員可以將Jetty容器實例化成一個對象,可以迅速爲一些獨立運行的Java應用提供網絡和Web連接。在附錄C中有對Jetty的簡單介紹,感興趣的讀者可以選擇閱讀。
  • [3] Netty是由Jboss提供的一個基於NIO的客戶、服務器端編程框架,使用Netty 可以確保你快速、簡單的開發出一個網絡應用,例如實現了某種協議的客戶,服務端應用。

注:如下涉及的源碼版本爲Spark2.4,下面部分圖片摘之網上大佬博客。

基本概念

首先我們給出兩張UML圖(簡易版 + 複雜版)
在這裏插入圖片描述
在這裏插入圖片描述
上圖中繼承RpcEndpoint接口的類是實現業務邏輯的重點,其中直接繼承RpcEndpoint的類支持併發,而繼承ThreadSafeRpcEndpoint不支持併發,保證了線程安全,上圖繼承類只列出了Master和Worker兩個,不夠全面,下面單獨給出一張RpcEndpoint繼承圖供參考:
在這裏插入圖片描述
下面介紹一些重要概念:

  • RpcEnv:RpcEnv 抽象類表示一個 RPC Environment,管理着整個RpcEndpoint的生命週期,每個 Rpc 端點運行時依賴的環境稱之爲 RpcEnv。
  • NettyRpcEnv: RpcEnv的唯一實現類
  • RpcEndpoint:RPC 端點 ,Spark 將每個通信實體都都稱之一個Rpc端點,且都實現 RpcEndpoint 接口,比如DriverEndpoint,MasterEndpont,內部根據不同端點的需求,設計不同的消息和不同的業務處理。
  • Dispatcher:消息分發器(來自netty的概念),負責將 RpcMessage 分發至對應的 RpcEndpoint。Dispatcher 中包含一個 MessageLoop,它讀取 LinkedBlockingQueue 中的投遞 RpcMessage,根據客戶端指定的 Endpoint 標識,找到 Endpoint 的 Inbox,然後投遞進去,由於是阻塞隊列,當沒有消息的時候自然阻塞,一旦有消息,就開始工作。Dispatcher 的 ThreadPool 負責消費這些 Message。
  • EndpointData:每個endpoint都有一個對應的EndpointData,EndpointData內部包含了RpcEndpoint、NettyRpcEndpointRef信息,與一個Inbox,收信箱Inbox內部有一個InboxMessage鏈表,發送到該endpoint的消息,就是添加到該鏈表,同時將整個EndpointData添加Dispatcher到阻塞隊列receivers中,由Dispatcher線程異步處理
  • Inbox:一個本地端點對應一個收件箱,Inbox 裏面有一個 InboxMessage 的鏈表,InboxMessage 有很多子類,可以是遠程調用過來的 RpcMessage,可以是遠程調用過來的 fire-and-forget 的單向消息 OneWayMessage,還可以是各種服務啓動,鏈路建立斷開等 Message,這些 Message 都會在 Inbox 內部的方法內做模式匹配,調用相應的 RpcEndpoint 的函數。
  • RpcEndPointRef: RpcEndpointRef是一個對RpcEndpoint的遠程引用對象,通過它可以向遠程的RpcEndpoint端發送消息以進行通信。
  • NettyRpcEndpointRef:RpcEndpointRef 的唯一實現類,RpcEndpointRef的NettyRpcEnv版本。此類的行爲取決於它的創建位置。在“擁有”RpcEndpoint的節點上,它是RpcEndpointAddress實例的簡單包裝器。
  • RpcEndpointAddress:主要包含了 RpcAddress (host和port) 和 rpc endpoint name的信息
  • Outbox:一個遠程端點對應一個發件箱,NettyRpcEnv 中包含一個 ConcurrentHashMap[RpcAddress, Outbox]。當消息放入 Outbox 後,緊接着將消息通過 TransportClient 發送出去。
  • TransportContext:是一個創建TransportServer, TransportClientFactory,使用TransportChannelHandler建立netty channel pipeline的上下文,這也是它的三個主要功能。TransportClient 提供了兩種通信協議:控制層面的RPC以及數據層面的 “chunk抓取”。用戶通過構造方法傳入的 rpcHandler 負責處理RPC 請求。並且 rpcHandler 負責設置流,這些流可以使用零拷貝IO以數據塊的形式流式傳輸。TransportServer 和 TransportClientFactory 都爲每一個channel創建一個 TransportChannelHandler對象。每一個TransportChannelHandler 包含一個 TransportClient,這使服務器進程能夠在現有通道上將消息發送回客戶端。
  • TransportServer:TransportServer是RPC框架的服務端,可提供高效的、低級別的流服務。
  • TransportServerBootstrap:定義了服務端引導程序的規範,服務端引導程序旨在當客戶端與服務端建立連接之後,在服務端持有的客戶端管道上執行的引導程序。用於初始化TransportServer
  • TransportClientFactory:創建傳輸客戶端(TransportClient)的傳輸客戶端工廠類。
  • TransportClient:RPC框架的客戶端,用於獲取預先協商好的流中的連續塊。TransportClient旨在允許有效傳輸大量數據,這些數據將被拆分成幾百KB到幾MB的塊。簡言之,可以認爲TransportClient就是Spark Rpc 最底層的基礎客戶端類。主要用於向server端發送rpc 請求和從server 端獲取流的chunk塊。
  • TransportClientBootstrap:是在TransportClient上執行的客戶端引導程序,主要對連接建立時進行一些初始化的準備(例如驗證、加密)。TransportClientBootstrap所作的操作往往是昂貴的,好在建立的連接可以重用。用於初始化TransportClient
  • TransportChannelHandler:傳輸層的handler,負責委託請求給TransportRequestHandler,委託響應給TransportResponseHandler。在傳輸層中創建的所有通道都是雙向的。當客戶端使用RequestMessage啓動Netty通道(由服務器的RequestHandler處理)時,服務器將生成ResponseMessage(由客戶端的ResponseHandler處理)。但是,服務器也會在同一個Channel上獲取句柄,因此它可能會開始向客戶端發送RequestMessages。這意味着客戶端還需要一個RequestHandler,而Server需要一個ResponseHandler,用於客戶端對服務器請求的響應。此類還處理來自io.netty.handler.timeout.IdleStateHandler的超時。如果存在未完成的提取或RPC請求但是至少在“requestTimeoutMs”上沒有通道上的流量,我們認爲連接超時。請注意,這是雙工流量;如果客戶端不斷髮送但是沒有響應,我們將不會超時。
    當TransportChannelHandler讀取到的request是RequestMessage類型時,則將此消息的處理進一步交給TransportRequestHandler,當request是ResponseMessage時,則將此消息的處理進一步交給TransportResponseHandler。
  • TransportResponseHandler:用於處理服務端的響應,並且對發出請求的客戶端進行響應的處理程序。
  • TransportRequestHandler:用於處理客戶端的請求並在寫完塊數據後返回的處理程序。
  • MessageEncoder:在將消息放入管道前,先對消息內容進行編碼,防止管道另一端讀取時丟包和解析錯誤。
  • MessageDecoder:對從管道中讀取的ByteBuf進行解析,防止丟包和解析錯誤;
  • TransportFrameDecoder:對從管道中讀取的ByteBuf按照數據幀進行解析;
  • StreamManager:處理ChunkFetchRequest和StreamRequest請求
  • RpcHandler:處理RpcRequest和OneWayMessage請求
  • Message:Message是消息的抽象接口,消息實現類都直接或間接的實現了RequestMessage或ResponseMessage接口。

組件原理

Message消息

協議是應用層通信的基礎,它提供了應用層通信的數據表示,以及編碼和解碼的能力。在Spark Network Common中,繼承AKKA中的定義,將協議命名爲Message,它繼承Encodable,提供了encode的能力。
在這裏插入圖片描述
其中RequestMessage的具體實現有四種,分別是:

  • StreamRequest:此消息表示向遠程的服務發起請求,以獲取流式數據。Stream消息主要用於driver到executor傳輸jar、file文件等。
  • RpcRequest:此消息類型由遠程的Rpc服務端進行處理的消息,是一種需要服務端向客戶端回覆的RPC請求信息類型。
  • ChunkFetchRequest:請求獲取流的單個塊的序列。ChunkFetch消息用於抽象所有spark中涉及到數據拉取操作時需要傳輸的消息。
  • OneWayMessage:此消息也需要由遠程的RPC服務端進行處理,與RpcRequest不同的是不需要服務端向客戶端回覆。

由於OneWayMessage 不需要響應,所以ResponseMessage的對於成功或失敗狀態的實現各有兩種,分別是:

  • StreamResponse:處理StreamRequest成功後返回的消息;
  • StreamFailure:處理StreamRequest失敗後返回的消息;
  • RpcResponse:處理RpcRequest成功後返回的消息;
  • RpcFailure:處理RpcRequest失敗後返回的消息;
  • ChunkFetchSuccess:處理ChunkFetchRequest成功後返回的消息;
  • ChunkFetchFailure:處理ChunkFetchRequest失敗後返回的消息;

通信架構

Spark的Rpc框架是基於Actor模型,各個組件可以認爲是一個個獨立的實體,各個實體之間通過消息來進行通信。具體各個組件之間的關係圖如下:
在這裏插入圖片描述

  • RpcEnv:爲RpcEndpoint提供處理消息的環境,是整個的核心。RpcEnv負責RpcEndpoint整個生命週期的管理,包括:註冊endpoint,endpoint之間消息的路由,以及停止endpoint,而NettyRpcEnv目前是其唯一實現。
  • RpcEndpoint:服務端,是根據接收的消息來進行對應的處理,一個RpcEndpoint經歷的過程依次是:create -> onStart -> receive -> onStop。其中onStart在接收任務消息前調用(在註冊時候做爲第一個自己處理的消息調用),receivereceiveAndReply分別用來接收sendask過來的消息。
  • RpcEndpointRef:客戶端,是對遠程RpcEndpoint的一個引用。當我們需要向一個具體的RpcEndpoint發送消息時,一般需要獲取到該RpcEndpoint的引用,然後通過該引用發送消息,提供了send(單向發送,提供fire-and-forget語義)和ask(帶返回的請求,提供請求響應的語義)的消息發送方式,其中需要返回response的ask方式,帶有超時機制,可以同步阻塞等待,也可以返回一個Future句柄,不阻塞發起請求的工作線程。另外RpcEndpointRef能夠自動的區分做到本地調用或者遠程Rpc調用。
  • RpcAddress:表示遠程的RpcEndpointRef的地址,包含【Host + Port】。

SparkEnv的初始化

SparkEnv保存着 Application 運行時的環境信息,包括 RpcEnv、Serializer、Block Manager 和 ShuffleManager 等,併爲 Driver 端和 Executor 端分別提供了不同的創建方式。其中RpcEnv 維持着 Spark 節點間的通信,並負責將傳遞過來的消息轉發給RpcEndpoint。
在這裏插入圖片描述
我們知道Executor啓動是由CoarseGrainedExecutorBackend爲入口類的,我們以sparkExecutor的RpcEnv初始化說明爲例,看一下createExecutorEnv()的代碼邏輯:
在這裏插入圖片描述

RpcEnv

RpcEnv不僅從外部接口與Akka基本一致,在內部的實現上,也基本差不多,都是按照MailBox的設計思路來實現的;
在這裏插入圖片描述
如上圖所示,RpcEnv即充當着Server,同時也爲Client內部實現。 當As Server,RpcEnv會初始化一個Server,並註冊NettyRpcHandler,一般情況下,簡單業務可以在RpcHandler直接完成請求的處理,但是考慮一個RpcEnv的Server上會掛載了很多個RpcEndpoint,每個RpcEndpoint的RPC請求頻率不可控,因此需要對一定的分發機制和隊列來維護這些請求,其中Dispatcher爲分發器,InBox即爲請求隊列;
在將RpcEndpoint註冊到RpcEnv過程中,也間接的將RpcEnv註冊到Dispatcher分發器中,Dispatcher針對每個RpcEndpoint維護一個InBox,在Dispatcher維持一個線程池(線程池大小默認爲系統可用的核數,當然也可以通過spark.rpc.netty.dispatcher.numThreads進行配置),線程針對每個InBox裏面的請求進行處理。當然實際的處理過程是由RpcEndpoint來完成。
其次RpcEnv也完成Client的功能實現,RpcEndpointRef是以RpcEndpoint爲單位,即如果一個進程需要和遠程機器上N個RpcEndpoint服務進行通信,就對應N個RpcEndpointRef(後端的實際的網絡連接是公用,這個是TransportClient內部提供了連接池來實現的),當調用一個RpcEndpointRef的ask/send等接口時候,會將把“消息內容+RpcEndpointRef+本地地址”一起打包爲一個RequestMessage,交由RpcEnv進行發送。注意這裏打包的消息裏面包括RpcEndpointRef本身是很重要的,從而可以由Server端識別出這個消息對應的是哪一個RpcEndpoint。
和發送端一樣,在RpcEnv中,針對每個remote端的host:port維護一個隊列,即OutBox,RpcEnv的發送僅僅是把消息放入到相應的隊列中,但是和發送端不一樣的是:在OutBox中沒有維護一個所謂的線程池來定時清理OutBox,而是通過一堆synchronized來實現的
下面我們看下RpcEnv相關類圖:
在這裏插入圖片描述
RpcEnv相對於ActorSystem,主要提供以下作用:

  • 首先As Server,它通過NettyRpcHandler來提供了Server的服務能力,
  • 其次它作爲RpcEndpoint的容器,它提供了 setupEndpoint(name: String, endpoint: RpcEndpoint)) 接口,從而實現將一個RpcEndpoint以一個Name對應關係註冊到容器中,從而通過Server對外提供Service
  • As Client的適配器,它提供了setupEndpointRef(address: RpcAddress, endpointName: String) | setupEndpointRefByURI(uri: String)接口,通過指定Server端的Host和PORT,並指定RpcEndpointName,從而獲取服務端Endpoint通信的引用。

Rpc服務端的啓動流程

首先我們看下Rpc服務端啓動過程,我們以sparkExecutor的RpcEnv初始化說起,由前面章節可知RpcEnv最終會調用RpcEnv.create()函數,我們從此開始分析:
在這裏插入圖片描述
經過上圖中的三步之後就正式的開始初始化netty server,首先我們先看Spark對netty的封裝,先看類圖:
在這裏插入圖片描述

  • TransportContext維護Transport的上下文環境,主要用來創建TransportServer和TransportClientFactory。
  • TransportServer通過構造函數啓動netty,提供響應請求服務。

本節啓動過程分析只涉及TransportContext和TransportServer(如上圖虛線框住部分),我們接上面的源碼繼續分析如下:
在這裏插入圖片描述
如上圖,初始化流程最終會調用 context.initializePipeline(ch, rpcHandler) ,這個會初始化Netty的消息處理Pipeline,在繼續介紹之前我們先介紹TransportChannelHandler,Netty處理Rpc類型請求依賴TransportChannelHandler,在TransportServer初始化時添加到pipeline中,其中TransportChannelHandler包含兩個重要變量TransportResponseHandler和TransportRequestHandler,分別處理Response和Request請求。繼續分析如下:
在這裏插入圖片描述
首先創建消息處理TransportChannelHandler添加到消息處理Pipline後,最後就可以設置爲Netty的childHandler(),啓動Netty監控即可接受消息,至此Rpc服務器的啓動就完全完成了。
但我們還需要分析Netty服務器啓動後,在接受到消息後如何做進行請求消息的處理流轉,如下:
接受的消息首先在Pipline中先進行解碼等處理,然後進行消息業務處理(即 TransportChannelHandler.channelRead()),根據消息請求類型調用不同的Handler處理,由於本次分析涉及到Request請求,我們繼續分析:
在這裏插入圖片描述
TransportRequestHandler.handle()中根據消息類型調用不同的處理方法,如果是RpcRequest消息則會調用rpcHandler.receive()處理,由於RpcRequest提供有Response,所以在回調函數中返回Response,由於RpcHandler的實現類是NettyRpcHandler,也即是發送給NettyRpcHandler.receive()函數,從上圖可以看出其實現是轉發消息給dispatcher進行後續的消息調度分發。
最後我們給出一張Rpc框服務端處理請求、響應流程圖:
在這裏插入圖片描述

Rpc服務端處理請求流程

在這裏插入圖片描述
如上圖介紹服務端通過NettyRpcHandler來提供了Server的服務能力,即NettyRpcHandler接受轉發消息給Dispatcher進行消息的調度處理,代碼如下:
在這裏插入圖片描述
RpcRequest的請求處理依賴於Dispatcher和Inbox的協調工作,首先看下 Dispatcher 的UML圖:
在這裏插入圖片描述
Dispatcher主要職責如下:

  • 內部使用集合endpoints和endpointRefs維護Endpoint、EndpointRef,對外通過registerRpcEndpoint、removeRpcEndpointRef、getRpcEndpointRef等方法提供Endpoint註冊刪除和獲取EndpointRef等服務。
  • 利用EndpointData和Inbox結構完成消息的存儲。
  • 創建線程池threadpool,執行MessageLoop線程,消費消息。

接着看下 Inbox 的類定義:
在這裏插入圖片描述
Inbox的作用:

  • 內部了維護了鏈表messages,用於存儲消息,同時維護該消息對應消費者Endpoint,其實現類包含兩個:OneWayMessage(單向)和RpcMessage(雙向,帶回調函數)
  • 提供了post和process兩個方法,分別用於添加消息到messages和消費消息,process方法在MessageLoop中被調用。

因爲最後會根據消息類型做對應的處理,所以有必要先了解一下Inbox的消息類型,其子類包含:
在這裏插入圖片描述
下面看下Dispatcher的消息分發處理過程:
在這裏插入圖片描述
首先Dispatcher通過postRemoteMessage(message: RequestMessage, callback: RpcResponseCallback)postLocalMessage(message: RequestMessage, p: Promise[Any])postOneWayMessage(message: RequestMessage)三種方式接受消息後,首先都會封裝消息RequestMessage -> RpcMessage,然後調用postMessage(endpointName: String, message: InboxMessage, callbackIfStopped: (Exception) => Unit),首先調用val data = endpoints.get(endpointName)根據endpointName獲得已註冊的的EndpointData(包含inbox和endpoint,前者用於投遞消息,後者在消費端用於調用其業務邏輯代碼),然後把EndpointData添加到Dispatcher的receivers,代碼如下圖:
在這裏插入圖片描述
消息添加進來後,那又如何消費消息呢?繼續分析Dispatcher,我們會發現在創建Dispatcher時候,會在其內部創建一個threadpool線程池後臺循環消費receivers變量,執行函數體在內部類的MessageLoop.run()函數,繼續跟蹤代碼可以發現取出變量中存儲的EndpointData並調用其Inbox.process()處理,代碼如下:
在這裏插入圖片描述
在上面在Inbox.process()函數中根據消息的類型調用對應的endpoint中的函數進行最終的業務處理。
下面我們以心跳交互演示整個過程(主要這部分功能簡單,利於分析):
在這裏插入圖片描述

Rpc客戶端發送請求流程

在這裏插入圖片描述
如上圖介紹,客戶端發送消息用的是持有RpcEndpoint的RpcEndpointRef,最終消息的實際發送是通過 Outbox 發送,RpcEnv的實現類中NettyRpcEnv維護了outboxes變量用於存儲不同的Outbox,而Outbox內部除了維護messages用於存儲消息外,還存儲有client用於與Rpc服務器進行通信。Outbox類結構如下:
在這裏插入圖片描述
由於消息的發送是通過OutboxMessage的sendWith完成的,所以有必要先了解一下OutboxMessage,Outbox對應的消息類型爲OutboxMessage,對應子類有RpcOutboxMessage(有返回值)和OneWayOutboxMessage(無返回值),其類圖如下:
在這裏插入圖片描述
我們依然以上節中的心跳交互爲例,首先需要明確無論是註冊Endpoint還是註冊EndpointRef都需要先初始化RpcEnv,經分析發現客戶端發送心跳消息用的RpcEnv初始化是在CoarseGrainedExecutorBackend.run()函數中,其初始化過程上文已介紹,爲了便於介紹再次給出截圖:
在這裏插入圖片描述
初始化RpcEnv後就可以使用註冊EndpointRef了,代碼分析如下(在進入以下代碼前會經過executor向driver發送【RegisterExecutor】消息,然後driver向executor發送【RegisteredExecutor】消息,由於此部分的交互不是我們的重點,我們直接跳過進入心跳過程):
在這裏插入圖片描述
註冊完成後即可獲得heartbeatReceiverRef,然後在 Executor 中開闢一個線程定時向Driver發送心跳。如下圖:
在這裏插入圖片描述
到這,我們終於進入我們的主題 heartbeatReceiverRef.askSync[HeartbeatResponse](),下面我們重點分析發送請求流程:
在這裏插入圖片描述
下面代碼分析如下:
在這裏插入圖片描述
如上圖,如果消息的遠程服務器地址和本地服務器一樣,就直接發送給本機Dispatcher進行消息的響應處理(繼續分析見上節);否則把參數封裝爲RpcOutboxMessage,調用postToOutbox()函數,我們繼續分析
在這裏插入圖片描述
OutBox並沒有啓動單獨線程進行發送而是在drainOutbox()函數中調用message.sendWith(_client)方法發送消息,由於前面把心跳消息封裝爲RpcOutboxMessage類型了,所以繼續調用RpcOutboxMessage.sendWith()函數繼續發送消息,最後消息再通過TransportClient.sendRpc()函數調用Netty客戶端向遠程服務器發送,代碼如下:
在這裏插入圖片描述
至此,Rpc客戶端發送請求流程也分析完成了,但我們的分析過程並沒有結束,因爲還有一個問題就是Netty客戶端是如何初始化的呢?我們繼續分析,Netty客戶端的初始化即是TransportClient的初始化,我們同樣給出類圖(上文已給出過此圖,但此處分析Client端的封裝,如虛線框住部分):
在這裏插入圖片描述

  • TransportContext維護Transport的上下文環境,主要用來創建TransportServer和TransportClientFactory。
  • TransportClientFactory用來創建TransportClient。
  • TransportClient和對應的TransportServer通信。

由前面的分析我們知道TransportContext初始化是在RpcEnv創建中完成的,那麼我們直接看TransportClientFactory和TransportClient如何完成的初始化的,通過上面分析可知Client發送消息中間有一環節會調用Outbox.drainOutbox(),代碼如下圖:
在這裏插入圖片描述
如上圖NettyRpcEnv創建時候會初始化transportContextclientFactory變量,之後在Outbox.drainOutbox()函數首先判斷TransportClient類型的client變量是否爲null,如果爲null則調用Outbox.launchConnectTask() -> NettyRpcEnv.createClient() -> TransportClientFactory.createClient()新創建,下面我們重點分析TransportClientFactory.createClient(remoteHost, remotePort)函數。我們首先了解一下TransportClientFactory類,其內部會創建多個連接池(this.connectionPool = ConcurrentHashMap<SocketAddress, ClientPool>()),每個遠程地址都對應一個連接池(ClientPool),ClientPool是TransportClientFactory的內部類,內部實現是創建數組存儲TransportClient,每次連接時候隨機去除一個空閒的用於和TransportServer交互(其實質就是連接池的作用),TransportClient類定義如下:
在這裏插入圖片描述
下面我們分析TransportClientFactory.createClient(remoteHost, remotePort)函數的實現,分析見代碼註釋:
在這裏插入圖片描述
我們繼續分析TransportClientFactory.createClient(address)如下:
在這裏插入圖片描述
從上圖可以看出實現了Netty客戶端初始化,設置Pipline管道中,把SocketChannel傳遞給新建TransportClient實例,並把TransportClient實例返回即完成了TransportClient的初始化工作,這樣向TransportServer發送消息即是在TransportClient實例中向SocketChannel發送消息。從我們上節的分析可以看出發送Rpc消息即是調用TransportClient.sendRpc()函數,代碼如下:
在這裏插入圖片描述
最後再給出一個客戶端請求、響應流程圖:
在這裏插入圖片描述

Rpc請求回調處理流程

如果客戶端使用RpcEndpointRef.send()函數發送消息則是單向請求;如果使用RpcEndpointRef.ask()函數發送消息則是雙向請求,此時需要用回調函數把服務端的結果返回給調用方。還拿心跳交互進行舉例,首先我們跳過中間環節直接看Client和Server的處理過程:
在這裏插入圖片描述
可以看出客戶端傳入Heartbeat消息,返回HeartbeatResponse類型的返回值。我們看下RpcEndpointRef.ask函數的定義,參數解釋見註釋:
在這裏插入圖片描述
在分析源碼之前,我們先看回調函數涉及的類:
在這裏插入圖片描述
其中,Client端是由RpcResponseCallback在負責異步提取結果;Server的回調過程是由RpcCallContext和RpcResponseCallback共同完成。
由於前面章節我們已經逐步分析了整個發送請求的過程,這次我們重點在分析回調過程,因此只分析關鍵環節:
在這裏插入圖片描述
由上圖我們可以看到,在NettyRpcEndpointRef.ask()函數中,首先把任意類型(Any)的消息封裝爲RequestMessage,然後進入NettyRpcEnv.ask()函數,此函數是重點,首先判斷如果是調用本地服務(即client和server在同一臺機器上),則使用scala.concurrent.Promise機制先定義兩個Promise變量promisep,在消息處理端完成對變量p的賦值後在p.future.onComplete回調函數對變量promise賦值,最後對promise值做函數的最終返回,這裏比較簡單不再深入分析;如果是遠程調用,原理相同,對promise變量的處理相同,對消息處理端的返回採用了自定義回調機制,把RequestMessage封裝爲RpcOutboxMessage,在RpcOutboxMessage中主要工作是添加自定義回調函數,在後續調用返回後,在回調函數中完成對promise的賦值,最後對promise值做函數的最終返回。下面我們分析如何實現自定義回調的,我們先看下封裝成的RpcOutboxMessage的類定義:
在這裏插入圖片描述
可以看出,可以關注下onSuccess()onFailure()回調函數,並未做特殊處理,只是繼續封裝便於後續調用,發送消息會調用sendWith()函數(調用到sendWith函數的細節見上面章節分析),我們繼續分析:
在這裏插入圖片描述
TransportClient.sendRpc()函數中生成requestId,同時將(requestId, callback)信息添加到TransportResponseHandler的outstandingRpcs中,後續Client會根據這個requestId處理Server返回的信息,然後調用Netty的channel.writeAndFlush()方法發送給遠程Server,RpcRequest內容爲(requestId + message)。代碼如下:
在這裏插入圖片描述
Server處理RpcRequest,會返回帶requestId的RpcResponse。爲了分析的連貫性,我們先跳過服務端的處理,後面再分析,直接看Client如何處理帶requestId的RpcResponse,從上面章節分析可知,Client在創建TransportClient對象時,也會將TransportChannelHandler註冊到底層的Netty的Pipeline中,因此返回的消息在TransportChannelHandler判斷如果是ResponseMessage,則會把請求轉給TransportResponseHandler進行處理。代碼如下:
在這裏插入圖片描述
handle方法中根據server返回的requestId從集合outstandingRpcs中獲取callback對象,這個callback對象就是前面說的RpcOutboxMessage,調用addRpcRequest(requestId, callback)方法添加進去的,然後調用回調函數的onSuccess函數將結果返回。
在這裏插入圖片描述
這樣在前面說的NettyRpcEnv.ask()函數中的promise就能拿到返回值了,最終把結果返回給調用方。

那接下來我們分析一下,Server端如何處理帶requestId的RpcRequest並返回帶requestId的RpcResponse?
從上面章節我們可以知道Server的Netty服務啓動後會將TransportChannelHandler註冊到底層的Netty的Pipeline中,然後就開始監聽接受消息,當Client發送帶RpcRequest消息後,Server端註冊的TransportChannelHandler判斷如果是RequestMessage,則會把請求轉給TransportRequestHandler進行處理。
在這裏插入圖片描述
如上圖,在TransportRequestHandler.handle()函數中判斷是否如果是RpcRequest請求,則調用TransportRequestHandler.processRpcRequest()函數進行處理,處理過程爲首先創建一個RpcResponseCallback回調函數做爲參數,傳給NettyRpcHandler.receive()做上游調用業務邏輯處理,處理完成會調用RpcResponseCallback.onSuccess()封裝(requestId + message)爲RpcResponse消息向Client進行返回。我們繼續分析上游代碼:
在這裏插入圖片描述
如上圖在Dispatcher.postRemoteMessage()函數中會新新建RemoteNettyRpcCallContext封裝RpcResponseCallback,之後使用RemoteNettyRpcCallContext作爲參數繼續調用,我們跳過中間環節直接看業務邏輯處理代碼如何調用RpcResponseCallback的?同樣以心跳交互爲例代碼如下:
在這裏插入圖片描述
至此,Server端處理RpcRequest並返回帶requestId的RpcRespons的過程也分析完成了。
最後給出一張Request-Response流程簡圖:
在這裏插入圖片描述

參考

Spark Network 模塊分析
Spark RPC之RpcRequest請求處理流程
Spark RPC之Dispatcher、Inbox、Outbox
Spark RPC模塊源碼學習
kraps-rpc
Spark Netty通信傳輸層架構

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