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 国际许可协议发布,欢迎转载,演绎或用于商业目的,可是必须保留本文的署名及连接。