Hadoop源码分析笔记(九):数据节点--数据块存储

数据节点简介

        我们继续来了解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架构设计与实现原理】一书,仅作为学习笔记,用于技术交流,其商业版权由原作者保留,推荐大家购买图书研究,转载请保留原作者,谢谢!
      
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值