Hadoop RPC 分析總結

引用:

http://jimmee.iteye.com/blog/1201398

http://jimmee.iteye.com/blog/1201982

http://jimmee.iteye.com/blog/1206201

http://jimmee.iteye.com/blog/1206598


一. 調用
nio的reactor模式

具體的處理方式: 
  •     1.一個線程來處理所有連接(使用一個Selector)
  •     2.一組線程來讀取已經建立連接的數據(多個Selector,這裏的線程數一般和cpu的核數相當);
  •     3.一個線程池(這個線程池大小可以根據業務需求進行設置)
  •     4.一個線程處理所有的連接的數據的寫操作(一個Selector)
二. Client
主要的幾個類說明: 
  • 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),此方法是調用入口,代碼分析: 
Java代碼  收藏代碼
  1. /** 
  2. * 創建一個call,並得到連接,之後在連接中保存call,之後向連接的輸出流寫入請求 
  3. * 並返回, 底層使用的是oio(即blocking io),採用什麼樣的io與異步消息機制沒有 
  4. * 必然聯繫. 
  5. **/  
  6. Call call = new Call(param);  
  7. Connection connection = getConnection(remoteId, call);  
  8. connection.sendParam(call);    
  9. // 接口是同步的,異步變同步的操作再這裏  
  10. synchronized (call) {  
  11.       while (!call.done) {  
  12.         try {  
  13.           call.wait();                           // wait for the result  
  14.         } catch (InterruptedException ie) {  
  15.           // save the fact that we were interrupted  
  16.           interrupted = true;  
  17.         }  
  18.       }  
  19.     ……  
  20. }  


對應的,可以看到Connection類的receiveResponse方法裏處理從server裏讀到的結果: 
    
Java代碼  收藏代碼
  1. int id = in.readInt();                    // try to read an id  
  2.      Call call = calls.get(id);  
  3.      Writable value = ReflectionUtils.newInstance(valueClass, conf);  
  4.      value.readFields(in);                 // read value  
  5.      call.setValue(value);  
  6.      calls.remove(id);  

Call調用setValue方式,會執行notify操作. 

備註: 
  • 1. 一個Client會對應多個Connection,並且會對這些Connection進行緩存;
  • 2. 一個Connection對應一個線程,這主要是內網中調用,節點之間的連接量應該不會太多(我想太多時,估計一個連接一個線程時就有問題了)
  • 3. 當出現異常時,直接關閉連接,並處理沒有返回結果的call
  • 4. Connection中保存Call的調用,使用的是一個HashTable,沒有進行數量限制,若服務器阻塞後,Call會積累,感覺這個時候是有問題的。
三. Server
Server類是個抽象,只所以抽象,是具有如下抽象方法: 
Java代碼  收藏代碼
  1. public abstract Writable call(Class<?> protocol, Writable param, long receiveTime)  
  2. 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負責從這個隊列中讀取數據並處理
總之按照這個流程去理解server端的代碼就ok。

四. 實現
有了Client,有了Server,那整個過程怎麼運行起來? 
先說一下基本原理: 
  • 1. 首先客戶端和服務器端之間要有一個協議,這裏的協議就是以java接口類的方式暴露出來的
  • 2. 雖然Client類和Server類之間已經具有通信的能力,也有了協議,那麼一個真正的客戶端要調用服務器端rpc調用的實現,只需要解決參數及具體的調用實現兩個問題即可
  • 3. 客戶端要做的,就是要將參數(這個一般稱爲存根)通過網絡傳遞到服務器端。這個自然而然想到使用代理模式,因爲Client已經具備網絡通信的能力,只要通過代理,實現獲取參數進行傳輸即可,爲什麼不在Client這裏實現參數的獲取,如果這樣的話,就違反了單一職責的原則,且擴展性不行,總不能一個客戶端的調用實現一個特定的Client類吧。因此,將Client的功能單一獨立出來,只負責將參數通過網絡傳遞到服務器端
  • 4. 服務器要做的工作,只需要進行調用的真正的實現即可,當然了, 最後需要能夠返回正確的結果。


