前言
在Hadoop中,所有的元数据的保存都是在namenode节点之中,每次重新启动整个集群,Hadoop都需要从这些持久化了的文件中恢复数据到内存中,然后通过镜像和编辑日志文件进行定期的扫描与合并,ok,这些稍微了解Hadoop的人应该都知道,这不就是SecondNameNode干的事情嘛,但是很多人只是了解此机制的表象,内部的一些实现机理估计不是每个人都又去深究过,你能想象在写入编辑日志的过程中,用到了双缓冲区来加大并发量的写吗,你能想象为了避免操作的一致性性,作者在写入的时候做过多重的验证操作,还有你有想过作者是如何做到操作中断的处理情况吗,如果你不能很好的回答上述几个问题,那么没有关系,下面来分享一下我的学习成果。
相关涉及类
建议读者在阅读本文的过程中,结合代码一起阅读, Hadoop的源码可自行下载,这样效果可能会更好。与本文有涉及的类包括下面一些,比上次的分析略多一点,个别主类的代码量已经在上千行了。
1.FSImage--命名空间镜像类,所有关于命名空间镜像的操作方法都被包括在了这个方法里面。
2.FSEditLog--编辑日志类,编辑日志类涉及到了每个操作记录的写入。
3.EditLogInputStream和EditLogOutputStream--编辑日志输入类和编辑日志输出类,编辑日志的许多文件写入读取操作都是在这2个类的基础上实现的,这2个类是普通输入流类的继承类,对外开放了几个针对于日志读写的特有方法。
4.Storage和StorageInfo--存储信息相关类。其中后者是父类,Storage继承了后者,这2个类是存储目录直接相关的类,元数据的备份相关的很多目录操作都与此相关。
5.SecondaryNameNode--本来不想把这个类拉进去来的,但是为了使整个备份机制更加完整的呈现出来,同样也是需要去了解这一部分的代码。
ok,介绍完了这些类,下面正式进入元数据备份机制的讲解,不过在此之前,必须要了解各个对象的具体操作实现,里面有很多巧妙的设计,同时也为了防止被后面的方法绕晕,方法的确很多,但我会挑出几个代表性的来讲。
命名空间镜像
命名空间镜像在这里简称为FsImage,镜像这个词我最早听的时候是在虚拟机镜像恢复的时候听过的,很强大,不过在这里的镜像好像比较小规模一些,只是用于目录树的恢复。镜像的保存路径是由配置文件中的下面这个属性所控制
${dfs.name.dir}
当然你可以不配,是有默认值的。在命名空间镜像中,FSImage起着主导的作用,他管理着存储空间的生存期。下面是这个类的基本变量定义
/**
* FSImage handles checkpointing and logging of the namespace edits.
* fsImage镜像类
*/
public class FSImage extends Storage {
//标准时间格式
private static final SimpleDateFormat DATE_FORM =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//
// The filenames used for storing the images
// 在命名空间镜像中可能用的几种名称
//
enum NameNodeFile {
IMAGE ("fsimage"),
TIME ("fstime"),
EDITS ("edits"),
IMAGE_NEW ("fsimage.ckpt"),
EDITS_NEW ("edits.new");
private String fileName = null;
private NameNodeFile(String name) {this.fileName = name;}
String getName() {return fileName;}
}
// checkpoint states
// 检查点击几种状态
enum CheckpointStates{START, ROLLED_EDITS, UPLOAD_START, UPLOAD_DONE; }
/**
* Implementation of StorageDirType specific to namenode storage
* A Storage directory could be of type IMAGE which stores only fsimage,
* or of type EDITS which stores edits or of type IMAGE_AND_EDITS which
* stores both fsimage and edits.
* 名字节点目录存储类型
*/
static enum NameNodeDirType implements StorageDirType {
//名字节点存储类型定义主要有以下4种定义
UNDEFINED,
IMAGE,
EDITS,
IMAGE_AND_EDITS;
public StorageDirType getStorageDirType() {
return this;
}
//做存储类型的验证
public boolean isOfType(StorageDirType type) {
if ((this == IMAGE_AND_EDITS) && (type == IMAGE || type == EDITS))
return true;
return this == type;
}
}
protected long checkpointTime = -1L;
//内部维护了编辑日志类,与镜像类配合操作
protected FSEditLog editLog = null;
private boolean isUpgradeFinalized = false;
马上看到的是几个文件状态的名称,什么edit,edit.new,fsimage.ckpt,后面这些都会在元数据机制的中进行细致讲解,可以理解为就是临时存放的目录名称。而对于这些目录的遍历,查询操作,都是下面这个类实现的
//目录迭代器
private class DirIterator implements Iterator<StorageDirectory> {
//目录存储类型
StorageDirType dirType;
//向前的指标,用于移除操作
int prevIndex; // for remove()
//向后指标
int nextIndex; // for next()
DirIterator(StorageDirType dirType) {
this.dirType = dirType;
this.nextIndex = 0;
this.prevIndex = 0;
}
public boolean hasNext() {
....
}
public StorageDirectory next() {
StorageDirectory sd = getStorageDir(nextIndex);
prevIndex = nextIndex;
nextIndex++;
if (dirType != null) {
while (nextIndex < storageDirs.size()) {
if (getStorageDir(nextIndex).getStorageDirType().isOfType(dirType))
break;
nextIndex++;
}
}
return sd;
}
public void remove() {
...
}
}
根据传入的目录类型,获取不同的目录,这些存储目录指的就是editlog,fsimage这些目录文件,有一些共有的信息,如下
/**
* Common class for storage information.
* 存储信息公告类
* TODO namespaceID should be long and computed as hash(address + port)
* 命名空间ID必须足够长,ip地址+端口号做哈希计算而得
*/
public class StorageInfo {
//存储信息版本号
public int layoutVersion; // Version read from the stored file.
//命名空间ID
public int namespaceID; // namespace id of the storage
//存储信息创建时间
public long cTime; // creation timestamp
public StorageInfo () {
//默认构造函数,全为0
this(0, 0, 0L);
}
下面从1个保存镜像的方法作为切入口
/**
* Save the contents of the FS image to the file.
* 保存镜像文件
*/
void saveFSImage(File newFile) throws IOException {
FSNamesystem fsNamesys = FSNamesystem.getFSNamesystem();
FSDirectory fsDir = fsNamesys.dir;
long startTime = FSNamesystem.now();
//
// Write out data
//
DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream(newFile)));
try {
//写入版本号
out.writeInt(FSConstants.LAYOUT_VERSION);
//写入命名空间ID
out.writeInt(namespaceID);
//写入目录下的孩子总数
out.writeLong(fsDir.rootDir.numItemsInTree());
//写入时间
out.writeLong(fsNamesys.getGenerationStamp());
byte[] byteStore = new byte[4*FSConstants.MAX_PATH_LENGTH];
ByteBuffer strbuf = ByteBuffer.wrap(byteStore);
// save the root
saveINode2Image(strbuf, fsDir.rootDir, out);
// save the rest of the nodes
saveImage(strbuf, 0, fsDir.rootDir, out);
fsNamesys.saveFilesUnderConstruction(out);
fsNamesys.saveSecretManagerState(out);
strbuf = null;
} finally {
out.close();
}
LOG.info("Image file of size " + newFile.length() + " saved in "
+ (FSNamesystem.now() - startTime)/1000 + " seconds.");
}
从上面的几行可以看到,一个完整的镜像文件首部应该包括版本号,命名空间iD,文件数,数据块版本块,然后后面是具体的文件信息。在这里人家还保存了构建节点文件信息以及安全信息。在保存文件目录的信息时,采用的saveINode2Image()先保留目录信息,然后再调用saveImage()保留孩子文件信息,因为在saveImage()中会调用