引用:
http://jimmee.iteye.com/blog/1201398
http://jimmee.iteye.com/blog/1201982
http://jimmee.iteye.com/blog/1206201
http://jimmee.iteye.com/blog/1206598
- 1.一個線程來處理所有連接(使用一個Selector)
- 2.一組線程來讀取已經建立連接的數據(多個Selector,這裏的線程數一般和cpu的核數相當);
- 3.一個線程池(這個線程池大小可以根據業務需求進行設置)
- 4.一個線程處理所有的連接的數據的寫操作(一個Selector)
- 1. Call,表示一次rpc的調用請求
- 2. Connection,表示一個client與server之間的連接,一個連接一個線程啓動
- 3. ConnectionId:連接的標記(包括server地址,協議,其他一些連接的配置項信息)
- 4. ParallelCall:實現並行調用的請求
- 5. ParallelResults:並行調用的執行結果
執行邏輯:
1
- . 當要執行一個調用時,將call放到connectin的map中;同時將請求發送到connection的輸出流中,之後返回,並不一直持有connection並等待結果,所以是異步的處理過程;
- 2. connection自身線程不停從server讀取請求返回,服務器返回的結果中包含請求的id,因此根據id從map中找到對應的call,從而設置call的調用結果;
可以看到,client的端的調用是很簡單。
可以簡單的來看一下代碼:
public Writable call(Writable param, ConnectionId remoteId),此方法是調用入口,代碼分析:
- /**
- * 創建一個call,並得到連接,之後在連接中保存call,之後向連接的輸出流寫入請求
- * 並返回, 底層使用的是oio(即blocking io),採用什麼樣的io與異步消息機制沒有
- * 必然聯繫.
- **/
- Call call = new Call(param);
- Connection connection = getConnection(remoteId, call);
- connection.sendParam(call);
- // 接口是同步的,異步變同步的操作再這裏
- synchronized (call) {
- while (!call.done) {
- try {
- call.wait(); // wait for the result
- } catch (InterruptedException ie) {
- // save the fact that we were interrupted
- interrupted = true;
- }
- }
- ……
- }
對應的,可以看到Connection類的receiveResponse方法裏處理從server裏讀到的結果:
- int id = in.readInt(); // try to read an id
- Call call = calls.get(id);
- Writable value = ReflectionUtils.newInstance(valueClass, conf);
- value.readFields(in); // read value
- call.setValue(value);
- calls.remove(id);
Call調用setValue方式,會執行notify操作.
備註:
- 1. 一個Client會對應多個Connection,並且會對這些Connection進行緩存;
- 2. 一個Connection對應一個線程,這主要是內網中調用,節點之間的連接量應該不會太多(我想太多時,估計一個連接一個線程時就有問題了)
- 3. 當出現異常時,直接關閉連接,並處理沒有返回結果的call
- 4. Connection中保存Call的調用,使用的是一個HashTable,沒有進行數量限制,若服務器阻塞後,Call會積累,感覺這個時候是有問題的。
- public abstract Writable call(Class<?> protocol, Writable param, long receiveTime)
- throws IOException;
也就是說,服務器端接收到請求後,最後對這個請求的真正處理是通過此方法執行的。又具體的實現類實現這個方法。
回顧前面http://jimmee.iteye.com/blog/1201398關於nio的reactor模式,hadoop的rpc調用的Server完全按照這個模式來實現
- 1. Listener類,啓動一個線程使用一個Selector來處理Channel的Accept;
- 2. Listenter.Reader類,啓動多個線程來(當然也可以配置成一個,關鍵看cpu是怎樣的了)處理Channel的讀,也就是得到client;
- 3. Handler類,業務線程處理類,這個類處理真正的調用,線程池配置多大,就啓動多少個線程,其實我覺得直接使用java.util.concurrent中的線程池更方便;
- 4. Responder類,這個類啓動一個線程,使用一個Selector來處理寫(這裏值得注意的是,並不是每個Channel都添加到Selector裏來處理寫操作,而是有沒有寫完的數據時候,才添加進去)
- 5. Call類,可以理解爲與Client端的Call對應;
- 6. Connection,代表與Client端的連接,讀取客戶端的call並放到一個阻塞隊列中,Handler負責從這個隊列中讀取數據並處理
先說一下基本原理:
- 1. 首先客戶端和服務器端之間要有一個協議,這裏的協議就是以java接口類的方式暴露出來的
- 2. 雖然Client類和Server類之間已經具有通信的能力,也有了協議,那麼一個真正的客戶端要調用服務器端rpc調用的實現,只需要解決參數及具體的調用實現兩個問題即可
- 3. 客戶端要做的,就是要將參數(這個一般稱爲存根)通過網絡傳遞到服務器端。這個自然而然想到使用代理模式,因爲Client已經具備網絡通信的能力,只要通過代理,實現獲取參數進行傳輸即可,爲什麼不在Client這裏實現參數的獲取,如果這樣的話,就違反了單一職責的原則,且擴展性不行,總不能一個客戶端的調用實現一個特定的Client類吧。因此,將Client的功能單一獨立出來,只負責將參數通過網絡傳遞到服務器端
- 4. 服務器要做的工作,只需要進行調用的真正的實現即可,當然了, 最後需要能夠返回正確的結果。
上面說的這些,都全部在hadoop的這個RPC裏進行了實現。
客戶端的主要代理實現方法如下:
- public static <T> ProtocolProxy<T> getProtocolProxy(Class<T> protocol,
- long clientVersion,
- InetSocketAddress addr,
- UserGroupInformation ticket,
- Configuration conf,
- SocketFactory factory,
- int rpcTimeout) throws IOException {
- if (UserGroupInformation.isSecurityEnabled()) {
- SaslRpcServer.init(conf);
- }
- return getProtocolEngine(protocol,conf).getProxy(protocol,
- clientVersion, addr, ticket, conf, factory, rpcTimeout);
- }
其中是調用RpcEngine的下面這個接口方法來進行實現的:
- public <T> ProtocolProxy<T> getProxy(Class<T> protocol, long clientVersion,
- InetSocketAddress addr, UserGroupInformation ticket,
- Configuration conf, SocketFactory factory,
- int rpcTimeout)
- throws IOException
對應的,可以查看一個具體實現的代碼,WritableRpcEngine類的實現:
- public <T> ProtocolProxy<T> getProxy(Class<T> protocol, long clientVersion,
- InetSocketAddress addr, UserGroupInformation ticket,
- Configuration conf, SocketFactory factory,
- int rpcTimeout)
- throws IOException {
- T proxy = (T) Proxy.newProxyInstance(protocol.getClassLoader(),
- new Class[] { protocol }, new Invoker(protocol, addr, ticket, conf,
- factory, rpcTimeout));
- return new ProtocolProxy<T>(protocol, proxy, true);
真正的代理處理在InVoker類裏實現(關於JDK的動態代理,可參看http://jimmee.iteye.com/admin/blogs/776820)
- public Object invoke(Object proxy, Method method, Object[] args)
- throws Throwable {
- long startTime = 0;
- if (LOG.isDebugEnabled()) {
- startTime = System.currentTimeMillis();
- }
- ObjectWritable value = (ObjectWritable)
- // 這裏取得要調用的方法,參數列表,之後通過Client對象傳遞給服務器端
- client.call(new Invocation(method, args), remoteId);
- if (LOG.isDebugEnabled()) {
- long callTime = System.currentTimeMillis() - startTime;
- LOG.debug("Call: " + method.getName() + " " + callTime);
- }
- return value.get();
- }
服務器端真正的實現,也在RpcEngine的一個具體實現裏:
- public Writable call(Class<?> protocol, Writable param, long receivedTime)
- throws IOException {
- ….
- Invocation call = (Invocation)param;
- if (verbose) log("Call: " + call);
- Method method = protocol.getMethod(call.getMethodName(),
- call.getParameterClasses());
- method.setAccessible(true);
- // Verify rpc version
- ….
- //Verify protocol version.
- ……
- long startTime = System.currentTimeMillis();
- // 真正的調用
- Object value = method.invoke(instance, call.getParameters());
- int processingTime = (int) (System.currentTimeMillis() - startTime);
- int qTime = (int) (startTime-receivedTime);
- if (LOG.isDebugEnabled()) {
- LOG.debug("Served: " + call.getMethodName() +
- " queueTime= " + qTime +
- " procesingTime= " + processingTime);
- }
- rpcMetrics.addRpcQueueTime(qTime);
- rpcMetrics.addRpcProcessingTime(processingTime);
- rpcDetailedMetrics.addProcessingTime(call.getMethodName(),
- processingTime);
- if (verbose) log("Return: "+value);
- return new ObjectWritable(method.getReturnType(), value);
- …..
- }
一個簡單的例子:
客戶端和服務器端的協議及實現:
- package cn.edu.jimmee;
- import org.apache.hadoop.io.BooleanWritable;
- import org.apache.hadoop.io.IntWritable;
- import org.apache.hadoop.io.Text;
- import org.apache.hadoop.ipc.VersionedProtocol;
- /**
- * rpc的協議接口
- * @author jimmee
- */
- public interface RpcProtocol extends VersionedProtocol {
- public BooleanWritable printMsg(IntWritable id, Text msg);
- }
- package cn.edu.jimmee;
- import java.io.IOException;
- import org.apache.hadoop.io.BooleanWritable;
- import org.apache.hadoop.io.IntWritable;
- import org.apache.hadoop.io.Text;
- /**
- * rpc的協議實現接口
- * @author jimmee
- */
- public class RpcProtocolImpl implements RpcProtocol {
- @Override
- public BooleanWritable printMsg(IntWritable id, Text msg) {
- System.out.println("id=" + id.get() + ", msg=" + msg.toString());
- if (Math.random() < 0.5) {
- return new BooleanWritable(true);
- } else {
- return new BooleanWritable(false);
- }
- }
- @Override
- public long getProtocolVersion(String protocol,
- long clientVersion)
- throws IOException {
- return 0;
- }
- }
服務器代碼:
- package cn.edu.jimmee;
- import java.io.IOException;
- import org.apache.hadoop.conf.Configuration;
- import org.apache.hadoop.ipc.RPC;
- import org.apache.hadoop.ipc.RPC.Server;
- /**
- * rpc的server
- * @author jimmee
- */
- public class RpcServer {
- public static void main(String[] args) throws IOException {
- RpcProtocol instance = new RpcProtocolImpl();
- Configuration conf = new Configuration();
- Server server = RPC.getServer(instance, "127.0.0.1", 7777, conf);
- server.start();
- }
- }
客戶端的代碼:
- package cn.edu.jimmee;
- import java.io.IOException;
- import java.net.InetSocketAddress;
- import org.apache.hadoop.conf.Configuration;
- import org.apache.hadoop.io.IntWritable;
- import org.apache.hadoop.io.Text;
- import org.apache.hadoop.ipc.RPC;
- /**
- * rpc的client端的實現
- * @author jimmee
- */
- public class RpcClient {
- public static void main(String[] args) throws IOException {
- Configuration conf = new Configuration();
- RpcProtocol rpcClientImpl = (RpcProtocol) RPC.getProxy(RpcProtocol.class, 0, new InetSocketAddress("127.0.0.1", 7777), conf);
- for (int i = 0; i < 10; i++) {
- System.out.println(rpcClientImpl.printMsg(new IntWritable(i), new Text("hello" + i)));
- }
- }
- }
五. 應用
1、心跳機制
心跳的機制大概是這樣的:
1) master啓動的時候,會開一個ipc server在那裏。
2) slave啓動時,會連接master,並每隔3秒鐘主動向master發送一個“心跳”,將自己的狀態信息告訴master,然後master也是通過這個心跳的返回值,向slave節點傳達指令。
2、找到心跳的代碼
拿namenode和datanode來說,在datanode的offerService方法中,每隔3秒向namenode發送心跳的代碼:
- /**
- * Main loop for the DataNode. Runs until shutdown,
- * forever calling remote NameNode functions.
- */
- public void offerService() throws Exception {
- ...
- //
- // Now loop for a long time....
- //
- while (shouldRun) {
- try {
- long startTime = now();
- //
- // Every so often, send heartbeat or block-report
- //
- // 如果到了3秒鐘,就向namenode發心跳
- if (startTime - lastHeartbeat > heartBeatInterval) {
- //
- // All heartbeat messages include following info:
- // -- Datanode name
- // -- data transfer port
- // -- Total capacity
- // -- Bytes remaining
- //
- lastHeartbeat = startTime;
- DatanodeCommand[] cmds = namenode.sendHeartbeat(dnRegistration,
- data.getCapacity(),
- data.getDfsUsed(),
- data.getRemaining(),
- xmitsInProgress.get(),
- getXceiverCount());
- // 注意上面這行代碼,“發送心跳”竟然就是調用namenode的一個方法??
- myMetrics.heartbeats.inc(now() - startTime);
- //LOG.info("Just sent heartbeat, with name " + localName);
- // 處理對心跳的返回值(namenode傳給datanode的指令)
- if (!processCommand(cmds))
- continue;
- }
- // 這裏省略很多代碼
- ...
- } // while (shouldRun)
- } // offerService
上面這段代碼,如果是單機的程序,沒什麼值得奇怪的。但是,這是hadoop集羣!datanode和namenode在2臺不同的機器(或2個JVM)上運行!datanode機器竟然直接調用namenode的方法!這是怎麼實現的?難道是傳說中的RMI嗎??
下面我們主要就來分析這個方法調用的細節。
3、心跳的底層細節一:datanode怎麼獲得namenode對象的?
首先,DataNode類中,有一個namenode的成員變量:
- public class DataNode extends Configured
- implements InterDatanodeProtocol, ClientDatanodeProtocol, FSConstants, Runnable {
- ...
- public DatanodeProtocol namenode = null;
- ...
- }
下面是NameNode類的定義:
- public class NameNode implements ClientProtocol, DatanodeProtocol,
- NamenodeProtocol, FSConstants,
- RefreshAuthorizationPolicyProtocol {
- ...
- }
注意:NameNode實現了DatanodeProtocol接口,DatanodeProtocol接口定義了namenode和datanode之間通信的方法。
那麼,DataNode類是怎麼獲取到NameNode類的引用呢?
在Datanode端,爲namenode變量賦值的代碼:
- // connect to name node
- this.namenode = (DatanodeProtocol)
- RPC.waitForProxy(DatanodeProtocol.class,
- DatanodeProtocol.versionID,
- nameNodeAddr,
- conf);
在繼續去RPC類中追蹤:
- VersionedProtocol proxy =
- (VersionedProtocol) Proxy.newProxyInstance(
- protocol.getClassLoader(), new Class[] { protocol },
- new Invoker(addr, ticket, conf, factory));
現在,明白了!
1) 對namenode的賦值,並不是真正的new了一個實現了DatanodeProtocol接口的對象,而是獲得了一個動態代理!!
2) 上面這段代碼中,protocol的類型是DatanodeProtocol.class
3) 對namenode的所有調用,都被委託(delegate)給了Invoker
4、心跳的底層細節二:看看Invoker類
Invoker類是org.apache.hadoop.ipc.RPC類的一個靜態內部類:
- private static class Invoker implements InvocationHandler {
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- ...
- ObjectWritable value = (ObjectWritable)
- client.call(new Invocation(method, args), address,
- method.getDeclaringClass(), ticket);
- ...
- return value.get();
- }
所有的方法調用又被delegate給client的call方法了!
client是Invoker中的成員變量:
- private Client client;
所以可以看出:DatanodeProtocol中的每個方法調用,都被包裝成一個Invocation對象,再由client.call()調用
5、心跳的底層細節三:Invocation類
Invocation類是org.apache.hadoop.ipc.RPC類的一個靜態內部類
沒有什麼業務邏輯方法,主要作用就是一個VO
6、心跳的底層細節四:client類的call方法
接下來重點看client類的call方法:
- public Writable call(Writable param, InetSocketAddress addr,
- Class<?> protocol, UserGroupInformation ticket)
- throws InterruptedException, IOException {
- Call call = new Call(param);
- // 將Invocation轉化爲Call
- Connection connection = getConnection(addr, protocol, ticket, call);
- // 連接遠程服務器
- connection.sendParam(call); // send the parameter
- // 將“序列化”後的call發給過去
- boolean interrupted = false;
- synchronized (call) {
- while (!call.done) {
- try {
- call.wait(); // wait for the result
- // 等待調用結果
- } catch (InterruptedException ie) {
- // save the fact that we were interrupted
- interrupted = true;
- }
- }
- if (interrupted) {
- // set the interrupt flag now that we are done waiting
- Thread.currentThread().interrupt();
- }
- if (call.error != null) {
- if (call.error instanceof RemoteException) {
- call.error.fillInStackTrace();
- throw call.error;
- } else { // local exception
- throw wrapException(addr, call.error);
- }
- } else {
- return call.value;
- // 返回
- }
- }
- }
7、現在,一目瞭然了
- datanode向namenode發送heartbeat過程是這樣的:
- a) 在datanode初始化獲得namenode的proxy
- b) 在datanode上,調用namenode proxy的heartbeat方法:
- namenode.sendHeartbeat(dnRegistration,
- data.getCapacity(),
- data.getDfsUsed(),
- data.getRemaining(),
- xmitsInProgress.get(),
- getXceiverCount());
- c) 在datanode上的namenode動態代理類將這個調用包裝成(或者叫“序列化成”)一個Invocation對象,並調用client.call方法
- d) client call方法將Invocation轉化爲Call對象
- e) client 將call發送到真正的namenode服務器
- f) namenode接收後,轉化成namenode端的Call,並process後,通過Responder發回來!
- g) datanode接收結果,並將結果轉化爲DatanodeCommand[]
8、再看動態代理
動態代理:讓“只有接口,沒事對應的實現類”成爲可能,因爲具體方法的實現可以委託給另一個類!!
在這個例子中,就datanode而言,DatanodeProtocol接口是沒有實現類的!