Hdfs文件系统目录树以及INode类分析

        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中与文件相关的类如下:

  1. INode--底层抽象类,保存了hdfs目录与文件所拥有的共同属性,比如:文件/目录名、用户组、访问权限、修改时间等
  2. INodeFile--文件节点类,继承自INodeWithAdditionalFields,表示一个hdfs文件
    1. INodeFileUnderConstruction--处于构建状态的文件类,可以从INodeFile中转化而来。
  3. INodeDirectory--文件目录类,也是继承自INode,其内部成员变量children列表用来保存目录/文件的INode对象
    1. INodeDirectoryWithQuota--有配额限制的目录

INode

     INode基础抽象类,其内部保存了hdfs文件和目录共有的基本属性;包括当前节点的父节点的INode对象的引用、文件、目录名、用户组、访问权限等。需要注意的是,INode类的设计采用了模板模式。INode类定义的方法多为2个,一个是final的接口方法,用于规范接口的调用;另一个是abstract抽象方法,抽象方法留给子类具体实现。INode类中只有一个字段,就是parent,表名当前INode的父目录。

其内部定义了以下公共属性字段对应的方法:

  1. userName:文件/目录所属用户名
  2. groupName:文件/目录所属用户组
  3. fsPermission:文件或者目录权限
  4. modificationTime:上次修改时间
  5. accessTime:上次访问时间

以及INode的元数据信息对应的方法:

  1. id:INode的id
  2. name:文件/目录的名称
  3. fullPathName:文件/目录的完整路径
  4. 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允许管理员为每个目录设置配额。配额有两种:

  1. 节点配额:用于限制目录下的名字数量,如果创建文件或目录时超过了该配额,操作失败。这个配额用于控制用于对于名字节点资源的占用,保存在成员变量nsQuota中。
  2. 空间配额:限制存储在目录树下的所有文件的总规模,空间配额保证用户不会过多咱用数据节点的资源,该配额由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);
      }
    }
  }
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值