數據節點簡介
我們繼續來了解Hadoop分佈式文件系統各個實體內部的工作原理。首先是數據節點,它以數據塊的形式在本地Linux文件系統上保存HDFS文件的內容,並對外提供文件數據訪問功能。客戶端讀寫HDFS文件時,必須根據名字節點提供的信息,進一步和數據節點交互;同時,數據節點還必須接受名字節點的管理,執行名字節點指令,並上報名字節點感興趣的事件,以保證文件系統穩定、可靠、高效地運行。
數據塊存儲
HDFS採用了一種非常典型的文件系統實現方法。對一個不定長度的文件進行分塊,並將塊保存在存儲設備上。文件數據分塊,保存在HDFS的存儲設備--數據節點上。數據節點將數據塊以Linux文件的形式保存在節點的存儲系統上。數據節點的第一個基本功能,就是管理這些保存在Linux文件系統中的數據塊。
在HDFS中,名字節點、數據節點和第二名字節點都需要在磁盤上組織、存儲持久化數據,它們會在磁盤上維護一定的文件結構。在第一次啓動HDFS集羣前,需要通過hadoop namenode -format對名字節點進行格式化,讓名字節點建立對應的文件結構。
由於需要在HDFS集羣運行時動態添加或刪除數據節點。所以,數據節點和名字節點不一樣,它們不需要進行格式化,而是在第一次啓動的時候,創建存儲目錄。另外,數據節點可以管理多個數據目錄,被管理的目錄通過配置項${dfs.data.dir}指定,如果該配置項的值爲“/data/datanode,/data2/datanode”,則數據節點會管理這兩個目錄,並把它們作爲數據塊保存目錄。
${dfs.data.dir}目錄介紹
${dfs.data.dir}目錄下一般有4個目錄和2個文件。(目錄:blocksBeginWriten、current、detach、tmp,文件:in_use.lock、storage)
其中各個目錄的作用如下:
blocksBeginWritten:由名字可以知道,該文件夾保存着當前正在“寫”的數據塊。和位於“tmp”目錄下保存的正在“寫”的數據塊相比,差別在於“blocksBeginWritten”中的數據塊寫操作由客戶端發起。
current:數據節點管理的最重要目錄,它保存着已經寫入HDFS文件系統的數據塊,也就是寫操作已經結束的已“提交”數據塊。該目錄還包含一些系統工作時需要的文件。
detach:用戶配合數據節點升級,供數據塊分離操作保存臨時工作文件。
tmp:該文件夾也保存着當前正在“寫”的數據塊,這裏的寫操作時由數據塊複製引發的,另一個數據節點正在發送數據到數據塊。
上述目錄其實也隱含了數據塊在數據節點上可能存在的狀態,以上述目錄名作爲狀態。
數據節點上的數據塊最開始會處於“blocksBeingWriteen”(由客戶端寫入創建)或“tmp”(由數據塊複製寫入創建)狀態,當數據寫入順利結束後,提交數據塊,它們的狀態會遷移到“current”。已經提交的數據塊可以重新被打開,追加數據,這時,它的狀態會放回到“blocksBeginWritten”。處於上述的三個狀態的數據塊都可以被刪除,並轉移到“最終狀態”。
狀態“detach”比較特殊,它用於配合數據節點升級,是數據塊可能存在的一個臨時、短暫的狀態。
${dfs.data.dir}目錄下還有兩個文件,其中,“storage”保存着如下一段提示信息:
protected void writeCorruptedData(RandomAccessFile file) throws IOException {
final String messageForPreUpgradeVersion =
"\nThis file is INTENTIONALLY CORRUPTED so that versions\n"
+ "of Hadoop prior to 0.13 (which are incompatible\n"
+ "with this directory layout) will fail to start.\n";
file.seek(0);
file.writeInt(FSConstants.LAYOUT_VERSION);
org.apache.hadoop.io.UTF8.writeString(file, "");
file.writeBytes(messageForPreUpgradeVersion);
file.getFD().sync();
}
該文件的最開始保存着一個二進制整數,它是一個二進制文件,不是文本文件。由這段信息可知,0.13版本以前的Hadoop,使用“storage”作爲數據塊的保存目錄,和現在的目錄結構不兼容。這是一種值得學習的技巧,可以防止過舊的Hadoop版本在新的目錄結構上成功啓動,損壞系統。
${dfs.data.dir}目錄下的另一個文件是”in_use.lock“,它表明目錄已經被使用,實現了一種”鎖“機制。如果停止數據節點,該文件會消失,通過文件”in_use.lock“,數據節點可以保證獨自佔用該目錄,防止兩個數據節點(當然,可以在一個節點上啓動歸屬不同HDFS集羣的多個數據節點)實例共享一個目錄,造成混亂。
${dfs.data.dir}/current目錄
current目錄是${dfs.data.dir}下唯一帶目錄結構的子目錄,其他三個(blocksBeingWritten、detach和tmp)都沒有子目錄。
”current“中既包含目錄,也包含文件。其中,大部分文件都以blk_作爲前綴,這些文件有兩種類型:
1、 HDFS數據塊,用來保存HDFS文件的內容,如”blk_1221212122“。
2、使用meta後綴標識的校驗信息文件,用來保存數據塊的校驗信息。
和ChecksumFileSystem文件系統不同,數據節點中,數據塊的校驗信息文件不是隱藏文件,但它們的文件內容格式是一樣的,都由包含校驗配置信息的文件頭與一系列校驗信息組成。圖中,文件”blk_1222“對應的校驗信息文件時”blk_1222“。
當目錄中存儲的數據塊增加到一定規模時,數據節點會創建一個新的目錄,用於保存新的塊及元數據。目錄塊中的塊數據達到64時,(由配置項${dfs.datanode.numblocks}指定),便會創建子目錄(如:subdir56),並形成一個更寬的目錄樹結構。同時,同一個父目錄下最多會創建64個子目錄,也就是說,默認配置下,一個目錄下最多隻有64個數據塊(128個文件)和64個目錄。通過這兩個手段,數據節點既保證了目錄的深度不會太深,影響了檢索文件的性能,同時避免了某個目錄保存了大量數據塊,確保每個目錄中的文件塊數是可控的。
${dfs.data.dir}/current目錄中還有三個特殊的文件,”VERSION“文件是一個Java屬性文件,包含了運行的HDFS版本信息。另外兩個”dncp_block_verification.log.curr“和”dncp_block_verification.log.prev“,則是數據塊掃描器工作時需要的文件。
數據節點存儲的實現
根據軟件開發的一般原則,數據節點的業務邏輯不會在數據節點的文件結構上直接操作,當需要對磁盤上的數據進行操作時,業務邏輯只需要調用管理這些文件結構的對象提供的服務即可。數據節點的文件結構管理包括兩部分內容,數據(節點)存儲DataStorage和文件系統數據集FSDataset。
HDFS的服務器實體,名字節點、數據節點和第二名字節點都需要使用磁盤保存數據。所以,在org.apache.hadoop.hdfs.server.commom包中定義了一些基礎類,他們抽象了磁盤上的數據組織。
數據節點存儲DataStorage是抽象類Storage的子類,而抽象類Storage又繼承自StorageInfo。在這個繼承體系中,和DataStorage同級的FSImage類用於組織名字節點的磁盤數據。FSImage的子類CheckpointStorage和FSImage的繼承關係,體現了名字節點和第二名字節點的密切聯繫。
這個繼承結構中的另外兩個類:NamespaceInfo和CheckpointSignature,出現在HDFS節點間的通信的遠程接口中,它們分別應用於數據節點和名字節點的通信的DatanodeProtocol接口和第二名字節點和名字節點通信的NamenodeProtocol接口。NamespaceInfo包含HDFS集羣存儲的一些信息,CheckpointSignature對象,則用於標識名字節點元數據的檢查點。另外,StorageInfo包含的三個字段的含義,分別是HDFS存儲系統信息結構的版本號layoutVersion、存儲系統標識namespaceID和存儲系統創建時間cTime。在${dfs.data.dir}/current目錄中,文件”VERSION“保存着這些信息。
“VERSION”是一個典型的Java屬性文件,數據節點的“VERSION”文件除了上面提到的三個屬性外,還有storageType和strorangeID屬性。
storageType屬性的意義是一目瞭然,它可以表明這是一個數據節點數據存儲目錄(如:storageType=DATA_NODE);storageID用於名字節點標識數據節點,一個集羣的不同數據節點擁有不同的storageID。
storageID屬性保存在DataStorage類中,如下所示:
/**
* Data storage information file.
* <p>
* @see Storage
*/
public class DataStorage extends Storage {
// Constants
final static String BLOCK_SUBDIR_PREFIX = "subdir";
final static String BLOCK_FILE_PREFIX = "blk_";
final static String COPY_FILE_PREFIX = "dncp_";
private String storageID;
......
}
StorageInfo是一個非常簡單的類,包含三個屬性和相應的get/set方法。它的抽象子類Storage也定義在org.apache.hadoop.hdfs.server,common包中,爲數據節點、名字節點等提供通用的存儲服務。Storage可以管理多個目錄,存儲目錄StorageDirectory是Storage的內部類。提供了存儲目錄上的一些通用操作,它們的實現都很簡單,值得分析的是StorageDirectory的成員變量和getVersionFile()、tryLock()等方法。
成員變量StorageDirectory.root保存着存儲目錄的根,dirType保存着該目錄對應的類型。需要注意的是類型爲java.nio.channels.FileLock的成員變量lock。FileLock,就是文件鎖。Java的文件鎖,要麼獨佔,那麼共享。在這裏,StorageDirectory使用的是獨佔文件鎖,對lock的加鎖代碼在tryLock()中。
數據節點的文件結構中,當數據節點運行時,${dfs.data.dir}下會有一個名爲“in_use.lock”的文件,就是由tryLock()方法創建並上鎖的。注意,“創建並上鎖”兩個操作缺一不可,如果只是創建文件但不加鎖,不能防止用戶對文件的誤操作,如刪除文件或移動文件造成“in_use.lock”文件丟失,導致StorageDirectory.tryLock()判斷邏輯失效。通過java.nio.channels.FileChannel.tryLock()方法,StorageDirectory的tryLock()方法獲得了文件的獨佔鎖定,可以避免上述問題,並通過數據節點的實現邏輯,保證對StorageDirectory對象指向目錄的獨佔使用。同時,“in_use.lock”文件會在數據節點退出時刪除,對應的實現代碼是lockF.deleteOnExit();
deleteOnExit()方法時由java.io.File類提供,當虛擬機退出時,調用了該方法的文件會被虛擬機自動刪除。當然,如果tryLock()加鎖失敗,deleteOnExit()方法自然也不會其作用。相關代碼如下:
/**
* One of the storage directories.
*/
public class StorageDirectory {
File root; // root directory
FileLock lock; // storage lock
StorageDirType dirType; // storage dir type
.......
/**
* Attempts to acquire an exclusive lock on the storage.
*
* @return A lock object representing the newly-acquired lock or
* <code>null</code> if storage is already locked.
* @throws IOException if locking fails.
*/
FileLock tryLock() throws IOException {
File lockF = new File(root, STORAGE_FILE_LOCK);
lockF.deleteOnExit();
RandomAccessFile file = new RandomAccessFile(lockF, "rws");
FileLock res = null;
try {
res = file.getChannel().tryLock();
} catch(OverlappingFileLockException oe) {
file.close();
return null;
} catch(IOException e) {
LOG.error("Cannot create lock on " + lockF, e);
file.close();
throw e;
}
return res;
}
......
}
StorageDirectory還提供了一系列的get/set方法,如獲取存儲目錄中的“VERSION”文件的File對象,可以調用getVersionFile()方法。StorageDirectory.analyzeStorage()和StorageDirectory.doRecover()涉及系統升級。
Storage管理着一個或多個StorageDirectory對象,所有Storage類中的方法都很簡單,或者是在StorageDirectory基礎提供的整體操作,或者是對保存的StorageDirectory對象進行管理。Storage是個抽象類,有兩個抽象方法,它們和系統升級相關。
和Storage一樣,DataStorage的代碼可以分爲兩部分,升級相關代碼和升級無關代碼。與升級無關的代碼很少,需要關注的是DataStorage定義的三個常量,對照數據節點的文件結構。代碼如下:
public class DataStorage extends Storage {
// Constants
final static String BLOCK_SUBDIR_PREFIX = "subdir";
final static String BLOCK_FILE_PREFIX = "blk_";
final static String COPY_FILE_PREFIX = "dncp_";
private String storageID;
DataStorage() {
super(NodeType.DATA_NODE);
storageID = "";
}
......
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();
}
}
數據節點在第一次啓動的時候,會調用DataStorage.format()創建存儲目錄結果。 format()方法很簡單,通過StorageDirectory.clearDirectory()刪除原有目錄及數據並重新創建目錄,然後“VERSION”文件中的屬性賦值並將其持久化到磁盤。format()的第一個參數類型是StorageDirectory,如果數據節點管理這多個目錄,這個方法會被調用多次,在不同的目錄下創建節點文件結構;第二個參數nsInfo是從名字節點返回的NamespaceInfo對象,攜帶了存儲系統標識namespaceID等信息,該標識最終存放在“VERSION”文件中。注意,由於“VERSION”文件保存在current目錄中,保存文件的同時會創建對應的目錄。
數據節點升級
上面介紹將DataStorage的功能分爲升級相關和升級無關兩個部分有點勉強,其實,DataStorage的主要功能是對存儲空間的生存期進行管理,通過DataStorage.format()創建存儲目錄,或者利用DataStoarage.doUpgrade()進行升級,都是存儲空間生存期管理的一部分。對於數據節點,存儲空間生存期管理的關注點還是系統升級。
Hadoop實現了嚴格的版本兼容性要求,只有來自相同版本的組件才能保證相互的兼容性。大型分佈式系統的升級需要一個周密的計劃,必須考慮到諸如:持久化的信息是否改變?如果數據佈局改變,如何自動地將原有數據和元數據遷移到新版本格式?升級過程中如果出現錯誤,如何保證數據不丟失?升級出現問題時,怎麼才能夠回滾升級前的狀態?
如果文件系統的佈局不需要改變,集羣的升級變得非常簡單,關閉舊進程,升級配置文件,然後啓動新版本的守護進程,客戶端使用新的庫,就可以完成升級。同時,升級回滾也很簡單,使用舊版本和舊配置文件,重啓系統即可。如果持久化的信息在上述過程中需要改變到新的格式,特別是考慮到可能的回滾,升級的過程就變得複雜。
HDFS升級時,需要複製以前版本的元數據(對名字節點)和數據(對數據節點)。在數據節點上,升級並不需要兩倍的集羣存儲空間,數據節點使用了Linux文件系統的硬鏈接,保留了對同一個數據塊的兩個引用,即當前版本和以前版本。通過這樣的技術,就可以在需要的時候,輕鬆回滾到以前版本的文件系統。注意,在已升級系統上對數據的修改,升級回滾結束後將會消失。
1、升級
爲了簡化實現,HDFS最多保留前一版本的文件系統,沒有多版本的升級、回滾機制。同時,引入升級提交機制(通過hadoop dfsadmin-finalizeUpgrade可提交一次升級),該機制用於刪除以前的版本,所以在升級提交後,就不能回滾到以前的版本了。
數據節點升級的實現在DataStorage.doUpgrade()方法中。其中,升級過程涉及如下幾個目錄:
curDir:當前版本目錄,通過StorageDirectory.getCurrentDir()獲得,目錄名爲“current”。
prevDir:上以版本目錄,目錄名爲"previous",可通過StorageDirectory的getPreviousDir()方法得到;這裏的“上一版本”指的是升級前的版本。
tmpDir:上一個版本臨時目錄,即目錄${dfs.data.dir}/previous.tmp
doUpgrade()方法的主要流程是:首先確保上述涉及的工作目錄處於正常狀態。如檢查目錄“current”目錄是否存在,如果“previous”目錄存在,刪除該目錄。注意,該刪除操作相當於提交了上一次升級,同時保證HDFS最多保留前一段版本的要求;保證“tmpDir”目錄不存在。應該說,在(通過硬鏈接)移動數據前,“previous”目錄和“previous.tmp”都是不存在的。
支持升級回滾,就必須保留升級前的數據,在數據節點,就是保存數據塊以及數據塊的校驗信息文件。在doUpgrade()中,保留升級前數據是通過建立文件的硬鏈接實現的。
DataStorage.doUpgrade()保留升級前數據的動作一共有兩步:首先將“current”目錄名改爲“previous.tmp”,然後調用linkBlocks(),在新創建的“current”目錄下,建立到“pervious.tmp”目錄中數據塊和數據塊校驗信息文件的硬鏈接。DataStorage.lineBlocks()執行結束後,doUpgrade()方法還需要在“current”目錄下創建這版本的“VERSION”文件,最後,將“previous.tmp”目錄改爲"previous",完成升級。這個時候,數據節點的存儲空間會有"previous"和“current”兩個目錄,而且,“previous”和“current”包含了同樣的數據塊和數據塊校驗信息文件,但它們有各自“VERSION”文件。另外,升級過程中需要的“previous.tmp”目錄已經消失。代碼如下:
/**
* Move current storage into a backup directory,
* and hardlink all its blocks into the new current directory.
*
* @param sd storage directory
* @throws IOException
*/
void doUpgrade(StorageDirectory sd,
NamespaceInfo nsInfo
) throws IOException {
LOG.info("Upgrading storage directory " + sd.getRoot()
+ ".\n old LV = " + this.getLayoutVersion()
+ "; old CTime = " + this.getCTime()
+ ".\n new LV = " + nsInfo.getLayoutVersion()
+ "; new CTime = " + nsInfo.getCTime());
// enable hardlink stats via hardLink object instance
HardLink hardLink = new HardLink();
File curDir = sd.getCurrentDir();
File prevDir = sd.getPreviousDir();
assert curDir.exists() : "Current directory must exist.";
// delete previous dir before upgrading
if (prevDir.exists())
deleteDir(prevDir);
File tmpDir = sd.getPreviousTmp();
assert !tmpDir.exists() : "previous.tmp directory must not exist.";
// rename current to tmp
rename(curDir, tmpDir);
// hardlink blocks
linkBlocks(tmpDir, curDir, this.getLayoutVersion(), hardLink);
// write version file
this.layoutVersion = FSConstants.LAYOUT_VERSION;
assert this.namespaceID == nsInfo.getNamespaceID() :
"Data-node and name-node layout versions must be the same.";
this.cTime = nsInfo.getCTime();
sd.write();
// rename tmp to previous
rename(tmpDir, prevDir);
LOG.info( hardLink.linkStats.report());
LOG.info("Upgrade of " + sd.getRoot()+ " is complete.");
}
2、升級回滾 升級回滾doRollBack()的實現有如下要點:
1)在完成對各個工作目錄的狀態的檢查後,需要保證升級能夠回滾到正確的版本上去。這個步驟是通過比較保存着在“previous”目錄下“VERSION”文件中的HDFS存儲系統信息結構的版本號layoutVersion和存儲系統創建時間cTime和回滾後相應的layoutVersion和cTime來做判斷。
2)由於“pervious”目錄保存了升級前的所有數據,所以,doRollback()其實只需要簡單的將“previous”目錄改名稱“current”,就可以完成回滾。但由於現在版本的工作目錄“current”目錄存在,所有采用了三步操作完成改名動作:先將“current”目錄改爲“removed.tmp”,然後將“previous”目錄名修改爲“current”,最後刪除“removed.tmp”目錄。代碼如下:
void doRollback( StorageDirectory sd,
NamespaceInfo nsInfo
) throws IOException {
File prevDir = sd.getPreviousDir();
// regular startup if previous dir does not exist
if (!prevDir.exists())
return;
DataStorage prevInfo = new DataStorage();
StorageDirectory prevSD = prevInfo.new StorageDirectory(sd.getRoot());
prevSD.read(prevSD.getPreviousVersionFile());
// We allow rollback to a state, which is either consistent with
// the namespace state or can be further upgraded to it.
if (!(prevInfo.getLayoutVersion() >= FSConstants.LAYOUT_VERSION
&& prevInfo.getCTime() <= nsInfo.getCTime())) // cannot rollback
throw new InconsistentFSStateException(prevSD.getRoot(),
"Cannot rollback to a newer state.\nDatanode previous state: LV = "
+ prevInfo.getLayoutVersion() + " CTime = " + prevInfo.getCTime()
+ " is newer than the namespace state: LV = "
+ nsInfo.getLayoutVersion() + " CTime = " + nsInfo.getCTime());
LOG.info("Rolling back storage directory " + sd.getRoot()
+ ".\n target LV = " + nsInfo.getLayoutVersion()
+ "; target CTime = " + nsInfo.getCTime());
File tmpDir = sd.getRemovedTmp();
assert !tmpDir.exists() : "removed.tmp directory must not exist.";
// rename current to tmp
File curDir = sd.getCurrentDir();
assert curDir.exists() : "Current directory must exist.";
rename(curDir, tmpDir);
// rename previous to current
rename(prevDir, curDir);
// delete tmp dir
deleteDir(tmpDir);
LOG.info("Rollback of " + sd.getRoot() + " is complete.");
}
3、升級提交
升級提交doFinalize()簡單更簡單,它只需要將“previous”目錄刪除即可。但實際上,DataStorage.doFinalize()需要將“previous”目錄名改爲“finalized.tmp”,然後刪除“finalized.tmp”來刪除目錄,而不是直接刪除“previous”來提交升級。代碼如下:
void doFinalize(StorageDirectory sd) throws IOException {
File prevDir = sd.getPreviousDir();
if (!prevDir.exists())
return; // already discarded
final String dataDirPath = sd.getRoot().getCanonicalPath();
LOG.info("Finalizing upgrade for storage directory "
+ dataDirPath
+ ".\n cur LV = " + this.getLayoutVersion()
+ "; cur CTime = " + this.getCTime());
assert sd.getCurrentDir().exists() : "Current directory must exist.";
final File tmpDir = sd.getFinalizedTmp();
// rename previous to tmp
rename(prevDir, tmpDir);
// delete tmp dir in a separate thread
new Daemon(new Runnable() {
public void run() {
try {
deleteDir(tmpDir);
} catch(IOException ex) {
LOG.error("Finalize upgrade for " + dataDirPath + " failed.", ex);
}
LOG.info("Finalize upgrade for " + dataDirPath + " is complete.");
}
public String toString() { return "Finalize " + dataDirPath; }
}).start();
}
也許讀者會有疑問,爲什麼在doFinalize()中不能直接刪除“previous”目錄?同樣,在doRollback()和doUpgrade()中,都存在“removed.tmp”和“previous.tmp”這樣的臨時目錄。
在升級、升級提交或升級回滾都需要進行一定的操作。在執行這些任務的中間,如果設備出現故障(如:停電),那麼,存儲空間就可能處於某一箇中間狀態。引入上述這些臨時目錄,系統就能夠判斷目前doUpgrade()處於什麼階段,並採取相應的應對措施。
Storage和DataStorage一起提供了一個完美的HDFS數據節點升級機制,簡化了大型分佈式系統的運維。它們不但解決了升級過程中數據格式轉換、升級回滾等常見問題、並保證了這個過程中不會丟失數據;同時,它們的設計,特別是Storage狀態機,以及配合狀態機工作的臨時文件,提供了一個完備的升級方法,在升級過程或者回滾過程中的任意一個步驟出現錯誤,都可以通過狀態機,恢復到正常狀態。HDFS數據節點升級的Storage和DataStorage實現,非常具有借鑑意義。
文件系統數據集的工作機制
DataStorage專注於數據節點的存儲空間的生存期管理,在存儲空間上,與數據節點邏輯密切相關的存儲服務,如創建數據塊文件、維護數據塊文件和數據塊校驗信息文件的關係等,則實現在文件系統數據集FSDataset中。 FSDataset繼承了FSDatasetInterface接口。
FSDatasetInterface接口的方法可以分爲以下三類:
數據塊相關的方法:FSDataset管理了數據節點上的數據塊,大量的FSDataset方法和數據塊相關,如創建數據塊、打開數據塊上的輸入、輸出流、提交數據塊等。
數據塊校驗信息文件相關:包括維護數據塊和校驗信息文件關係,獲取校驗信息文件輸入流等。
其他:包括FSDataset健康檢查、關閉FSDataset的shutdown()方法等。
FSDataset借鑑了Linux邏輯卷管理的一些思想。LVM使系統管理員可以更方便地分配存儲空間,它的出現,改變了磁盤空間的管理理念。傳統的文件系統是基於分區的,一個文件系統對應一個分期,這種方式直觀,但不靈活,在實際應用中,會出現分去不平衡,存儲空間和硬件能力得不到充分利用等問題。
FSDataset沒有使用Linx LVM的方式管理它的存儲空間,但是FSDataset借鑑了LVM的一些概念,可以管理多個數據目錄。一般情況下,這些不同的數據目錄配置在不同的物理設備上,以提高磁盤的數據吞吐量。
文件數據集將它管理的存儲空間分爲三個級別,分別是FSDir、FSVolume和FSVolumeSet進行抽象。
FSDir對象表示了“current”目錄下的子目錄,其成員變量children包括了目錄下的所有子目錄,形成了目錄樹。FSVolume是數據目錄配置項${dfs.data.dir}中的一項,數據節點可以管理一個或者多個數據目錄,系統中也就存在一個或者多個FSVolum對象,這些FSVolume對象由FSVolumSet管理。
由於上述內部類存在,FSDataset的成員變量不多,比較重要的有volumes,它管理數據節點的所有存儲空間。成員ongoingCreates和VolomeMap是兩個HaseMap,其中,ongoingCreates保存着當前正在進行寫操作的數據塊和對應文件的影射,volumeMap則保存着已經提交的數據塊和對應的文件影射。代碼如下:
/**************************************************
* FSDataset manages a set of data blocks. Each block
* has a unique name and an extent on disk.
*
***************************************************/
public class FSDataset implements FSConstants, FSDatasetInterface {
/** Find the metadata file for the specified block file.
/**
* Start writing to a block file
* If isRecovery is true and the block pre-exists, then we kill all
volumeMap.put(b, v);
volumeMap.put(b, v);
* other threads that might be writing to this block, and then reopen the file.
* If replicationRequest is true, then this operation is part of a block
* replication request.
*/
public BlockWriteStreams writeToBlock(Block b, boolean isRecovery,
boolean replicationRequest) throws IOException {
//
// Make sure the block isn't a valid one - we're still creating it!
//
if (isValidBlock(b)) {
if (!isRecovery) {
throw new BlockAlreadyExistsException("Block " + b + " is valid, and cannot be written to.");
}
// If the block was successfully finalized because all packets
// were successfully processed at the Datanode but the ack for
// some of the packets were not received by the client. The client
// re-opens the connection and retries sending those packets.
// The other reason is that an "append" is occurring to this block.
detachBlock(b, 1);
}
long blockSize = b.getNumBytes();
//
// Serialize access to /tmp, and check if file already there.
//
File f = null;
List<Thread> threads = null;
synchronized (this) {
//
// Is it already in the create process?
//
ActiveFile activeFile = ongoingCreates.get(b);
if (activeFile != null) {
f = activeFile.file;
threads = activeFile.threads;
if (!isRecovery) {
throw new BlockAlreadyExistsException("Block " + b +
" has already been started (though not completed), and thus cannot be created.");
} else {
for (Thread thread:threads) {
thread.interrupt();
}
}
ongoingCreates.remove(b);
}
FSVolume v = null;
if (!isRecovery) {
v = volumes.getNextVolume(blockSize);
// create temporary file to hold block in the designated volume
f = createTmpFile(v, b, replicationRequest);
} else if (f != null) {
DataNode.LOG.info("Reopen already-open Block for append " + b);
// create or reuse temporary file to hold block in the designated volume
v = volumeMap.get(b).getVolume();
volumeMap.put(b, new DatanodeBlockInfo(v, f));
} else {
// reopening block for appending to it.
DataNode.LOG.info("Reopen Block for append " + b);
v = volumeMap.get(b).getVolume();
f = createTmpFile(v, b, replicationRequest);
File blkfile = getBlockFile(b);
File oldmeta = getMetaFile(b);
File newmeta = getMetaFile(f, b);
// rename meta file to tmp directory
DataNode.LOG.debug("Renaming " + oldmeta + " to " + newmeta);
if (!oldmeta.renameTo(newmeta)) {
throw new IOException("Block " + b + " reopen failed. " +
" Unable to move meta file " + oldmeta +
" to tmp dir " + newmeta);
}
// rename block file to tmp directory
DataNode.LOG.debug("Renaming " + blkfile + " to " + f);
if (!blkfile.renameTo(f)) {
if (!f.delete()) {
throw new IOException("Block " + b + " reopen failed. " +
" Unable to remove file " + f);
}
if (!blkfile.renameTo(f)) {
throw new IOException("Block " + b + " reopen failed. " +
" Unable to move block file " + blkfile +
" to tmp dir " + f);
}
}
}
if (f == null) {
DataNode.LOG.warn("Block " + b + " reopen failed " +
" Unable to locate tmp file.");
throw new IOException("Block " + b + " reopen failed " +
" Unable to locate tmp file.");
}
// If this is a replication request, then this is not a permanent
// block yet, it could get removed if the datanode restarts. If this
// is a write or append request, then it is a valid block.
if (replicationRequest) {
volumeMap.put(b, new DatanodeBlockInfo(v));
} else {
volumeMap.put(b, new DatanodeBlockInfo(v, f));
}
ongoingCreates.put(b, new ActiveFile(f, threads));
}
try {
if (threads != null) {
for (Thread thread:threads) {
thread.join();
}
}
} catch (InterruptedException e) {
throw new IOException("Recovery waiting for thread interrupted.");
}
//
// Finally, allow a writer to the block file
// REMIND - mjc - make this a filter stream that enforces a max
// block size, so clients can't go crazy
//
File metafile = getMetaFile(f, b);
DataNode.LOG.debug("writeTo blockfile is " + f + " of size " + f.length());
DataNode.LOG.debug("writeTo metafile is " + metafile + " of size " + metafile.length());
return createBlockWriteStreams( f , metafile);
}
synchronized File createTmpFile( FSVolume vol, Block blk,
boolean replicationRequest) throws IOException {
if ( vol == null ) {
vol = volumeMap.get( blk ).getVolume();
if ( vol == null ) {
throw new IOException("Could not find volume for block " + blk);
}
}
return vol.createTmpFile(blk, replicationRequest);
}
private synchronized void finalizeBlockInternal(Block b, boolean reFinalizeOk)
throws IOException {
ActiveFile activeFile = ongoingCreates.get(b);
if (activeFile == null) {
if (reFinalizeOk) {
return;
} else {
throw new IOException("Block " + b + " is already finalized.");
}
}
File f = activeFile.file;
if (f == null || !f.exists()) {
throw new IOException("No temporary file " + f + " for block " + b);
}
FSVolume v = volumeMap.get(b).getVolume();
if (v == null) {
throw new IOException("No volume for temporary file " + f +
" for block " + b);
}
File dest = null;
dest = v.addBlock(b, f);
volumeMap.put(b, new DatanodeBlockInfo(v, dest));
ongoingCreates.remove(b);
}
}
打開數據塊和提交數據塊的方法如上代碼所示。
版權申明:本文部分摘自【蔡斌、陳湘萍】所著【Hadoop技術內幕 深入解析Hadoop Common和HDFS架構設計與實現原理】一書,僅作爲學習筆記,用於技術交流,其商業版權由原作者保留,推薦大家購買圖書研究,轉載請保留原作者,謝謝!