求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
Hadoop源码分析之DateNode的目录构成与类继承结构
 
火龙果软件    发布于 2014-02-19
 

在继续分析NameNode启动之前,先看看DateNode与NameNode的目录构成与类继承结构,NameNode与DataNode的启动过程与这些目录结构和类继承结构相关,在启动的过程中会检查目录的状态。这里所分析的类的继承结构是与存储文件相关的类,与数据交换有关的类暂不考虑。

DataNode的文件目录结构

DataNode目录就是数据节点上存放与数据块相关数据的目录,由${dfs.data.dir}属性指定,这个属性可以指定多个值,值之间使用逗号隔开,如“/data1/datanode,/data2/datanode”指定类两个目录来存放数据节点。如果不指定${data.data.dir}数据块将会放在一个名为tmp的临时目录中,下文中就用${dfs.data.dir}代表数据节点的存储目录。DataNode启动之后${dfs.data.dir}目录中有四个目录和两个文件,如下图所示。

其中in_use.lock文件是在DataNode节点启动之后产生的,其中各个目录的作用如下:

blocksBeingWritten:该文件夹保存着当前正在”写“的数据块。

current:保存着HDFS文件系统中的数据块,这些数据块是成功提交到HDFS中的数据块。detach:用于配合数据节点升级,共数据块分离操作保存临时工作文件。

tmp:该文件夹保存着当前正在”写“的数据块,和blockBeingWritten文件夹的区别是,blockBeingWritten中的数据块写操作由客户端发起,tmp中的写操作由数据块复制引发,另一个数据节点正在发送数据到数据块中。

storage:0.13版本以前的Hadoop使用storage文件作为数据块的保存目录,和现在的目录结构不兼容,这个文件用于防止过旧的Hadoop版本在新的目录结构上启动,损坏系统。

in_use.lock:表明目录已经被使用,停止数据节点,该文件会消失,通过in_use.lock文件,数据节点可以保证独自占用该目录,防止两个数据节点示例共享一个目录,造成混乱。

current目录是数据节点中最重要的一个目录,它用于存放数据块,该目录中既包含目录,也包含文件,其中文件有两种类型:

HDFS数据块,保存着HDFS文件的内容;

用于保存数据块的校验信息的校验信息文件,以meta后缀名标识;

VERSION文件是一个Java属性文件,包含了HDFS的版本信息。

current目录如下图所示:

在这个图片中,没有目录,是因为当前的数据节点中的文件块的数量较少,只有当目录中存储的数据块增加到一定规模时,子目录名以subdir为前缀,然后后面加上目录编号,数据节点会创建一个新目录,用于保存新的块及元数据。目录中的数据块数达到64时,便会创建子目录,并形成一个更宽的目录结构,同时\统一父目录下最多会创建64个子目录,所以在默认配置下,一个目录下最多只有64个文件块和64个子目录。这种目录管理方式既保证了目录深度不会太深,而影响检索文件性能,同时也避免了目录保存大量数据块,确保每个目录中的文件块是可控的。

DataNode中类的继承结构

在HDFS中,与上面的目录和文件对应的类有多个,这些类组成一个继承结构,根类是StorageInfo类,继承结构如下图所示:

其中NamespaceInfo包含HDFS集群的一些信息,CheckpointSignature则用于标识名字节点元数据的检查,这两个类出现在HDFS节点间通信的远程接口中,分别应用于DataNode节点和NameNode节点通信的DatanodeProtocol接口和SecondNameNode节点和NameNode节点通信的NamespaceProtocol。StorageInfo包含了三个成员变量,类的定义和成员变量的代码如下:

public class StorageInfo {
/**是一个负整数,保存了HDFS存储系统信息结构的版本号**/
public int layoutVersion; // Version read from the stored file.
/**存储系统的唯一标识符**/
public int namespaceID; // namespace id of the storage
/**该信息的创建时间**/
public long cTime; // creation timestamp
}

这三个成员变量分别对应这VERSION文件中的内容,在上文中说过VERSION文件是一个Java属性文件,包含了HDFS的版本信息。DataNode节点的current目录中有一个VERSION文件,来标识当前DataNode节点的版本信息,NameNode节点同样有一个VERSION节点,来标识NameNode节点的版本信息,但是DataNode节点目录中的VERSION文件比NameNode节点目录中的VERSION文件多一个storageID属性,它表示一个数据节点在集群中的唯一标识,VERSION文件中的属性及其意义如下:

namespaceID:存储系统的标识

layoutVersion:HDFS存储系统信息结构的版本号

ctime:存储系统创建时间

