理解Hadoop通信 RPC

遠程過程調用(Remote Procedure Call, RPC) 是一種常用的分佈式網絡通信協議, 它允許運行於一臺計算機的程序調用另一臺計算機的子程序, 同時將網絡的通信細節隱藏起來, 使得用戶無須額外地爲這個交互作用編程。

RPC 是一種通過網絡從遠程計算機上請求服務, 但不需要了解底層網絡技術的協議。RPC 協議假定某些傳輸協議(如 TCP 或 UDP 等) 已經存在, 並通過這些傳輸協議爲通信程序之間傳遞訪問請求或者應答信息。 在 OSI 網絡通信模型中, RPC 跨越了傳輸層和應用層。 RPC 使得開發分佈式應用程序更加容易 。RPC 通常採用客戶機 / 服務器模型。 請求程序是一個客戶機, 而服務提供程序則是一個服務器。 一個典型的 RPC 框架如圖所示:

這裏寫圖片描述

主要包括以下幾個部分:

  • 通信模塊。 兩個相互協作的通信模塊實現”請求 - 應答”協議, 它們在客戶和服務器之間傳遞請求和應答消息, 一般不會對數據包進行任何處理。“請求 – 應答”協議的實現 方式有同步方式和異步方式兩種。

如圖上圖所示, 同步模式下客戶端程序一直阻塞到服務器端發送的應答請求到達本地 ;而異步模式不同, 客戶端將請求發送到服務器端後, 不必等待應答返回, 可以做其他事情,待服務器端處理完請求後, 主動通知客戶端。 在高併發應用場景中, 一般採用異步模式以降低訪問延遲和提高帶寬利用率。

  • Stub 程序。 客戶端和服務器端均包含 Stub 程序, 可將之看做代理程序。 它使得遠程函數調用表現得跟本地調用一樣,對用戶程序完全透明。 在客戶端, 它表現得就 像一個本地程序, 但不直接執行本地調用, 而是將請求信息通過網絡模塊發送給服務器端。此外, 當服務器發送應答後, 它會解碼對應結果。 在服務器端, Stub 程序 依次進行解碼請求消息中的參數、調用相應的服務過程和編碼應答結果的返回值等 處理。
  • 調度程序。 調度程序接收來自通信模塊的請求消息, 並根據其中的標識選擇一個 Stub 程序進行處理。 通常客戶端併發請求量比較大時,會採用線程池提高處理效率。
  • 客戶程序 / 服務過程。 請求的發出者和請求的處理者。 如果是單機環境, 客戶程序 可直接通過函數調用訪問服務過程, 但在分佈式環境下,需要考慮網絡通信, 這不 得增加通信模塊和 Stub 程序(保證函數調用的透明性)。

通常而言, 一個 RPC 請求從發送到獲取處理結果, 所經歷的步驟如下圖下所示:

這裏寫圖片描述

1) 客戶程序以本地方式調用系統產生的 Stub 程序;
2) 該 Stub 程序將函數調用信息按照網絡通信模塊的要求封裝成消息包, 並交給通信模塊發送到遠程服務器端。
3) 遠程服務器端接收此消息後, 將此消息發送給相應的 Stub 程序;
4) Stub 程序拆封消息, 形成被調過程要求的形式, 並調用對應函數;
5) 被調用函數按照所獲參數執行, 並將結果返回給 Stub 程序;
6) Stub 程序將此結果封裝成消息, 通過網絡通信模塊逐級地傳送給客戶程序。

Hadoop RPC 的特點概述

RPC 實際上是分佈式計算中 C/S(Client/Server) 模型的一個應用實例, 對於 HadoopRPC 而言, 它具有以下幾個特點。

  • 透明性。 這是所有 RPC 框架最根本的特點, 即當用戶在一臺計算機的程序調用另外 一臺計算機上的子程序時,用戶自身不應感覺到其間涉及跨機器間的通信, 而是感 覺像是在執行一個本地調用。
  • 高性能。 Hadoop 各個系統(如 HDFS、 YARN、 MapReduce 等) 均採用了 Master/ Slave 結構,其中, Master 實際上是一個 RPC Server, 它負責處理集羣中所有 Slave 發送的服務請求, 爲了保證Master的併發處理能力, RPC Server 應是一個高性能服 務器, 能夠高效地處理來自多個 Client 的併發 RPC 請求。
  • 可控性。 JDK 中已經自帶了一個 RPC 框架—RMI(Remote Method Invocation, 遠程方法調用),之所以不直接使用該框架, 主要是考慮到 RPC 是 Hadoop 最底層最 核心的模塊之一, 保證其輕量級、 高性能和可控性顯得尤爲重要,而 RMI 重量級過大且用戶可控之處太少(如網絡連接、 超時和緩衝等均難以定製或者修改) 。

