namenode主要負責文件元信息的管理和文件到數據塊的映射。其中,建立目錄只涉及文件元信息的操做。本文分析namenode建立目錄過程的源碼實現,爲後面分析寫文件過程打基礎。java
源碼版本:Apache Hadoop 2.6.0node
可參考猴子追源碼時的速記打斷點,親自debug一遍。git
根據HDFS-1.x、2.x的RPC接口與源碼|HDFS之NameNode:啓動過程,咱們得知,與建立目錄過程聯繫最緊密的是ClientProtocol協議、RpcServer線程、FSNamesystem、FSDirectory。github
具體過程以下:數組
建立目錄的RPC接口爲ClientProtocol#mkdirs():性能優化
public boolean mkdirs(String src, FsPermission masked, boolean createParent) throws AccessControlException, FileAlreadyExistsException, FileNotFoundException, NSQuotaExceededException, ParentNotDirectoryException, SafeModeException, UnresolvedLinkException, SnapshotAccessControlException, IOException;
複製代碼
對應的RPCServer實現爲NameNodeRpcServer#mkdirs()。bash
後文將以NameNodeRpcServer#mkdirs()爲主流程進行分析。多線程
建立一個目錄/test/mkdir
,觸發提早設置好的斷點:併發
./bin/hadoop fs -mkdir -p /test/mkdir
複製代碼
執行命令前,該目錄不存在,同時-p
選項會遞歸建立不存在的父目錄。根目錄是啓動namenode時建立(或從備份中加載)的,則理想狀況下,HDFS會前後建立/test
、/mkdir
兩個目錄,最後成功返回。app
NameNodeRpcServer#mkdirs():
public boolean mkdirs(String src, FsPermission masked, boolean createParent) throws IOException {
if(stateChangeLog.isDebugEnabled()) {
stateChangeLog.debug("*DIR* NameNode.mkdirs: " + src);
}
// 檢查目標目錄的字符串長度(不超過8000)和路徑深度(不超過1000)
if (!checkPathLength(src)) {
throw new IOException("mkdirs: Pathname too long. Limit "
+ MAX_PATH_LENGTH + " characters, " + MAX_PATH_DEPTH + " levels.");
}
// 一般NameNodeRpcServer中的RPC方法實現只有一些簡單的檢查,細節都交給FSNamesystem
return namesystem.mkdirs(src,
new PermissionStatus(getRemoteUser().getShortUserName(),
null, masked), createParent);
}
複製代碼
一般NameNodeRpcServer只作一些簡單的檢查,而後直接調用FSNamesystem中的同名方法。
FSNamesystem#mkdirs():
boolean mkdirs(String src, PermissionStatus permissions, boolean createParent) throws IOException, UnresolvedLinkException {
boolean ret = false;
try {
ret = mkdirsInt(src, permissions, createParent);
} catch (AccessControlException e) {
logAuditEvent(false, "mkdirs", src);
throw e;
}
return ret;
}
...
private boolean mkdirsInt(final String srcArg, PermissionStatus permissions, boolean createParent) throws IOException, UnresolvedLinkException {
String src = srcArg;
if(NameNode.stateChangeLog.isDebugEnabled()) {
NameNode.stateChangeLog.debug("DIR* NameSystem.mkdirs: " + src);
}
// 進一步檢查路徑
if (!DFSUtil.isValidName(src)) {
throw new InvalidPathException(src);
}
FSPermissionChecker pc = getPermissionChecker();
// 不加鎖檢查HA狀態下的權限
checkOperation(OperationCategory.WRITE);
// 將src中的每層目錄轉成一個byte[],多個層級組成byte[][]
byte[][] pathComponents = FSDirectory.getPathComponentsForReservedPath(src);
HdfsFileStatus resultingStat = null;
boolean status = false;
writeLock();
try {
// 加鎖 re-check HA狀態下的權限
checkOperation(OperationCategory.WRITE);
checkNameNodeSafeMode("Cannot create directory " + src);
src = resolvePath(src, pathComponents);
// 見下
status = mkdirsInternal(pc, src, permissions, createParent);
if (status) {
resultingStat = getAuditFileInfo(src, false);
}
} finally {
writeUnlock();
}
// 手動將日誌同步到磁盤
getEditLog().logSync();
if (status) {
logAuditEvent(true, "mkdirs", srcArg, null, resultingStat);
}
return status;
}
複製代碼
在繼續分析FSNamesystem#mkdirsInternal()以前。須要注意幾個要點,在之後的分析中會無數次重逢:
FSNamesystem#mkdirsInternal():
private boolean mkdirsInternal(FSPermissionChecker pc, String src, PermissionStatus permissions, boolean createParent) throws IOException, UnresolvedLinkException {
assert hasWriteLock();
...// 目錄的權限檢查等
// 若是createParent爲false,則須要檢查父目錄是否存在。此處createParent爲true,不須要檢查
if (!createParent) {
verifyParentDir(src);
}
// 檢查inode和block的總數是否超過的namenode限制的對象最大數量${dfs.namenode.max.objects},默認不限制
checkFsObjectLimit();
// 不然,遞歸建立全部目錄
if (!mkdirsRecursively(src, permissions, false, now())) {
throw new IOException("Failed to create directory: " + src);
}
// 不論是否建立成功,都返回true
return true;
}
複製代碼
咱們在FS Shell
中使用了-p
選項,對應createParent參數爲true,則不須要檢查父目錄是否存在,調用FSNamesystem#mkdirsRecursively()遞歸建立全部目錄。隨着後面的分析,咱們會知道,除非存在異常,不然不論是否建立成功,FSNamesystem#mkdirsRecursively()必定返回true。
「遞歸」一詞只是直譯,實際的實現是循環。
此處「mkdirs - mkdirsInt - mkdirsInternal」
三級結構,是namenode處理客戶端RPC請求時經常使用的編碼風格,之後分析其餘RPC請求的處理流程時也會看到。
FSNamesystem#mkdirsRecursively()從第一級不存在的目錄開始,逐級建立全部目錄層級:
private boolean mkdirsRecursively(String src, PermissionStatus permissions, boolean inheritPermission, long now) throws FileAlreadyExistsException, QuotaExceededException, UnresolvedLinkException, SnapshotAccessControlException, AclException {
src = FSDirectory.normalizePath(src);
// 按層級順序劃分的String[]
String[] names = INode.getPathNames(src);
// components是names數組的字節表示形式,參考FSNamesystem#mkdirs()
byte[][] components = INode.getPathComponents(names);
final int lastInodeIndex = components.length - 1;
dir.writeLock();
try {
// INodesInPath封裝了按照層級組織components與inodes
INodesInPath iip = dir.getExistingPathINodes(components);
...// 快照相關
INode[] inodes = iip.getINodes();
// 將i移動到第一個目錄不存在(inodes[i] == null)的層級
StringBuilder pathbuilder = new StringBuilder();
int i = 1;
for(; i < inodes.length && inodes[i] != null; i++) {
pathbuilder.append(Path.SEPARATOR).append(names[i]);
// 目錄的全部祖先目錄必定要是目錄,不然拋出IOE
if (!inodes[i].isDirectory()) {
throw new FileAlreadyExistsException(
"Parent path is not a directory: "
+ pathbuilder + " "+inodes[i].getLocalName());
}
}
...// 權限檢查
// 從第一級不存在的目錄開始,逐級建立全部目錄層級
for(; i < inodes.length; i++) {
pathbuilder.append(Path.SEPARATOR).append(names[i]);
// 在目錄樹中建立目錄節點,見後
dir.unprotectedMkdir(allocateNewInodeId(), iip, i, components[i],
(i < lastInodeIndex) ? parentPermissions : permissions, null,
now);
// 若是發現inodes[i]爲null表示有異常,返回false通知外層拋出IOE
if (inodes[i] == null) {
return false;
}
// 小知識點:目錄建立也包含在 FilesCreated 的統計內
NameNode.getNameNodeMetrics().incrFilesCreated();
final String cur = pathbuilder.toString();
// 記錄建立目錄的日誌
getEditLog().logMkDir(cur, inodes[i]);
if(NameNode.stateChangeLog.isDebugEnabled()) {
NameNode.stateChangeLog.debug(
"mkdirs: created directory " + cur);
}
}
} finally {
dir.writeUnlock();
}
// 除了異常狀況,統一返回true
return true;
}
複製代碼
INodesInPath按照目錄的層級組織目錄名與INode節點,分別保存在INodesInPath#path(byte[][])與INodesInPath#inodes(INode[])中。在建立節點(目錄節點/文件節點,對應建立目錄/建立文件操做)期間,INodesInPath#inodes的後續位置可能爲null,表示該層級的INode還未建立(見FSNamesystem#mkdirsRecursively()方法);在節點建立後,將對應位置置爲目標節點(見FSDirectory#unprotectedMkdir()方法)。
回顧發起RPC請求時的前提條件:當前命名空間中不存在/test
目錄。則INodesInPath#path(components)的三級應分別對應""
(byte[0])、"test"
byte[4]、"mkdir"
byte[5],INodesInPath#inodes的三級分別對應INodeDirectory "/"
、null
、null
。所以,這裏要建立的第一級目錄是/test
,建立該目錄時知足i == 1
;而後依次建立/mkdir
目錄,知足i == 2
。看IDE的變量提示驗證:
PS:此處偷懶用了後面FSDirectory#unprotectedMkdir()方法的inodesInPath參數的配圖。不過截圖時尚未修改inodesInPath,與此處iip的引用是相等的,沒有影響。
INodesInPath的結構在目錄樹操做中很是經常使用。
正常狀況下,39行建立目錄節點後,目錄節點必定已經建立,inodes[i]必定不爲null。所以,若是43行發現inodes[i]仍爲null,就說明39行執行過程當中發生了異常狀況,返回false通知外層拋出IOE(回顧FSNamesystem#mkdirsInternal()方法)。
另外,39行FSDirectory#unprotectedMkdir()的「不受保護」指「不包含備份機制的相關邏輯」。所以,每建立一級目錄後(修改目錄樹),都要執行52行FSNamesystem#getEditLog#logMkDir()在日誌中記錄建立的目錄和inode。"FSDirectory#unprotectedMkdir() + FSNamesystem#getEditLog#logMkDir()"
組合是namenode備份機制的經常使用編碼風格,之後的分析中也很常見。
注意39行、43行、52行三處邏輯的配合:若是39行目錄建立異常,將提早經過43行
return false
,不會觸發52行的備份。
最後,若是不考慮43行的影響,則無論FSDirectory#unprotectedMkdir()是否執行成功,FSNamesystem#mkdirsRecursively()都會返回true。從這個角度上看,mkdir是一個冪等操做;繼續後面的分析可進一步得知,當且僅當目錄已存在時,FSDirectory#unprotectedMkdir()會返回false,此時偏偏不須要建立目錄,保證了冪等性。
FSDirectory#unprotectedMkdir():
void unprotectedMkdir(long inodeId, INodesInPath inodesInPath, int pos, byte[] name, PermissionStatus permission, List<AclEntry> aclEntries, long timestamp) throws QuotaExceededException, AclException {
assert hasWriteLock();
// 建立目標的目錄節點 dir。此處即"test"目錄
final INodeDirectory dir = new INodeDirectory(inodeId, name, permission,
timestamp);
// 將目錄節點 dir 添加到目錄樹中,見後
if (addChild(inodesInPath, pos, dir, true)) {
if (aclEntries != null) {
AclStorage.updateINodeAcl(dir, aclEntries, Snapshot.CURRENT_STATE_ID);
}
// 將第pos級(從0開始)INode 設置爲 dir(以前是null),見後
inodesInPath.setINode(pos, dir);
}
}
複製代碼
仍以建立"test"目錄爲例。根據FSNamesystem#mkdirsRecursively()方法的分析,此處傳入的pos爲1,name爲"test"的byte[]形式,inodesInPath.inodes[0]指向「test」目錄的父目錄節點,inodesInPath.inodes[1]爲null。
在10行成功添加節點後,15行將第pos級(從0開始)INode設置爲dir以前是null)。也就是FSNamesystem#mkdirsRecursively()中介紹的InodesInPath用法。
如今看19行的FSDirectory#addChild():
private boolean addChild(INodesInPath iip, int pos, INode child, boolean checkQuota) throws QuotaExceededException {
final INode[] inodes = iip.getINodes();
// 檢查路徑是否與保留路徑"/.reserved"衝突
if (pos == 1 && inodes[0] == rootDir && isReservedName(child)) {
throw new HadoopIllegalArgumentException(
"File name \"" + child.getLocalName() + "\" is reserved and cannot "
+ "be created. If this is during upgrade change the name of the "
+ "existing file or directory to another name before upgrading "
+ "to the new release.");
}
// 此處傳入的checkQuota爲true,須要檢查quota
if (checkQuota) {
// 限制節點名最大長度${dfs.namenode.fs-limits.max-component-length},默認255
verifyMaxComponentLength(child.getLocalNameBytes(), inodes, pos);
// 限制子節點最大數量${dfs.namenode.fs-limits.max-directory-items},默認1024*1024
verifyMaxDirItems(inodes, pos);
}
// 檢查節點名是否與保留名".snapshot"衝突
verifyINodeName(child.getLocalNameBytes());
// 更新quota
final Quota.Counts counts = child.computeQuotaUsage();
updateCount(iip, pos,
counts.get(Quota.NAMESPACE), counts.get(Quota.DISKSPACE), checkQuota);
// 判斷是不是rename操做。暫時忽略
boolean isRename = (child.getParent() != null);
// 根據對INodesInPath的分析,第pos - 1級節點即當前節點的父目錄
final INodeDirectory parent = inodes[pos-1].asDirectory();
boolean added;
try {
// 將child添加到父目錄節點parent下,返回是否添加成功的標誌(存在同名節點返回false,不然返回true),見後
added = parent.addChild(child, true, iip.getLatestSnapshotId());
} catch (QuotaExceededException e) {
// 發生異常,則以前的更新是多餘的,回退quota
updateCountNoQuotaCheck(iip, pos,
-counts.get(Quota.NAMESPACE), -counts.get(Quota.DISKSPACE));
throw e;
}
if (!added) { // 若是節點已存在,則以前的更新是多餘的,回退quota
updateCountNoQuotaCheck(iip, pos,
-counts.get(Quota.NAMESPACE), -counts.get(Quota.DISKSPACE));
} else { // 不然,表示已經成功添加的節點
// 沒明白這裏設置父目錄幹嗎??
iip.setINode(pos - 1, child.getParent());
if (!isRename) {
AclStorage.copyINodeDefaultAcl(child);
}
// 將child節點添加至FSDirectory#inodeMap
addToInodeMap(child);
}
return added;
}
複製代碼
傳入的pos爲1,child爲外層建立的dir,checkQuota爲true。暫時不考慮rename操做,那麼思路很簡單:
補充下如何判斷rename操做:
29行,若是child的父目錄不爲null(FSDirectory#unprotectedMkdir()中建立該節點時父目錄爲null),則認爲是rename操做。若是是添加節點的操做,則35行INodeDirectory#addChild()添加節點成功後會將child的父目錄置爲parent,所以,再次進入FSDirectory#addChild()時必然是rename操做。
INodeDirectory#addChild():
public boolean addChild(INode node, final boolean setModTime, final int latestSnapshotId) throws QuotaExceededException {
// 在孩子節點列表中查找同名節點
final int low = searchChildren(node.getLocalNameBytes());
// 若是目標節點已存在,則返回false
if (low >= 0) {
return false;
}
...// 快照相關
addChild(node, low);
if (setModTime) {
updateModificationTime(node.getModificationTime(), latestSnapshotId);
}
// 不然,添加目標節點後返回true
return true;
}
...
private void addChild(final INode node, final int insertionPoint) {
if (children == null) {
children = new ArrayList<INode>(DEFAULT_FILES_PER_DIRECTORY);
}
// 設置節點node的父目錄
node.setParent(this);
// 在孩子節點中檢查是否存在同名節點
children.add(-insertionPoint - 1, node);
if (node.getGroupName() == null) {
node.setGroup(getGroupName());
}
}
...
int searchChildren(byte[] name) {
return children == null? -1: Collections.binarySearch(children, name);
}
複製代碼
當前節點即parent,傳入的node即外層的child。重載的兩個INodeDirectory#addChild()方法很簡單,注意這裏的一個性能優化點便可:
二分查找
:若是不存在同名節點,則返回 -insertionIndex - 1
,表示在insertionIndex位置插入目標節點後不破壞有序性;不然,返回節點序號low >= 0
,說明存在同名節點,插入失敗,6-8行返回false。此處不存在任何孩子節點,所以insertionIndex = 0
,則low = -1
,繼續執行。insertionPoint == -insertionIndex - 1
,則插入位置知足insertionIndex == -insertionPoint - 1
。如上,在insertionIndex位置插入目標節點,不會破壞INodeDirectory#children的有序性。24行設置父目錄,也就是FSDirectory#addChild()中介紹的rename操做判斷方法。
至此,第一級目錄"test"的建立已經分析完畢。接下來,程序將回到FSNamesystem#mkdirsRecursively()繼續建立第二級目錄"mkdir"。最後,將建立結果返回給客戶端。
namenode建立目錄的過程只涉及文件元信息的操做,邏輯相對簡單。歸納起來,需掌握幾個點:
「mkdirs - mkdirsInt - mkdirsInternal」
三級結構。"FSDirectory#unprotectedMkdir() + FSEditLog#logMkDir()"
組合。本文連接:源碼|HDFS之NameNode:建立目錄
做者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議發佈,歡迎轉載,演繹或用於商業目的,可是必須保留本文的署名及連接。