HDFS2.x之RPC流程分析

HDFS2.x之RPC流程分析

1 概述

    Hadoop提供了一個統一的RPC機制來處理client-namenode, namenode-dataname,client-dataname之間的通信。RPC是整個Hadoop中通信框架的核心,目前採用ProtocolBuf作爲RPC的默認實現。RPC的整體調用流程如下:

 

2 Protobuf

    Protocol buffer(以下簡稱PB),PB是Google開源的一種輕便高效的結構化數據存儲格式,可以用於結構化數據的序列化和反序列化,很適合做數據存儲或 RPC 數據交換格式,目前提供了 C++、Java、Python 三種語言的 API。序列化/反序列化速度快,網絡或者磁盤IO傳輸的數據少。

RPC就是一臺機器上的某個進程要調用另外一臺機器上的某個進程的方法,中間通信傳輸的就是類似於“方法名、參數1、參數2……”這樣的信息,是結構化的。

我們要定義一種PB實現的RPC傳輸格式,首先要定義相應的.proto文件,在Hadoop common工程裏,這些文件放在hadoop-common\src\main\proto目錄下;在Hadoop HDFS工程裏這些文件放在hadoop-hdfs\src\main\proto目錄下,以此類推。Hadoop編譯腳本會調用相應的protoc二進制程序來編譯這些以.proto結尾的文件,生成相應的.java文件。

 

由proto文件生成的類,均提供了讀寫二進制數據的方法:

(1)byte[] toByteArray():序列化message並且返回一個原始字節類型的字節數組;

(2)static Person parseFrom(byte[] data): 將給定的字節數組解析爲message;

(3)void writeTo(OutputStream output): 將序列化後的message寫入到輸出流;

(4)static Person parseFrom(InputStream input): 讀入並且將輸入流解析爲一個message;

    另外,PB類中都有一些Builder子類,利用其中的build方法,可以完成對象的創建。PB的具體應用會在下面的RPC的Client和Server的分析中說明。

3 RPC Client端

以create方法爲例,來說明RPC的具體執行流程。首先看下在Client端的執行過程。

 

    由HDFS客戶端發起的create操作,在經過一系列的前置步驟之後,會通過DFSClient類中的namenode代理來完成,其定義如下:

final ClientProtocol namenode;
……

NameNodeProxies.ProxyAndInfo<ClientProtocol> proxyInfo =
NameNodeProxies.createProxy(conf, nameNodeUri, ClientProtocol.class);

this.dtService = proxyInfo.getDelegationTokenService();

this.namenode = proxyInfo.getProxy();


這說明此處的namenode實現的接口是ClientProtocol,也就是Client與NameNode之間RPC通信的協議。

HDFS2.x引入了NameNode的HA,這就使得Client端的底層代理是有多個的,分別連接Active NN和Standby NN。但是在實際運行過程中需要對Client調用呈現統一的接口,那麼就出現了一個上層代理來統一上述這兩個底層代理。所有由Clientfa來的方法調用都是先到達上層代理,通過上層代理轉發到下層代理。並且,上層代理還會根據底層代理返回的Exception來決定是否進行Failover或者Retry等操作。

在使用HA模式時,客戶端創建代理的總體流程是:

 

其中,

(1)RetryProxy.create方法會創建上層代理,用於接收客戶端的請求,並根據情況調用連接到當前兩個NameNode的底層代理。

Proxy.newProxyInstance(
        proxyProvider.getInterface().getClassLoader(),

        new Class<?>[] { iface },

        new RetryInvocationHandler(proxyProvider, retryPolicy)
);


生成的這個代理對象實現了ClientProtocol接口,Client可以通過這個代理對象調用ClientProtocol接口中相應的方法。根據Java的動態代理機制,用戶對這個代理對象的方法調用都轉換爲對RetryInvocationHandler(proxyProvider, retryPolicy)對象中invoke()方法的調用了。RetryInvocationHandler是與FailoverProxyProvider密切相關的,因爲它需要FailoverProxyProvider提供底層代理的支持。

(2)當代理對象接收到請求的時候,會調用invoke方法來進行處理,這裏的invoke方法是上層代理中的RetryInvocationHanlder.invoke方法。

首先要獲取一個RetryPolicy,默認的策略是在構造RetryInvocationHandler時的參數。在Client與NameNode之間的ClientProtocol的RetryPolicy是:

RetryPolicies.failoverOnNetworkException(RetryPolicies.TRY_ONCE_THEN_FAIL, config.maxFailoverAttempts, config.failoverSleepBaseMillis,

              config.failoverSleepMaxMillis)

    接着,會調用invokeMethod方法調用底層的代理進行實際的處理:

Object ret = invokeMethod(method, args);

               -> method.invoke(currentProxy, args);