HadoopRPC 總體架構

同其他 RPC 框架一樣, Hadoop RPC 主要分爲四個部分, 分別是序列化層、 函數調用層、 網絡傳輸層和服務器端處理框架, 具體實現機制如下:

  • 序列化層。 序列化主要作用是將結構化對象轉爲字節流以便於通過網絡進行傳輸或寫入持久存儲, 在 RPC 框架中,它主要用於將用戶請求中的參數或者應答轉化成字節流以便跨機器傳輸。 Protocol Buffers 和 Apache Avro均可用在序列化層, Hadoop 本身也提供了一套序列化框架, 一個類只要實現 Writable 接口即可支持對象序列化與反序列化。
  • 函數調用層。 函數調用層主要功能是定位要調用的函數並執行該函數, Hadoop RPC 採用了 Java反射機制與動態代理實現了函數調用。
  • 網絡傳輸層。 網絡傳輸層描述了 Client 與 Server 之間消息傳輸的方式, Hadoop RPC 採用了基於TCP/IP的 Socket 機制。
  • 服務器端處理框架。 服務器端處理框架可被抽象爲網絡 I/O 模型, 它描述了客戶端 與服務器端間信息交互方式,它的設計直接決定着服務器端的併發處理能力, 常見 的網絡 I/O 模型有阻塞式 I/O、 非阻塞式 I/O、 事件驅動 I/O 等, 而Hadoop RPC 採 用了基於 Reactor 設計模式的事件驅動 I/O 模型。

Haddoop RPC 總體架構如圖所示:

這裏寫圖片描述

自下而上可分爲兩層, 第一層是一個基於 JavaNIO(New I/O) 實現的客戶機 – 服務器(C/S) 通信模型。 其中, 客戶端將用戶的調用方法及其參數封裝成請求包後發送到服務器端。服務器端收到請求包後, 經解包、 調用函數、打包結果等一系列操作後, 將結果返回給客戶端。 爲了增強 Sever 端的擴展性和併發處理能力, Hadoop RPC 採用了基於事件驅動的 Reactor 設計模式, 在具體實現時, 用到了 JDK提供的各種功能包, 主要包括 java.nio(NIO)、 java.lang.reflect(反射機制和動態代理)、java.net(網絡編程庫) 等。 第二層是供更上層程序直接調用的 RPC 接口, 這些接口底層即爲 C/S 通信模型。

Hadoop RPC 使用方法
Hadoop RPC 對外主要提供了兩種接口分別是:

  • public static ProtocolProxy getProxy/waitForProxy(…) :構造一個客戶端代理對象(該對象實現了某個協議), 用於向服務器發送 RPC 請求。

這裏寫圖片描述

  • public static Server RPC.Builder (Configuration).build() : 爲某個協議(實際上是 Java 接口) 實例構造一個服務器對象, 用於處理客戶端發送的請求。

這裏寫圖片描述

通常而言, 使用 Hadoop RPC 可分爲以下 4 個步驟。
1. 定義 RPC 協議
RPC 協議是客戶端和服務器端之間的通信接口, 它定義了服務器端對外提供的服務接口。 如下所示, 我們定義一個 ClientProtocol 通信接口, 聲明瞭 echo() 和 add() 兩個方法。需要注意的是, Hadoop 中所有自定義 RPC 接口都需要繼承 VersionedProtocol 接口, 它描述了協議的版本信息。

interface ClientProtocol extends org.apache.hadoop.ipc.VersionedProtocol {
// 版本號,默認情況下,不同版本號的 RPC Client 和 Server 之間不能相互通信
public static final long versionID = 1L;
String echo(String value) throws IOException;
int add(int v1, int v2) throws IOException;
}

2. 實現 RPC 協議
Hadoop RPC 協議通常是一個 Java 接口, 用戶需要實現該接口。 對 ClientProtocol 接口進行簡單的實現如下所示:

