这里,对重要的FSImage类进行阅读分析。
该类的继承层次关系如下所示:
- ◦org.apache.hadoop.hdfs.server.common.StorageInfo
- ◦org.apache.hadoop.hdfs.server.common.Storage
- ◦org.apache.hadoop.hdfs.server.namenode.FSImage
我们一个一个地分析:
- StorageInfo类
该类是一个存储信息的通用实体类, 该类定义了如下三个属性:
- public int layoutVersion; // 从存储的文件中读取版本信息.
- public int namespaceID; // 文件系统命名空间的存储ID
- public long cTime; // 存储(文件或目录)访问时间
其中,namespaceID表示的是文件系统命名空间的存储ID,它的生成基于Hash算法的,根据address + port来进行散列得到的。
- Storage类
该类继承自StorageInfo,是一个抽象类。
本地存储信息被存储在一个单独的文件VERSION中,它包含结点的类型、存储布局(Layout)版本、 文件系统命名空间ID、文件系统状态的创建时间。本地存储可以分布在多个目录中,每个目录中应该保存着相同的VERSION文件,当Hadoop servers (name-node and data-nodes)启动的时候从这些VERSION文件中读取本地的存储信息。当Hadoop servers启动的时候,会对每个存储目录都持有一把锁,这是为了使得其他的结点不能因为共享同一个存储目录而启动。当Hadoop servers停止的时候,会释放掉它们所持有的锁,以便其它结点来使用该存储目录。
首先,看存储状态的枚举定义:
- public enum StorageState {
- NON_EXISTENT, // 不存在
- NOT_FORMATTED, // 未格式化
- COMPLETE_UPGRADE, // 完成升级
- RECOVER_UPGRADE, // 恢复升级
- COMPLETE_FINALIZE, // 完成确认
- COMPLETE_ROLLBACK, // 完成回滚
- RECOVER_ROLLBACK, // 恢复回滚
- COMPLETE_CHECKPOINT, // 完成检查点
- RECOVER_CHECKPOINT, // 恢复检查点
- NORMAL; // 正常
- }
下面是与存储相关的几个文件或目录:
- private static final String STORAGE_FILE_LOCK = "in_use.lock";
- protected static final String STORAGE_FILE_VERSION = "VERSION";
- public static final String STORAGE_DIR_CURRENT = "current";
- private static final String STORAGE_DIR_PREVIOUS = "previous";
- private static final String STORAGE_TMP_REMOVED = "removed.tmp";
- private static final String STORAGE_TMP_PREVIOUS = "previous.tmp";
- private static final String STORAGE_TMP_FINALIZED = "finalized.tmp";
- private static final String STORAGE_TMP_LAST_CKPT = "lastcheckpoint.tmp";
- private static final String STORAGE_PREVIOUS_CKPT = "previous.checkpoint";
我们再看一下,该类中定义的3个与存储相关的内部类:
1、StorageDirType接口类
该接口定义了与存储目录的类型相关的方法:
- public interface StorageDirType {
- public StorageDirType getStorageDirType(); // 获取存储目录类型
- public boolean isOfType(StorageDirType type); // 指定存储目录是否是指定的存储目录类型type
- }
Storage类中,并没有实现该存储目录类型的内部类。
2、StorageDirectory类:
该类表示一个单独的存储目录的实体类,它定义了如下三个属性:
- File root; // 根目录
- FileLock lock; // 存储锁
- StorageDirType dirType; // 存储目录类型
前面提到,每个存储对应一个VERSION文件,也就是在每个存储目录中保存。
StorageDirectory类提供了对VERSION文件的读写操作,而且还给出了对一个存储目录进行一致性检查的操作analyzeStorage:根据Hadoop servers启动的选项StartupOption,来返回不正常的存储状态,如果正常启动则检查上面与存储相关的文件或目录是否存在,并返回对应的存储状态。
该类中定义的doRecover方法,能够从上一个失败的事务中来完成或恢复存储状态,主要包括对下述状态进行恢复操作:
COMPLETE_UPGRADE: 将previous.tmp重命名为previous
RECOVER_UPGRADE: 将previous.tmp重命名为current
COMPLETE_ROLLBACK: 删除removed.tmp
RECOVER_ROLLBACK: 将removed.tmp重命名为current
COMPLETE_CHECKPOINT: 将lastcheckpoint.tmp重命名为 previous.checkpoint
RECOVER_CHECKPOINT: 将lastcheckpoint.tmp重命名为current
另外,StorageDirectory类能够控制对当前存储进行加锁lock、解锁unlock、尝试获取锁tryLock的操作。
3、DirIterator类
该类是StorageDirectory的迭代器类,用来方便地迭代出一个StorageDirectory列表中的StorageDirectory存储目录实例。
下面,继续看Storage类的实现。
用来描述一个存储的信息,应该包括使用该存储的结点(通过结点类型来标识) 、该存储包含的目录,如下所示:
- private NodeType storageType; // 使用该存储的结点的类型(包括NodeType.NAME_NODE与NodeTypeDATA_NODE这两种)
- protected List<StorageDirectory> storageDirs = new ArrayList<StorageDirectory>();
基本上,在Storage类中实现的操作都是与存储目录相关的,比如,获取到一个StorageDirectory列表的迭代器实例、删除目录等等,可以查看该类的具体实现。
- FsImage类
首先,看该类中定义的三个枚举类:
- /**
- * 存储映像的文件名称
- */
- 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;}
- }
- /**
- * 检查点状态
- */
- enum CheckpointStates{START, ROLLED_EDITS, UPLOAD_START, UPLOAD_DONE; }
- /**
- * 实现了StorageDirType接口,并指定为namenode存储
- * 一个存储目录的类型应该是仅仅存储fsimage的IMAGE类型,或者是存储edits的EDITS类型,或者是存储fsimage与edits的IMAGE_AND_EDITS类型
- */
- static enum NameNodeDirType implements StorageDirType {
- 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;
- }
- }
再看一个内部类DatanodeImage,该类用于将Datanode的持久化信息存储到FsImage映像中,实现如下所示:
- static class DatanodeImage implements Writable { // 实现了Writable接口,因此是可序列化的
- DatanodeDescriptor node = new DatanodeDescriptor();
- /**
- * 序列化Datanode的信息,存储到fsImage中
- */
- public void write(DataOutput out) throws IOException {
- new DatanodeID(node).write(out);
- out.writeLong(node.getCapacity());
- out.writeLong(node.getRemaining());
- out.writeLong(node.getLastUpdate());
- out.writeInt(node.getXceiverCount());
- }
- /**
- * 从fsImage中读取Datanode的信息,即反序列化
- */
- public void readFields(DataInput in) throws IOException {
- DatanodeID id = new DatanodeID();
- id.readFields(in);
- long capacity = in.readLong();
- long remaining = in.readLong();
- long lastUpdate = in.readLong();
- int xceiverCount = in.readInt();
- // 根据读取到的Datanode信息,更新Datanode结点
- node.updateRegInfo(id);
- node.setStorageID(id.getStorageID());
- node.setCapacity(capacity);
- node.setRemaining(remaining);
- node.setLastUpdate(lastUpdate);
- node.setXceiverCount(xceiverCount);
- }
- }
对FSImage类源代码的阅读,我们从构造一个FSImage实例的方法来看,如下所示:
- FSImage() {
- super(NodeType.NAME_NODE); // 为Namenode创建一个新的存储
- this.editLog = new FSEditLog(this); // 构造FSEditLog实例
- }
- FSImage(Collection<File> fsDirs, Collection<File> fsEditsDirs) throws IOException {
- this();
- setStorageDirectories(fsDirs, fsEditsDirs); // 将文件系统目录集合与EditLog日志文件目录集合,加入到创建的存储中
- }
- public FSImage(StorageInfo storageInfo) {
- super(NodeType.NAME_NODE, storageInfo); // 将StorageInfo指定为Namenode的存储
- }
- /**
- * 表示一个映像(image and edit file)
- */
- public FSImage(File imageDir) throws IOException {
- this();
- ArrayList<File> dirs = new ArrayList<File>(1);
- ArrayList<File> editsDirs = new ArrayList<File>(1);
- dirs.add(imageDir);
- editsDirs.add(imageDir);
- setStorageDirectories(dirs, editsDirs);
- }
可见,FSImage实例对应着一个特定的为Namenode而创建的存储实例,该存储实例的内容,实际上维护着一个目录列表,在构造FSImage实例的过程中,全部加载并初始化。
接着,介绍FSImage类的重要方法。
1、加载FsImage映像文件
对应的实现方法为loadFSImage,该类实现了两个重载的加载映像文件的方法。
首先看第一个带参数的loadFSImage方法,从一个文件中加载映像到文件系统中,实现如下所示:
- boolean loadFSImage(File curFile) throws IOException {
- assert this.getLayoutVersion() < 0 : "Negative layout version is expected.";
- assert curFile != null : "curFile is null";
- FSNamesystem fsNamesys = FSNamesystem.getFSNamesystem(); // 获取到FSNamesystem实例
- FSDirectory fsDir = fsNamesys.dir; // 获取到FSNamesystem实例的dir引用
- boolean needToSave = true;
- DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(curFile))); // 打开映像文件的输入流
- try {
- int imgVersion = in.readInt(); // 读取映像文件版本号(第一次出现为-1)
- this.namespaceID = in.readInt(); // 读取文件系统命名空间ID(第一次出现为-2)
- long numFiles; // 映像文件数变量
- if (imgVersion <= -16) {
- numFiles = in.readLong(); // 读取文件数量
- } else {
- numFiles = in.readInt();
- }
- this.layoutVersion = imgVersion;
- if (imgVersion <= -12) {
- long genstamp = in.readLong(); // 读取最后设置的时间戳
- fsNamesys.setGenerationStamp(genstamp); // 同步到FSNamesystem实例上
- }
- needToSave = (imgVersion != FSConstants.LAYOUT_VERSION);
- short replication = FSNamesystem.getFSNamesystem().getDefaultReplication(); // 获取默认副本数
- LOG.info("Number of files = " + numFiles);
- String path;
- String parentPath = "";
- INodeDirectory parentINode = fsDir.rootDir; // 获取到根目录
- for (long i = 0; i < numFiles; i++) {
- long modificationTime = 0;
- long atime = 0;
- long blockSize = 0;
- path = readString(in); // 读取文件
- replication = in.readShort(); // 读取副本数
- replication = FSEditLog.adjustReplication(replication); // 必要时调整副本数
- modificationTime = in.readLong(); // 读取最后修改时间
- if (imgVersion <= -17) {
- atime = in.readLong(); // 读取访问时间
- }
- if (imgVersion <= -8) {
- blockSize = in.readLong(); // 读取块大小
- }
- int numBlocks = in.readInt(); // 读取块列表大小
- Block blocks[] = null;
- if ((-9 <= imgVersion && numBlocks > 0) || (imgVersion < -9 && numBlocks >= 0)) {
- blocks = new Block[numBlocks];
- for (int j = 0; j < numBlocks; j++) {
- blocks[j] = new Block();
- if (-14 < imgVersion) {
- blocks[j].set(in.readLong(), in.readLong(), Block.GRANDFATHER_GENERATION_STAMP);
- } else {
- blocks[j].readFields(in); // 读取块
- }
- }
- }
- if (-8 <= imgVersion && blockSize == 0) {
- if (numBlocks > 1) {
- blockSize = blocks[0].getNumBytes();
- } else {
- long first = ((numBlocks == 1) ? blocks[0].getNumBytes(): 0);
- blockSize = Math.max(fsNamesys.getDefaultBlockSize(), first);
- }
- }
- long nsQuota = -1L;
- if (imgVersion <= -16 && blocks == null) {
- nsQuota = in.readLong(); // 如果是目录,读取namespace配额信息
- }
- long dsQuota = -1L;
- if (imgVersion <= -18 && blocks == null) {
- dsQuota = in.readLong(); // 读取磁盘配额信息
- }
- PermissionStatus permissions = fsNamesys.getUpgradePermission();
- if (imgVersion <= -11) {
- permissions = PermissionStatus.read(in); // 读取权限
- }
- if (path.length() == 0) { // 如果是根目录,设置其属性值
- if (nsQuota != -1 || dsQuota != -1) {
- fsDir.rootDir.setQuota(nsQuota, dsQuota);
- }
- fsDir.rootDir.setModificationTime(modificationTime);
- fsDir.rootDir.setPermissionStatus(permissions);
- continue;
- }
- // 检查新的inode是否属于相同的父目录
- if(!isParent(path, parentPath)) {
- parentINode = null;
- parentPath = getParent(path);
- }
- // 添加一个新的inode
- parentINode = fsDir.addToParent(path, parentINode, permissions, blocks, replication, modificationTime, atime, nsQuota, dsQuota, blockSize);
- }
- this.loadDatanodes(imgVersion, in); // 加载Datanode信息
- this.loadFilesUnderConstruction(imgVersion, in, fsNamesys); // 加载待创建的文件
- } finally {
- in.close();
- }
- return needToSave;
- }
再看第二个不带参数的loadFSImage方法,它从一个目录中选择最新的映像文件并加载到内存中,同时与这个目录中的EditLog日志文件合并如下所示:
- boolean loadFSImage() throws IOException {
- // 首先检查FSImage对应的存储中的目录列表,找到最新的
- long latestNameCheckpointTime = Long.MIN_VALUE, latestEditsCheckpointTime = Long.MIN_VALUE;
- StorageDirectory latestNameSD = null, latestEditsSD = null;
- boolean needToSave = false, isUpgradeFinalized = true;
- Collection<String> imageDirs = new ArrayList<String>();
- Collection<String> editsDirs = new ArrayList<String>();
- for (Iterator<StorageDirectory> it = dirIterator(); it.hasNext();) {
- StorageDirectory sd = it.next();
- if (!sd.getVersionFile().exists()) {
- needToSave |= true;
- continue; // 如果某个sd目录的版本文件不存在,说明该目录还没有格式化
- }
- boolean imageExists = false, editsExists = false;
- if (sd.getStorageDirType().isOfType(NameNodeDirType.IMAGE)) { // 判断指定的sd的类型是否是Namenode的IMAGE类型
- imageExists = getImageFile(sd, NameNodeFile.IMAGE).exists();
- imageDirs.add(sd.getRoot().getCanonicalPath());
- }
- if (sd.getStorageDirType().isOfType(NameNodeDirType.EDITS)) { // 判断指定的sd的类型是否是Namenode的EDITS类型
- editsExists = getImageFile(sd, NameNodeFile.EDITS).exists();
- editsDirs.add(sd.getRoot().getCanonicalPath());
- }
- checkpointTime = readCheckpointTime(sd); // 获取sd检查点时间
- if ((checkpointTime != Long.MIN_VALUE) && ((checkpointTime != latestNameCheckpointTime) || (checkpointTime != latestEditsCheckpointTime))) {
- needToSave |= true; // 根据sd的检查点时间,如果多个不同的时间则强制保存一个新的映像文件
- }
- if (sd.getStorageDirType().isOfType(NameNodeDirType.IMAGE) && (latestNameCheckpointTime < checkpointTime) && imageExists) {
- latestNameCheckpointTime = checkpointTime; // 得到最新检查点时间
- latestNameSD = sd; // 找到最新映像文件目录
- }
- if (sd.getStorageDirType().isOfType(NameNodeDirType.EDITS) && (latestEditsCheckpointTime < checkpointTime) && editsExists) {
- latestEditsCheckpointTime = checkpointTime;
- latestEditsSD = sd; // 找到最新EditLog日志文件目录
- }
- if (checkpointTime <= 0L)
- needToSave |= true;
- isUpgradeFinalized = isUpgradeFinalized && !sd.getPreviousDir().exists(); // 设置升级完成标志
- }
- // 至少应该存在一个FSImage映像文件目录和一个EditLog日志文件目录
- if (latestNameSD == null)
- throw new IOException("Image file is not found in " + imageDirs);
- if (latestEditsSD == null)
- throw new IOException("Edits file is not found in " + editsDirs);
- // 保证从同一个检查点加载映像文件与日志文件
- if (latestNameCheckpointTime != latestEditsCheckpointTime)
- throw new IOException("Inconsitent storage detected, " + "name and edits storage do not match");
- // 从上一个中断的检查点执行恢复操作
- needToSave |= recoverInterruptedCheckpoint(latestNameSD, latestEditsSD);
- long startTime = FSNamesystem.now(); // 开始加载FSImage文件时间
- long imageSize = getImageFile(latestNameSD, NameNodeFile.IMAGE).length(); // 获取映像文件大小
- latestNameSD.read(); // 读取版本文件VERSION
- needToSave |= loadFSImage(getImageFile(latestNameSD, NameNodeFile.IMAGE)); // 调用重载的loadFSImage执行加载
- LOG.info("Image file of size " + imageSize + " loaded in " + (FSNamesystem.now() - startTime)/1000 + " seconds.");
- needToSave |= (loadFSEdits(latestEditsSD) > 0); // 加载最新的EditLog日志文件
- return needToSave;
- }
2、从一个中断的检查点恢复加载映像文件
该方法recoverInterruptedCheckpoint的实现如下所示:
- boolean recoverInterruptedCheckpoint(StorageDirectory nameSD, StorageDirectory editsSD) throws IOException {
- boolean needToSave = false;
- File curFile = getImageFile(nameSD, NameNodeFile.IMAGE); // 获取到fsimage文件
- File ckptFile = getImageFile(nameSD, NameNodeFile.IMAGE_NEW); // 获取到fsimage.ckpt文件
- if (ckptFile.exists()) { // 如果正在一个检查点过程当中,fsimage.ckpt文件存在
- needToSave = true;
- if (getImageFile(editsSD, NameNodeFile.EDITS_NEW).exists()) { // 获取到edits.new文件,如果存在
- // 检查点进程执行时会上载一个新的合并的映像文件,但是在Namenode服务器挂机之前不能保证该映像文件完成上载,故需要保证它是被删除掉的
- if (!ckptFile.delete()) {
- throw new IOException("Unable to delete " + ckptFile);
- }
- } else { // 正常情况
- // 当Namenode服务器关闭时检查点进程仍在执行,fsimage.ckpt被创建,edits.new文件被重命名为edits
- // 为了完成该次检查点执行,将fsimage.new重命名为fsimage,而fstime不需要更新
- if (!ckptFile.renameTo(curFile)) {
- if (!curFile.delete())
- LOG.warn("Unable to delete dir " + curFile + " before rename");
- if (!ckptFile.renameTo(curFile)) {
- throw new IOException("Unable to rename " + ckptFile + " to " + curFile);
- }
- }
- }
- }
- return needToSave;
- }
通过上面可以看出,检查点进程所执行的操作是对fsimage映像文件进行检查,从而生成一个fsimage.ckpt文件。而很可能会(只要fsimage.ckpt文件存在)从一个fsimage.ckpt文件来加载fsimage映像文件,在Namenode停止的时候,将fsimage.ckpt重命名为fsimage完成此次检查点进程的执行,并把edits.new重命名为edits。
3、切换映像
实现方法rollFSImage,能够将fsimage.ckpt重命名为fsImage,将edits.new重命名为edits,并打开一个空的edits文件。实现如下所示:
- void rollFSImage() throws IOException {
- if (ckptState != CheckpointStates.UPLOAD_DONE) { // 如果检查点fsimage.ckpt文件没有上载完成,不能执行回滚操作
- throw new IOException("Cannot roll fsImage before rolling edits log.");
- }
- // 验证edits.new与fsimage.ckpt文件存在于所有的检查点目录中
- if (!editLog.existsNew()) {
- throw new IOException("New Edits file does not exist");
- }
- for (Iterator<StorageDirectory> it = dirIterator(NameNodeDirType.IMAGE); it.hasNext();) { // 迭代
- StorageDirectory sd = it.next();
- File ckpt = getImageFile(sd, NameNodeFile.IMAGE_NEW); // 获取fsimage.ckpt文件
- if (!ckpt.exists()) {
- throw new IOException("Checkpoint file " + ckpt + " does not exist");
- }
- }
- editLog.purgeEditLog(); // 重命名edits.new为edits
- for (Iterator<StorageDirectory> it = (NameNodeDirType.IMAGE); it.hasNext();) {
- StorageDirectory sd = it.next();
- File ckpt = getImageFile(sd, NameNodeFile.IMAGE_NEW); // 获取到fsimage.ckpt文件
- File curFile = getImageFile(sd, NameNodeFile.IMAGE); // 获取到fsimage文件
- if (!ckpt.renameTo(curFile)) { // 将fsimage.ckpt重命名为fsimage
- curFile.delete(); // 如果fsimage已经存在则删除它
- if (!ckpt.renameTo(curFile)) { // 再次尝试将fsimage.ckpt重命名为fsimage
- if (sd.getStorageDirType().isOfType(NameNodeDirType.EDITS))
- editLog.processIOError(sd);
- removedStorageDirs.add(sd); // 显然,sd存储目录中无法将fsimage.ckpt重命名为fsimage,该sd已经失效,需要被删除掉
- it.remove();
- }
- }
- }
- // 在fsimage与edits对应的所有的目录中,更新fstime文件,并写VERSION文件
- this.layoutVersion = FSConstants.LAYOUT_VERSION;
- this.checkpointTime = FSNamesystem.now();
- for (Iterator<StorageDirectory> it = dirIterator(); it.hasNext();) { // 迭代
- StorageDirectory sd = it.next();
- if (!sd.getStorageDirType().isOfType(NameNodeDirType.EDITS)) {
- File editsFile = getImageFile(sd, NameNodeFile.EDITS);
- editsFile.delete(); // 删除旧的edits文件
- }
- if (!sd.getStorageDirType().isOfType(NameNodeDirType.IMAGE)) {
- File imageFile = getImageFile(sd, NameNodeFile.IMAGE);
- imageFile.delete(); // 删除旧的fsimage文件
- }
- try {
- sd.write(); // 向sd目录中写版本文件VERSION
- } catch (IOException e) {
- LOG.error("Cannot write file " + sd.getRoot(), e);
- if (sd.getStorageDirType().isOfType(NameNodeDirType.EDITS))
- editLog.processIOError(sd);
- removedStorageDirs.add(sd);
- it.remove(); // 删除sd
- }
- }
- ckptState = FSImage.CheckpointStates.START; // 修改检查点状态为开始,准备切换
- }
4、保存映像文件
实现方法为saveFSImage,存在两个重载的方法。
第一个是带参数的方法,将fsimage.new或fsimage文件的内容进行保存。如下所示:
- void saveFSImage(File newFile) throws IOException {
- FSNamesystem fsNamesys = FSNamesystem.getFSNamesystem();
- FSDirectory fsDir = fsNamesys.dir;
- long startTime = FSNamesystem.now();
- DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(newFile))); // 创建指定文件的输出流,准备写入
- try {
- out.writeInt(FSConstants.LAYOUT_VERSION); // 写入版本号
- out.writeInt(namespaceID); // 写入namespace的ID
- out.writeLong(fsDir.rootDir.numItemsInTree()); // 写入fsDir目录的根目录树中INode的数量
- out.writeLong(fsNamesys.getGenerationStamp()); // 写入时间戳
- byte[] byteStore = new byte[4*FSConstants.MAX_PATH_LENGTH];
- ByteBuffer strbuf = ByteBuffer.wrap(byteStore);
- saveINode2Image(strbuf, fsDir.rootDir, out); // 将fsDir.rootDir的属性都写入到指定文件中
- saveImage(strbuf, 0, fsDir.rootDir, out); // 将fsDir.rootDir的目录树属性写入到指定文件中
- fsNamesys.saveFilesUnderConstruction(out); // 将INodeUnderConstruction写入到指定文件中
- strbuf = null;
- } finally {
- out.close();
- }
- LOG.info("Image file of size " + newFile.length() + " saved in " + (FSNamesystem.now() - startTime)/1000 + " seconds.");
- }
上面调用saveImage方法,实际上方法循环调用了saveINode2Image来写入fsDir.rootDir目录树的属性信息的,可以参考这两个方法的实现。
上面方法是将一些必要的信息都序列化到了指定的映像文件中,作为Namenode掌握当前HDFS集群中重要信息的内存映像。
第二个是不带参数的该方法,保存fsimage.ckpt文件的内容,并创建一个新的edits文件。实现如下所示:
- public void saveFSImage() throws IOException {
- editLog.createNewIfMissing(); // 如果edits.new不存在,则创建一个新的
- for (Iterator<StorageDirectory> it = dirIterator(); it.hasNext();) { // 迭代
- StorageDirectory sd = it.next();
- NameNodeDirType dirType = (NameNodeDirType)sd.getStorageDirType();
- if (dirType.isOfType(NameNodeDirType.IMAGE))
- saveFSImage(getImageFile(sd, NameNodeFile.IMAGE_NEW)); // 调用,保存fsimage.ckpt文件的内容
- if (dirType.isOfType(NameNodeDirType.EDITS)) {
- editLog.createEditLogFile(getImageFile(sd, NameNodeFile.EDITS));
- File editsNew = getImageFile(sd, NameNodeFile.EDITS_NEW);
- if (editsNew.exists())
- editLog.createEditLogFile(editsNew); // 创建一个edits.new文件
- }
- }
- ckptState = CheckpointStates.UPLOAD_DONE; // 设置检查点状态为上载完成
- rollFSImage(); // 然后执行映像的切换操作:将fsimage.ckpt改成fsimage,将edits.new改成edits
- }
5、 从检查点目录加载fsimage映像文件
实现的方法为doImportCheckpoint,如下所示:
- void doImportCheckpoint() throws IOException {
- FSImage ckptImage = new FSImage(); // 创建一个FSImage实例
- FSNamesystem fsNamesys = FSNamesystem.getFSNamesystem();
- FSImage realImage = fsNamesys.getFSImage(); // 当前FSNamesystem中的当前fsimage映像
- assert realImage == this;
- fsNamesys.dir.fsImage = ckptImage; // 切换
- try {
- ckptImage.recoverTransitionRead(checkpointDirs, checkpointEditsDirs,StartupOption.REGULAR); // 从检查点目录加载fsimage映像
- } finally {
- ckptImage.close();
- }
- realImage.setStorageInfo(ckptImage); // 更新layoutVersion、namespaceID、cTime属性值
- fsNamesys.dir.fsImage = realImage; // 更新fsNamesys.dir.fsImage
- saveFSImage(); // 保存:将fsimage.ckpt改成fsimage,将edits.new改成edits
- }
6、初始化分布式升级
主要通过获取到UpgradeManagerNamenode 升级管理器,判断是否进行了升级之前的初始化工作,只有做好初始化工作,才开始对fsimage映像文件进行初始化。
实现方法initializeDistributedUpgrade如下所示:
- private void initializeDistributedUpgrade() throws IOException {
- UpgradeManagerNamenode um = FSNamesystem.getFSNamesystem().upgradeManager;
- if(! um.initializeUpgrade())
- return;
- FSNamesystem.getFSNamesystem().getFSImage().writeAll(); // 将与fsimage相关的数据都写入到存储中
- NameNode.LOG.info("/n Distributed upgrade for NameNode version "
- + um.getUpgradeVersion() + " to current LV "
- + FSConstants.LAYOUT_VERSION + " is initialized.");
- }
7、执行升级
实现方法为doUpgrade,如下所示:
- private void doUpgrade() throws IOException {
- if(getDistributedUpgradeState()) { // 通过升级管理器,获取当前升级状态
- // 只有分布式升级才执行,并且不对版本升级
- this.loadFSImage(); // 加载fsimage映像文件
- initializeDistributedUpgrade(); // 初始化升级准备工作
- return;
- }
- // 如果当前不存在文件系统的任何信息,可以向下执行升级过程
- for (Iterator<StorageDirectory> it = dirIterator(); it.hasNext();) {
- StorageDirectory sd = it.next();
- if (sd.getPreviousDir().exists())
- throw new InconsistentFSStateException(sd.getRoot(),
- "previous fs state should not exist during upgrade. "
- + "Finalize or rollback first.");
- }
- this.loadFSImage(); // 加载最新的映像文件
- long oldCTime = this.getCTime();
- this.cTime = FSNamesystem.now(); // 设置当前时间
- int oldLV = this.getLayoutVersion();
- this.layoutVersion = FSConstants.LAYOUT_VERSION;
- this.checkpointTime = FSNamesystem.now();
- for (Iterator<StorageDirectory> it = dirIterator(); it.hasNext();) { // 迭代:对每个目录分别进行升级准备操作
- StorageDirectory sd = it.next();
- LOG.info("Upgrading image directory " + sd.getRoot()
- + "./n old LV = " + oldLV
- + "; old CTime = " + oldCTime
- + "./n new LV = " + this.getLayoutVersion()
- + "; new CTime = " + this.getCTime());
- File curDir = sd.getCurrentDir(); // 获取到current目录
- File prevDir = sd.getPreviousDir(); // 获取到previous目录
- File tmpDir = sd.getPreviousTmp(); // 获取到previous.tmp目录
- assert curDir.exists() : "Current directory must exist.";
- assert !prevDir.exists() : "prvious directory must not exist.";
- assert !tmpDir.exists() : "prvious.tmp directory must not exist.";
- rename(curDir, tmpDir); // 将current目录重命名为previous.tmp
- if (!curDir.mkdir()) // 创建current目录
- throw new IOException("Cannot create directory " + curDir);
- saveFSImage(getImageFile(sd, NameNodeFile.IMAGE)); // 保存最新的映像
- editLog.createEditLogFile(getImageFile(sd, NameNodeFile.EDITS)); // 创建一个新的edits文件
- sd.write(); // 写版本文件
- rename(tmpDir, prevDir); // 将previous.tmp重命名为previous
- isUpgradeFinalized = false; // 等待升级进程确认
- LOG.info("Upgrade of " + sd.getRoot() + " is complete.");
- }
- initializeDistributedUpgrade(); // 初始化分布式升级的工作
- editLog.open(); // 打开edits日志文件
- }
8、执行升级后的后继清理
当执行升级的时候,可能需要执行目录的重命名,或者删除一些临时文件目录,对指定的StorageDirectory目录执行清理工作的方法为doFinalize,如下所示:
- private void doFinalize(StorageDirectory sd) throws IOException {
- File prevDir = sd.getPreviousDir();
- if (!prevDir.exists()) { // 该临时目录previous.tmp已经被清理了
- LOG.info("Directory " + prevDir + " does not exist.");
- LOG.info("Finalize upgrade for " + sd.getRoot()+ " is not required.");
- return;
- }
- LOG.info("Finalizing upgrade for storage directory " + sd.getRoot() + "." + (getLayoutVersion()==0 ? "" : "/n cur LV = "
- + this.getLayoutVersion() + "; cur CTime = " + this.getCTime()));
- assert sd.getCurrentDir().exists() : "Current directory must exist.";
- final File tmpDir = sd.getFinalizedTmp(); // 获取全部finalized.tmp临时目录
- rename(prevDir, tmpDir); // 将previous.tmp重命名为finalized.tmp
- deleteDir(tmpDir); // 删除finalized.tmp
- isUpgradeFinalized = true; // 升级之后的清理工作已经完成
- LOG.info("Finalize upgrade for " + sd.getRoot()+ " is complete.");
- }
上面的方法比较容易。如果想要执行批量清理,调用方法finalizeUpgrade可以实现,如下所示:
- void finalizeUpgrade() throws IOException {
- for (Iterator<StorageDirectory> it = dirIterator(); it.hasNext();) {
- doFinalize(it.next());
- }
- }
该方法对全部的目录执行清理工作。
9、回滚操作
执行回滚操作时有条件的,如果文件系统不存在一个先前的状态,是不能够执行回滚操作的,因为根本无法将当前的fsimag改变成上一个状态。另外,文件系统先前的一个状态的信息保存在一个或多个存储目录中,获取到这些先前的状态信息才能够执行回滚操作。而对于不存在文件系统状态信息的存储目录是不需要回滚的。
实现回滚操作的方法为doRollback,如下所示:
- private void doRollback() throws IOException {
- boolean canRollback = false;
- FSImage prevState = new FSImage(); // 获取先前文件系统的映像
- prevState.layoutVersion = FSConstants.LAYOUT_VERSION;
- for (Iterator<StorageDirectory> it = dirIterator(); it.hasNext();) {
- StorageDirectory sd = it.next();
- File prevDir = sd.getPreviousDir();
- if (!prevDir.exists()) { // use current directory then
- LOG.info("Storage directory " + sd.getRoot() + " does not contain previous fs state.");
- sd.read(); // 读取版本VERSION文件,验证与其他目录的一致性
- continue;
- }
- StorageDirectory sdPrev = prevState.new StorageDirectory(sd.getRoot());
- sdPrev.read(sdPrev.getPreviousVersionFile()); // 读取版本VERSION文件,验证与先前目录的一致性
- canRollback = true; // 可以执行回滚操作标志
- }
- if (!canRollback)
- throw new IOException("Cannot rollback. " + "None of the storage directories contain previous fs state.");
- // 所有目录的一致性检查通过,执行回滚,使得每个目录都包含先前的状态
- for (Iterator<StorageDirectory> it = dirIterator(); it.hasNext();) {
- StorageDirectory sd = it.next();
- File prevDir = sd.getPreviousDir(); // 获取到先前的previous目录
- if (!prevDir.exists())
- continue;
- LOG.info("Rolling back storage directory " + sd.getRoot() + "./n new LV = " + prevState.getLayoutVersion() + "; new CTime = " + prevState.getCTime());
- File tmpDir = sd.getRemovedTmp(); // 获取到removed.tmp目录
- assert !tmpDir.exists() : "removed.tmp directory must not exist.";
- File curDir = sd.getCurrentDir(); // 获取到current目录
- assert curDir.exists() : "Current directory must exist.";
- rename(curDir, tmpDir); // 将current重命名为removed.tmp
- rename(prevDir, curDir); // 将previous重命名为current
- deleteDir(tmpDir); // 删除临时removed.tmp目录
- LOG.info("Rollback of " + sd.getRoot()+ " is complete.");
- }
- isUpgradeFinalized = true; // 清理完成
- verifyDistributedUpgradeProgress(StartupOption.REGULAR); // 检查Namenode是否能够以REGULAR模式启动
- }
可见,回滚操作就是,在保证全部目录一致性检查通过的情况下,将当前存在的previous目录重命名为current目录,表示将previous目录状态覆盖到current目录上,然后删除被覆盖掉的current目录(通过先重命名current为removed.tmp,然后执行删除)。
10、格式化操作
对文件系统中的StorageDirectory存储目录进行格式化操作,存在两个重载的方法,分别介绍如下。
第一个带参数的方法,是对指定的存储目录进行格式化,如下所示:
- void format(StorageDirectory sd) throws IOException {
- sd.clearDirectory(); // 创建currrent目录,如果该目录存在,则会删除存在的current目录树
- sd.lock(); // 加锁,对应的文件为in_use.lock
- try {
- NameNodeDirType dirType = (NameNodeDirType)sd.getStorageDirType(); // 获取到存储目录类型
- if (dirType.isOfType(NameNodeDirType.IMAGE)) // 如果是fsimage目录
- saveFSImage(getImageFile(sd, NameNodeFile.IMAGE)); // 保存fsimage映像
- if (dirType.isOfType(NameNodeDirType.EDITS)) // 如果是edits日志文件目录
- editLog.createEditLogFile(getImageFile(sd, NameNodeFile.EDITS)); // 创建一个新的edits文件
- sd.write(); // 写版本文件VERSION
- } finally {
- sd.unlock(); // 释放锁,清除in_use.lock文件
- }
- LOG.info("Storage directory " + sd.getRoot() + " has been successfully formatted.");
- }
比较容易理解。
第二个不带参数的方法,是批量进行格式化,如下所示:
- public void format() throws IOException {
- this.layoutVersion = FSConstants.LAYOUT_VERSION;
- this.namespaceID = newNamespaceID();
- this.cTime = 0L;
- this.checkpointTime = FSNamesystem.now();
- for (Iterator<StorageDirectory> it = dirIterator(); it.hasNext();) { // 循环
- StorageDirectory sd = it.next();
- format(sd); // 对每一个sd执行格式化操作
- }
- }
11、加载Datanode信息
事实上,对于新版本的Hadoop已经不再使用Datanode的映像了,该方法是为了兼容旧版本的,如下所示:
- void loadDatanodes(int version, DataInputStream in) throws IOException {
- if (version > -3) // 之前版本不存在Datanode信息的映像
- return;
- if (version <= -12) {
- return; // 新版本不需要存储Datanode映像
- }
- int size = in.readInt();
- for(int i = 0; i < size; i++) {
- DatanodeImage nodeImage = new DatanodeImage(); // 通过DatanodeImage来加载Datanode的映像
- nodeImage.readFields(in);
- }
- }
简单做个总结:
FSImage类是与fsimage相关的,主要保存了文件系统的状态信息。其中,在加载该映像到内存中的时候,相关的日志文件是edits,edits总是保存当前对文件系统执行操作的最新记录,根据需要启动检查点进程来将edits事务日志记录作用于fsimage映像上,从而系统通过fsimage将发生的事务作用在文件系统实例所维护的存储目录上。
在对fsimage操作的过程中,相关的文件主要包括fsimage、fsimage.ckpt、edits、edits.new这四个,而且在适当的时候,启动检查点进程对内存中映像fsimage执行同步操作。