Hadoop-0.20.0源代碼分析(10)

DFSClient是分佈式文件系統客戶端,它能夠連接到Hadoop文件系統執行指定任務,那麼它要與Namenode與Datanode基於一定的協議來進行通信。這個通信過程中,涉及到不同進程之間的通信。在org.apache.hadoop.ipc包中,定義了進程間通信的Client端與Server端的抽象,也就是基於C/S模式進行通信。這裏先對org.apache.hadoop.ipc包中有關類的源代碼閱讀分析。

首先看該包中類的繼承關係,如下所示:

[java] view plaincopy
  1. 。java.lang.Object  
  2.     。org.apache.hadoop.ipc.Client  
  3.     。org.apache.hadoop.ipc.RPC  
  4.     。org.apache.hadoop.ipc.Server  
  5.           。org.apache.hadoop.ipc.RPC.Server  
  6.     。java.lang.Throwable (implements java.io.Serializable)  
  7.           。java.lang.Exception  
  8.                 。java.io.IOException  
  9.                       。org.apache.hadoop.ipc.RemoteException  
  10.                       。org.apache.hadoop.ipc.RPC.VersionMismatch  

我閱讀該包源程序的方法是,先從C/S通訊的兩端Client類與Server類來閱讀分析,然後再對實現的一個RPC類進行分析。

Client類

首先從Client客戶端類的實現開始,該類定義瞭如下屬性:

[java] view plaincopy
  1. private Hashtable<ConnectionId, Connection> connections = new Hashtable<ConnectionId, Connection>(); // 客戶端維護到服務端的一組連接  
  2. private Class<? extends Writable> valueClass;   // class of call values  
  3. private int counter;                            // counter for call ids  
  4. private AtomicBoolean running = new AtomicBoolean(true); // 客戶端進程是否在運行  
  5. final private Configuration conf; // 配置類實例  
  6. final private int maxIdleTime; // 連接的最大空閒時間  
  7. final private int maxRetries; //Socket連接時,最大Retry次數  
  8. private boolean tcpNoDelay; // 設置TCP連接是否延遲  
  9. private int pingInterval; // ping服務端的間隔  
  10.   
  11. private SocketFactory socketFactory;           // Socket工廠,用來創建Socket連接  
  12. private int refCount = 1;  
  13.   
  14. final private static String PING_INTERVAL_NAME = "ipc.ping.interval"// 通過配置文件讀取ping間隔  
  15. final static int DEFAULT_PING_INTERVAL = 60000//  默認ping間隔爲1分鐘  
  16. final static int PING_CALL_ID = -1;  

從屬性可以看出,一個Clinet主要處理的是與服務端進行連接的工作,包括連接的創建、監控等。爲了能夠瞭解到Client如何實現它所抽象的操作,先分別看一下該類定義的5個內部類:

  • Client.Call內部類

該內部類,是客戶端調用的一個抽象,主要定義了一次調用所需要的條件,以及修改Client客戶端的一些全局統計變量,如下所示:

[java] view plaincopy
  1. private class Call {  
  2.   int id;                                       // 調用ID  
  3.   Writable param;                               // 調用參數  
  4.   Writable value;                               // 調用返回的值  
  5.   IOException error;                            // 異常信息  
  6.   boolean done;                                 // 調用是否完成  
  7.   
  8.   protected Call(Writable param) {  
  9.     this.param = param;  
  10.     synchronized (Client.this) {  
  11.       this.id = counter++; // 互斥修改法:對多個連接的調用線程進行統計  
  12.     }  
  13.   }  
  14.   
  15.   /** 調用完成,設置標誌,喚醒其它線程  */  
  16.   protected synchronized void callComplete() {  
  17.     this.done = true;  
  18.     notify();                                 // 喚醒其它調用者  
  19.   }  
  20.   
  21.   /**  
  22.    * 調用出錯,同樣置調用完成標誌,並設置出錯信息 
  23.    */  
  24.   public synchronized void setException(IOException error) {  
  25.     this.error = error;  
  26.     callComplete();  
  27.   }  
  28.     
  29.   /**  
  30.    * 調用完成,設置調用返回的值 
  31.    */  
  32.   public synchronized void setValue(Writable value) {  
  33.     this.value = value;  
  34.     callComplete();  
  35.   }  
  36. }  