public static class ClientProtocolImpl implements ClientProtocol {
// 重載的方法,用於獲取自定義的協議版本號,
public long getProtocolVersion(String protocol, long clientVersion) {
return ClientProtocol.versionID;
}
// 重載的方法,用於獲取協議簽名
public ProtocolSignature getProtocolSignature(String protocol, long clientVersion,
inthashcode) {
return new ProtocolSignature(ClientProtocol.versionID, null);
}
public String echo(String value) throws IOException {
return value;
}
public int add(int v1, int v2) throws IOException {
return v1 + v2;
}
}

3. 構造並啓動 RPC Server

直接使用靜態類 Builder 構造一個 RPC Server, 並調用函數 start() 啓動該 Server:

Server server = new RPC.Builder(conf).setProtocol(ClientProtocol.class)
.setInstance(new ClientProtocolImpl()).setBindAddress(ADDRESS).setPort(0)
.setNumHandlers(5).build();
server.start();

其中, BindAddress(由函數 setBindAddress 設置) 和 Port(由函數 setPort 設置, 0 表示由系統隨機選擇一個端口號) 分別表示服務器的 host 和監聽端口號, 而 NnumHandlers(由函數 setNumHandlers 設置) 表示服務器端處理請求的線程數目。 到此爲止, 服務器處理監聽狀態, 等待客戶端請求到達。

4. 構造 RPC Client 併發送 RPC 請求

使用靜態方法 getProxy 構造客戶端代理對象, 直接通過代理對象調用遠程端的方法,具體如下所示:

proxy = (ClientProtocol)RPC.getProxy(
ClientProtocol.class, ClientProtocol.versionID, addr, conf);
int result = proxy.add(5, 6);
String echoResult = proxy.echo("result");

經過以上四步, 我們便利用 Hadoop RPC 搭建了一個非常高效的客戶機 – 服務器網絡模型。

Hadoop RPC 類詳解

Hadoop RPC 主要由三個大類組成, 即 RPC、 Client 和 Server, 分別對應對外編程接口、客戶端實現和服務器實現。

1. ipc.RPC 類分析

RPC 類實際上是對底層客戶機 – 服務器網絡模型的封裝, 以便爲程序員提供一套更方便簡潔的編程接口。如圖 所示,

這裏寫圖片描述

RPC 類定義了一系列構建和銷燬 RPC 客戶端的方法, 構建方法分爲getProxy 和 waitForProxy 兩類, 銷燬方只有一個, 即爲 stopProxy。 RPC 服務器的構建則由靜態內部類 RPC.Builder, 該類提供了一些列 setXxx 方法(Xxx 爲某個參數名稱) 供用戶設置一些基本的參數, 比如 RPC 協議、 RPC 協議實現對象、 服務器綁定地址、 端口號等,一旦設置完成這些參數後, 可通過調用 RPC.Builder.build() 完成一個服務器對象的構建, 之後直接調用 Server.start() 方法便可以啓動該服務器。

2. ipc.Client
Client 主要完成的功能是發送遠程過程調用信息並接收執行結果。 它涉及到的類關係如圖所示:

這裏寫圖片描述

Client 類對外提供了一類執行遠程調用的接口, 這些接口的名稱一樣, 僅僅是參數列表不同, 比如其中一個的聲明如下所示:

  public Writable call(RPC.RpcKind rpcKind, 
                       Writable rpcRequest,
                       ConnectionId remoteId, 
                       AtomicBoolean fallbackToSimpleAuth)
      throws IOException {
    return call(rpcKind, rpcRequest, remoteId, RPC.RPC_SERVICE_CLASS_DEFAULT,
      fallbackToSimpleAuth);
  }

