DFSClient是分佈式文件系統客戶端,它能夠連接到Hadoop文件系統執行指定任務,那麼它要與Namenode與Datanode基於一定的協議來進行通信。這個通信過程中,涉及到不同進程之間的通信。在org.apache.hadoop.ipc包中,定義了進程間通信的Client端與Server端的抽象,也就是基於C/S模式進行通信。這裏先對org.apache.hadoop.ipc包中有關類的源代碼閱讀分析。
首先看該包中類的繼承關係,如下所示:
- 。java.lang.Object
- 。org.apache.hadoop.ipc.Client
- 。org.apache.hadoop.ipc.RPC
- 。org.apache.hadoop.ipc.Server
- 。org.apache.hadoop.ipc.RPC.Server
- 。java.lang.Throwable (implements java.io.Serializable)
- 。java.lang.Exception
- 。java.io.IOException
- 。org.apache.hadoop.ipc.RemoteException
- 。org.apache.hadoop.ipc.RPC.VersionMismatch
我閱讀該包源程序的方法是,先從C/S通訊的兩端Client類與Server類來閱讀分析,然後再對實現的一個RPC類進行分析。
Client類
首先從Client客戶端類的實現開始,該類定義瞭如下屬性:
- private Hashtable<ConnectionId, Connection> connections = new Hashtable<ConnectionId, Connection>(); // 客戶端維護到服務端的一組連接
- private Class<? extends Writable> valueClass; // class of call values
- private int counter; // counter for call ids
- private AtomicBoolean running = new AtomicBoolean(true); // 客戶端進程是否在運行
- final private Configuration conf; // 配置類實例
- final private int maxIdleTime; // 連接的最大空閒時間
- final private int maxRetries; //Socket連接時,最大Retry次數
- private boolean tcpNoDelay; // 設置TCP連接是否延遲
- private int pingInterval; // ping服務端的間隔
- private SocketFactory socketFactory; // Socket工廠,用來創建Socket連接
- private int refCount = 1;
- final private static String PING_INTERVAL_NAME = "ipc.ping.interval"; // 通過配置文件讀取ping間隔
- final static int DEFAULT_PING_INTERVAL = 60000; // 默認ping間隔爲1分鐘
- final static int PING_CALL_ID = -1;
從屬性可以看出,一個Clinet主要處理的是與服務端進行連接的工作,包括連接的創建、監控等。爲了能夠瞭解到Client如何實現它所抽象的操作,先分別看一下該類定義的5個內部類:
- Client.Call內部類
該內部類,是客戶端調用的一個抽象,主要定義了一次調用所需要的條件,以及修改Client客戶端的一些全局統計變量,如下所示:
- private class Call {
- int id; // 調用ID
- Writable param; // 調用參數
- Writable value; // 調用返回的值
- IOException error; // 異常信息
- boolean done; // 調用是否完成
- protected Call(Writable param) {
- this.param = param;
- synchronized (Client.this) {
- this.id = counter++; // 互斥修改法:對多個連接的調用線程進行統計
- }
- }
- /** 調用完成,設置標誌,喚醒其它線程 */
- protected synchronized void callComplete() {
- this.done = true;
- notify(); // 喚醒其它調用者
- }
- /**
- * 調用出錯,同樣置調用完成標誌,並設置出錯信息
- */
- public synchronized void setException(IOException error) {
- this.error = error;
- callComplete();
- }
- /**
- * 調用完成,設置調用返回的值
- */
- public synchronized void setValue(Writable value) {
- this.value = value;
- callComplete();
- }
- }
上面的Call內部類主要是對一次調用的實例進行監視與管理,即使獲取調用返回值,如果出錯則獲取出錯信息,同時修改Client全局統計變量。
- Client.ConnectionId內部類
該內部類是一個連接的實體類,標識了一個連接實例的Socket地址、用戶信息UserGroupInformation、連接的協議類。每個連接都是通過一個該類的實例唯一標識。如下所示:
- InetSocketAddress address;
- UserGroupInformation ticket;
- Class<?> protocol;
- private static final int PRIME = 16777619;
該類中有一個用來判斷兩個連接ConnectionId實例是否相等的equals方法:
- @Override
- public boolean equals(Object obj) {
- if (obj instanceof ConnectionId) {
- ConnectionId id = (ConnectionId) obj;
- return address.equals(id.address) && protocol == id.protocol && ticket == id.ticket;
- }
- return false;
- }
只有當Socket地址、用戶信息UserGroupInformation、連接的協議類這三個屬性的值相等時,才被認爲是同一個ConnectionId實例。
- Client.ParallelResults內部類
該內部類是用來收集在並行調用環境中結果的實體類,如下所示:
- private static class ParallelResults {
- private Writable[] values; // 並行調用對應多次調用,對應多個返回值
- private int size; // 並行調用返回值個數統計
- private int count; // 並行調用次數
- public ParallelResults(int size) {
- this.values = new Writable[size];
- this.size = size;
- }
- /** 收集並行調用返回值 */
- public synchronized void callComplete(ParallelCall call) {
- values[call.index] = call.value; // 存儲返回值
- count++; // 統計返回值個數
- if (count == size) // 並行調用的多個調用完成
- notify(); // 喚醒下一個實例
- }
- }
- Client.ParallelCall內部類
該內部類繼承自上面的內部類Call,只是返回值使用上面定義的ParallelResults實體類來封裝,如下所示:
- private class ParallelCall extends Call {
- private ParallelResults results;
- private int index;
- public ParallelCall(Writable param, ParallelResults results, int index) {
- super(param);
- this.results = results;
- this.index = index;
- }
- /** 收集並行調用返回結果值 */
- protected void callComplete() {
- results.callComplete(this);
- }
- }
- Client.Connection內部類
該類是一個連接管理內部線程類,該內部類是一個連接線程,繼承自Thread類。它讀取每一個Call調用實例執行後從服務端返回的響應信息,並通知其他調用實例。每一個連接具有一個連接到遠程主機的Socket,該Socket能夠實現多路複用,使得多個調用複用該Socket,客戶端收到的調用得到的響應可能是無序的。
該類定義的屬性如下所示:
- private InetSocketAddress server; // 服務端ip:port
- private ConnectionHeader header; // 連接頭信息,該實體類封裝了連接協議與用戶信息UserGroupInformation
- private ConnectionId remoteId; // 連接ID
- private Socket socket = null; // 客戶端已連接的Socket
- private DataInputStream in;
- private DataOutputStream out;
- private Hashtable<Integer, Call> calls = new Hashtable<Integer, Call>(); // 當前活躍的調用列表
- private AtomicLong lastActivity = new AtomicLong();// 最後I/O活躍的時間
- private AtomicBoolean shouldCloseConnection = new AtomicBoolean(); // 連接是否關閉
- private IOException closeException; // 連接關閉原因
上面使用到了java.util.concurrent.atomic包中的一些工具,像AtomicLong、AtomicBoolean,這些類能夠以原子方式更新其值,支持在單個變量上解除鎖二實現線程的安全。這些類能夠使用get方法讀取volatile變量的內存效果,set方法可以設置對應變量的內存值。通過後面的代碼可以看到該類工具類的使用。例如:
- private void touch() {
- lastActivity.set(System.currentTimeMillis());
- }
上面定義的calls集合,是用來保存當前活躍的調用實例,以鍵值對的形式保存,鍵是一個Call的id,值是Call的實例,因此,該類一定提供了向該集合中添加新的調用實例、移除調用實例等等操作,分別將方法簽名列表如下:
- /**
- * 向calls集合中添加一個<Call.id, Call>
- */
- private synchronized boolean addCall(Call call);
- /*
- * 等待某個調用線程喚醒自己,可能開始如下操作:
- * 1、讀取RPC響應數據
- * 2、idle時間過長
- * 3、被標記爲應該關閉
- * 4、客戶端已經終止
- */
- private synchronized boolean waitForWork();
- /*
- * 接收到響應(因爲每次從DataInputStream in中讀取響應信息只有一個,無需同步)
- */
- private void receiveResponse();
- /*
- * 關閉連接,需要迭代calls集合,清除連接
- */
- private synchronized void close();
可以看到,當每次調用touch方法的時候,都會將lastActivity原子變量設置爲系統的當前時間,更新了變量的值。該操作是對多個線程進行互斥的,也就是每次修改lastActivity的值的時候,都會對該變量加鎖,從內存中讀取該變量的當前值,因此可能會出現阻塞的情況。
下面看一個Connection實例的構造實現:
- public Connection(ConnectionId remoteId) throws IOException {
- this.remoteId = remoteId; // 遠程服務端連接
- this.server = remoteId.getAddress(); // 遠程服務器地址
- if (server.isUnresolved()) {
- throw new UnknownHostException("unknown host: " + remoteId.getAddress().getHostName());
- }
- UserGroupInformation ticket = remoteId.getTicket(); // 用戶信息
- Class<?> protocol = remoteId.getProtocol(); // 協議
- header = new ConnectionHeader(protocol == null ? null : protocol.getName(), ticket); // 連接頭信息
- this.setName("IPC Client (" + socketFactory.hashCode() +") connection to " + remoteId.getAddress().toString() + " from " + ((ticket==null)?"an unknown user":ticket.getUserName()));
- this.setDaemon(true); // 並設置一個連接爲後臺線程
- }
通過Collection實例的構造,可以看到,客戶端所擁有的Connection實例,通過一個遠程ConnectionId實例來建立到客戶端到服務端的連接。接着看一下Connection線程類線程體代碼:
- public void run() {
- if (LOG.isDebugEnabled())
- LOG.debug(getName() + ": starting, having connections " + connections.size());
- while (waitForWork()) {// 等待某個連接實例空閒,如果存在則喚醒它執行一些任務
- receiveResponse(); // 接收RPC響應
- }
- close(); // 關閉
- if (LOG.isDebugEnabled())
- LOG.debug(getName() + ": stopped, remaining connections " + connections.size());
- }
客戶端Client類提供的最基本的功能就是執行RPC調用,其中,提供了兩種調用方式,一種就是串行單個調用,另一種就是並行調用,分別介紹如下。首先是串行單個調用的實現方法call,如下所示:
- /*
- * 執行一個調用,通過傳遞參數值param到運行在addr上的IPC服務器,IPC服務器基於protocol與用戶的ticket來認證,並響應客戶端
- */
- public Writable call(Writable param, InetSocketAddress addr, Class<?> protocol, UserGroupInformation ticket) throws InterruptedException, IOException {
- Call call = new Call(param); // 使用請求參數值構造一個Call實例
- Connection connection = getConnection(addr, protocol, ticket, call); // 從連接池connections中獲取到一個連接(或可能創建一個新的連接)
- connection.sendParam(call); // 向IPC服務器發送參數
- boolean interrupted = false;
- synchronized (call) {
- while (!call.done) {
- try {
- call.wait(); // 等待IPC服務器響應
- } catch (InterruptedException ie) {
- 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; // 調用返回的響應值
- }
- }
- }
然後,就是並行調用的實現call方法,如下所示:
- /*
- * 執行並行調用
- * 每個參數都被髮送到相關的IPC服務器,然後等待服務器響應信息
- */
- public Writable[] call(Writable[] params, InetSocketAddress[] addresses, Class<?> protocol, UserGroupInformation ticket) throws IOException {
- if (addresses.length == 0) return new Writable[0];
- ParallelResults results = new ParallelResults(params.length); // 根據待調用的參數個數來構造一個用來封裝並行調用返回值的ParallelResults對象
- synchronized (results) {
- for (int i = 0; i < params.length; i++) {
- ParallelCall call = new ParallelCall(params[i], results, i); // 構造並行調用實例
- try {
- Connection connection = getConnection(addresses[i], protocol, ticket, call); // 從連接池中獲取到一個連接
- connection.sendParam(call); // 發送調用參數
- } catch (IOException e) {
- LOG.info("Calling "+addresses[i]+" caught: " + e.getMessage(),e);
- results.size--; // 更新統計數據
- }
- }
- while (results.count != results.size) {
- try {
- results.wait(); // 等待一組調用的返回值(如果調用失敗,會返回錯誤信息或null值,但與計數相關)
- } catch (InterruptedException e) {}
- }
- return results.values; // 調用返回一組響應值
- }
- }
客戶端可以根據服務端暴露的遠程地址,來與服務器建立連接,並執行RPC調用,發送調用參數數據。
Server端有點複雜,後面單獨分析其實現過程和機制。