數據節點整體運行
數據節點通過數據節點存儲和文件系統數據集,管理着保存在Linux文件系統上的數據塊,通過流式接口提供數據塊的讀、寫、替換、複製和校驗信息等功能。建立在上述基礎上的數據節點,還需要維護和名字節點的關係,週期性地檢查數據塊,並作爲一個整體保證文件系統的正常工作。
數據節點與名字節點的交互
數據節點提供了對HDFS文件數據的讀寫支持,但是,客戶端訪問HDFS文件,在讀數據時,必須瞭解文件對應的數據塊的存儲位置;在寫數據時,需要名字節點提供目標數據節點列表,這些操作的實現,都離不開數據節點和名字節點的配合。
數據節點和名字節點間通過IPC接口DatanodeProtocol進行通信,數據節點是客戶端,名字節點是服務器。這個接口一共有11個方法,在對寫數據塊的流程進行分析時,已經涉及了這個接口中的遠程方法blockReceived()、reportBadBlocks()、commitBlockSynchronization()和nextGenerationStamp()。其中,前兩個方法用於告知名字節點寫數據的處理結果,後兩個方法直接應用與數據塊恢復流程中。
1、握手、註冊、數據塊上報和心跳
無論數據節點是以“-regular”參數啓動或者“-rollback”啓動,DataNode.startNode()方法都會通過handshake()間接調用DatanodeProtocol.versionRequest(),獲取名字節點的NamespaceInfo。NamespaceInfo包含了一些存儲管理相關的信息,數據節點的後續處理,如版本檢查、註冊,都需要使用NamespaceInfo中的信息。例如,在handshake()中,數據節點會保證它的構建信息和存儲系統結構版本號和名字節點一致,代碼如下:
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) {}
}
}
String errorMsg = null;
// verify build version
if( ! nsInfo.getBuildVersion().equals( Storage.getBuildVersion() )) {
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;
}
正常啓動數據節點,在handshake()調用後,數據節點必須通過遠程方法register()方法後名字節點註冊,註冊的輸入和輸出都是DatanodeRegistration對象,即數據節點的成員變量dnRegistration。DataNode.startNode()方法會根據數據節點的執行環境,構造對象。代碼如下:
/**
* This method starts the data node with the specified conf.
*
* @param conf - the configuration
* if conf's CONFIG_PROPERTY_SIMULATED property is set
* then a simulated storage based data node is created.
*
* @param dataDirs - only for a non-simulated storage data node
* @throws IOException
* @throws MalformedObjectNameException
* @throws MBeanRegistrationException
* @throws InstanceAlreadyExistsException
*/
void startDataNode(Configuration conf,
AbstractList<File> dataDirs, SecureResources resources
) throws IOException {
.....
storage = new DataStorage();
// construct registration
this.dnRegistration = new DatanodeRegistration(machineName + ":" + tmpPort);
.....
if (simulatedFSDataset) {
setNewStorageID(dnRegistration);
dnRegistration.storageInfo.layoutVersion = FSConstants.LAYOUT_VERSION;
dnRegistration.storageInfo.namespaceID = nsInfo.namespaceID;
// it would have been better to pass storage as a parameter to
// constructor below - need to augment ReflectionUtils used below.
conf.set("StorageId", dnRegistration.getStorageID());
try {
//Equivalent of following (can't do because Simulated is in test dir)
// this.data = new SimulatedFSDataset(conf);
this.data = (FSDatasetInterface) ReflectionUtils.newInstance(
Class.forName("org.apache.hadoop.hdfs.server.datanode.SimulatedFSDataset"), conf);
} catch (ClassNotFoundException e) {
throw new IOException(StringUtils.stringifyException(e));
}
} else { // real storage
// read storage info, lock data dirs and transition fs state if necessary
storage.recoverTransitionRead(nsInfo, dataDirs, startOpt);
// adjust
this.dnRegistration.setStorageInfo(storage);
// initialize data node internal structure
this.data = new FSDataset(storage, conf);
}
this.dnRegistration.setName(machineName + ":" + tmpPort);
// adjust info port
this.dnRegistration.setInfoPort(this.infoServer.getPort());
myMetrics = DataNodeInstrumentation.create(conf,
dnRegistration.getStorageID());
dnRegistration.setIpcPort(ipcServer.getListenerAddress().getPort());
......
}
DataNode的同名方法register()調用名字節點提供的註冊遠程方法,並處理應答。
數據節點順利註冊後,在register()方法中,還需要根據目前數據節點的配置情況執行一些後續處理,主要包括:可能的節點存儲DataStorage初始化,數據節點註冊完成後,名字節點會返回系統統一存儲標識等創建“VERSION”文件的必須信息,幫助數據節點完成節點存儲的初始化工作。至此,我們清楚數據節點“VERSION”文件中各個屬性的來源,它們大部分來自數據節點,是整個HDFS集羣的統一屬性,如namaspaceID和layoutVersion等;有一些是數據節點自己產生的,包括storageID和storageType。
register方法如下:
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
LOG.info("Problem connecting to server: " + getNameNodeAddr());
try {
Thread.sleep(1000);
} catch (InterruptedException ie) {}
}
}
assert ("".equals(storage.getStorageID())
&& !"".equals(dnRegistration.getStorageID()))
|| storage.getStorageID().equals(dnRegistration.getStorageID()) :
"New storageID can be assigned only if data-node is not formatted";
if (storage.getStorageID().equals("")) {
storage.setStorageID(dnRegistration.getStorageID());
storage.writeAll();
LOG.info("New storage id " + dnRegistration.getStorageID()
+ " is assigned to data-node " + dnRegistration.getName());
}
if(! storage.getStorageID().equals(dnRegistration.getStorageID())) {
throw new IOException("Inconsistent storage IDs. Name-node returned "
+ dnRegistration.getStorageID()
+ ". Expecting " + storage.getStorageID());
}
if (!isBlockTokenInitialized) {
/* first time registering with NN */
ExportedBlockKeys keys = dnRegistration.exportedKeys;
this.isBlockTokenEnabled = keys.isBlockTokenEnabled();
if (isBlockTokenEnabled) {
long blockKeyUpdateInterval = keys.getKeyUpdateInterval();
long blockTokenLifetime = keys.getTokenLifetime();
LOG.info("Block token params received from NN: keyUpdateInterval="
+ blockKeyUpdateInterval / (60 * 1000) + " min(s), tokenLifetime="
+ blockTokenLifetime / (60 * 1000) + " min(s)");
blockTokenSecretManager.setTokenLifetime(blockTokenLifetime);
}
isBlockTokenInitialized = true;
}
if (isBlockTokenEnabled) {
blockTokenSecretManager.setKeys(dnRegistration.exportedKeys);
dnRegistration.exportedKeys = ExportedBlockKeys.DUMMY_KEYS;
}
if (supportAppends) {
Block[] bbwReport = data.getBlocksBeingWrittenReport();
long[] blocksBeingWritten = BlockListAsLongs
.convertToArrayLongs(bbwReport);
namenode.blocksBeingWrittenReport(dnRegistration, blocksBeingWritten);
}
// random short delay - helps scatter the BR from all DNs
scheduleBlockReport(initialBlockReportDelay);
}
名字節點保存並持久化整個文件系統的文件目錄樹以及文件的數據快索引,但名字節點不持久化數據塊的保存位置。HDFS啓動時,數據節點需要報告它上面保存的數據塊信息,幫組名字節點建立數據塊和保存數據塊的數據節點的對應關係。這個操作會定時執行。通過FSDataset的getBlockReport()方法,DataNode.offerService()獲得當前數據節點上所有數據塊的列表,然後將這些數據塊信息序列化成一個長整形數組,發送數組到名字節點。遠程方法調用結束後,名字節點返回一個名字節點指令,數據節點隨後執行該指令。需要強調的是:數據塊上報blockReport()定期執行,爲數據節點和名字節點之間數據的一致性提供了重要的保證。代碼如下:
if (startTime - lastHeartbeat > heartBeatInterval) {
//
// All heartbeat messages include following info:
// -- Datanode name
// -- data transfer port
// -- Total capacity
// -- Bytes remaining
//
lastHeartbeat = startTime;
DatanodeCommand[] cmds = namenode.sendHeartbeat(dnRegistration,
data.getCapacity(),
data.getDfsUsed(),
data.getRemaining(),
xmitsInProgress.get(),
getXceiverCount());
myMetrics.addHeartBeat(now() - startTime);
//LOG.info("Just sent heartbeat, with name " + localName);
if (!processCommand(cmds))
continue;
}
......
// Send latest blockinfo report if timer has expired.
if (startTime - lastBlockReport > blockReportInterval) {
// Create block report
long brCreateStartTime = now();
Block[] bReport = data.getBlockReport();
// Send block report
long brSendStartTime = now();
DatanodeCommand cmd = namenode.blockReport(dnRegistration,
BlockListAsLongs.convertToArrayLongs(bReport));
// Log the block report processing stats from Datanode perspective
long brSendCost = now() - brSendStartTime;
long brCreateCost = brSendStartTime - brCreateStartTime;
myMetrics.addBlockReport(brSendCost);
LOG.info("BlockReport of " + bReport.length
+ " blocks took " + brCreateCost + " msec to generate and "
+ brSendCost + " msecs for RPC and NN processing");
//
// If we have sent the first block report, then wait a random
// time before we start the periodic block reports.
//
if (resetBlockReportTime) {
lastBlockReport = startTime - R.nextInt((int)(blockReportInterval));
resetBlockReportTime = false;
} else {
/* say the last block report was at 8:20:14. The current report
* should have started around 9:20:14 (default 1 hour interval).
* If current time is :
* 1) normal like 9:20:18, next report should be at 10:20:14
* 2) unexpected like 11:35:43, next report should be at 12:20:14
*/
lastBlockReport += (now() - lastBlockReport) /
blockReportInterval * blockReportInterval;
}
processCommand(cmd);
}
DataNode.offerService()循環中另一個和名字節點的重要交互式心跳,名字節點根據數據節點的定期心跳,判斷數據節點是否正常工作。心跳上報過程中,數據節點會發送能夠描述當前節點負載的一些信息,如數據節點存儲容器、目前已使用容量等,名字節點根據這些信息估計數據節點的工作狀態,均衡各個節點的負載。遠程方法sendHeartbeat()執行結束,會攜帶名字節點到數據節點的指令,數據節點執行這些指令,保證HDFS系統的健康、穩定運行。
名字節點指令的執行
名字節點通過IPC(主要是心跳)調用返回值,通知數據節點執行名字節點指令,這些指令最後都由DataNode.processCommand()方法處理。方法的主體是一個case語句,根據命令編號執行不同的方法。代碼如下:
private boolean processCommand(DatanodeCommand cmd) throws IOException {
if (cmd == null)
return true;
final BlockCommand bcmd = cmd instanceof BlockCommand? (BlockCommand)cmd: null;
switch(cmd.getAction()) {
case DatanodeProtocol.DNA_TRANSFER:
// Send a copy of a block to another datanode
transferBlocks(bcmd.getBlocks(), bcmd.getTargets());
myMetrics.incrBlocksReplicated(bcmd.getBlocks().length);
break;
case DatanodeProtocol.DNA_INVALIDATE:
//
// Some local block(s) are obsolete and can be
// safely garbage-collected.
//
Block toDelete[] = bcmd.getBlocks();
try {
if (blockScanner != null) {
blockScanner.deleteBlocks(toDelete);
}
data.invalidate(toDelete);
} catch(IOException e) {
checkDiskError();
throw e;
}
myMetrics.incrBlocksRemoved(toDelete.length);
break;
case DatanodeProtocol.DNA_SHUTDOWN:
// shut down the data node
this.shutdown();
return false;
case DatanodeProtocol.DNA_REGISTER:
// namenode requested a registration - at start or if NN lost contact
LOG.info("DatanodeCommand action: DNA_REGISTER");
if (shouldRun) {
register();
}
break;
case DatanodeProtocol.DNA_FINALIZE:
storage.finalizeUpgrade();
break;
case UpgradeCommand.UC_ACTION_START_UPGRADE:
// start distributed upgrade here
processDistributedUpgradeCommand((UpgradeCommand)cmd);
break;
case DatanodeProtocol.DNA_RECOVERBLOCK:
recoverBlocks(bcmd.getBlocks(), bcmd.getTargets());
break;
case DatanodeProtocol.DNA_ACCESSKEYUPDATE:
LOG.info("DatanodeCommand action: DNA_ACCESSKEYUPDATE");
if (isBlockTokenEnabled) {
blockTokenSecretManager.setKeys(((KeyUpdateCommand) cmd).getExportedKeys());
}
break;
case DatanodeProtocol.DNA_BALANCERBANDWIDTHUPDATE:
LOG.info("DatanodeCommand action: DNA_BALANCERBANDWIDTHUPDATE");
int vsn = ((BalancerBandwidthCommand) cmd).getBalancerBandwidthVersion();
if (vsn >= 1) {
long bandwidth =
((BalancerBandwidthCommand) cmd).getBalancerBandwidthValue();
if (bandwidth > 0) {
DataXceiverServer dxcs =
(DataXceiverServer) this.dataXceiverServer.getRunnable();
dxcs.balanceThrottler.setBandwidth(bandwidth);
}
}
break;
default:
LOG.warn("Unknown DatanodeCommand action: " + cmd.getAction());
}
return true;
}
命令DNA_TRANSFER、DNA_INVALIDATE和DNA_RECOVERBLOCK與數據塊相關,其中,DNA_INVALIDATE指令的實現較爲簡單,順序在數據塊掃描器和文件系統數據集對象中刪除數據塊,FSDataset.invalidate()通過異步磁盤操作服務FSDatasetAsyncDiskService刪除Linux文件系統上的數據塊文件和校驗信息文件,降低了processCommand()執行時間。
名字節點指令DNA_RECOVERBLOCK用於恢復數據塊,這是由名字節點發起的數據塊恢復,作爲整個系統故障恢復的重要措施。指令DNA_RECOVERBLOCK可以恢復客戶端永久崩潰形成的,還處於寫狀態的數據塊。由名字節點觸發的數據塊恢復,恢復策略永久是截斷,即將數據塊會參與到過程的各數據節點上的數據塊副本長度的最小值;同時,恢復過程結束後,主數據節點通知名字節點,關閉它上面還處於打開狀態的文件。
當名字節點發現某數據塊當前副本數小於目標值時,名字節點利用DNA_TRANSFER命令,通知某一個擁有該數據塊的數據節點將數據塊複製到其他數據節點。
數據塊掃描器
每個數據節點都會執行一個數據塊掃描器DataBlockScanner,它週期性地驗證節點所存儲的數據塊,通過DataBlockScanner數據節點可以儘早發現有問題的數據塊,並彙報給數據節點。
數據塊掃描器是數據節點中比較獨立的一個模塊,包括掃描器的主要實現DataBlockScanner和輔助類BlockScanInfo、LogEntry和LogFileHandler等。
BlockScanInfo類保存的數據塊掃描相關信息包括:
block:數據塊
lastScanTime:最後一次掃描的時間
lastLogTime:最後寫日誌的時間
lastScanType:掃描的類型,定義在枚舉DataBlockScanner.ScanType中
lastScanOk:掃描結果
其中,掃描的類型包括:
NONE:還沒有執行過掃描
REMOTE_READ:最後一次掃描結果是由客戶端讀產生
VERIFICATION_SCAN:掃描結果由數據塊的掃描器產生
前面介紹客戶端讀數據的過程時分析過,客戶端成功讀取了一個完整數據塊(包括校驗)後,會發送一個附加的響應碼,通知數據節點的校驗成功,這個信息會更新在DataBlockScanner的記錄中,對應的類型就是REMOTE_READ
BlockScanInfo對象是可比較的,比較方法compareTo()方法lastScanTime的大小,按時間先後排序,這樣保存着所有待掃描數據塊信息的成員變量DataBlockScanner.blockInfoSet中的元素,也就根據掃描時間排序。
輔助類LogEntry和LogFileHandler和掃描器日誌文件相關。由於數據塊每三週才被掃描一次,將掃描信息保存在文件裏,以防止數據節點重啓後丟失,就很有必要了。
數據節點的啓停
DataNode.main()是數據節點的入口方法,但他只完成一件事件,即調用secureMain()方法。secureMain()方法通過createDataNode()創建並啓動數據節點,然後通過調用DataNode對象的join(),等待數據節點停止運行。
DataNode.createDatNode()方法又是通過另外一個方法創建數據節點的實例,然後,方法調用runDatanodeDaemon()執行數據節點的主線程。
數據節點的構造函數很簡單,主要的初始化工作由startDataNode()方法。這個方法依次完成如下工作:
1)獲取節點的名字和名字節點的地址,讀入一些運行時需要的配置項
2)構造註冊需要的DatanodeRegistration對象
3)建立到數據節點的IPC連接並握手,即調用名字節點上的遠程方法handshake()
4)執行可能的存儲空間狀態恢復,構造數據節點使用的FSDataset對象
5)創建流接口服務器DataXceiverServer
6)創建數據塊掃描器DataBlockScanner
7)創建數據節點上的HTTP服務器,該服務器提供了數據節點的運行狀態信息
8)創建數據節點IPC服務器,對外提供ClientDatanodeProtocol和InterDatanodeProtocol遠程服務
可見,在startNode()方法中,幾經對數據節點上的主要模塊進行了初始化,但模塊提供的服務都還沒有投入正式使用。接下來,DataNode.createNode()會調用runDatanodeDaemon()方法,首先通過前面介紹的DataNode.register()向名字節點註冊當前數據節點,成功註冊後,執行數據節點的服務主線程。
DataNode類實現了Runable接口,runDatanodeDaemon()在數據節點服務主線程對象上調用start()方法,線程執行DataNode.run()方法,run()方法入口處就啓動了流式接口和IPC接口的服務,這個時候,數據節點處於正常服務狀態,可以接受外部請求。
DataNode.run()循環調用offerService()方法,而offerService()其實也就是一個循環,在這個循環裏會執行如下操作:
1)發送到名字節點的心跳,並執行名字節點指令
2)通過blockReceiverd()上報數據節點上接收到的數據塊
3)根據需求調用遠程接口DatanodeProtocol.blockReport(),報告數據節點目前保存的數據塊信息
4)根據需要啓動數據塊掃描器DataBlockScanner
數據節點的停止
相對於啓動,數據節點停止不需要做太多工作。DataNode.shutdown()用於停止數據節點,它的調用時機如下:
1)DataNode構造失敗,startDataNode()方法拋出IOException異常
2)數據節點服務主線程捕獲如下異常:UnregisteredDatanodeException(節點未註冊)、DisallowedDatanodeException(節點被撤銷)和IncorrectVersionException(節點版本不正確)
3)收到名字節點指令DatanodeProtocol.DNA_SHUTDOWN
4)數據節點服務主線程退出前
shutdown()代碼如下:
public void shutdown() {
this.unRegisterMXBean();
if (infoServer != null) {
try {
infoServer.stop();
} catch (Exception e) {
LOG.warn("Exception shutting down DataNode", e);
}
}
if (ipcServer != null) {
ipcServer.stop();
}
this.shouldRun = false;
if (dataXceiverServer != null) {
((DataXceiverServer) this.dataXceiverServer.getRunnable()).kill();
this.dataXceiverServer.interrupt();
// wait for all data receiver threads to exit
if (this.threadGroup != null) {
while (true) {
this.threadGroup.interrupt();
LOG.info("Waiting for threadgroup to exit, active threads is " +
this.threadGroup.activeCount());
if (this.threadGroup.activeCount() == 0) {
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
}
}
// wait for dataXceiveServer to terminate
try {
this.dataXceiverServer.join();
} catch (InterruptedException ie) {
}
}
RPC.stopProxy(namenode); // stop the RPC threads
if(upgradeManager != null)
upgradeManager.shutdownUpgrade();
if (blockScannerThread != null) {
blockScannerThread.interrupt();
try {
blockScannerThread.join(3600000L); // wait for at most 1 hour
} catch (InterruptedException ie) {
}
}
if (storage != null) {
try {
this.storage.unlockAll();
} catch (IOException ie) {
}
}
if (dataNodeThread != null) {
dataNodeThread.interrupt();
try {
dataNodeThread.join();
} catch (InterruptedException ie) {
}
}
if (data != null) {
data.shutdown();
}
if (myMetrics != null) {
myMetrics.shutdown();
}
}
通過這個方法回顧數據節點中的關鍵組件。首先停止的是數據節點上的HTTP服務器和提供ClientDatanodeProtocol和InterDatanodeProtocol遠程接口的IPC服務器。由於DataNode構造過程失敗也會調用shutdown()方法,所以所有的資源釋放都需要判斷相應的對象是否存儲。上述兩個服務器停止後,數據節點的成員變量shouldRun設置爲false。
這裏的shouldRun是一種典型的volatitle變量用法:檢查某個狀態標記以判斷是否退出循環。在DataNode.offerService()、DataNode.run()、DataXceiverServer.run()、PacketResponder.run()、DataBlockScanner.run()等大量方法中,實現邏輯裏的循環都會判斷這個標記,以儘快在數據節點停止時,退出循環。
接下來要停止的是流式接口的相關服務。由於流式接口的監聽線程和服務線程都可能涉及Socket上阻塞操作中,根據shouldRun標記結束循環並退出線程。
其他的清理工作還有:
1)關閉數據節點到名字節點的IPC客戶端,關閉數據塊掃描器
2)對文件"in_use.lock"解鎖並刪除文件
3)中斷數據節點服務線程,關閉數據節點擁有的FSDataset對象
shutdown()方法執行結束後,數據節點的服務線程退出,secureMain()方法的join()調用返回,接着,DataNode.secureMain()執行它的finally語句,在日誌系統裏打印“Exiting Datanode”信息後,數據節點結束運行並退出。
版權申明:本文部分摘自【蔡斌、陳湘萍】所著【Hadoop技術內幕 深入解析Hadoop Common和HDFS架構設計與實現原理】一書,僅作爲學習筆記,用於技術交流,其商業版權由原作者保留,推薦大家購買圖書研究,轉載請保留原作者,謝謝!