上面的Call內部類主要是對一次調用的實例進行監視與管理,即使獲取調用返回值,如果出錯則獲取出錯信息,同時修改Client全局統計變量。

  • Client.ConnectionId內部類

該內部類是一個連接的實體類,標識了一個連接實例的Socket地址、用戶信息UserGroupInformation、連接的協議類。每個連接都是通過一個該類的實例唯一標識。如下所示:

[java] view plaincopy
  1. InetSocketAddress address;  
  2. UserGroupInformation ticket;  
  3. Class<?> protocol;  
  4. private static final int PRIME = 16777619;  

該類中有一個用來判斷兩個連接ConnectionId實例是否相等的equals方法:

[java] view plaincopy
  1. @Override  
  2. public boolean equals(Object obj) {  
  3.  if (obj instanceof ConnectionId) {  
  4.    ConnectionId id = (ConnectionId) obj;  
  5.    return address.equals(id.address) && protocol == id.protocol && ticket == id.ticket;  
  6.  }  
  7.  return false;  
  8. }  

只有當Socket地址、用戶信息UserGroupInformation、連接的協議類這三個屬性的值相等時,才被認爲是同一個ConnectionId實例。

  • Client.ParallelResults內部類

該內部類是用來收集在並行調用環境中結果的實體類,如下所示:

[java] view plaincopy
  1. private static class ParallelResults {  
  2.   private Writable[] values; // 並行調用對應多次調用,對應多個返回值  
  3.   private int size; // 並行調用返回值個數統計  
  4.   private int count; // 並行調用次數  
  5.   
  6.   public ParallelResults(int size) {  
  7.     this.values = new Writable[size];  
  8.     this.size = size;  
  9.   }  
  10.   
  11.   /** 收集並行調用返回值 */  
  12.   public synchronized void callComplete(ParallelCall call) {  
  13.     values[call.index] = call.value;            // 存儲返回值  
  14.     count++;                                    // 統計返回值個數  
  15.     if (count == size)                          // 並行調用的多個調用完成  
  16.       notify();                                 // 喚醒下一個實例  
  17.   }  
  18. }  

  • Client.ParallelCall內部類

該內部類繼承自上面的內部類Call,只是返回值使用上面定義的ParallelResults實體類來封裝,如下所示:

[java] view plaincopy
  1. private class ParallelCall extends Call {  
  2.   private ParallelResults results;  
  3.   private int index;  
  4.     
  5.   public ParallelCall(Writable param, ParallelResults results, int index) {  
  6.     super(param);  
  7.     this.results = results;  
  8.     this.index = index;  
  9.   }  
  10.   
  11.   /** 收集並行調用返回結果值 */  
  12.   protected void callComplete() {  
  13.     results.callComplete(this);  
  14.   }  
  15. }  

  • Client.Connection內部類

該類是一個連接管理內部線程類,該內部類是一個連接線程,繼承自Thread類。它讀取每一個Call調用實例執行後從服務端返回的響應信息,並通知其他調用實例。每一個連接具有一個連接到遠程主機的Socket,該Socket能夠實現多路複用,使得多個調用複用該Socket,客戶端收到的調用得到的響應可能是無序的。

該類定義的屬性如下所示:

[java] view plaincopy
  1. private InetSocketAddress server;             // 服務端ip:port  
  2. private ConnectionHeader header;              // 連接頭信息,該實體類封裝了連接協議與用戶信息UserGroupInformation  
  3. private ConnectionId remoteId;                // 連接ID  
  4.   
  5. private Socket socket = null;                 // 客戶端已連接的Socket  
  6. private DataInputStream in;  
  7. private DataOutputStream out;  
  8.   
  9. private Hashtable<Integer, Call> calls = new Hashtable<Integer, Call>(); // 當前活躍的調用列表  
  10. private AtomicLong lastActivity = new AtomicLong();// 最後I/O活躍的時間  
  11. private AtomicBoolean shouldCloseConnection = new AtomicBoolean();  // 連接是否關閉  
  12. private IOException closeException; // 連接關閉原因  