上面說的這些,都全部在hadoop的這個RPC裏進行了實現。 
客戶端的主要代理實現方法如下: 
 
Java代碼  收藏代碼
  1. public static <T> ProtocolProxy<T> getProtocolProxy(Class<T> protocol,  
  2.                                 long clientVersion,  
  3.                                 InetSocketAddress addr,  
  4.                                 UserGroupInformation ticket,  
  5.                                 Configuration conf,  
  6.                                 SocketFactory factory,  
  7.                                 int rpcTimeout) throws IOException {      
  8.     if (UserGroupInformation.isSecurityEnabled()) {  
  9.       SaslRpcServer.init(conf);  
  10.     }  
  11.     return getProtocolEngine(protocol,conf).getProxy(protocol,  
  12.         clientVersion, addr, ticket, conf, factory, rpcTimeout);  
  13.   }  


其中是調用RpcEngine的下面這個接口方法來進行實現的: 
 
Java代碼  收藏代碼
  1. public <T> ProtocolProxy<T> getProxy(Class<T> protocol, long clientVersion,  
  2.                          InetSocketAddress addr, UserGroupInformation ticket,  
  3.                          Configuration conf, SocketFactory factory,  
  4.                          int rpcTimeout)  
  5.     throws IOException  

對應的,可以查看一個具體實現的代碼,WritableRpcEngine類的實現: 
Java代碼  收藏代碼
  1. public <T> ProtocolProxy<T> getProxy(Class<T> protocol, long clientVersion,  
  2.                         InetSocketAddress addr, UserGroupInformation ticket,  
  3.                         Configuration conf, SocketFactory factory,  
  4.                         int rpcTimeout)  
  5.    throws IOException {      
  6.   
  7.    T proxy = (T) Proxy.newProxyInstance(protocol.getClassLoader(),  
  8.        new Class[] { protocol }, new Invoker(protocol, addr, ticket, conf,  
  9.            factory, rpcTimeout));  
  10.    return new ProtocolProxy<T>(protocol, proxy, true);  


真正的代理處理在InVoker類裏實現(關於JDK的動態代理,可參看http://jimmee.iteye.com/admin/blogs/776820
  
Java代碼  收藏代碼
  1. public Object invoke(Object proxy, Method method, Object[] args)  
  2.       throws Throwable {  
  3.       long startTime = 0;  
  4.       if (LOG.isDebugEnabled()) {  
  5.         startTime = System.currentTimeMillis();  
  6.       }  
  7.           
  8.       ObjectWritable value = (ObjectWritable)  
  9.       // 這裏取得要調用的方法,參數列表,之後通過Client對象傳遞給服務器端  
  10. client.call(new Invocation(method, args), remoteId);  
  11.       if (LOG.isDebugEnabled()) {  
  12.         long callTime = System.currentTimeMillis() - startTime;  
  13.         LOG.debug("Call: " + method.getName() + " " + callTime);  
  14.       }  
  15.       return value.get();  
  16.     }  


服務器端真正的實現,也在RpcEngine的一個具體實現裏: 
Java代碼  收藏代碼
  1. public Writable call(Class<?> protocol, Writable param, long receivedTime)   
  2.     throws IOException {  
  3.         ….  
  4.         Invocation call = (Invocation)param;  
  5.         if (verbose) log("Call: " + call);  
  6.   
  7.         Method method = protocol.getMethod(call.getMethodName(),  
  8.                                            call.getParameterClasses());  
  9.         method.setAccessible(true);  
  10.   
  11.         // Verify rpc version  
  12.         ….  
  13.           
  14.         //Verify protocol version.  
  15.        ……  
  16.   
  17.         long startTime = System.currentTimeMillis();  
  18.           // 真正的調用  
  19.         Object value = method.invoke(instance, call.getParameters());  
  20.         int processingTime = (int) (System.currentTimeMillis() - startTime);  
  21.         int qTime = (int) (startTime-receivedTime);  
  22.         if (LOG.isDebugEnabled()) {  
  23.           LOG.debug("Served: " + call.getMethodName() +  
  24.                     " queueTime= " + qTime +  
  25.                     " procesingTime= " + processingTime);  
  26.         }  
  27.         rpcMetrics.addRpcQueueTime(qTime);  
  28.         rpcMetrics.addRpcProcessingTime(processingTime);  
  29.         rpcDetailedMetrics.addProcessingTime(call.getMethodName(),  
  30.                                              processingTime);  
  31.         if (verbose) log("Return: "+value);  
  32.   
  33.         return new ObjectWritable(method.getReturnType(), value);  
  34.         …..       
  35.   }  

一個簡單的例子: 
客戶端和服務器端的協議及實現: 
Java代碼  收藏代碼
  1. package cn.edu.jimmee;  
  2. import org.apache.hadoop.io.BooleanWritable;  
  3. import org.apache.hadoop.io.IntWritable;  
  4. import org.apache.hadoop.io.Text;  
  5. import org.apache.hadoop.ipc.VersionedProtocol;  
  6. /** 
  7.  * rpc的協議接口 
  8.  * @author jimmee 
  9.  */  
  10. public interface RpcProtocol extends VersionedProtocol {  
  11.     public BooleanWritable printMsg(IntWritable id, Text msg);  
  12. }  

Java代碼  收藏代碼
  1. package cn.edu.jimmee;  
  2. import java.io.IOException;  
  3. import org.apache.hadoop.io.BooleanWritable;  
  4. import org.apache.hadoop.io.IntWritable;  
  5. import org.apache.hadoop.io.Text;  
  6. /** 
  7.  * rpc的協議實現接口 
  8.  * @author jimmee 
  9.  */  
  10. public class RpcProtocolImpl implements RpcProtocol {  
  11.     @Override  
  12.     public BooleanWritable printMsg(IntWritable id, Text msg) {  
  13.         System.out.println("id=" + id.get() + ", msg=" + msg.toString());     
  14.         if (Math.random() < 0.5) {  
  15.             return new BooleanWritable(true);  
  16.         } else {  
  17.             return new BooleanWritable(false);  
  18.         }  
  19.     }  
  20.   
  21.     @Override  
  22.     public long getProtocolVersion(String protocol,  
  23.             long clientVersion)  
  24.             throws IOException {  
  25.         return 0;  
  26.     }  
  27. }  


服務器代碼: 
Java代碼  收藏代碼
  1. package cn.edu.jimmee;  
  2. import java.io.IOException;  
  3. import org.apache.hadoop.conf.Configuration;  
  4. import org.apache.hadoop.ipc.RPC;  
  5. import org.apache.hadoop.ipc.RPC.Server;  
  6. /** 
  7.  * rpc的server 
  8.  * @author jimmee 
  9.  */  
  10. public class RpcServer {  
  11.     public static void main(String[] args) throws IOException {  
  12.         RpcProtocol instance = new RpcProtocolImpl();  
  13.         Configuration conf = new Configuration();  
  14.         Server server = RPC.getServer(instance, "127.0.0.1"7777, conf);  
  15.         server.start();  
  16.     }  
  17. }  

客戶端的代碼: 
Java代碼  收藏代碼
  1. package cn.edu.jimmee;  
  2. import java.io.IOException;  
  3. import java.net.InetSocketAddress;  
  4. import org.apache.hadoop.conf.Configuration;  
  5. import org.apache.hadoop.io.IntWritable;  
  6. import org.apache.hadoop.io.Text;  
  7. import org.apache.hadoop.ipc.RPC;  
  8. /** 
  9.  * rpc的client端的實現 
  10.  * @author jimmee 
  11.  */  
  12. public class RpcClient {  
  13.     public static void main(String[] args) throws IOException {  
  14.         Configuration conf = new Configuration();  
  15.         RpcProtocol rpcClientImpl = (RpcProtocol) RPC.getProxy(RpcProtocol.class0new InetSocketAddress("127.0.0.1"7777), conf);  
  16.         for (int i = 0; i < 10; i++) {  
  17.             System.out.println(rpcClientImpl.printMsg(new IntWritable(i), new Text("hello" + i)));  
  18.         }  
  19.     }  
  20. }  

五. 應用

1、心跳機制 


心跳的機制大概是這樣的: 
1) master啓動的時候,會開一個ipc server在那裏。 
2) slave啓動時,會連接master,並每隔3秒鐘主動向master發送一個“心跳”,將自己的狀態信息告訴master,然後master也是通過這個心跳的返回值,向slave節點傳達指令。 


