Hadoop-RPC底層
RPC 是遠程過程調用(Remote Procedure Call),即遠程調用其他虛擬機中運行的 java object。RPC 是一種客戶端/服務器模式,那麼在使用時包括服務端代碼和客戶端代碼,還有我們調用的遠程過程對象。
Hadoop在實現時拋棄了JDK自帶的一個RPC實現——RMI,而自己基於IPC模型實現了一個更高效的輕量級RPC。 RPC是hadoop框架運行的基礎。
1. 服務端提供的對象必須是一個接口,接口extends VersioinedProtocal
2. 客戶端對象中的方法必須位於對象的接口中。
RPC通常採用客戶端服務器模型,其框架主要有以下幾部分
- 通信模塊:實現請求應該協議。主要分爲同步方式和異步方式。
- stub程序:客戶端和服務器均包含stub程序,可以看做代理程序。使得遠程函數表現的跟本地調用一樣,對用戶程序完全透明。
- 調度程序:接受來自通信模塊的請求消息,根據標識選擇stub程序處理。併發量大一般採用線程池處理。
- 客戶程序/服務過程:請求發出者和請求的處理者。
1.3 RPC流程圖
HDFS通信協議有兩種,一種是Hadoop RPC接口,一種是流式接口,那麼這兩種接口各自有各自的分工,前者主要是負責一些連接的管理、節點的管理以及一些數據的管理,而後者主要是數據的讀寫傳輸。
不同於流式接口,Hadoop RPC接口是基於protobuf實現的,protobuf是google的一種數據格式,這裏不做細究。那麼Hadoop RPC的接口主要有那麼幾個,包括ClientProtocol,ClientDatanodeProtocol,DatanodeProtocol,InterDatanodeProtocol,NamenodeProtocol這幾個,這幾個接口都是節點間的主要通信接口。
RPC接口
NameNode 本身就是一個 java 進程。RPC.getServer()方法的第一個參數是 this,說明 NameNode 本身就是一個位於服務端的被調用對象,即 NameNode 中的方法是可以被客戶端代碼調用的。根據 RPC 運行原理可知,NameNode暴露給客戶端的方法位於接口中。繼續查看namenode類的接口實現,可以看到 NameNode 實現了 ClientProtocal、DatanodeProtocal、NamenodeProtocal 等接口。
hadoop 和hbase中的大部分服務都是通過hadoop.ipc.RPC這個類來實現的。hadoop.ipc.RPC 實現了一種遠程過程調用的框架,應用可以直接定義過程調用的協議接口和協議的server端實現,就可以直接通過RPC框架獲得RPC server和client端的接口代理。
hadoop.ipc.RPC 的實現利用了 hadoop.ipc.Server 和 hadoop.ipc.Client這兩個類, 這兩個類實現了網絡中非常典型的Request-Response模式服務器和客戶端框架。用戶可以通過定義一個協議接口並實現出Request和Response類,以及Server端的抽象處理接口(Server.call()) 就可以實現出完整的服務器程序,而客戶端程序只需要在創建hadoop.ipc.Client實體時,指定協議接口和網絡相關參數,然後調用 call() 就可以發送請求並獲取響應。
Hadoop.ipc.RPC作爲Hadoop的底層核心組件,在hadoop HDFS,MapReduce以及HBase中都有廣泛的使用。 HDFS中NameNode,DataNode等都是通過實現對應協議的接口,然後利用hadoop.ipc.RPC獲取服務器實體的。 HBase中的HBaseRPC採用的也是與hadoop.ipc.RPC類似的實現,其中的Region Server, Master Server 都是通過實現對應的協議接口直接獲取服務器實體的。 hadoop.ipc將應用邏輯與網絡消息的處理分離開,並且使得邏輯對象在不同的進程或組件之間有同樣的語言接口,無需區分遠程對象和本地對象,使得開發者可以關注於應用的處理邏輯。
hadoop.ipc.RPC類中有兩個重要的函數getServer和getProxy,getServer通過接口協議實現的實體來獲取真正的server,getProxy獲取遠程訪問的本地代理。
客戶端和服務器端的關係
- Client-NameNode之間,其中NameNode是服務器
- Client-DataNode之間,其中DataNode是服務器
- DataNode-NameNode之間,其中NameNode是服務器
- DataNode-DateNode之間,其中某一個DateNode是服務器,另一個是客戶端
1. ClientDatanodeProtocol接口,這個接口是Client端和Datanode端通信使用的,主要有getReplicationVisibleLength()、getBlockLocalPathInfo()、refreshNamenodes()、deleteBlockPool()、getHdfsBlocksMetadata()、shutdownDatanode()這麼些方法,我們從這些方法名可以看到,這些方法基本上都是與數據塊的管理相關,很顯然嘛,Datanode主要的用途就是存儲數據嘛,他又不能自己管理數據。
2. Clientprotocol是主要接口,由80多個方法,如create、delete、mkdirs、rename等方法,由DFSclient調用。DFSClient可以連接hdfs文件系統並執Clientprotocol是主要接口,由80多個方法,如create、delete、mkdirs、rename等方法,由DFSclient調用。DFSClient可以連接hdfs文件系統並執行一些基本的文件任務,它使用ClientProtocol與NameNode建立通信另外它也可以直接連接DataNode並讀取或更改上面的數據塊。
注意,我們在程序中使用不能直接使用DFSClient對象,我們通過FileSystem對象來使用DFSClient對象。它使用ClientProtocol與NameNode建立通信。另外它也可以直接連接DataNode並讀取或更改上面的數據塊。 我們在程序中使用不能直接使用DFSClient對象,我們通過FileSystem對象來使用DFSClient對象。
3. NameNode實現了DataNodeProtocol ,DataNode通過調用此接口與NameNode建立並保持通信,將自己的塊信息和負載信息上傳給NameNode(registerDatanode方法和sendHeartbeat方法)。Namenode再存入本身的兩張表中。NameNode 不 能 向 DataNode 發 送 消 息 , 只 能 通 過 該 接 口 中 方 法 的 返 回 值 向DataNode 傳遞消息。本身實現的方法除了傳遞信息外,只包括管理數據和元數據等,並不能直接操作數據本身。
4. NamenodeProtocal 由SecondaryNameNode 調用,專門做NameNode 中 edits 文件向 fsimage 合併數據的。
5. InterDatanodeProtocol接口,datanode之間相互通信的接口,我們所說的副本就是通過datanode之間的通信來實現複製的而不是通過namenode同時將文件數據寫到三個副本中。
流式接口
流式接口有兩種,一種是基於TCP的DataTransferProtocol,一種是HA機制的active namenode和standby namenode間的HTTP接口,第二種先不說,因爲涉及HA機制節點的切換以及fsimage和editlog的合併方式等等
DataTransferProtocol,這個接口最主要的方法就是readBlock()、writeBlock()和transferBlock()了。讀數據塊、寫數據塊以及數據塊的複製就是靠這些方法來實現。其中數據塊的複製是依賴於InterDatanodeProtocol接口建立通信
HDFS文件讀寫:首先是HDFS客戶端會調用DistributedFileSystem.open()打開跟集羣的連接,並且打開文件,這個方法底層會調用ClientProtocol接口的open()方法,然後返回一個數據流給客戶端,此時客戶端會在調用接口的getBlockLocations()方法得到文件的一個數據塊的位置等等信息,然後客戶端就會通過數據流調用read()方法從這些位置信息裏面選出一個最有的節點來進行數據讀取(一般三個副本位置會選取網絡開銷最少的那個節點,如本地節點),傳輸完畢之後,客戶端會再調用getBlockLocations方法得到下一個數據塊的位置信息,然後開始讀,知道數據讀取結束,客戶端調用close()方法,關閉數據流。
寫文件就稍微比讀文件要複雜一些。首先客戶端會調用DistributedFileSystem.create()方法在hdfs中創建一個新的空文件,這個方法會在底層調用ClientProtocol.create()方法,namenode會在文件目錄樹下添加一個新的文件,並且將操作更新到editlog中,此舉之後,集羣會返回一個數據輸出流,然後客戶端就可以開始通過調用write()方法寫數據到數據流中了,但是此時namenode中並沒有任何這個數據的數據塊元數據映射,所以數據流會調用addBlock()方法獲取要寫入datanode節點的信息,然後就是write()方法的調用寫數據了,寫到一個節點上之後,這個節點就開始建立與另外的datanode的連接,然後將數據複製到其他datanode,從而實現副本。當副本寫完之後,第一個datanode就會返回一個確認包,確認數據已經寫入完畢,並且調用blockReceivedAndDeleted()方法告訴namenode要更新內存元數據的數據,然後開始下一個數據塊的寫入,當數據寫入完畢之後,調用close()方法關閉數據流。
接口的連接過程
hadoop爲何要使用RPC?在HDFS中,我們通過jsp可查看到有DataNode,NameNode,SecondaryNameNode主要進程(樓主只啓動了HDFS),我們客戶端Client與NameNode通信,NameNode與DataNode的通信,都是在不同進程間,不同系統間的通信。
- 通過socket,建立按需連接的共享TCP
- 尋址:節點A告訴節點B如何連接到端口,使用什麼方法
- 將數據序列化傳遞到節點B(服務器),服務器反序列化並執行
具體實現:RPC實現中有一個接口,負責綁定IP和端口。基於對HDFS的RPC底層通信理解,我們試着手動實現一個Hadoop類型的RPC通信框架,由client和server組成。
自定義的RPC
首先定義遠程調用類的接口,接口繼承的 VersionedProtocal,是hadoop 的 RPC 的接口,所有的 RPC 通信必須實現這個一接口,用於保證客戶端和服務端的端口一致。服務端被調用的類必須繼承這個接口 VersionedProtocal。
package com.RPC;
import org.apache.hadoop.ipc.VersionedProtocol;
public interface MyBizable extends VersionedProtocol{
//定義抽象類方法hello
public abstract String hello(String name);
}
2.然後編寫遠程調用類,實現這個接口MyBizable,這裏面有兩個方法被實現,一個就是 hello方法,另一個是 getProtocalVersion 方法。
package com.RPC;
import java.io.IOException;
//實現接口MyBizable,重寫hello和getProtocolVersion方法
public class MyBiz implements MyBizable{
public static long BIZ_VERSION = 123456L;
@Override
public String hello(String name) {
System.out.println("我是ByBiz,我被調用了。");
return "hello" + name;
}
@Override
public long getProtocolVersion(String protocol, long clientVersion)
throws IOException {
//<span style="color:#FF0000;">返回BIZ_VERSION,保證服務器和客戶端請求版本一致</span>
return BIZ_VERSION;
}
}
3.有了遠程調用對象,我們就可以編寫服務器端代碼,詳細在代碼中有介紹。
package com.RPC;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.ipc.RPC;
import org.apache.hadoop.ipc.RPC.Server;
public class MyServer {
//定義final類型服務器地址和端口
public static final String SERVER_ADDRESS = "localhost";
public static final int SERVER_PORT = 1234;
/**
* RPC是遠程過程調用(Remote Procedure Call)
*/
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
//重點RPC.getServer方法,該方法有四個參數,第一個參數是被調用的 java對象,
//第二個參數是服務器的地址,第三個參數是服務器的端口。獲得服務器對象後,
//啓動服務器。這樣,服務器就在指定端口監聽客戶端的請求。
final Server server = RPC.getServer(new MyBiz(), SERVER_ADDRESS, SERVER_PORT, conf);
server.start();
}
}
4.最後,我們就可以編寫客戶端代碼,來調用服務器方法,注意方法在服務器實現。
package com.RPC;
import java.net.InetSocketAddress;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.ipc.RPC;
public class MyClient {
/**
* RPC客戶端
*/
public static void main(String[] args) throws Exception {
//RPC.getProxy(),該方法有四個參數,第一個參數是被調用的接口類,
//第二個是客戶端版本號,第三個是服務端地址。返回的代理對象,
//就是服務端對象的代理,內部就是使用 java.lang.Proxy 實現的。
final MyBizable proxy = (MyBizable) RPC.getProxy(MyBizable.class,
MyBiz.BIZ_VERSION, new InetSocketAddress(MyServer.SERVER_ADDRESS, MyServer.SERVER_PORT) ,new Configuration() );
//調用接口中的方法
final String result = proxy.hello("world");
//打印返回結果,然後關閉網絡連接
System.out.println(result);
RPC.stopProxy(proxy);
}
}
注意上面RPC獲取代理方法中接口是調用對象的接口對象,由此可以得出在客戶端調用的業務類的方法是定義在業務類的接口中的。該接口實現了 VersionedProtocal 接口。完成了上面的操作,我們先啓動服務端,再啓動客戶端。觀察服務端和客戶端輸出信息。然後,我們在命令行輸入之前查看hadoop節點運行情況的命令jps。
我們可以看到一個 java 進程,是“MyServer”,該進程正是我們剛剛運行的 rpc 的服務端類MyServer。那麼可以判斷,hadoop 啓動時產生的 5 個 java 進程也應該是RPC 的服務端。我們觀察 NameNode 的源代碼,可以看到 NameNode 確實創建了RPC 的服務端(在namenode類的初始化Initialize方法中,爲方便觀察,我複製源碼,重點查看create server部分)
/**
* Initialize name-node.
*
* @param conf the configuration
*/
private void initialize(Configuration conf) throws IOException {
InetSocketAddress socAddr = NameNode.getAddress(conf);
UserGroupInformation.setConfiguration(conf);
SecurityUtil.login(conf, DFSConfigKeys.DFS_NAMENODE_KEYTAB_FILE_KEY,
DFSConfigKeys.DFS_NAMENODE_USER_NAME_KEY, socAddr.getHostName());
int handlerCount = conf.getInt("dfs.namenode.handler.count", 10);
// set service-level authorization security policy
if (serviceAuthEnabled =
conf.getBoolean(
ServiceAuthorizationManager.SERVICE_AUTHORIZATION_CONFIG, false)) {
PolicyProvider policyProvider =
(PolicyProvider)(ReflectionUtils.newInstance(
conf.getClass(PolicyProvider.POLICY_PROVIDER_CONFIG,
HDFSPolicyProvider.class, PolicyProvider.class),
conf));
ServiceAuthorizationManager.refresh(conf, policyProvider);
}
myMetrics = NameNodeInstrumentation.create(conf);
this.namesystem = new FSNamesystem(this, conf);
if (UserGroupInformation.isSecurityEnabled()) {
namesystem.activateSecretManager();
}
<span style="color:#FF0000;">// create rpc server</span>
InetSocketAddress dnSocketAddr = getServiceRpcServerAddress(conf);
if (dnSocketAddr != null) {
int serviceHandlerCount =
conf.getInt(DFSConfigKeys.DFS_NAMENODE_SERVICE_HANDLER_COUNT_KEY,
DFSConfigKeys.DFS_NAMENODE_SERVICE_HANDLER_COUNT_DEFAULT);
this.serviceRpcServer = <span style="color:#FF0000;">RPC.getServer(this, dnSocketAddr.getHostName(),
dnSocketAddr.getPort(), serviceHandlerCount,
false, conf, namesystem.getDelegationTokenSecretManager());</span>
this.serviceRPCAddress = this.serviceRpcServer.getListenerAddress();
setRpcServiceServerAddress(conf);
}
this.server = RPC.getServer(this, socAddr.getHostName(),
socAddr.getPort(), handlerCount, false, conf, namesystem
.getDelegationTokenSecretManager());
// The rpc-server port can be ephemeral... ensure we have the correct info
this.serverAddress = this.server.getListenerAddress();
FileSystem.setDefaultUri(conf, getUri(serverAddress));
LOG.info("Namenode up at: " + this.serverAddress);
startHttpServer(conf);
this.server.start(); //start RPC server
if (serviceRpcServer != null) {
serviceRpcServer.start();
}
startTrashEmptier(conf);
}
由上可以看到 NameNode 本身就是一個 java 進程。觀察圖 5-2 中 RPC.getServer()方法的第一個參數,發現是 this,說明 NameNode 本身就是一個位於服務端的被調用對象,即 NameNode 中的方法是可以被客戶端代碼調用的。根據 RPC 運行原理可知,NameNode暴露給客戶端的方法是位於接口中的。繼續查看namenode類的接口實現,可以看到 NameNode 實現了 ClientProtocal、DatanodeProtocal、NamenodeProtocal 等接口。
對於datanode節點的接口實現,分析思路大體一致,就是根據具體接口,查看接口協議功能,這是查看hadoop源碼的學習經驗,常用的一些myeclipse快捷鍵如下:
- 查看一個基類或者接口的派生類或實現類---鼠標指向類名,Ctrl + T ;
- 查看函數的調用關係(找到所有調用該方法的函數)--Ctrl + Alt + H (ubuntu系統快捷鍵佔用,可以類名右鍵找open call Hierarchy,結果在控制檯輸出) ;
- 快速查找類對象的相關信息 -- Ctrl + O(查找類名的所有成員變量和方法),F3查看類名的定義
具體快捷鍵可以通過鼠標類名,右鍵查看所有可用方法。