currentProxy是現在正在使用的底層代理。當NN發生主從切換的時候,這個currentProxy也會發生相應的變化。

    如果在調用過程中出現了異常,則針對不同的異常會做出不同的處理,這裏的判斷是根據生成動態代理(上層代理)的時候給定的RetryPolicy策略,默認的RetryPolicy是FailoverOnNetworkExceptionRetry,所以調用對應的shouldRetry()函數。

(2.1)如果Retry的次數已經超過最大嘗試的次數了,那麼就返回一個

RetryAction.RetryDecision.FAIL的RetryAction。

(2.2) 如果拋出的異常是ConnectionException、NoRouteToHostException、UnKnownHostException、StandbyException、RemoteException中的一個,說明底層代理在RPC過程中Active NN連不上或者宕機或者已經發生主從切換了,那麼就需要返回一個RetryAction.RetryDecision.FAILOVER_AND_RETRY的RetryAction,需要執行performFailover()操作,然後用另外一個NN的底層代理重試。

(2.3)如果拋出的異常是SocketException、 IOException或者其他非RemoteException的異常,那麼就無法判斷這個RPC命令到底是不是執行成功了。可能是本地的Socket或者IO出問題,也可能是NN端的Socket或者IO問題。那就進行進一步的判斷:如果被調用的方法是idempotent的,也就是多次執行是沒有副作用的,那麼就連接另外的一個底層代理重試;否則直接返回RetryAction.RetryDecision.FAIL

(3)FailoverProxyProvider類的當前實現類爲ConfiguredFailoverProxyProvider。它負責管理那兩個activeNN和standbyNN的代理,當上層代理接收到來自用戶的一個RPC命令之後,轉發給當前正在使用的底層代理(由ConfiguredFailoverProxyProvider.currentProxyIndex決定,表示當前的代理對象的序號)執行,然後看是否拋出異常。如果拋出了異常,根據異常的種類來判斷是執行failover,還是retry,或者兩者都不做。如果需要切換NameNode代理的話,則會執行:

currentProxyIndex = (currentProxyIndex + 1) % proxies.size();

    底層代理的實現是用的非HA模式:

current.namenode = NameNodeProxies.createNonHAProxy(conf,

            current.address, xface, ugi, false).getProxy();

進一步調用->NameNodeProxies.createNNProxyWithClientProtocol

            ->RPC.getProtocolProxy

方法,並把生成的ClientNamenodeProtocolPB類型的代理對象proxy封裝成ClientNamenodeProtocolTranslatorPB類型。

    這裏又會涉及到Java的動態代理,是在RPC.getProtocolProxy方法生成proxy對象的時候,RPC.getProtocolProxy的實現代碼爲:

return getProtocolEngine(protocol,conf).getProxy(protocol, clientVersion, addr, ticket, conf, factory, rpcTimeout, connectionRetryPolicy);

這裏的引擎就是protocolbuf,所以,所有的RPC請求最終都會調用ProtobufRpcEngine類中的invoke方法進行和RPC的Server端通信以及數據的序列化和反序列化操作。

    把Client的請求封裝成call的操作返回也是在invoke中進行的:

val = (RpcResponseWritable) client.call(RPC.RpcKind.RPC_PROTOCOL_BUFFER,

            new RpcRequestWritable(rpcRequest), remoteId);

    封裝的具體實現是調用的Client類中的call方法:

//封裝成call

Call call = new Call(rpcKind, rpcRequest);

//建立和NameNode的連接

Connection connection = getConnection(remoteId, call);

//向NameNode發送數據

connection.sendParam(call);

RPC客戶端的執行流程(HA模式)爲:

 

4 RPC Server端

RPC的Server端的初始化方法是NameNode中被調用的:

rpcServer = createRpcServer(conf);

實際上初始化NameNodeRpcServer對象,調用其構造函數:

return new NameNodeRpcServer(conf, this);

    在構造方法中,會初始化兩個RPCServer,一個是serviceRpcServer,用來處理數據節點發來的RPC請求,另一個是clientRpcServer,用於處理客戶端發來的RPC請求。

    NameNodeRpcServer的構造方法會初始化RPC的Server端所需要的handler的數目(默認爲10個),設置好處理引擎爲Protocolbuf,初始化ClientNamenodeProtocolServerSideTranslatorPB類型的對象clientProtocolServerTranslator用來對傳來的數據進行反序列化,對發送的數據進行序列化。

    另外,會初始化提供不同RPC服務的對象BlockingService,針對客戶端、數據節點端的有:

BlockingService clientNNPbService = ClientNamenodeProtocol.newReflectiveBlockingService(clientProtocolServerTranslator);