上面使用到了java.util.concurrent.atomic包中的一些工具,像AtomicLong、AtomicBoolean,這些類能夠以原子方式更新其值,支持在單個變量上解除鎖二實現線程的安全。這些類能夠使用get方法讀取volatile變量的內存效果,set方法可以設置對應變量的內存值。通過後面的代碼可以看到該類工具類的使用。例如:

[java] view plaincopy
  1. private void touch() {  
  2.   lastActivity.set(System.currentTimeMillis());  
  3. }  

上面定義的calls集合,是用來保存當前活躍的調用實例,以鍵值對的形式保存,鍵是一個Call的id,值是Call的實例,因此,該類一定提供了向該集合中添加新的調用實例、移除調用實例等等操作,分別將方法簽名列表如下:

[java] view plaincopy
  1. /** 
  2.  * 向calls集合中添加一個<Call.id, Call> 
  3.  */  
  4. private synchronized boolean addCall(Call call);  
  5.   
  6. /*  
  7.  * 等待某個調用線程喚醒自己,可能開始如下操作: 
  8.  * 1、讀取RPC響應數據 
  9.  * 2、idle時間過長 
  10.  * 3、被標記爲應該關閉 
  11.  * 4、客戶端已經終止 
  12.  */  
  13. private synchronized boolean waitForWork();  
  14.   
  15. /*  
  16.  * 接收到響應(因爲每次從DataInputStream in中讀取響應信息只有一個,無需同步) 
  17.  */  
  18. private void receiveResponse();  
  19.   
  20. /*  
  21.  * 關閉連接,需要迭代calls集合,清除連接 
  22.  */  
  23. private synchronized void close();  

可以看到,當每次調用touch方法的時候,都會將lastActivity原子變量設置爲系統的當前時間,更新了變量的值。該操作是對多個線程進行互斥的,也就是每次修改lastActivity的值的時候,都會對該變量加鎖,從內存中讀取該變量的當前值,因此可能會出現阻塞的情況。

下面看一個Connection實例的構造實現:

[java] view plaincopy
  1. public Connection(ConnectionId remoteId) throws IOException {  
  2.   this.remoteId = remoteId; // 遠程服務端連接  
  3.   this.server = remoteId.getAddress(); // 遠程服務器地址  
  4.   if (server.isUnresolved()) {  
  5.     throw new UnknownHostException("unknown host: " + remoteId.getAddress().getHostName());  
  6.   }  
  7.     
  8.   UserGroupInformation ticket = remoteId.getTicket(); // 用戶信息  
  9.   Class<?> protocol = remoteId.getProtocol(); // 協議  
  10.   header = new ConnectionHeader(protocol == null ? null : protocol.getName(), ticket); // 連接頭信息        
  11.   this.setName("IPC Client (" + socketFactory.hashCode() +") connection to " + remoteId.getAddress().toString() + " from " + ((ticket==null)?"an unknown user":ticket.getUserName()));  
  12.   this.setDaemon(true); // 並設置一個連接爲後臺線程  
  13. }  

通過Collection實例的構造,可以看到,客戶端所擁有的Connection實例,通過一個遠程ConnectionId實例來建立到客戶端到服務端的連接。接着看一下Connection線程類線程體代碼:

[java] view plaincopy
  1. public void run() {  
  2.   if (LOG.isDebugEnabled())  
  3.     LOG.debug(getName() + ": starting, having connections " + connections.size());  
  4.   
  5.   while (waitForWork()) {// 等待某個連接實例空閒,如果存在則喚醒它執行一些任務  
  6.     receiveResponse(); // 接收RPC響應  
  7.   }   
  8.     
  9.   close(); // 關閉  
  10.     
  11.   if (LOG.isDebugEnabled())  
  12.     LOG.debug(getName() + ": stopped, remaining connections " + connections.size());  
  13. }  