storageType:表示节点类型

StorageInfo类中的三个成员变量分别对应上述的namespaceID、layoutVersion和ctime。

StorageInfo的子类Storage是一个抽象类,它为数据节点、名字节点等提供通用的存储服务,在Storage类中有一个内部类StorageDirectory类,一个Storage对象管理着多个StorageDirectory对象,在Storage中用成员变量storageDirs表示,它是一个ArrayList<StorageDirectory>类型的变量。Storage类提供类大量的常量和方法用于为HDFS提供存储服务,它还有一个内部类DirIterator,用于对storageDirs进行迭代,来处理每一个SotrageDirectory对象。Storage类还有一个成员变量storageType,用于标识当前的节点类型,是NameNode还是DataNode。

关于Storage重点要分析的是StorageDirectory类,它提供了存储目录上的一些通用操作,其定义和成员变量代码如下:

public class StorageDirectory {
/**存储目录的根**/
File root; // root directory
/**使用的是独用文件锁**/
FileLock lock; // storage lock
/**目录对应的类型,即存储空间的类型**/
StorageDirType dirType; // storage dir type
}

root标识保存存储目录的根目录,dirType标识目录的类型,StorageDirType是一个枚举类型有四个值,分别是UNDEFINED、IMAGE、EDITS、IMAGE_AND_EDITS,其意义分别是:

UNDEFINED:为定义;

IMAGE:存储fsimage

EDITS:存储edits

IMAGE_AND_EDITS:存储fsimage和edits

fsimage和edits是NameNode节点的镜像和编辑日志文件,由于NameNode节点在运行时必须记录下其每个时刻的一个状态,然后存储在一个镜像文件中,以便出故障之后可以从镜像中恢复,但是每个时刻都生成一个镜像文件就会得不偿失,导致NameNode节点大量的资源用于生成镜像文件,所以引入编辑日志(edits),记录生成镜像文件之后的一段时间对NameNode节点所做的更改,当生成一定量的编辑日志之后,再利用SecondNameNode节点生成镜像文件。所以就有了上面的几个枚举值的定义,IMAGE标识这个目录只存储镜像(fsimage),EDITS标识这个目录值存储编辑日志(edits),IMAGE_AND_EDITS表示存储镜像和编辑日志。StorageDirectory类中的lock成员变量用于对目录进行加锁,上面分析数据节点的目录结构时,有一个in_use.lock文件,这个文件就是在数据节点运行时,由tryLock()方法创建的,并且lock表示的锁是一个独占锁。

Storage类的子类DataStorage类对DataNode节点的存储空间的生存期进行管理,它有三个常量和一个变量,其类定义代码为:

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;
}

由上面的代码可以看出subdir即为目录的前缀,blk是文件块的前缀,dncp是数据节点进行数据块扫描时用到的文件名前缀。storageID则对应DataNode节点的VERSION文件中的storageID属性,标识当前DataNode节点在HDFS中的唯一标识。在DataStorage类中有一个format()方法,数据节点在第一次启动时调用这个方法创建存储目录。

与数据节点相关的类基本上就是上面分析的几个类,下面结合DataStorage类中的方法doUpgrade,doRollback,doFinalize来分析数据节点中的目录的几个状态。

数据节点的升级、回滚与提交

在Storage类中使用枚举类StorageState定义类目录的10中状态,其定义代码如下:

/**
* 定义了存储空间的可能状态,升级,升级提交或升级回滚等状态
*
*/
public enum StorageState {
/**以非FORMAT选项启动时,目录不存在,或者目录不可写, 或者配置项对应的路径是个文件,而非目录等,都可以认为存储空间不存在**/
NON_EXISTENT,
/**以FORMAT选项启动,同时目录不存在,这是存储空间状态为没有格式化**/
NOT_FORMATTED,
/**“previous.tmp”目录存在,同时“current”目录下的"VERSION"存在,可完成升级状态**/
COMPLETE_UPGRADE,
/**“previous.tmp”目录存在,“current/VERSION”文件不存在,存储空间应该从升级过程中恢复**/
RECOVER_UPGRADE,
/**存在“finalized.tmp”目录,存储空间可以继续执行提交升级**/
COMPLETE_FINALIZE,
/**存在“previous.tmp”目录,同时“current/VERSION”文件存在,可以继续执行提交回滚**/
COMPLETE_ROLLBACK,
/**存在“previsous.tmp”目录,“current”目录下“VERSION”文件不存在, 需要中断升级提交,恢复到上一个状态**/
RECOVER_ROLLBACK,
/**应用于名字节点,用于导入元数据检查,存储空间可完成检查点导入**/
COMPLETE_CHECKPOINT,
/**应用与名字节点,用于导入元数据检查,需要恢复导入检查点前状态**/
RECOVER_CHECKPOINT,
/**目录处于正常工作状态**/
NORMAL;
}