Client 內部有兩個重要的內部類, 分別是 Call 和 Connection。

  • Call 類 :

    這裏寫圖片描述

    封裝了一個 RPC 請求, 它包含 5 個成員變量, 分別是唯一標識 id、 函數調用信息 param、 函數執行返回值value、 出錯或者異常信息 error 和執行完成標識符done。 由於 Hadoop RPC Server採用異步方式處理客戶端請求, 這使遠程過程調用的發生順序與結果返回順序無直接關係, 而 Client 端正是通過 id識別不同的函數調用的。 當客戶端向服務器端發送請求時, 只需填充 id 和 param 兩個變量, 而剩下的3 個變量(value、error 和 done) 則由服務器端根據函數執行情況填充。

  • Connection 類 :

    這裏寫圖片描述

    Client 與每個 Server 之間維護一個通信連接, 與該連接相關的基本信息及操作被封裝到Connection 類中, 基本信息主要包括通信連接唯一標識(remoteId)、 與 Server 端通信的 Socket(socket)、 網絡輸入數據流(in)、 網絡輸出數據流(out)、 保存 RPC 請求的哈希表(calls) 等。 操作則包括:

  • addCall—將一個 Call 對象添加到哈希表中;

  • sendParam—向服務器端發送 RPC 請求;

  • receiveResponse —從服務器端接收已經處理完成的 RPC 請求;

  • run—Connection 是一個線程類, 它的 run 方法調用了 receiveResponse 方法, 會一直等待接收 RPC返回結果。

當調用 call 函數執行某個遠程方法時, Client 端需要進行(如圖所示) 以下 4 個
步驟。

這裏寫圖片描述

1) 創建一個 Connection 對象, 並將遠程方法調用信息封裝成 Call 對象, 放到 Connection對象中的哈希表中;
2) 調用 Connection 類中的 sendRpcRequest() 方法將當前 Call 對象發送給 Server 端;
3) Server 端 處 理 完 RPC 請 求 後, 將 結 果 通 過 網 絡 返 回 給 Client 端, Client 端 通 過receiveRpcResponse() 函數獲取結果;
4) Client 檢查結果處理狀態(成功還是失敗), 並將對應 Call 對象從哈希表中刪除。

3. ipc.Server 類分析

採用了 Master/Slave 結構, 其中 Master 是整個系統的單點, 如 NameNode 或JobTracker , 這是制約系統性能和可擴展性的最關鍵因素之一 ; 而 Master 通過 ipc.Server接收並處理所有 Slave 發送的請求, 這就要求ipc.Server 將高併發和可擴展性作爲設計目標。 爲此, ipc.Server 採用了很多提高併發處理能力的技術, 主要包括線程池、 事件驅動和Reactor 設計模式等, 這些技術均採用了 JDK 自帶的庫實現, 這裏重點分析它是如何利用Reactor 設計模式提高整體性能的。
Reactor 是併發編程中的一種基於事件驅動的設計模式, 它具有以下兩個特點 : 通過派發 / 分離 I/O 操作事件提高系統的併發性能 ; 提供了粗粒度的併發控制, 使用單線程實現,避免了複雜的同步處理。 典型的 Reactor 實現原理如圖 所示:

這裏寫圖片描述

典型的 Reactor 模式中主要包括以下幾個角色。

  • Reactor: I/O 事件的派發者。
  • Acceptor : 接受來自 Client 的連接, 建立與 Client 對應的 Handler, 並向 Reactor 注 冊此
    Handler。
  • Handler : 與一個 Client 通信的實體, 並按一定的過程實現業務的處理。 Handler 內部往往會有更進一步的層次劃分, 用來抽象諸如 read、 decode、 compute、 encode 和 send 等過程。 在Reactor 模式中, 業務邏輯被分散的 I/O 事件所打破, 所以 Handler 需要有適當的機制在所需的信息還不全(讀到一半)的時候保存上下文, 並在下一 次 I/O 事件到來的時候(另一半可讀) 能繼續上次中斷的處理。
  • Reader/Sender : 爲了加速處理速度, Reactor 模式往往構建一個存放數據處理線程的線程池, 這樣數據讀出後,立即扔到線程池中等待後續處理即可。 爲此, Reactor 模式一般分離 Handler 中的讀和寫兩個過程,分別註冊成單獨的讀事件和寫事件, 並由對應的 Reader 和 Sender 線程處理。

ipc.Server 實際上實現了一個典型的 Reactor 設計模式, 其整體架構與上述完全一致。

這裏寫圖片描述

一旦讀者瞭解典型 Reactor 架構便可很容易地學習 ipc.Server 的設計思路及實現。 接下來,我們分析 ipc.Server 的實現細節。前面提到,ipc.Server 的主要功能是接收來自客戶端的 RPC 請求, 經過調用相應的函數獲取結果後, 返回給對應的客戶端。爲此, ipc.Server 被劃分成 3 個階段 : 接收請求、 處理請求和返回結果, 如圖所示。 各階段實現細節如下。