BlockingService dnProtoPbService = DatanodeProtocolService.newReflectiveBlockingService(dnProtoPbTranslator);

    緊接着,會獲取RPC的Server對象:

this.clientRpcServer = RPC.getServer(org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolPB.class,
                                     clientNNPbService, socAddr.getHostName(),
                                     socAddr.getPort(), handlerCount, 
                                     false, 
                                     conf,                     
                             namesystem.getDelegationTokenSecretManager());


此對象主要負責接收網絡連接,讀取數據,調用處理數據函數,返回結果。前兩個參數表示如果RPC發送過來的是ClientNamenodeProtocolPB協議,那麼負責處理這個協議的服務(com.google.protobuf.BlockingService類型的對象)就是clientNNPbService。

這個Server對象裏有Listener, Handler, Responder內部類:

(1) Listener Thread:Server端會啓一個Listener線程主要用於監聽Client發送過來的Request,Listene會啓動一個Reader的線程組,並把客戶端發來的Connection對象通過NIO的SelectionKey傳遞給Reader, Listener相當於只作了一層轉發;

(2) Reader Thread Pool:主要用於讀取Listener傳過來的Connection,並調用Connection的readAndProcess方法來讀取Request,並封裝成一個Call放到Call Queue中;

(3) Hanlder Thread Pool:Server會啓動一組線程組來處理Call Queue中Call,並把處理的Respone中放到response queue中;

(4) Responder Thread:主要處理response queue中的response,並把response發送給client,如果當前response queue爲空,則第一個新增的response會馬上發送給client端,不會通過responer thread來發送。

這個RPC.getServer()會經過層層調用,因爲現在默認的RPCEngine是ProtobufRpcEngine(ProtobufRpcEngine.java),就會調用到ProtobufRpcEngine.getServer這個函數,在這生成了一個Server對象,就是用於接收client端RPC請求,處理,回覆的Server。這個Server對象是一個純粹的網絡服務的Server,在RPC中起到基礎網絡IO服務的作用。

RPC的Server端創建的總體流程是:

 

4.1 Reader處理

Server裏的Reader線程也是基於Selector的異步IO模式,每次Select選出一個SelectionKey之後,會調用SelectionKey.attachment()把這個SelectionKey所attach的Connection對象獲取(在Listener的run方法中進行的attatch),然後執行對應的readAndProcess()方法,把這個SelectionKey所對應的管道上的網絡IO數據讀入緩衝區。readAndProcess()方法會層層調用到Server.processData()方法,在這個方法內部,會把剛纔從網絡IO中讀取的數據反序列化成對象rpcRequest對象。

rpcRequest對象的類型是繼承自Writable類型的子類的對象,也就是說可以序列化/反序列化的類。這裏rpcRequest對象裏包含的RPC請求的內容對象是由.proto文件中Message生成的類,也就是說PB框架自動編譯出來的類,後面可以通過調用這個類的get方法獲取RPC中真正傳輸的數據。之後把生成的rpcRequest對象放到一個Call對象裏面,再把Call對象放到隊列Server.callQueue裏面。

Reader的處理流程圖如下:

 

4.2 Handler處理

Handler線程默認有10個,所以處理邏輯是多線程的。每個Handler線程會從剛纔提到的callQueue中取一個Call對象,然後調用Server.call()方法執行這個Call對象中蘊含的RPC請求。Server.call()->RPC.Server.call()->Server.getRpcInvoker()->ProtobufRpcInvoker.call()在最後這個call()函數裏面真正執行。

call方法會首先校驗這個請求發過來的數據是不是合理的。然後就是獲取實現這個協議的服務。實現協議的服務在初始化的時候已經註冊過了,就是前面說的那個com.google.protobuf.BlockingService類型的對象clientNNPbService

這個就是實現Client和NameNode之間的ClientNamenodeProtocol協議的服務,通過調用這句代碼:

result = service.callBlockingMethod(methodDescriptor, null, param);

就會執行這個RPC請求的邏輯。service對象會把相應的方法調用轉移到一個繼承自BlockingInterface接口的實現類上。Service的真正實現類就是clientProtocolServerTranslator,是newReflectiveBlockingService()這個函數的參數。並且此類是ClientNamenodeProtocolProtos中的子類,是在HDFS編譯的時候根據proto文件創建的。由於clientProtocolServerTranslator的構造方法中傳遞的參數是NameNodeRpcServer,因此進一步的方法調用都在NameNodeRpcServer中實現的。

    Handler處理流程如下:

 

如果元數據操作邏輯NameNodeRpcServer裏面拋出IOException,那麼它都會把它封裝成ServiceException,然後一路傳遞給client端。在client端,會通過ProtobufHelper.getRemoteException()把封裝在ServiceException中的IOException獲取出來。

    RPC的Server端總體處理流程如下:

 

 

 

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