客戶端Client類提供的最基本的功能就是執行RPC調用,其中,提供了兩種調用方式,一種就是串行單個調用,另一種就是並行調用,分別介紹如下。首先是串行單個調用的實現方法call,如下所示:

[java] view plaincopy
  1. /* 
  2.  * 執行一個調用,通過傳遞參數值param到運行在addr上的IPC服務器,IPC服務器基於protocol與用戶的ticket來認證,並響應客戶端  
  3.  */  
  4. public Writable call(Writable param, InetSocketAddress addr, Class<?> protocol, UserGroupInformation ticket) throws InterruptedException, IOException {  
  5.   Call call = new Call(param); // 使用請求參數值構造一個Call實例  
  6.   Connection connection = getConnection(addr, protocol, ticket, call); // 從連接池connections中獲取到一個連接(或可能創建一個新的連接)  
  7.   connection.sendParam(call);                 // 向IPC服務器發送參數  
  8.   boolean interrupted = false;  
  9.   synchronized (call) {  
  10.     while (!call.done) {  
  11.       try {  
  12.         call.wait();                           // 等待IPC服務器響應  
  13.       } catch (InterruptedException ie) {  
  14.         interrupted = true;  
  15.       }  
  16.     }  
  17.   
  18.     if (interrupted) {  
  19.       // set the interrupt flag now that we are done waiting  
  20.       Thread.currentThread().interrupt();  
  21.     }  
  22.   
  23.     if (call.error != null) {  
  24.       if (call.error instanceof RemoteException) {  
  25.         call.error.fillInStackTrace();  
  26.         throw call.error;  
  27.       } else { // local exception  
  28.         throw wrapException(addr, call.error);  
  29.       }  
  30.     } else {  
  31.       return call.value; // 調用返回的響應值  
  32.     }  
  33.   }  
  34. }  

然後,就是並行調用的實現call方法,如下所示:

[java] view plaincopy
  1. /* 
  2.  * 執行並行調用 
  3.  * 每個參數都被髮送到相關的IPC服務器,然後等待服務器響應信息 
  4.  */  
  5. public Writable[] call(Writable[] params, InetSocketAddress[] addresses, Class<?> protocol, UserGroupInformation ticket) throws IOException {  
  6.   if (addresses.length == 0return new Writable[0];  
  7.   ParallelResults results = new ParallelResults(params.length); // 根據待調用的參數個數來構造一個用來封裝並行調用返回值的ParallelResults對象  
  8.   synchronized (results) {  
  9.     for (int i = 0; i < params.length; i++) {  
  10.       ParallelCall call = new ParallelCall(params[i], results, i); // 構造並行調用實例  
  11.       try {  
  12.         Connection connection = getConnection(addresses[i], protocol, ticket, call); // 從連接池中獲取到一個連接  
  13.         connection.sendParam(call);             // 發送調用參數  
  14.       } catch (IOException e) {  
  15.         LOG.info("Calling "+addresses[i]+" caught: " + e.getMessage(),e);  
  16.         results.size--;                         //  更新統計數據  
  17.       }  
  18.     }  
  19.     while (results.count != results.size) {  
  20.       try {  
  21.         results.wait();                    // 等待一組調用的返回值(如果調用失敗,會返回錯誤信息或null值,但與計數相關)  
  22.       } catch (InterruptedException e) {}  
  23.     }  
  24.   
  25.     return results.values; // 調用返回一組響應值  
  26.   }  
  27. }  

客戶端可以根據服務端暴露的遠程地址,來與服務器建立連接,並執行RPC調用,發送調用參數數據。

Server端有點複雜,後面單獨分析其實現過程和機制。


發佈了4 篇原創文章 · 獲贊 2 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章