NameNode会维护文件系统的命名空间,hdfs文件系统的命名空间是以"/" 为根目录开始的整棵目录树,整棵目录树是通过FSDirectory类来管理的。
在HDFS中,无论是目录还是文件,在文件系统目录树中都被看做是一个INode节点,如果是目录,则对应的类是INodeDirectory,如果是文件,则对应的类是INodeFile;INodeDirectory类以及INodeFile类都是INode的子类。其中INodeDirectory中包含一个成员集合变量children,如果该目录下有子目录或者文件,其子目录或文件的INode引用就会被保存在children集合中。HDFS就是通过这种方式维护整个文件系统的目录结构。
HDFS会将命名空间保存到NameNode的本地文件系统上一个叫fsimage的文件中。利用这个文件,NameNode每次重启的时候都能将整个HDFS的命名空间重构,fsimage是由FSImage类来负责的。另外对HDFS的操作,NameNode都会在操作日志editlog中进行记录,以便于周期性的将该操作日志editlog与fsImage进行合并生成新的fsimage。该日志文件editlog也在NameNode的本地文件系统中保存,由FSEditLog类来进行管理。
HDFS文件相关的类设计
在HDFS中与文件相关的类如下:
- INode--底层抽象类,保存了hdfs目录与文件所拥有的共同属性,比如:文件/目录名、用户组、访问权限、修改时间等
- INodeFile--文件节点类,继承自INodeWithAdditionalFields,表示一个hdfs文件
- INodeFileUnderConstruction--处于构建状态的文件类,可以从INodeFile中转化而来。
- INodeDirectory--文件目录类,也是继承自INode,其内部成员变量children列表用来保存目录/文件的INode对象
- INodeDirectoryWithQuota--有配额限制的目录
INode
INode基础抽象类,其内部保存了hdfs文件和目录共有的基本属性;包括当前节点的父节点的INode对象的引用、文件、目录名、用户组、访问权限等。需要注意的是,INode类的设计采用了模板模式。INode类定义的方法多为2个,一个是final的接口方法,用于规范接口的调用;另一个是abstract抽象方法,抽象方法留给子类具体实现。INode类中只有一个字段,就是parent,表名当前INode的父目录。
其内部定义了以下公共属性字段对应的方法:
- userName:文件/目录所属用户名
- groupName:文件/目录所属用户组
- fsPermission:文件或者目录权限
- modificationTime:上次修改时间
- accessTime:上次访问时间
以及INode的元数据信息对应的方法:
- id:INode的id
- name:文件/目录的名称
- fullPathName:文件/目录的完整路径
- parent:文件/目录的父节点
public abstract class INode implements INodeAttributes, Diff.Element<byte[]> {
/** parent is either an {@link INodeDirectory} or an {@link INodeReference}.*/
// 表示当前INode的父目录
private INode parent = null;
INode(INode parent) {
this.parent = parent;
}
/** Get inode id */
public abstract long getId();
/**
* Check whether this is the root inode.
*/
final boolean isRoot() {
return getLocalNameBytes().length == 0;
}
// 模板设计模式
// abstract抽象方法留给子类具体实现
// final方法用于接口调用,规范接口调用
/** Set user */
abstract void setUser(String user);
/** Set user */
final INode setUser(String user, int latestSnapshotId)
throws QuotaExceededException {
recordModification(latestSnapshotId);
setUser(user);
return this;
}
}
INode作为基础的抽象类,其内部的permission字段表示用户的操作权限,在INode中,permission的数据类型为long长整型,其将permission的64字节分为三段,分别用于保存访问权限、用户组标识符和用户名标识符,并巧妙地利用了Java的枚举,建立长整型上的分段操作,实现了上述三个文件属性的操作。代码如下:
static enum PermissionStatusFormat {
MODE(null, 16),
GROUP(MODE.BITS, 25),
USER(GROUP.BITS, 23);
final LongBitFormat BITS;
private PermissionStatusFormat(LongBitFormat previous, int length) {
BITS = new LongBitFormat(name(), previous, length, 0);
}
// 提取最后23个bit的user信息
static String getUser(long permission) {
final int n = (int)USER.BITS.retrieve(permission);
return SerialNumberManager.INSTANCE.getUser(n);
}
// 提取中间25个bit的group信息
static String getGroup(long permission) {
final int n = (int)GROUP.BITS.retrieve(permission);
return SerialNumberManager.INSTANCE.getGroup(n);
}
// 提取前16个bit的mode信息
static short getMode(long permission) {
return (short)MODE.BITS.retrieve(permission);
}
/** Encode the {@link PermissionStatus} to a long. */
// 将其转化为一个long型的权限信息
static long toLong(PermissionStatus ps) {
long permission = 0L;
final int user = SerialNumberManager.INSTANCE.getUserSerialNumber(
ps.getUserName());
permission = USER.BITS.combine(user, permission);
final int group = SerialNumberManager.INSTANCE.getGroupSerialNumber(
ps.getGroupName());
permission = GROUP.BITS.combine(group, permission);
final int mode = ps.getPermission().toShort();
permission = MODE.BITS.combine(mode, permission);
return permission;
}
}
枚举PermissionStatusFormat有三个值,MODE、GROUP、USER,分别用于处理访问权限、用户组标识符和用户名标识符。上述三个枚举值创建时,都会调用PermissionStatusFormat的构造函数,构造函数需要链各个参数,分别是枚举值对应的属性在长整形permission中的偏移量和长度。
在使用INode.getUser(permission)获取用户名的时候,由于用户名标识符保存在permission的41~63位,其会使用枚举USER.retrieve(),它会把成员变量permission和掩码USER.MASK进行与运算,然后右移,获得标识符的值;然后再保存标识符和用户名对应关系的SerialNumberManager实例中进行查找,以得到字符串形式的用户名。INode.setUser()用于设置节点的用户名,它的实现也利用了PermissionStatusFormat,使用USER.combine()设置文件所有者标识符对应位。在HDFS中,用户名和用户标识的影射、用户组名和用户组标识符的影射保存在SerialNumberManager对象中。通过SerialNumberManaer,名字节点不必在INode对象中保存字符串形式的用户名和用户组名,节省了对象对内存的占用。
INode中的方法大多比较简单,提供了对成员变量的访问/设置能力。这些方法中需要额外注意的就是isRoot()判断当前节点是否是目录树的根节点,目录树的根节点是HDFS中最重要的一个目录,所有目录都由根目录衍生。如果INode的成员属性name长度为0,约定这是HDFS的根节点,INode.isRoot()返回true,否则返回false,该节点便不是HDFS的根。
INodeFile
在文件系统目录树中,使用INodeFile抽象成一个hdfs文件,它也是INode的子类。INodeFile中包含了两个文件特有的属性:文件信息头header和文件对应的数据块信息blocks。
private long header = 0L; private BlockInfo[] blocks;
- header:使用了和INode.permission一样的方法,在一个长整形变量里保存了文件的副本系数和文件数据块的大小,它的高16字节存放着副本系数,低48位存放了数据块大小。
- blocks数组:保存了文件拥有的所有数据块信息,数组元素类型是BlockInfo。BlockInfo类继承自Block类,他保存了数据块与文件、数据块与数据节点的对应关系。
其基本的数据块操作也就是操作blocks数组,如下:
Block getLastBlock() {
if (this.blocks == null || this.blocks.length == 0)
return null;
return this.blocks[this.blocks.length - 1];
}
void addBlock(BlockInfo newblock) {
if (this.blocks == null) {
this.setBlocks(new BlockInfo[]{newblock});
} else {
int size = this.blocks.length;
BlockInfo[] newlist = new BlockInfo[size + 1];
System.arraycopy(this.blocks, 0, newlist, 0, size);
newlist[size] = newblock;
this.setBlocks(newlist);
}
}
INodeFileUnderConstrucation
INodeFileUnderConstruction是INodeFile的子类。它是指处于构建状态的文件索引节点,当客户端为写或者追加数据打开HDFS文件时,该文件就处于构建状态,在HDFS的目录树中,相应的节点就是一个INodeFileUnderConstruction对象。
INodeFileUnderConstuction相关:
- clientName:发起文件写的客户端名称,这个属性也用于租约管理中,在HDFS中租约是名字节点维护的,给予客户端在一定期限内可以进行文件写操作的权限的合同。
- clientMachine:客户端所在的主机。
- clientNode:如果客户端运行在集群内的某一个数据节点上时,对应的数据节点信息,DatanodeDescriptor是名字节点内部使用的,用于保存数据节点信息的类,它继承自DatanodeInfo。
- targets:最后一个数据块的数据流管道成员,即当前参与到写数据的数据节点的列表。primaryNodeIndex和lastRecoveryTime:都用于名字节点发起的数据块恢复,由名字节点发起的数据块恢复也叫租约恢复,这两个变量分别保存恢复时的主数据节点索引和恢复开始时间。
class INodeFileUnderConstruction extends INodeFile { //处于构建状态的文件节点
String clientName; // lease holder 写文件的客户端名称,也是这个租约的持有者
private final String clientMachine; // 客户端所在的主机
// 如果客户端同样存在于集群中,则记录所在的节点
private final DatanodeDescriptor clientNode; // if client is a cluster node too.
// 租约恢复时的节点
private int primaryNodeIndex = -1; //the node working on lease recovery
// 最后一个block块所处的节点组,又名数据流管道成员
private DatanodeDescriptor[] targets = null; //locations for last block
// 最近租约恢复时间
private long lastRecoveryTime = 0;
}
对于处于构建状态的节点来说,他的操作也是往最后一个block添加数据,并且保留了最后一个块的所在的数据节点列表。相关方法如下:
// 设置新的block块,并且为最后的块赋值新的targes节点
synchronized void setLastBlock(BlockInfo newblock, DatanodeDescriptor[] newtargets
) throws IOException {
// ......
BlockInfo oldLast = blocks[blocks.length - 1];
if (oldLast.getBlockId() != newblock.getBlockId()) {
throw new IOException();
}
// 如果新的block时间比老block的还小的话,则进行警告
if (oldLast.getGenerationStamp() > newblock.getGenerationStamp()) {
NameNode.stateChangeLog.warn();
}
blocks[blocks.length - 1] = newblock;
setTargets(newtargets);
// 重置租约恢复时间,这样操作的话,下次租约检测时将会过期
lastRecoveryTime = 0;
}
INodeDirectory
INodeDirectory抽象了HDFS中的目录,目录里面保存了文件和其他子目录。在INodeDirectory实现中的体现是其成员变量children,它是一个用于保存INode的列表。INodeDirectory中的大部分方法都是在操作这个列表,如创建子目录项、查询或遍历子目录项、替换子目录项等,它们的实现都比较简单。如下所示:
class INodeDirectory extends INode {
private List<INode> children = null; // 保存子目录或子文件
INode removeChild(INode node) { // 移除节点方法
assert children != null;
int low = Collections.binarySearch(children, node.name); // 用二分法寻找文件节点
if (low >= 0) {
return children.remove(low);
} else {
return null;
}
}
<T extends INode> T addChild(final T node, boolean inheritPermission) {
if (inheritPermission) {
FsPermission p = getFsPermission();
//make sure the permission has wx for the user
// 判断用户是否有写权限
if (!p.getUserAction().implies(FsAction.WRITE_EXECUTE)) {
p = new FsPermission(p.getUserAction().or(FsAction.WRITE_EXECUTE),
p.getGroupAction(), p.getOtherAction());
}
node.setPermission(p);
}
if (children == null) {
children = new ArrayList<INode>(DEFAULT_FILES_PER_DIRECTORY);
}
int low = Collections.binarySearch(children, node.name); // 二分查找
if(low >= 0)
return null;
node.parent = this;
children.add(-low - 1, node); // 在孩子列表中进行添加
// ......
return node;
}
// ......
}
INodeDirectoryWithQuota
INodeDirectory有一个子类INodeDirectoryWithQuota,用于实现HDFS的配额机制,HDFS允许管理员为每个目录设置配额。配额有两种:
- 节点配额:用于限制目录下的名字数量,如果创建文件或目录时超过了该配额,操作失败。这个配额用于控制用于对于名字节点资源的占用,保存在成员变量nsQuota中。
- 空间配额:限制存储在目录树下的所有文件的总规模,空间配额保证用户不会过多咱用数据节点的资源,该配额由dsQuota变量保存。
HDFS的dfsadmin工具提供了修改目录配额的命令,该命令会修改INodeDirectoryWithQuota对象相应的成员变量。方法INodeDirectoryWithQuota.verifyQuota()则用于检测对目录树的更新是否满足设置的配额,如果不满足,则该方法或抛出异常。
// 存在配额限制的目录节点,继承自目录节点
class INodeDirectoryWithQuota extends INodeDirectory {
private long nsQuota; // NameSpace quota 命名空间配额
private long nsCount; // 名字空间计数
private long dsQuota; // disk space quota 磁盘空间配额
private long diskspace; // 磁盘空间占用大小
void verifyQuota(long nsDelta, long dsDelta) throws QuotaExceededException {
// 根据误差值计算新的计数值
long newCount = nsCount + nsDelta;
long newDiskspace = diskspace + dsDelta;
if (nsDelta>0 || dsDelta>0) {
// 判断新的值是否超出配额的值大小
if (nsQuota >= 0 && nsQuota < newCount) {
throw new NSQuotaExceededException(nsQuota, newCount);
}
if (dsQuota >= 0 && dsQuota < newDiskspace) {
throw new DSQuotaExceededException(dsQuota, newDiskspace);
}
}
}
}