這裏寫圖片描述

(1) 接收請求

該階段主要任務是接收來自各個客戶端的 RPC 請求, 並將它們封裝成固定的格式(Call 類) 放到一個共享隊列(callQueue) 中, 以便進行後續處理。 該階段內部又分爲建立連接和接收請求兩個子階段, 分別由 Listener 和Reader 兩種線程完成。整個 Server 只有一個 Listener 線程, 統一負責監聽來自客戶端的連接請求, 一旦有新的請求到達, 它會採用輪詢的方式從線程池中選擇一個 Reader 線程進行處理, 而 Reader 線程可同時存在多個, 它們分別負責接收一部分客戶端連接的 RPC 請求, 至於每個 Reader 線程負責哪些客戶端連接, 完全由 Listener 決定, 當前 Listener 只是採用了簡單的輪詢分配機制。Listener 和 Reader 線程內部各自包含一個 Selector 對象, 分別用於監聽 SelectionKey.OP_ACCEPT 和 SelectionKey.OP_READ 事件。 對於 Listener 線程, 主循環的實現體是監聽是否有新的連接請求到達, 並採用輪詢策略選擇一個 Reader 線程處理新連接 ; 對於 Reader線程, 主循環的實現體是監聽(它負責的那部分) 客戶端連接中是否有新的 RPC 請求到達, 並將新的 RPC 請求封裝成 Call 對象, 放到共享隊列 callQueue 中。

(2) 處理請求

該階段主要任務是從共享隊列 callQueue 中獲取 Call 對象, 執行對應的函數調用, 並將結果返回給客戶端, 這全部由 Handler 線程完成。Server 端可同時存在多個 Handler 線程, 它們並行從共享隊列中讀取 Call 對象, 經執行對應的函數調用後, 將嘗試着直接將結果返回給對應的客戶端。 但考慮到某些函數調用返回結果很大或者網絡速度過慢, 可能難以將結果一次性發送到客戶端, 此時 Handler 將嘗試着將後續發送任務交給 Responder 線程。

(3) 返回結果

前面提到, 每個 Handler 線程執行完函數調用後, 會嘗試着將執行結果返回給客戶端,但對於特殊情況, 比如函數調用返回結果過大或者網絡異常情況(網速過慢), 會將發送任務交給 Responder 線程。Server 端僅存在一個 Responder 線程, 它的內部包含一個 Selector 對象, 用於監聽SelectionKey.OP_WRITE 事件。 當 Handler 沒能將結果一次性發送到客戶端時, 會向該Selector 對象註冊 SelectionKey.OP_WRITE 事件, 進而由 Responder 線程採用異步方式繼續發送未發送完成的結果。

Hadoop RPC 參數調優

Hadoop RPC 對外提供了一些可配置參數, 以便於用戶根據業務需求和硬件環境對其進行調優。 主要的配置參數如下。

  • Reader 線程數目。 由參數 ipc.server.read.threadpool.size 配置, 默認是 1, 也就是說,默認情況下, 一個 RPC Server 只包含一個 Reader 線程。

  • 每個 Handler 線程對應的最大 Call 數目。 由參數 ipc.server.handler.queue.size 指定,默認是 100, 也就是說, 默認情況下, 每個 Handler 線程對應的 Call 隊列長度爲 100。 比如, 如果 Handler數目爲 10, 則整個 Call 隊列(即共享隊列 callQueue) 最 大長度爲: 100×10=1000。

  • Handler 線程數目。 在 Hadoop 中, ResourceManager 和 NameNode 分別是 YARN 和 HDFS兩個子系統中的 RPC Server, 其對應的 Handler 數目分別由參數 yarn.resourcemanager.resource-tracker.client.thread-count 和dfs.namenode.service.handler.count 指定, 默認值分別 爲 50 和 10, 當集羣規模較大時,這兩個參數值會大大影響系統性能。

  • 客戶端最大重試次數。在分佈式環境下, 因網絡故障或者其他原因迫使客戶端重試 連接是很常見的,但嘗試次數過多可能不利於對實時性要求較高的應用。客戶端最大重試次數由參數ipc.client.connect.max.retries指定, 默認值爲 10,也就是會連續 嘗試 10 次(每兩次之間相隔 1 秒)。

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