枚举值在代码中已经注释了,不再重复。在文章Hadoop源码分析之NameNode的启动与停止中已经分析到了HDFS的启动参数,这些启动参数都定义在枚举类StartupOption中,如果HDFS以参数update启动,那么会执行到DataStorage.doUpgrade()方法,对HDFS的数据节点进行升级,先来看DataStorage.doUpgrade()方法,方法代码如下:

/**
* 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();//上一版本目录,目录名为“previous”
//检查current目录是否存在,如果不存在就不需要升级
assert curDir.exists() : "Current directory must exist.";
// delete previous dir before upgrading,删除该目录相当于提交了上一次升级, 同时保证了HDFS最多保留前一版本的数据要求
if (prevDir.exists())
deleteDir(prevDir);
//tempDir目录不应该存在,tempDir表示上一版本的临时目录,目录名为“previous.tmp”
File tmpDir = sd.getPreviousTmp();
assert !tmpDir.exists() : "previous.tmp directory must not exist.";
// rename current to tmp,目录改名,current->previous.tmp
rename(curDir, tmpDir);
// hardlink blocks,为数据块和数据块校验信息文件建立硬链接
linkBlocks(tmpDir, curDir, this.getLayoutVersion(), hardLink);
// write version file,写新的VERSION文件
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();//将数据写入VERSION文件
// rename tmp to previous,目录改名,previous.tmp->previous
rename(tmpDir, prevDir);
LOG.info( hardLink.linkStats.report());
LOG.info("Upgrade of " + sd.getRoot()+ " is complete");
}

枚举值在代码中已经注释了,不再重复。在文章Hadoop源码分析之NameNode的启动与停止中已经分析到了HDFS的启动参数,这些启动参数都定义在枚举类StartupOption中,如果HDFS以参数update启动,那么会执行到DataStorage.doUpgrade()方法,对HDFS的数据节点进行升级,先来看DataStorage.doUpgrade()方法,方法代码如下:

/**
* 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();//上一版本目录,目录名为“previous”
//检查current目录是否存在,如果不存在就不需要升级
assert curDir.exists() : "Current directory must exist.";
// delete previous dir before upgrading,删除该目录相当于提交了上一次升级, 同时保证了HDFS最多保留前一版本的数据要求
if (prevDir.exists())
deleteDir(prevDir);
//tempDir目录不应该存在,tempDir表示上一版本的临时目录,目录名为“previous.tmp”
File tmpDir = sd.getPreviousTmp();
assert !tmpDir.exists() : "previous.tmp directory must not exist.";
// rename current to tmp,目录改名,current->previous.tmp
rename(curDir, tmpDir);
// hardlink blocks,为数据块和数据块校验信息文件建立硬链接
linkBlocks(tmpDir, curDir, this.getLayoutVersion(), hardLink);
// write version file,写新的VERSION文件
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();//将数据写入VERSION文件
// rename tmp to previous,目录改名,previous.tmp->previous
rename(tmpDir, prevDir);
LOG.info( hardLink.linkStats.report());
LOG.info("Upgrade of " + sd.getRoot()+ " is complete");
}

在doUpgrade()方法中,先获取到当前的current目录,然后获取名为previous的上一个版本的目录,如果previous目录存在,则进行删除,因为HDFS最多保存两个版本的文件系统,升级成功之后,当前的current目录就变成previous目录,并且会生成新版本的current目录。然后将当前的current目录改名为preivious.tmp目录,创建current目录,为current将previous.tmp目录中的文件通过硬链接添加到current目录中,保存当前的版本信息以便生成VERSION文件,最后将previous.tmp目录改名为previous目录。在这个过程中有一个previous.tmp,先将当前的current目录改名为previous.tmp目录,再同过硬链接为新创建的current目录添加文件,最后将previous.tmp目录改名为previous,既然这样为什么不将前一版本的current目录直接改名为previous,还要在中间加一个previous.tmp目录的过程?因为在doUpgrade()方法执行的过程中节点可能会出故障,假设节点出故障的时刻,已经将当前版本的current目录改名为previous.tmp目录,生成current目录,但是没有将previous.tmp目录,那么当数据节点再次启动时它就可以根据previous.tmp目录来知道当前的状态是在升级的过程中出故障,这样就可以确定当前节点的状态。但是如果没有previous.tmp目录的过程,直接将current目录改名为previous,即使数据节点出故障了,数据节点下次启动时,也不能确定数据节点的停止的是由于出故障了还是正常停止的。

doRollback()方法主要用于对HDFS的升级进行回滚。进行升级回滚时,节点中有previous目录和current目录,假设回滚过程不出故障,那么可以直接删除current目录,再将previous目录直接改名为current目录,但是考虑到出故障的情况,类似doUpgrade()方法中采取的方式,先将current目录改名为removed.tmp目录,再将previous目录改名为current目录,最后删除removed.tmp目录。

doFinalize()方法对HDFS的升级进行提交,提交之后,HDFS中只有一个最新版本。假设提交过程不出故障,那么直接删除previous目录即可,但是考虑到出故障的情况,类似doUpgrade()方法中采取的方式,先将previous目录改名为finalized.tmp目录,然后删除finalized.tmp目录。

在Storage类中有一个方法analyzeStorage,它的作用是检查文件目录的状态,即确定当前给定的目录是StorageState枚举类中中定义了目录的十种状态中的哪一种状态,该方法就是根据这些状态之间的转换关系判断当前目录所处的状态,方法代码如下:

    /**
* Check consistency of the storage directory.
* 检查文件目录的状态,在{@link StorageState}中定义了目录的十种状态,该方法可以根据这些状态之间的转换
* 关系判断当前目录所处的状态
* @param startOpt a startup option.
*
* @return state {@link StorageState} of the storage directory
* @throws InconsistentFSStateException if directory state is not
* consistent and cannot be recovered.
* @throws IOException
*/
public StorageState analyzeStorage(StartupOption startOpt) throws IOException {
assert root != null : "root is null";
String rootPath = root.getCanonicalPath();
try { // check that storage exists
//以非FORMAT选项启动时,目录不存在,或者目录不可写,或 者配置项对应路径是个文件,而非目录,都可以认为存储空间不存在
if (!root.exists()) {//如果根目录不存在
// storage directory does not exist
if (startOpt != StartupOption.FORMAT) {
LOG.info("Storage directory " + rootPath + " does not exist");
return StorageState.NON_EXISTENT;
}
LOG.info(rootPath + " does not exist. Creating...");
if (!root.mkdirs())//创建根目录
throw new IOException("Cannot create directory " + rootPath);
}
// or is inaccessible,访问不了
if (!root.isDirectory()) {
LOG.info(rootPath + "is not a directory");
return StorageState.NON_EXISTENT;
}
//不可写
if (!root.canWrite()) {
LOG.info("Cannot access storage directory " + rootPath);
return StorageState.NON_EXISTENT;
}
} catch(SecurityException ex) {
LOG.info("Cannot access storage directory " + rootPath, ex);
return StorageState.NON_EXISTENT;
}
this.lock(); // lock storage if it exists
//只要是以FORMAT启动,都认为是NOT_FORMATE状态
if (startOpt == HdfsConstants.StartupOption.FORMAT)
return StorageState.NOT_FORMATTED;
if (startOpt != HdfsConstants.StartupOption.IMPORT) {
//make sure no conversion is required
checkConversionNeeded(this);//检查hadoop的版本,如果是hadoop0.13版本, 那么在根目录下会存在image目录,否则这个目录无用
}
// check whether current directory is valid
File versionFile = getVersionFile();//获取VERSION文件
boolean hasCurrent = versionFile.exists();
// check which directories exist
boolean hasPrevious = getPreviousDir().exists();//“previous”文件夹
boolean hasPreviousTmp = getPreviousTmp().exists();//“previous.tmp”文件夹
boolean hasRemovedTmp = getRemovedTmp().exists();//"removed.mp"文件夹
boolean hasFinalizedTmp = getFinalizedTmp().exists();//“finalized.tmp”文件夹
boolean hasCheckpointTmp = getLastCheckpointTmp().exists();//checkpo
if (!(hasPreviousTmp || hasRemovedTmp
|| hasFinalizedTmp || hasCheckpointTmp)) {//没有tmp文件夹
// no temp dirs - no recovery
if (hasCurrent)//没有tmp文件夹,那么就处于NORMAL状态
return StorageState.NORMAL;
if (hasPrevious)//有previous文件夹,没有current文件夹
throw new InconsistentFSStateException(root,
"version file in current directory is missing.");
return StorageState.NOT_FORMATTED;//current目录不存在, 没有*.tmp目录,且没有previous目录,则是NOT_FORMAT状态
}
//同时存在着至少两个tmp文件夹,则出现错误
if ((hasPreviousTmp?1:0) + (hasRemovedTmp?1:0)
+ (hasFinalizedTmp?1:0) + (hasCheckpointTmp?1:0) > 1)
// more than one temp dirs
throw new InconsistentFSStateException(root,
"too many temporary directories.");
// # of temp dirs == 1 should either recover or complete a transition
if (hasCheckpointTmp) {//名字节点元数据检查点
return hasCurrent ? StorageState.COMPLETE_CHECKPOINT //如果有lastcheckpoint.tmp目录,且有current目录,完成检查点导入
: StorageState.RECOVER_CHECKPOINT; //如果有lastcheckpoint.tmp目录,且没有current目录,就是没有完成检查点导入
}
if (hasFinalizedTmp) {//有finalized.tmp文件夹
if (hasPrevious)//有previous文件夹,但是finalized.tmp文件夹是previous在提交过程中改名的文件夹, 这两个文件夹同时存在说明出现问题
throw new InconsistentFSStateException(root,
STORAGE_DIR_PREVIOUS + " and " + STORAGE_TMP_FINALIZED
+ "cannot exist together.");
return StorageState.COMPLETE_FINALIZE;//有finalized.tmp文件夹, 没有previous文件夹,说明升级完成并且提交
}
if (hasPreviousTmp) {//有previous.tmp文件夹
if (hasPrevious)
throw new InconsistentFSStateException(root,
STORAGE_DIR_PREVIOUS + " and " + STORAGE_TMP_PREVIOUS
+ " cannot exist together.");
if (hasCurrent)//有previous.tmp文件夹和current文件夹,则就是完成升级状态
return StorageState.COMPLETE_UPGRADE;
return StorageState.RECOVER_UPGRADE; //有previous.tmp文件夹没有current文件夹,说明升级过程中出现问题而中断
}
//没有执行if (!(hasPreviousTmp || hasRemovedTmp
// || hasFinalizedTmp || hasCheckpointTmp)) { 这行代码说明至少存在一个tmp文件夹,如果存在一个finalized.tmp或者
//previous.tmp文件夹或者checkpoint.tmp文件夹那么在上面已经返回, 执行到这里说明只存在removed.tmp文件夹,说明是回滚过
//程中出现问题,所以removed.tmp文集夹一定存在
assert hasRemovedTmp : "hasRemovedTmp must be true";
if (!(hasCurrent ^ hasPrevious))
throw new InconsistentFSStateException(root,
"one and only one directory " + STORAGE_DIR_CURRENT
+ " or " + STORAGE_DIR_PREVIOUS
+ " must be present when " + STORAGE_TMP_REMOVED
+ " exists.");
if (hasCurrent)//有removed.tmp和current文件夹,回滚已经完成,tmp文件还没删除
return StorageState.COMPLETE_ROLLBACK;
return StorageState.RECOVER_ROLLBACK;//回滚过程没有完成
}

