datanode節點在啓動的時候,有一個向namenode註冊的過程,那麼註冊的過程中datanode到底向namenode傳遞了哪些信息?
private void register() throws IOException {
if (dnRegistration.getStorageID().equals("")) {
setNewStorageID(dnRegistration);
}
while(shouldRun) {
try {
// reset name to machineName. Mainly for web interface.
dnRegistration.name = machineName + ":" + dnRegistration.getPort();
dnRegistration = namenode.register(dnRegistration);//dnRegistration
break;
} catch(SocketTimeoutException e) { // namenode is busy
LOG.info("Problem connecting to server: " + getNameNodeAddr());
try {
Thread.sleep(1000);
} catch (InterruptedException ie) {}
}
}
dnRegistration對象封裝了datanode的註冊信息,datanode初始化的時候創建一個DatanodeRegistration實例,我們看是傳進去了機器名和端口號。this.dnRegistration = new DatanodeRegistration(machineName + ":" + tmpPort);啥是機器名?
if (conf.get("slave.host.name") != null) {
machineName = conf.get("slave.host.name");
}
if (machineName == null) {
machineName = DNS.getDefaultHost(
conf.get("dfs.datanode.dns.interface","default"),
conf.get("dfs.datanode.dns.nameserver","default"));
}
看到沒有,機器名你可以手工配置key就叫slave.host.name,但是這個多數都不配,因爲對於大規模集羣來說,一個個節點去配置機器名是很沒效率的。
所以datanode怎麼獲取呢,一種方式是找dns要,但是一般也不配置dns,因爲這樣又把dns服務牽扯進來,有些集羣的數據節點沒有配置dns或者dns壓力比較大,所以基本上都不配置dfs.datanode.dns.interface或者dfs.datanode.dns.nameserver,查看hdfs-default.xml可以看出都是default,那下面還有啥辦法?我擦只能找hosts了,就是本機配置的/etc/hosts文件,這裏裏邊建立了ip和機器名的對應關係,一般的例如hosts文件你可以這樣配置:
127.0.0.1 YZSJHL19-87 localhost.localdomain localhost
10.4.19.87 YZSJHL19-87.opi.com
public static String getDefaultHost(String strInterface, String nameserver)
throws UnknownHostException {
if (strInterface.equals("default"))
return InetAddress.getLocalHost().getCanonicalHostName();
if (nameserver != null && nameserver.equals("default"))
return getDefaultHost(strInterface);
String[] hosts = getHosts(strInterface, nameserver);
return hosts[0];
}
然後看上面返回的結果是啥,第一個if就返回了,返回結果是string類型的機器名即YZSJHL19-87.opi.com,InetAddress.getLocalHost()返回的結果如下
YZSJHL19-87.opi.com/10.4.19.87,也就說你使用shell命令行顯示的機器名是啥,dn往namenode註冊的機器名就是啥。
好了,現在看看端口號是什麼,就是前面提到的tmpPort,
public static String getServerAddress(Configuration conf,
String oldBindAddressName,
String oldPortName,
String newBindAddressName) {
String oldAddr = conf.get(oldBindAddressName);
String oldPort = conf.get(oldPortName);
String newAddrPort = conf.get(newBindAddressName);
if (oldAddr == null && oldPort == null) {
return newAddrPort;
}
String[] newAddrPortParts = newAddrPort.split(":",2);
if (newAddrPortParts.length != 2) {
throw new IllegalArgumentException("Invalid address/port: " +
newAddrPort);
}
if (oldAddr == null) {
oldAddr = newAddrPortParts[0];
} else {
LOG.warn("Configuration parameter " + oldBindAddressName +
" is deprecated. Use " + newBindAddressName + " instead.");
}
if (oldPort == null) {
oldPort = newAddrPortParts[1];
} else {
LOG.warn("Configuration parameter " + oldPortName +
" is deprecated. Use " + newBindAddressName + " instead.");
}
return oldAddr + ":" + oldPort;
}
首先,需要使用上面的方法獲取一個網絡地址,傳入的分別是下面的三個key,
public static InetSocketAddress getStreamingAddr(Configuration conf) {
String address =
NetUtils.getServerAddress(conf,
"dfs.datanode.bindAddress",
"dfs.datanode.port",
"dfs.datanode.address");
return NetUtils.createSocketAddr(address);
}
<property>
<name>dfs.datanode.address</name>
<value>0.0.0.0:50010</value>
<description>
The datanode server address and port for data transfer.
</description>
</property>
注意啊,在hdfs-default.xml文件裏就只配置了dfs.datanode.address,它的值是0.0.0.0:50010,這樣對於getServerAddress方法,這時候直接返回這個0.0.0.0:50010這個地址。
InetSocketAddress socAddr = DataNode.getStreamingAddr(conf);
int tmpPort = socAddr.getPort();
其實這個socAddr就用了這一下,目的就是拿到50010,前面的Ip地址不全是0也無所謂,都只拿到50010端口。
這樣datanode註冊時的節點名字就有了,就是”機器名:端口號”,注意不是ip。
再回到註冊部分的代碼,有了節點名後還不行,還要有個存儲id:
if (dnRegistration.getStorageID().equals("")) {
setNewStorageID(dnRegistration);
}
一開始dnRegistration是沒有StorageID的,看一下setNewStorageID是啥吧,很簡單是吧,存儲id是DS開頭+隨機號+ip+端口號+datanode註冊時的時間戳。
public static void setNewStorageID(DatanodeRegistration dnReg) {
String ip = "unknownIP";
try {
ip = DNS.getDefaultIP("default");
} catch (UnknownHostException ignored) {
LOG.warn("Could not find ip address of \"default\" inteface.");
}
int rand = 0;
try {
rand = SecureRandom.getInstance("SHA1PRNG").nextInt(Integer.MAX_VALUE);
} catch (NoSuchAlgorithmException e) {
LOG.warn("Could not use SecureRandom");
rand = R.nextInt(Integer.MAX_VALUE);
}
dnReg.storageID = "DS-" + rand + "-"+ ip + "-" + dnReg.getPort() + "-" +
System.currentTimeMillis();
}//DS-1402344465-10.4.19.59-50010-1374756186082
到這裏,datanode的自身的情況就搞定了。也說清楚了。但是datanode向namenode註冊,不是說你想註冊就能註冊,隨便起個服務就能向namenode註冊,你想加入幫派,至少對幫派要有所瞭解吧?所以這裏還有些比較重要的機制。
1、通過握手瞭解namenode,使datanode加強對組織的瞭解,namenode會發放一些信息,等datanode註冊的時候就要憑藉這個信物做簡單的認證。
private NamespaceInfo handshake() throws IOException {
NamespaceInfo nsInfo = new NamespaceInfo();
while (shouldRun) {
try {
nsInfo = namenode.versionRequest();
break;
} catch(SocketTimeoutException e) { // namenode is busy
LOG.info("Problem connecting to server: " + getNameNodeAddr());
try {
Thread.sleep(1000);
} catch (InterruptedException ie) {}
}
}//通過握手拿到namenode的namespace信息,包括namespaceID,版本號和其他問題
String errorMsg = null;
// verify build version
if( ! nsInfo.getBuildVersion().equals( Storage.getBuildVersion() )) {
//通過比較namenode和datanode自己的編譯版本號確定是否匹配
errorMsg = "Incompatible build versions: namenode BV = "
+ nsInfo.getBuildVersion() + "; datanode BV = "
+ Storage.getBuildVersion();
LOG.fatal( errorMsg );
notifyNamenode(DatanodeProtocol.NOTIFY, errorMsg);
throw new IOException( errorMsg );
}
assert FSConstants.LAYOUT_VERSION == nsInfo.getLayoutVersion() :
"Data-node and name-node layout versions must be the same."
+ "Expected: "+ FSConstants.LAYOUT_VERSION + " actual "+ nsInfo.getLayoutVersion();
return nsInfo;
}
datanode通過versionRequest方法獲取系統版本信息。 synchronized NamespaceInfo getNamespaceInfo() {
return new NamespaceInfo(dir.fsImage.getNamespaceID(),
dir.fsImage.getCTime(),
getDistributedUpgradeVersion());
}
namenode返回的是namespaceID(namespace的id都是在namenodeformat的時候產生的隨機數,取31位),Ctime初始化的時候都是0 ,只有系統升級的時候纔會改變這個數據,所有Ctime就認爲是0可以了;後面的分佈式升級版本號,先不管,生產環境中一般不這樣升級。這樣握手就得到了兩個比較重要的信息,一個是namespaceID,再一個是Ctime。
2、獲取datanode數據盤的VERSION信息,如果沒有就格式化。
recoverTransitionRead方法完成了這個過程,首先將每個數據盤都描述成StorageDirectory對象,StorageDirectory對象有個成員叫root所謂root就是數據目錄的首層,比如我們指定/disk6/hadoop/dfs.data爲其數據盤,那麼/disk6/hadoop/dfs.data就是這個存儲空間的根路徑。然後逐個數據目錄進行分析
a、首先必須確保root不爲空。
b、如果你指定的數據目錄不存在,比如/disk6這快盤是空的,那麼/disk6/hadoop/dfs.data顯然不存在,這種情況下,datanode啓動選項中如果沒有-format參數,那麼你就會在日誌中看到“Storage directory " + rootPath + " does not exist."”,比如我們在線修了一塊盤,修完回來肯定是空的,我們再按照原來的配置文件重啓datanode,那麼就會出現這個日誌。
c、如果你指定的數據盤不是目錄,對不起也不行。
d、如果你指定的數據盤不能寫入,比如這個目錄的權限是root的755,那麼普通的hadoop啓動賬戶也沒法使用這快盤,日誌“Cannot access storage directory " + rootPath”
下面,如果這個數據目錄正常,即存在的可寫目錄。
路徑檢查完了,還要處理一下storage這個文件,這是二進制文件,通過讀第一個int數據判斷存儲版本號是否過期,這部分回頭結合datanode的格式化再說吧。
繼續往下就是解析VERSION文件,如果這個文件存在就返回NORMAL狀態,其餘的先不梳理了,將專門有一篇博文處理這些細節。
如果既沒有Version文件也沒有那些中間狀態的目錄或者文件,那麼datanode認爲這個目錄需要格式化,比如你弄個測試集羣,你把datanode的Version文件刪掉,這時候,datanode對這個目錄的分析結果就是需要格式化,返回NOT_FORMATTED狀態,
這時就會自動格式化這個目錄:
void format(StorageDirectory sd, NamespaceInfo nsInfo) throws IOException {
sd.clearDirectory(); // create directory
this.layoutVersion = FSConstants.LAYOUT_VERSION;
this.namespaceID = nsInfo.getNamespaceID();
this.cTime = 0;
// store storageID as it currently is
sd.write();
}
看到沒有第一步就是可怕的清空存儲目錄:fullyDelete,好可怕。
public void clearDirectory() throws IOException {
File curDir = this.getCurrentDir();
if (curDir.exists())
if (!(FileUtil.fullyDelete(curDir)))
throw new IOException("Cannot remove current directory: " + curDir);
if (!curDir.mkdirs())
throw new IOException("Cannot create directory " + curDir);
}
然後寫入佈局版本,這個同一版本的hadoop的nn,dn,tt,jt都是一樣的。然後把從namenod握手得到的namespaceid,還記得嗎31位的隨機數,然後把這些東西全部寫到VERSION文件中。
格式化完了,這就是一個合格的數據存儲目錄了,把這個目錄加到存儲目錄的列表裏。然後全部更新本機全部存儲目錄的VERSION文件,因爲這一次的握手建立的信息要保證全部數據目錄保持一致。
這樣dn的存儲信息算是齊備了,後面還有兩點,一個是http服務端口號,同時datanode自己還起了一個rpcserver,這兩個服務的連接信息也會交給namenode,這時候要給註冊信息補充這些信息:
public void setStorageInfo(DataStorage storage) {
this.storageInfo = new StorageInfo(storage);
this.storageID = storage.getStorageID();
}
一個是StorageInfo,就是VERSION文件的內容,二是StorageID(DS-隨機數-IP-port-註冊的當前時間戳),再回到DataNode的註冊方法:
private void register() throws IOException {
if (dnRegistration.getStorageID().equals("")) {
setNewStorageID(dnRegistration);
}
while(shouldRun) {
try {
// reset name to machineName. Mainly for web interface.
dnRegistration.name = machineName + ":" + dnRegistration.getPort();
dnRegistration = namenode.register(dnRegistration);
break;
} catch(SocketTimeoutException e) { // namenode is busy
查看namenode端實現的register方法: public DatanodeRegistration register(DatanodeRegistration nodeReg
) throws IOException {
verifyVersion(nodeReg.getVersion());
namesystem.registerDatanode(nodeReg);
return nodeReg;
}
首先匹配一下layout版本,這個同一個版本的這個都是寫死的,這是爲了防止不同版本的dn註冊進來。再看registerDatanode方法:
String dnAddress = Server.getRemoteAddress();
if (dnAddress == null) {
// Mostly called inside an RPC.
// But if not, use address passed by the data-node.
dnAddress = nodeReg.getHost();//儘量拿到IPdatanode的IP地址,實在不行拿到主機名也行
}
// check if the datanode is allowed to be connect to the namenode
if (!verifyNodeRegistration(nodeReg, dnAddress)) {
throw new DisallowedDatanodeException(nodeReg);
}
String hostName = nodeReg.getHost();
// update the datanode's name with ip:port
DatanodeID dnReg = new DatanodeID(dnAddress + ":" + nodeReg.getPort(),
nodeReg.getStorageID(),
nodeReg.getInfoPort(),
nodeReg.getIpcPort());
nodeReg.updateRegInfo(dnReg);
nodeReg.exportedKeys = getBlockKeys();
NameNode.stateChangeLog.info(
"BLOCK* NameSystem.registerDatanode: "
+ "node registration from " + nodeReg.getName()
+ " storage " + nodeReg.getStorageID());
首先嚐試獲取datanode的ip,如果獲取失敗,就想辦法拿到datanode的機器名,然後檢查datanode是不是在黑名單裏,一般是不設置白名單的。
爲了描述一個datanode,這裏採用DataNodeID對象,主要信息是ip+端口號+存儲id+http服務器端口號+datanode自己的rpc服務器服務端口號。
DatanodeDescriptor nodeS = datanodeMap.get(nodeReg.getStorageID());
DatanodeDescriptor nodeN = host2DataNodeMap.getDatanodeByName(nodeReg.getName());
if (nodeN != null && nodeN != nodeS) {
NameNode.LOG.info("BLOCK* NameSystem.registerDatanode: "
+ "node from name: " + nodeN.getName());
// nodeN previously served a different data storage,
// which is not served by anybody anymore.
removeDatanode(nodeN);
// physically remove node from datanodeMap
wipeDatanode(nodeN);
nodeN = null;
}
namenode的大管家FSNamesystem對象,維護者2張表,一個是datanodeMap,以存儲ID做key,以datanode描述符做value;另一張表示機器機器域名或者IP做key,datanode描述符做value,至於這倆表幹啥用的,先不管了。
下面進行判斷,如果從第二張表找到了一個datanode描述符,說明這個機器上曾經註冊上來過一個datanode,但是又跟存儲id那張表找到的不是一個,那麼不管第一張表裏找到沒有,都認爲前期的那個版本失效,從心跳列表中將其刪除。同時從兩張表中將其刪除。
如果從第一張表裏即存儲id的datanode描述符存在,並且與第二張表找到的一樣,那麼認爲這個datanode是重啓的。如果不相等說明遺漏了這個存儲節點,需要繼續補充完整這個存儲節點的信息。具體涉及到心跳隊列、以及機架感知和網路拓撲。