2、找到心跳的代碼 

拿namenode和datanode來說,在datanode的offerService方法中,每隔3秒向namenode發送心跳的代碼: 

Java代碼  收藏代碼
  1. /** 
  2.   * Main loop for the DataNode.  Runs until shutdown, 
  3.   * forever calling remote NameNode functions. 
  4.   */  
  5.  public void offerService() throws Exception {  
  6.       
  7.    ...  
  8.   
  9.    //  
  10.    // Now loop for a long time....  
  11.    //  
  12.   
  13.    while (shouldRun) {  
  14.      try {  
  15.        long startTime = now();  
  16.   
  17.        //  
  18.        // Every so often, send heartbeat or block-report  
  19.        //  
  20.          
  21. // 如果到了3秒鐘,就向namenode發心跳  
  22.        if (startTime - lastHeartbeat > heartBeatInterval) {  
  23.          //  
  24.          // All heartbeat messages include following info:  
  25.          // -- Datanode name  
  26.          // -- data transfer port  
  27.          // -- Total capacity  
  28.          // -- Bytes remaining  
  29.          //  
  30.          lastHeartbeat = startTime;  
  31.          DatanodeCommand[] cmds = namenode.sendHeartbeat(dnRegistration,  
  32.                                                       data.getCapacity(),  
  33.                                                       data.getDfsUsed(),  
  34.                                                       data.getRemaining(),  
  35.                                                       xmitsInProgress.get(),  
  36.                                                       getXceiverCount());  
  37.   
  38.   // 注意上面這行代碼,“發送心跳”竟然就是調用namenode的一個方法??  
  39.   
  40.          myMetrics.heartbeats.inc(now() - startTime);  
  41.          //LOG.info("Just sent heartbeat, with name " + localName);  
  42.   
  43.   // 處理對心跳的返回值(namenode傳給datanode的指令)  
  44.          if (!processCommand(cmds))  
  45.            continue;  
  46.        }  
  47.   
  48.     // 這裏省略很多代碼  
  49. ...  
  50.    } // while (shouldRun)  
  51.  } // offerService  

上面這段代碼,如果是單機的程序,沒什麼值得奇怪的。但是,這是hadoop集羣!datanode和namenode在2臺不同的機器(或2個JVM)上運行!datanode機器竟然直接調用namenode的方法!這是怎麼實現的?難道是傳說中的RMI嗎?? 

下面我們主要就來分析這個方法調用的細節。 


3、心跳的底層細節一:datanode怎麼獲得namenode對象的? 

首先,DataNode類中,有一個namenode的成員變量: 
Java代碼  收藏代碼
  1. public class DataNode extends Configured   
  2.     implements InterDatanodeProtocol, ClientDatanodeProtocol, FSConstants, Runnable {  
  3.   ...  
  4.   public DatanodeProtocol namenode = null;  
  5.   ...   
  6. }  

下面是NameNode類的定義: 
Java代碼  收藏代碼
  1. public class NameNode implements ClientProtocol, DatanodeProtocol,  
  2.                                  NamenodeProtocol, FSConstants,  
  3.                                  RefreshAuthorizationPolicyProtocol {  
  4.   ...   
  5. }  


注意:NameNode實現了DatanodeProtocol接口,DatanodeProtocol接口定義了namenode和datanode之間通信的方法。 

那麼,DataNode類是怎麼獲取到NameNode類的引用呢? 

在Datanode端,爲namenode變量賦值的代碼: 
Java代碼  收藏代碼
  1. // connect to name node  
  2. this.namenode = (DatanodeProtocol)   
  3.   RPC.waitForProxy(DatanodeProtocol.class,  
  4.                    DatanodeProtocol.versionID,  
  5.                    nameNodeAddr,   
  6.                    conf);  


在繼續去RPC類中追蹤: 
Java代碼  收藏代碼
  1. VersionedProtocol proxy =  
  2.         (VersionedProtocol) Proxy.newProxyInstance(  
  3.             protocol.getClassLoader(), new Class[] { protocol },  
  4.             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類的一個靜態內部類: 

Java代碼  收藏代碼
  1. private static class Invoker implements InvocationHandler {  
在這個類中,看invoke方法: 
  
Java代碼  收藏代碼
  1. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  
  2.             ...  
  3.   
  4.               ObjectWritable value = (ObjectWritable)  
  5.                 client.call(new Invocation(method, args), address,   
  6.                             method.getDeclaringClass(), ticket);  
  7.                 ...  
  8.               return value.get();  
  9.            }  

所有的方法調用又被delegate給client的call方法了! 

client是Invoker中的成員變量: 
  
Java代碼  收藏代碼
  1. private Client client;  

所以可以看出:DatanodeProtocol中的每個方法調用,都被包裝成一個Invocation對象,再由client.call()調用 


5、心跳的底層細節三:Invocation類 

Invocation類是org.apache.hadoop.ipc.RPC類的一個靜態內部類 

沒有什麼業務邏輯方法,主要作用就是一個VO 


6、心跳的底層細節四:client類的call方法 

接下來重點看client類的call方法: 
Java代碼  收藏代碼
  1. public Writable call(Writable param, InetSocketAddress addr,   
  2.                      Class<?> protocol, UserGroupInformation ticket)    
  3.                      throws InterruptedException, IOException {  
  4.   
  5.   Call call = new Call(param);     
  6. // 將Invocation轉化爲Call  
  7.   Connection connection = getConnection(addr, protocol, ticket, call);  
  8. // 連接遠程服務器  
  9.   connection.sendParam(call);                 // send the parameter  
  10. // 將“序列化”後的call發給過去  
  11.   boolean interrupted = false;  
  12.   synchronized (call) {  
  13.     while (!call.done) {  
  14.       try {  
  15.         call.wait();                           // wait for the result  
  16. // 等待調用結果  
  17.       } catch (InterruptedException ie) {  
  18.         // save the fact that we were interrupted  
  19.         interrupted = true;  
  20.       }  
  21.     }  
  22.   
  23.     if (interrupted) {  
  24.       // set the interrupt flag now that we are done waiting  
  25.       Thread.currentThread().interrupt();  
  26.     }  
  27.   
  28.     if (call.error != null) {  
  29.       if (call.error instanceof RemoteException) {  
  30.         call.error.fillInStackTrace();  
  31.         throw call.error;  
  32.       } else { // local exception  
  33.         throw wrapException(addr, call.error);  
  34.       }  
  35.     } else {  
  36.       return call.value;  
  37. // 返回  
  38.     }  
  39.   }  
  40. }  



7、現在,一目瞭然了 

Java代碼  收藏代碼
  1. datanode向namenode發送heartbeat過程是這樣的:  
  2.   
  3.     a) 在datanode初始化獲得namenode的proxy  
  4.     b) 在datanode上,調用namenode proxy的heartbeat方法:  
  5.         namenode.sendHeartbeat(dnRegistration,  
  6.                                                        data.getCapacity(),  
  7.                                                        data.getDfsUsed(),  
  8.                                                        data.getRemaining(),  
  9.                                                        xmitsInProgress.get(),  
  10.                                                        getXceiverCount());  
  11.     c) 在datanode上的namenode動態代理類將這個調用包裝成(或者叫“序列化成”)一個Invocation對象,並調用client.call方法  
  12.     d) client call方法將Invocation轉化爲Call對象  
  13.     e) client 將call發送到真正的namenode服務器  
  14.     f) namenode接收後,轉化成namenode端的Call,並process後,通過Responder發回來!  
  15.     g) datanode接收結果,並將結果轉化爲DatanodeCommand[]  
  16.           



8、再看動態代理 

動態代理:讓“只有接口,沒事對應的實現類”成爲可能,因爲具體方法的實現可以委託給另一個類!! 

在這個例子中,就datanode而言,DatanodeProtocol接口是沒有實現類的! 




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