在对NameNode节点进行格式化时,调用了FSImage的saveFSImage()方法和FSEditLog.createEditLogFile()存储当前的元数据,启动NameNode节点时,又要从镜像和编辑日志中读取元数据。所以先分析FSImage是如何存储元数据到镜像文件和如何加载元数据到内存的。
存储元数据到镜像文件
在NameNode运行时会将内存中的元数据信息存储到所指定的文件,即${dfs.name.dir}/current目录下的fsimage文件,此外还会将另外一部分对NameNode更改的日志信息存储到${dfs.name.dir}/current目录下的edits文件中。fsimage文件和edits文件可以确定NameNode节点当前的状态,这样在NameNode节点由于突发原因崩溃时,可以根据这两个文件中的内容恢复到节点崩溃前的状态,所以对NameNode节点中内存元数据的每次修改都必须保存下来。但是如果每次都保存到fsimage文件中,这样效率就特别低效,所以引入编辑日志文件edits,保存对对元数据的修改信息,也就是fsimage文件保存NameNode节点中某一时刻内存中的元数据(即目录树),edits保存这一时刻之后的对元数据的更改信息。
镜像的保存
NameNode节点通过方法FSImage.saveFSImage()方法保存内存元数据到fsimage文件中,方法的代码如下:
/**
* 将当前时刻的命名空间镜像保存到newFile命名的文件中
*/
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);
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);//单独保存根节点,由于根节点是NameNode管理的特殊INode,它的成员属性INode.name长度为0,所以必须做特殊处理
// save the rest of the nodes
saveImage(strbuf, 0, fsDir.rootDir, out);//保存目录树中的其他节点
fsNamesys.saveFilesUnderConstruction(out);//保存构建中的节点
fsNamesys.saveSecretManagerState(out);//安全信息
strbuf = null; } finally { out.close(); } }
saveFSImage方法只有一个参数,这个参数代表内存元数据将要保存的位置。在saveFSImage方法中,先得到已经创建的FSNamesystem对象和FSDirectory(在调用FSImage.saveFSImage()方法之前,会创建FSNamesystem对象和FSDirectory对象),然后创建到文件参数newFile的一个输出流。再向文件中写入镜像文件头信息,如表示HDFS存储系统信息结构的版本号的FSConstants.LAYOUT_VERSION,存储系统标识namespaceID,目录树的节点数,文件系统的时间戳信息等。
在saveFSImage方法中,使用了FSConstants.MAX_PATH_LENGTH常量,它表示HDFS文件/目录的绝对路径所占用的最大空间,为8000字节,但是定义的缓存大小则是4*8000即31.25KB,为什么要使用8000*4呢?个人觉得这是为了兼容三种Unicode编码UTF-8/UTF-16/UTF-32,最大编码占用4个字节(参考http://hi.baidu.com/tinggu_android/item/77c2930ecb7811ca90571855),与次相关的另一个常量是FSConstants.MAX_PATH_DEPTH,它表示目录深度最多是1000层,这两个变量为何定义成这两个值,在https://issues.apache.org/jira/browse/HADOOP-438这里有讨论。定义完缓冲区的大小之后,再使用ButeBuffer类将其包装成一个ByteBuffer对象,这样对这个ByteBuffer对象的操作,可以在字节数组中表现出来,对字节数组的操作也可以在ByteBuffer对象中表现出来。
下面,调用saveINode2Image()方法来保存元数据所表示的目录树的根节点,因为根节点是NameNode管理的特殊INode,它的成员属性INode.name长度为0,所以必须做特殊处理,该刚发也会在saveImage()方法中调用,所以先来分析saveImage方法,代码如下:
private static void saveImage(ByteBuffer parentPrefix,
int prefixLength,
INodeDirectory current,
DataOutputStream out) throws IOException {
int newPrefixLength = prefixLength;
if (current.getChildrenRaw() == null)
return;//空目录
for(INode child : current.getChildren()) {//输出当前节点的所有子节点
// print all children first
parentPrefix.position(prefixLength);
parentPrefix.put(PATH_SEPARATOR).put(child.getLocalNameBytes());
saveINode2Image(parentPrefix, child, out);
}
for(INode child : current.getChildren()) {//子节点是目录,输出该目录下面的节点
if(!child.isDirectory())
continue;//文件,忽略
parentPrefix.position(prefixLength);
//准别参数
parentPrefix.put(PATH_SEPARATOR).put(child.getLocalNameBytes());
newPrefixLength = parentPrefix.position();
//递归调用
saveImage(parentPrefix, newPrefixLength, (INodeDirectory)child, out);
}
parentPrefix.position(prefixLength);
}
saveImage方法有四个参数,第一个参数parentPrefix是一个字节缓冲区,存储着当前目录要保存的目录的父目录路径,第二个参数是父路径的长度,即路径在字节缓冲区占内存空间的大小,第三个参数是当前的目录,第四个参数是镜像数据的输出流。如果当前的目录是/current目录,那么parentPrefix中的内容就是目录/current的字节表示。在saveImage方法中首先记录下当前的parentPrefix路径长度,因为saveImage方法是递归调用的,整个过程中都需要使用同一个缓冲区,所以每次保存一个目录时都需要保存当前的prefixLength变量。再判断当前目录下是否存在文件或目录,不存在就直接返回,如果存在,遍历这个目录下面的所有子节点,分别调用saveINode2Image方法进行输出。如何输出子节点呢?首先记录下子节点的路径,再调