一.簡介
一種通過網絡從遠程計算機程序上請求服務,而不需要了解底層網絡技術的協議。RPC協議假定某些傳輸協議的存在,如TCP或UDP,爲通信程序之間攜帶信息數據。在OSI網絡通信模型中,RPC跨越了傳輸層和應用層。RPC使得開發包括網絡分佈式程序在內的應用程序更加輕易。 (Hadoop 2.6版本)
二.RPC通信模型
RPC通常採用客戶機/服務器模型。
RPC處理過程
- 客戶程序以本地方式調用系統產生的Stub程序
- 該Stub程序將函數調用信息按照網絡通信模塊要求封裝成消息包,並交給通信模塊發送到遠程服務端。
- 遠程服務端接收到此消息後,將此消息發送給相應的Stub程序。
- Stub程序拆封消息,形成被調用過程要求的形式,並調用對應函數。
- 被調用函數按照所獲參數執行並將結果返回給Stub程序。
- Stub程序將此結果封裝成消息,通過網絡通信模塊逐級地傳送給客戶端程序。
三.Hadoop RPC特點
RPC實際上是一個分佈式計算中C/S(Client/Server模型)的一個應用實例。
透明性
高性能
可控性
四.RPC總體架構
Hadoop RPC主要分爲四部分,分爲序列化層、函數調用層、網絡傳輸層、服務端處理框架。
- 序列化層。序列化主要作用就是將結構化對象轉換爲字節流便於通過傳輸或寫入持久存儲,在RPC框架中,它主要作用於將用戶請求中參數或者應答轉換爲字節流以便跨機傳輸。
- 函數調用,函數調用層主要功能是定位要調用函數並執行該函數,HadoopRPC採用了基於Java反射機制與動態代理實現了函數調用。
- 網絡傳輸,網絡傳輸描述了Client與Server之間消息傳輸的方式,HadoopRPC採用基於TCP/IP的Socket機制。
- 服務器端處理,服務器端處理框架可抽象爲網絡I/O模型,它描述了客戶端與服務端間信息交互方式,它的涉及直接決定這服務端的併發能力,常見的網絡I/O模型有阻塞式I/O、非阻塞I/O、事件驅動I/O等,HadoopRPC採用了基於Reactor設計模式的事件驅動I/O模型。
Hadoop RPC總體架構自下而上可分爲兩層
- 第一層是一個基於JavaNIO實現的客戶機-服務器(C/S)通信模型。其中,客戶端將用戶的調用方法及其參數封裝成請求包發送服務端。服務器端收到請求包後,經解包、調用參數、打包結果等一系列操作後,將結果返回給客戶端。爲了增強Server端的擴展性和併發能力,Hadoop RPC採用了基於事件驅動的Reactor設計模式,在具體實現時,用到了JDK提供的各種功能包,主要包括java.nio、java.lang.reflect(反射機制和動態代理)、java.net(網絡編程)等。
- 第二層是供更上層程序直接調用的RPC接口,這些接口底層即爲C/S通信模型。
五.Hadoop RPC使用方法
Hadoop RPC對外主要提供兩種接口(org.apache.hadoop.ipc.RPC),分別是:
//構造一個客戶端代理對象(實現某個協議),用於向服務器發送RPC請求
public static <T> T getProxy(Class<T> protocol,
long clientVersion,
InetSocketAddress addr, Configuration conf,
SocketFactory factory) throws IOException {
return getProtocolProxy(
protocol, clientVersion, addr, conf, factory).getProxy();
}
org.apache.hadoop.ipc.RPC.Builder 靜態類,構造RPC Server
5.1 定義RPC協議
RPC協議是客戶端和服務端之間通信接口,它定義了服務器端對外提供的服務器接口。
public interface TestClientProtocol extends VersionedProtocol {
//版本號,默認情況下,不同版本號Client 和Server之間不能相互通信
public static final long versionID = 1L;
String echo(String value) throws IOException;
int add(int v1,int v2) throws IOException;
}
5.2 實現協議
Hadoop RPC協議通常是一個Java接口,用戶實現該接口。
public class TestClientProtocolImpl implements TestClientProtocol {
@Override
public String echo(String value) throws IOException {
return value;
}
@Override
public int add(int v1, int v2) throws IOException {
return v1+v2;
}
@Override
public long getProtocolVersion(String protocol, long clientVersion) throws IOException {
return TestClientProtocol.versionID;
}
@Override
public ProtocolSignature getProtocolSignature(String protocol, long clientVersion, int clientMethodsHash) throws IOException {
return new ProtocolSignature(TestClientProtocol.versionID,null);
}
}
5.3 構造並啓動RPC Server
直接使用靜態類Builder構造一個RPC Server,並調用函數start()啓動該Server。
RPC.Server server = new RPC.Builder(conf).setProtocol(TestClientProtocol.class)
.setInstance(new TestClientProtocolImpl()).setBindAddress(ADDRESS).setPort(99999)
.setNumHandlers(5).build();
server.start();
5.4 構造RPC Client併發送RPC請求
使用靜態方法getProxy構造客戶端代理對象。
TestClientProtocol proxy = RPC.getProxy(TestClientProtocol.class, TestClientProtocol.versionID, new InetSocketAddress(ADDRESS, 99999), conf);
int result = proxy.add(5, 6);
String test = proxy.echo("test");
System.out.println(result);
System.out.println(test);
六.Hadoop RPC類
Hadoop RPC主要由三大類組成,即RPC、Client、Server,分別對應對外編程接口、客戶端實現和服務器實現。
6.1 ipc.RPC
RPC類實際上是對底層客戶機 - 服務器網絡模型的封裝,以便爲程序員提供一套更方便簡潔的編程接口。
RPC類定義了一系列構建和銷燬RPC客戶端的方法,構建方法分爲getProxy和waitForProxy兩類,銷燬方只有一個,即爲stopProxy。RPC服務器的構建則由靜態內部類RPC.Builder,該類提供了一些方法共用戶設置一些基本的參數,設置完成參數,可調用build()完成一個服務器對象的構建,調用start()方法啓動該服務器。
6.2 ipc.Client
Client主要完成的功能是發送遠程過程調用信息並接收執行結果。
Client內部有兩個重要的內部類,分別Call和Connection。
Call類
封裝一個RPC請求,它包含5個成員變量。它包含5個成員變量,分別是唯一標識ID、函數調用信息param、函數執行返回值value、出錯或者異常信息error和執行完成標識符done。
private Call(RPC.RpcKind rpcKind, Writable param) {
this.rpcKind = rpcKind;
this.rpcRequest = param;
//callId ThreadLocal
final Integer id = callId.get();
if (id == null) {
this.id = nextCallId();
} else {
callId.set(null);
this.id = id;
}
//retryCount ThreadLocal
final Integer rc = retryCount.get();
if (rc == null) {
this.retry = 0;
} else {
this.retry = rc;
}
}
由於Hadoop RPC Server採用異步方式處理客戶端請求,這使遠程過程調用的發生順序與結果返回順序無直接關係,而Client端正式提供ID識別不同的函數調用的。當客戶端向服務器端發送請求時,只需填充id和param兩個變量,而剩下的三個變量則由服務器根據函數執行情況填充。
Connection類
Client與每個Server之間維護一個通信連接,與該連接相關的基本信息及操作被封裝到Connection類中,基本信息主要包括通信連接唯一標識、與Server端通信的Socket、網絡輸入數據流(in)、網絡輸出數據流(out)、保存RPC請求的哈希表(calls)等。
addCall——將一個Call對象添加到哈希表。
private synchronized boolean addCall(Call call) {
if (shouldCloseConnection.get())
return false;
calls.put(call.id, call);
notify();
return true;
}
sendParam——向服務端發送RPC請求。
public void sendRpcRequest(final Call call)
//sendParamsExecutor 線程池發送請求
receiveResponse——從服務器端接收處理已經處理完成的RPC請求。
run——Connection是一個線程類,它會run方法調用receiveResponse方法,會一直等待接收RPC返回結果。
@Override
public void run() {
if (LOG.isDebugEnabled())
LOG.debug(getName() + ": starting, having connections "
+ connections.size());
try {
while (waitForWork()) {//wait here for work - read or close connection
receiveRpcResponse();
}
} catch (Throwable t) {
// This truly is unexpected, since we catch IOException in receiveResponse
// -- this is only to be really sure that we don't leave a client hanging
// forever.
LOG.warn("Unexpected error reading responses on connection " + this, t);
markClosed(new IOException("Error reading responses", t));
}
close();
if (LOG.isDebugEnabled())
LOG.debug(getName() + ": stopped, remaining connections "
+ connections.size());
}
總體來說Client端實現比較簡單,用hashTable的結構來維護connectionId -> connections以及callId -> calls 對應關係,使得請求響應不需要有嚴格的順序性。
HadoopRPC Client處理流程
/**
* Same as {@link #call(RPC.RpcKind, Writable, ConnectionId)}
* 1.首先創建一個Call對象,封裝RPC請求,成員變量有唯一標識id、請求數據、返回數據、是否完成等
* 2、創建Connection對象(它是個線程),並與服務器連接,即Client與Server之間的一個通信連接,保存未完成的Call對象至哈希表,唯一標識ID,Server通信的Socket,網絡輸入輸出流。
* 3.調用connection.sendRpcRequest(call);將Call對象發送給Server
* 4.等待Server端處理Call請求,服務端處理完成,通過網絡返回服務端處理完成後,通過網絡返回給Client端。這部分代碼不在call方法裏,還記得1中Connection是個線程嗎?去run方法看看 線程一直循環,直到Server返回結果,然後調用receiveRpcResponse方法返回數據。
* 5.再次回到call方法,它也有個循環,一直在等待結果返回。結果返回後,檢查下成功失敗後,就將Call從哈希表中移除了。
* for RPC_BUILTIN
*/
public Writable call(Writable param, InetSocketAddress address)
throws IOException {
return call(RPC.RpcKind.RPC_BUILTIN, param, address);
}
6.3 ipc.Server
Hadoop採用了Master/Slave結構,其中Master是整個系統的單點,這是制約系統性能和可擴展性的最關鍵因素之一。
ipc.Server採用了很多提高併發處理能力的技術,主要包括線程池、事件驅動和Reactor設計模式等。
Reactor是併發編程中一種基於事件驅動的設計模式
- 通過派發/分離IO操作事件提高系統的併發性能。
- 提供了粗粒度的併發控制,使用單線程實現,避免了複雜的同步處理。
ipc.Server實際上實現了一個典型的Reactor設計模式
- Reactor : I/O事件的派發者。
- Acceptor : 接受來自Client的連接,建立與Client對應的Handler,並向Reactor註冊此Handler。
- Handler : 與一個Client通信的實體,並按一定的過程實現業務的處理。
- Reader/Sender : 爲了加速處理速度,Reactor模式往往構建一個存放數據處理線程的線程池,這樣數據讀出後,立即扔到線程喫中等待後續處理即可。爲此,Reactor模式一般分離Handler中的讀和寫兩個過程,分別註冊成單獨的讀事件和寫事件,並由對應的Reader和Sender線程處理。
HadoopRPC Server處理流程
接收請求
該階段主要任務是接收來自各個客戶端的RPC請求,並將它們封裝成固定的格式(Call類)放到一個共享隊列(CallQueue)中,該階段內部又分爲建立連接和接收請求兩個子階段,分別由Listener和Reader兩種線程完成。
整個Server只有一個Listener線程,統一負責監聽來自客戶端的連接請求,一旦由新的請求到達,它會採用輪詢的方式從線程池中選擇一個Reader線程進行處理,而Reader線程可同時存在多個,它們分別負責接收一部分客戶端連接的RPC請求,至於每個Reader線程負責哪些客戶端連接,完全由Listener決定,當前Listener只是採用了簡單的輪詢分配機制。
/** Listens on the socket. Creates jobs for the handler threads
* 只有一個Listener線程,統一服裝監聽一個來自客戶端連接請求,一旦有新的求到達,它會採用輪詢的方式從線程池中選賊一個Reader線程進行處理
* 而Reader線程可同時存在多個,他們分別負責接收一部分客戶端連接的RPC請求
*
* */
private class Listener extends Thread {
private ServerSocketChannel acceptChannel = null; //the accept channel
private Selector selector = null; //the selector that we use for the server
private Reader[] readers = null;
private int currentReader = 0;
private InetSocketAddress address; //the address we bind at
private int backlogLength = conf.getInt(
CommonConfigurationKeysPublic.IPC_SERVER_LISTEN_QUEUE_SIZE_KEY,
CommonConfigurationKeysPublic.IPC_SERVER_LISTEN_QUEUE_SIZE_DEFAULT);
public Listener() throws IOException {
address = new InetSocketAddress(bindAddress, port);
// Create a new server socket and set to non blocking mode
acceptChannel = ServerSocketChannel.open();
acceptChannel.configureBlocking(false);
// Bind the server socket to the local host and port
bind(acceptChannel.socket(), address, backlogLength, conf, portRangeConfig);
port = acceptChannel.socket().getLocalPort(); //Could be an ephemeral port
// create a selector;
selector= Selector.open();
readers = new Reader[readThreads];
for (int i = 0; i < readThreads; i++) {
Reader reader = new Reader(
"Socket Reader #" + (i + 1) + " for port " + port);
readers[i] = reader;
reader.start();
}
Listener和Reader線程內部各自包含一個Selector對象,分別用於監SelectionKey.OP_ACCEPT和SelectionKey.OP_READ事件。對於Listener線程,主循環的實現體是監聽是否有新的連接請求到達,並採用輪詢策略選擇一個Reader線程處理新連接;對於Reader線程,主循環的實現體是監聽客戶端連接中是否有新的RPC請求到達,並將新的RPC請求封裝成Call對象,放到共享隊列中。
處理請求
該階段主要任務是從共享隊列中獲取call對象,執行對應的函數調用,並將結果返回給客戶端,這全部由Handler線程完成。
Server端可同時存在多個Handler線程,它們並行從共享隊列中讀取Call對象,經執行對應的函數調用後,將嘗試着直接將結果返回給對應的客戶端。但考慮到某些函數調用返回結果很大或者網絡速度很慢,可能難以將結果一次性發送給客戶端,此時Handler將嘗試着將後續發送任務交給Responder線程
private class Handler extends Thread {
public Handler(int instanceNumber) {
this.setDaemon(true);
this.setName("IPC Server handler "+ instanceNumber + " on " + port);
}
@Override
public void run() {
LOG.debug(Thread.currentThread().getName() + ": starting");
SERVER.set(Server.this);
ByteArrayOutputStream buf =
new ByteArrayOutputStream(INITIAL_RESP_BUF_SIZE);
while (running) {
TraceScope traceScope = null;
try {
final Call call = callQueue.take(); // pop the queue; maybe blocked here
......
返回結果
Server僅存一個Responder線程,它的內部包含一個Selector對象,用於監聽SelectionKey.OP_WRITE事件。當Handler沒能將結果一次性發送到客戶端時,會向該Selector對象註冊SelectionKey.OP_WRITE事件,進而由Responder線程採用異步方式繼續發送未發送完成的結果。
/**
* 函數調用返回結果過大或者網絡異常情況(網絡過慢),
* 僅存一個selector對象,用於監聽SelectionKey.OP_WRITE
*
*/
private class Responder extends Thread {
private final Selector writeSelector;
private int pending; // connections waiting to register
final static int PURGE_INTERVAL = 900000; // 15mins
Responder() throws IOException {
this.setName("IPC Server Responder");
this.setDaemon(true);
writeSelector = Selector.open(); // create a selector
pending = 0;
}
@Override
public void run() {
LOG.info(Thread.currentThread().getName() + ": starting");
SERVER.set(Server.this);
try {
doRunLoop();
} finally {
LOG.info("Stopping " + Thread.currentThread().getName());
try {
writeSelector.close();
} catch (IOException ioe) {
LOG.error("Couldn't close write selector in " + Thread.currentThread().getName(), ioe);
}
}
}
七. Hadoop RPC參數調優
Reader線程數目。由參數ipc.server.read.threadpool.size配置,默認1,默認情況下,一個RPC Server 只包含一個Reader線程。
每個Handler線程對應最大Call數目。由參數ipc.server.handler.queue.size指定,默認100,默認情況下滅個Handler線程對應的Call隊列長度100。
Handler線程數目。在Hadoop中,ResourceManager和NameNode分別是Yarn和HDFS兩個子系統中的RPC Server ,其對應Handler數目分別爲參數yarn.resourcemananger.resource-tracker.client.thread-count 和 dfs.namenode.service.handler.count 指定。默認分別爲50和10,當集羣規模較大時,這兩個參數值會大大影響性能。
客戶端最大重試次數。ipc.client.connect.max.retries指定,默認10。
文獻
《Hadoop技術內幕 深入解析YARN架構設計與實現原理》