Hadoop-0.20.0源代码分析(19)

这里,对重要的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执行同步操作。

你可能感兴趣的:(String,image,File,存储,permissions,代码分析)