方法代码比较常,代码执行过程的在注释中已经给出,比较好理解,就不再分析了。

总结

在上面的分析中简单的分析了数据节点中目录结构组成,类的继承结构,和数据节点的目录所处的状态,其中类和方法只是简单的涉及到了,具体的代码分析,将在以后用到的时候分析,这篇文章的分析主要是为了引出目录的状态及其代表的意义。

相关文章

基于EA的数据库建模
数据流建模(EA指南)
“数据湖”:概念、特征、架构与案例
在线商城数据库系统设计 思路+效果
 
相关文档

Greenplum数据库基础培训
MySQL5.1性能优化方案
某电商数据中台架构实践
MySQL高扩展架构设计
相关课程

数据治理、数据架构及数据标准
MongoDB实战课程
并发、大容量、高性能数据库设计与优化
PostgreSQL数据库实战培训
 
分享到
 
 


MySQL索引背后的数据结构
MySQL性能调优与架构设计
SQL Server数据库备份与恢复
让数据库飞起来 10大DB2优化
oracle的临时表空间写满磁盘
数据库的跨平台设计
更多...   


并发、大容量、高性能数据库
高级数据库架构设计师
Hadoop原理与实践
Oracle 数据仓库
数据仓库和数据挖掘
Oracle数据库开发与管理


GE 区块链技术与实现培训
航天科工某子公司 Nodejs高级应用开发
中盛益华 卓越管理者必须具备的五项能力
某信息技术公司 Python培训
某博彩IT系统厂商 易用性测试与评估
中国邮储银行 测试成熟度模型集成(TMMI)
中物院 产品经理与产品管理
更多...