Hadoop源码分析之NameNode的启动与停止(续)
Hadoop源码分析之NameNode的启动与停止(续)
在上一篇文章Hadoop源码分析之NameNode的启动与停止中大致分析了NameNode节点的启动过程,下面来重点分析NameNode启动过程中执行的initialize方法的几个要点。
创建FSNamesystem对象
NameNode类的实现中,大量的具体调用工作是由FSNamesystem对象完成的,FSNamesystem类是整个名字节点的门面,封装了对HDFS目录操作,数据块块操作,数据节点操作的方法,如类型为FSDirectory的成员变量FSNamesystem.dir就用于操作HDFS目录树,所以在初始化NameNode对象的过程中要先创建一个FSNamesystem对象,在NameNode.initialize()方法中this.namesystem = new FSNamesystem(this, conf);这样代码创建了FSNamesystem对象namesystem,它是NameNode的一个成员变量。FSNamesystem类的构造方法也是调用了一个initialize方法,初始化的工作都在FSNamesystem.initialize()方法中完成。
FSNamesystem.initialize()方法中首先就是调用方法FSNamesystem.setConfigurationParameters()方法对FSNamesystem类的成员变量进行初始化,方法的代码比较长,但是具体的逻辑就是对FSNamesystem类的成员变量进行初始化,方法代码如下:
/** * Initializes some of the members from configuration */ private void setConfigurationParameters(Configuration conf) throws IOException { fsNamesystemObject = this; /**当前登录用户**/ fsOwner = UserGroupInformation.getCurrentUser(); this.fsOwnerShortUserName = fsOwner.getShortUserName(); LOG.info("fsOwner=" + fsOwner); //超级用户所在的组名,默认是supergroup,启动hadoop所使用的用户通常是superuser this.supergroup = conf.get("dfs.permissions.supergroup", "supergroup"); //如果dfs.permissions属性配置为false则不执行权限检查,否则就执行权限检查 this.isPermissionEnabled = conf.getBoolean("dfs.permissions", true); LOG.info("supergroup=" + supergroup); LOG.info("isPermissionEnabled=" + isPermissionEnabled); this.persistBlocks = conf.getBoolean(DFS_PERSIST_BLOCKS_KEY, DFS_PERSIST_BLOCKS_DEFAULT); //文件权限,和Linux/Unix一致 short filePermission = (short)conf.getInt("dfs.upgrade.permission", 0777); this.defaultPermission = PermissionStatus.createImmutable( fsOwnerShortUserName, supergroup, new FsPermission(filePermission)); //一次心跳中附带的待删除的无效数据块数量 this.blocksInvalidateWorkPct = DFSUtil.getInvalidateWorkPctPerIteration(conf); //同时向一个DN传输数据块的数量 this.blocksReplWorkMultiplier = DFSUtil.getReplWorkMultiplier(conf); //数据节点的网络拓扑 this.clusterMap = NetworkTopology.getInstance(conf); this.maxCorruptFilesReturned = conf.getInt( DFSConfigKeys.DFS_MAX_CORRUPT_FILES_RETURNED_KEY, DFSConfigKeys.DFS_MAX_CORRUPT_FILES_RETURNED_DEFAULT); //为数据块副本选择数据节点的策略 this.replicator = BlockPlacementPolicy.getInstance(conf, this, clusterMap); //默认副本数量 this.defaultReplication = conf.getInt("dfs.replication", 3); //所允许的最大的副本数量 this.maxReplication = conf.getInt("dfs.replication.max", 512); //最小副本数量 this.minReplication = conf.getInt("dfs.replication.min", 1); if (minReplication <= 0) throw new IOException( "Unexpected configuration parameters: dfs.replication.min = " + minReplication + " must be greater than 0"); if (maxReplication >= (int)Short.MAX_VALUE) throw new IOException( "Unexpected configuration parameters: dfs.replication.max = " + maxReplication + " must be less than " + (Short.MAX_VALUE)); if (maxReplication < minReplication) throw new IOException( "Unexpected configuration parameters: dfs.replication.min = " + minReplication + " must be less than dfs.replication.max = " + maxReplication); this.maxReplicationStreams = conf.getInt("dfs.max-repl-streams", 2); //两次心跳的时间间隔 long heartbeatInterval = conf.getLong("dfs.heartbeat.interval", 3) * 1000; this.heartbeatRecheckInterval = conf.getInt( "heartbeat.recheck.interval", 5 * 60 * 1000); // 5 minutes this.heartbeatExpireInterval = 2 * heartbeatRecheckInterval + 10 * heartbeatInterval; this.replicationRecheckInterval = conf.getInt("dfs.replication.interval", 3) * 1000L; //默认数据块的大小 this.defaultBlockSize = conf.getLong("dfs.block.size", DEFAULT_BLOCK_SIZE); //HDFS所支持的文件,数据块,目录的数量 this.maxFsObjects = conf.getLong("dfs.max.objects", 0); //default limit this.blockInvalidateLimit = Math.max(this.blockInvalidateLimit, 20*(int)(heartbeatInterval/1000)); //use conf value if it is set. //NameNode节点通知DN节点每次删除文件块的最大数量 this.blockInvalidateLimit = conf.getInt( DFSConfigKeys.DFS_BLOCK_INVALIDATE_LIMIT_KEY, this.blockInvalidateLimit); LOG.info(DFSConfigKeys.DFS_BLOCK_INVALIDATE_LIMIT_KEY + "=" + this.blockInvalidateLimit); //允许访问文件的时间,即可以访问一个文件多长时间 this.accessTimePrecision = conf.getLong("dfs.access.time.precision", 0); this.allowBrokenAppend = conf.getBoolean("dfs.support.broken.append", false); if (conf.getBoolean("dfs.support.append", false)) { LOG.warn("The dfs.support.append option is in your configuration, " + "however append is not supported. This configuration option " + "is no longer required to enable sync"); } this.durableSync = conf.getBoolean("dfs.durable.sync", true); if (!durableSync) { LOG.warn("Durable sync disabled. Beware data loss when running " + "programs like HBase that require durable sync!"); } this.isAccessTokenEnabled = conf.getBoolean( DFSConfigKeys.DFS_BLOCK_ACCESS_TOKEN_ENABLE_KEY, false); if (isAccessTokenEnabled) { this.accessKeyUpdateInterval = conf.getLong( DFSConfigKeys.DFS_BLOCK_ACCESS_KEY_UPDATE_INTERVAL_KEY, 600) * 60 * 1000L; // 10 hrs this.accessTokenLifetime = conf.getLong( DFSConfigKeys.DFS_BLOCK_ACCESS_TOKEN_LIFETIME_KEY, 600) * 60 * 1000L; // 10 hrs } LOG.info("isAccessTokenEnabled=" + isAccessTokenEnabled + " accessKeyUpdateInterval=" + accessKeyUpdateInterval / (60 * 1000) + " min(s), accessTokenLifetime=" + accessTokenLifetime / (60 * 1000) + " min(s)"); // set the value of stale interval based on configuration this.avoidStaleDataNodesForRead = conf.getBoolean( DFSConfigKeys.DFS_NAMENODE_AVOID_STALE_DATANODE_FOR_READ_KEY, DFSConfigKeys.DFS_NAMENODE_AVOID_STALE_DATANODE_FOR_READ_DEFAULT); this.avoidStaleDataNodesForWrite = conf.getBoolean( DFSConfigKeys.DFS_NAMENODE_AVOID_STALE_DATANODE_FOR_WRITE_KEY, DFSConfigKeys.DFS_NAMENODE_AVOID_STALE_DATANODE_FOR_WRITE_DEFAULT); this.staleInterval = getStaleIntervalFromConf(conf, heartbeatExpireInterval); this.ratioUseStaleDataNodesForWrite = getRatioUseStaleNodesForWriteFromConf(conf); if (avoidStaleDataNodesForWrite && staleInterval < heartbeatRecheckInterval) { this.heartbeatRecheckInterval = staleInterval; LOG.info("Setting heartbeat recheck interval to " + staleInterval + " since " + DFSConfigKeys.DFS_NAMENODE_STALE_DATANODE_INTERVAL_KEY + " is less than " + DFSConfigKeys.DFS_NAMENODE_HEARTBEAT_RECHECK_INTERVAL_KEY); } }
在FSNamesystem.initialize()方法中创建了一个FSDirectory对象,FSDirectory类主要为NameNode节点中复杂的文件目录树的操作提供了一个简单的接口,这样NameNode节点的其他子系统可以使用FSDirectory操作目录树,再由FSDirectory来实现相应的处理逻辑。FSDirectory类的定义,成员变量和构造方法的代码如下:
class FSDirectory implements FSConstants, Closeable { /**访问数据块管理的相关功能**/ final FSNamesystem namesystem; /**整个文件系统的根目录**/ final INodeDirectoryWithQuota rootDir; /**为FSDirectory提供了命名空间镜像和编辑日志的相关功能**/ FSImage fsImage; /**当FSDirectory.loadFSImage()完成命名空间镜像和编辑日志加载后,变量修改为true,这时可以对目录树进行操作**/ private boolean ready = false; private final int lsLimit; // max list limit /** * Caches frequently used file names used in {@link INode} to reuse * byte[] objects and reduce heap usage. */ private final NameCache<ByteArray> nameCache; /** Access an existing dfs name directory. */ FSDirectory(FSNamesystem ns, Configuration conf) { this(new FSImage(), ns, conf); fsImage.setCheckpointDirectories(FSImage.getCheckpointDirs(conf, null), FSImage.getCheckpointEditsDirs(conf, null)); } FSDirectory(FSImage fsImage, FSNamesystem ns, Configuration conf) { rootDir = new INodeDirectoryWithQuota(INodeDirectory.ROOT_NAME, ns.createFsOwnerPermissions(new FsPermission((short)0755)), Integer.MAX_VALUE, -1); this.fsImage = fsImage; //初始化FSImage的成员变量 fsImage.setRestoreRemovedDirs(conf.getBoolean( DFSConfigKeys.DFS_NAMENODE_NAME_DIR_RESTORE_KEY, DFSConfigKeys.DFS_NAMENODE_NAME_DIR_RESTORE_DEFAULT)); fsImage.setEditsTolerationLength(conf.getInt( DFSConfigKeys.DFS_NAMENODE_EDITS_TOLERATION_LENGTH_KEY, DFSConfigKeys.DFS_NAMENODE_EDITS_TOLERATION_LENGTH_DEFAULT)); namesystem = ns; int configuredLimit = conf.getInt( DFSConfigKeys.DFS_LIST_LIMIT, DFSConfigKeys.DFS_LIST_LIMIT_DEFAULT); this.lsLimit = configuredLimit>0 ? configuredLimit : DFSConfigKeys.DFS_LIST_LIMIT_DEFAULT; //文件被使用超过threshold次就会被缓存 int threshold = conf.getInt( DFSConfigKeys.DFS_NAMENODE_NAME_CACHE_THRESHOLD_KEY, DFSConfigKeys.DFS_NAMENODE_NAME_CACHE_THRESHOLD_DEFAULT); NameNode.LOG.info("Caching file names occuring more than " + threshold + " times "); nameCache = new NameCache<ByteArray>(threshold); } }
FSDirectory类的成员变量namesystem类型为FSNamesystem,在FSDirectory类中,主要使用namesystem访问数据块管理相关的功能,成员变量rootDir是整个NameNode节点的内存元数据文件/目录树的根节点,成员变量fsImage是FSImage类型,为FSDirectory类提供了持久化内存元数据到文件的功能,ready为布尔类型变量,当FSDirectory.loadFSImage()方法完成镜像和编辑日志加载后,变量修改为true,这样就可以对目录树进行操作了。
在FSNamesystem.initialize()方法中调用的是FSDirectory的带有两个参数的构造方法,然后在这个构造方法中新建了一个FSImage对象,再调用FSDirectory有三个参数的构造方法。在这个构造方法中,先初始化rootDir成员变量。对于rootDir变量的初始化过程,先创建一个FsPermission对象,然后利用FSNamesystem.crateFsOwnerPermissions()方法创建对rootDir这个目录的访问权限。INodeDirectory.ROOT_NAME为一个空字符串,用于表示根节点节点名,在整个元数据目录树中,只有根节点的名为空字符串,Integer.MAX_VALUE参数和-1分别作为rootDir的节点配额和空间配额传入。
FsPermission类保存文件或目录的所有者、文件组或其他用户权限信息,这三种信息使用类似于Linux文件系统的方式来表示,在FsPermission类中定义了三个FsAction变量,分别是useraction、groupaction和otherAction,分别代表用户权限,用户所在组权限和其他用户的权限。而FsAction则是一个枚举类,它定义了8个值,定义的代码如下:
public enum FsAction { // POSIX style NONE("---"),//没有权限 EXECUTE("--x"),//执行 WRITE("-w-"),//写 WRITE_EXECUTE("-wx"),//写和执行 READ("r--"),//读 READ_EXECUTE("r-x"),//读和执行 READ_WRITE("rw-"),//读和写 ALL("rwx");//读写执行 }
由上面的定义可以看出,对于一个文件,可以定义读、写、执行这三种方式,再两两结合,那么总共有8中权限。对于FsPermission中定义的三种角色权限,都可以用FsAction的这8个值来表示。在FsAction枚举类中,还有一个比较重要的方法implies,代码如下:
public boolean implies(FsAction that) { if (that != null) { return (ordinal() & that.ordinal()) == that.ordinal(); } return false; }
它有一个FsAction参数that,方法的作用是,用于判断当前权限是否隐含权限that,如FsAction.READ_WRITE包含 FsAction.READ。
rootDir对象创建成功后,就给FSDirectory的各个成员变量赋值,代码比较简单。
回到FSNamesystem.initialize()方法中,在再调用FSDirectory.loadFSImage()方法将保存的命名空间镜像和编辑日志数据加载到NameNode节点的内存中,建立文件/目录树。方法的代码如下:
void loadFSImage(Collection<File> dataDirs, Collection<File> editsDirs, StartupOption startOpt) throws IOException { // format before starting up if requested if (startOpt == StartupOption.FORMAT) { fsImage.setStorageDirectories(dataDirs, editsDirs); fsImage.format(); startOpt = StartupOption.REGULAR; } try { if (fsImage.recoverTransitionRead(dataDirs, editsDirs, startOpt)) { fsImage.saveNamespace(true); } FSEditLog editLog = fsImage.getEditLog(); assert editLog != null : "editLog must be initialized"; if (!editLog.isOpen()) editLog.open(); fsImage.setCheckpointDirectories(null, null); } catch(IOException e) { fsImage.close(); throw e; } synchronized (this) { this.ready = true; this.nameCache.initialized(); this.notifyAll(); } }
该方法的第一个参数和第二个参数分别是保存镜像空间的目录和保存编辑日志的目录,第三个参数是启动模式。在方法中,首先判断启动模式是否是StartupOption.FORMAT模式,如果是就对保存命名空间镜像的目录和保存编辑日志的目录进行格式化,然后再正常启动,不过这段代码是不是多余了?如果以StartupOption.FORMAT模式启动,那么会在NameNode.createNameNode()方法中格式化完成之后就退出系统,不会执行到这里了。接着往下分析,再执行FSImage.recoverTransitionRead()方法,这个方法先对保存命名空间镜像和保存编辑日志的目录进行分析,如果目录处于非正常状态,先对其进行恢复,然后再调用FSImage.loadFSImage()方法将镜像和编辑日志数据导入到内存中。FSImage.recoverTransitionRead()的方法的代码如下:
/** * 根据启动选项及其对应存储目录,分析存储目录,必要的话从先前的事务恢复过来 */ boolean recoverTransitionRead(Collection<File> dataDirs, Collection<File> editsDirs, StartupOption startOpt ) throws IOException { assert startOpt != StartupOption.FORMAT : "NameNode formatting should be performed before reading the image";//在此之前NameNode已被格式化 // none of the data dirs exist if (dataDirs.size() == 0 || editsDirs.size() == 0) throw new IOException( "All specified directories are not accessible or do not exist."); if(startOpt == StartupOption.IMPORT && (checkpointDirs == null || checkpointDirs.isEmpty())) throw new IOException("Cannot import image from a checkpoint. " + "\"fs.checkpoint.dir\" is not set." ); if(startOpt == StartupOption.IMPORT && (checkpointEditsDirs == null || checkpointEditsDirs.isEmpty())) throw new IOException("Cannot import image from a checkpoint. " + "\"fs.checkpoint.edits.dir\" is not set." ); setStorageDirectories(dataDirs, editsDirs); // 1. For each data directory calculate its state and // check whether all is consistent before transitioning. //计算每个存储数据目录的状态,并且检查目录在变化前是否一致 Map<StorageDirectory, StorageState> dataDirStates = new HashMap<StorageDirectory, StorageState>(); boolean isFormatted = false; for (Iterator<StorageDirectory> it = dirIterator(); it.hasNext();) { StorageDirectory sd = it.next(); StorageState curState; try { curState = sd.analyzeStorage(startOpt); // sd is locked but not opened switch(curState) { case NON_EXISTENT: // name-node fails if any of the configured storage dirs are missing throw new InconsistentFSStateException(sd.getRoot(), "storage directory does not exist or is not accessible."); case NOT_FORMATTED: break; case NORMAL: break; default: // recovery is possible,进行恢复 sd.doRecover(curState); } if (curState != StorageState.NOT_FORMATTED && startOpt != StartupOption.ROLLBACK) { sd.read(); // read and verify consistency with other directories isFormatted = true; } if (startOpt == StartupOption.IMPORT && isFormatted) // import of a checkpoint is allowed only into empty image directories throw new IOException("Cannot import image from a checkpoint. " + " NameNode already contains an image in " + sd.getRoot()); } catch (IOException ioe) { sd.unlock(); throw ioe; } dataDirStates.put(sd,curState); } if (!isFormatted && startOpt != StartupOption.ROLLBACK && startOpt != StartupOption.IMPORT) throw new IOException("NameNode is not formatted."); if (layoutVersion < LAST_PRE_UPGRADE_LAYOUT_VERSION) { checkVersionUpgradable(layoutVersion); } if (startOpt != StartupOption.UPGRADE && layoutVersion < LAST_PRE_UPGRADE_LAYOUT_VERSION && layoutVersion != FSConstants.LAYOUT_VERSION) throw new IOException( "\nFile system image contains an old layout version " + layoutVersion + ".\nAn upgrade to version " + FSConstants.LAYOUT_VERSION + " is required.\nPlease restart NameNode with -upgrade option."); // check whether distributed upgrade is reguired and/or should be continued verifyDistributedUpgradeProgress(startOpt); // 2. Format unformatted dirs. this.checkpointTime = 0L; for (Iterator<StorageDirectory> it = dirIterator(); it.hasNext();) { StorageDirectory sd = it.next(); StorageState curState = dataDirStates.get(sd); switch(curState) { case NON_EXISTENT: assert false : StorageState.NON_EXISTENT + " state cannot be here"; case NOT_FORMATTED: LOG.info("Storage directory " + sd.getRoot() + " is not formatted."); LOG.info("Formatting ..."); sd.clearDirectory(); // create empty currrent dir break; default: break; } } // 3. Do transitions switch(startOpt) { case UPGRADE: doUpgrade(); return false; // upgrade saved image already case IMPORT: doImportCheckpoint(); return true; case ROLLBACK: doRollback(); break; case REGULAR: // just load the image } return loadFSImage(startOpt.createRecoveryContext()); }
进入该方法首先,调用Storage.setStorageDirectories()对给定的目录进行区分(关于这个方法可以参考博文 Hadoop源码分析之NameNode的格式化)。recoverTransitionRead方法分为三个部分对各个目录进行分析和恢复,第一部分计算每个存储数据目录的状态,并且检查目录在变化前是否一致,第二部分对还未进行格式化的目录进行格式化,第三部分将目录上次停止之前没有执行完的操作执行完成,最后,如果方法没有返回,就执行FSImage.loadFSImage()方法将数据执行镜像导入。
recoverTransitionRead方法的第一部分是检查每个目录是否处于一致的状态,对目录的状态进行分析,是调用了Storage.analyzeStorage()方法,该方法在博文Hadoop源码分析之DateNode的目录构成与类继承结构分析过了。执行完analyzeStorage方法,返回对目录分析出的状态,如果是StorageState.NOT_EXIST状态,那么就抛出异常,因为NOTE_EXIST表示目录不存在,也就不能继续往下执行,根据analyzeStorage方法,有四种情况属于NOT_EXIST状态,分别是:
- 如果如果根目录(根目录就是NameNode的根目录)不存在,但是NameNode的启动参数也不是StartupOption.FORMAT,那么返回NOT_EXIST状态,如果启动参数是StartupOption.FORMAT,则会创建这个目录;
- 如果根目录访问不了,即执行root.isDirectory()方法返回false,那么就是这个目录访问不了,返回NOT_EXIST状态;
- 目录不可写,返回NOT_EXIST状态;
- 如果以上过程抛出SecurityException异常,也即目录访问不了,返回NOT_EXIST状态;
以上四种情况均返回NOT_EXIST状态,那么就抛出异常。如果是StorageState.NOT_FORMATTED和StorageState.NORMAL状态,则继续向下执行,如果是StorageState中的其它状态,则需要先执行StorageDirectory.doRecover(),然后再向下执行,因为此时这个目录处于一个过度状态,需要从这个过渡状态中恢复成正常状态。StorageDirectory.doRecover()的方法代码就是根据当前的状态进行恢复,在这里就不分析了。继续向下看,如果当前的目录状态不是StorageState.NOT_FORMATE,那么就读入目录下的VERSION文件的属性,并将局部变量isFormatted置为true,再将目录和目录状态作为键值对放入一个Map对象dataDirStates中,供下面使用。
recoverTransitionRead方法的第二个部分是对还未格式化的目录进行格式化,先遍历所有目录,然后从Map对象dataDirStates中该目录对应的状态,如果状态是NOT_FORMAT,则对其进行格式化,这里格式化不同于NameNode节点的格式化,这里只是创建新的current目录。
recoverTransitionRead方法的第二个部分是根据NameNode节点的启动参数进行响应的操作,如果是启动参数StartupOption.UPGRADE,则调用方法FSImage.doUpgrade()进行处理,然后返回false,如果启动参数是StartupOption.IMPORT,那么调用方法FSImage.doImportCheckpoint()进行处理,然后返回true,如果启动参数是StartupOption.ROLLBACK,那么调用FSImage.doRollback(),再向下执行FSImage.loadFSImage()方法,如果启动参数是StartupOption.REGULAR则什么也不做,直接进入FSImage.loadFSImage()方法的调用。
FSImage.loadFSImage()有两个重载方法,在recoverTransitionRead()方法中调用的是带有参数MetaRecoveryContext对象的方法,这个方法先找出所有存有最新版本镜像的目录,然后调用FSImage.loadFSImage()带有一个File对象的重载方法将这个目录下的镜像和编辑日志加载到内存中,形成元数据目录树,方法比较长,但是逻辑比较简单。
FSImage.recoverTransitionRead()方法会返回一个布尔值,这个值表示是否将当前内存中的数据持久化到磁盘中,如果这个值为true,那么在FSDirectory.loadFSImage()方法中会执行fsImage.saveNamespace(true);这行语句将当前内存中的元数据保存到磁盘中。FSImage.saveNamespace()这个方法主要执行逻辑与其他几种更新,升级操作类似,过程中会产生一个*.tmp文件,逻辑比较简单。
接下来在FSDirectory.loadFSImage()方法中,设置FSDirectory.ready变量为true,并且初始化FSDirectory.nameCache变量。
再回到FSNamesystem.initialize()方法,在剩余的代码中主要对几个线程进行了初始化,分别是心跳监听线程HeartbeatMonitor,租约监听线程LeaseManager.Monitor,副本监听线程ReplicationMonitor和负责解除数据节点的线程DecommissionManager.Monitor。这几个类会在以后的分析中进行分析。
FSNamesystem.initialize()方法执行完毕之后就创建了FSNamesystem对象,就会返回到NameNode.initialize()方法继续执行,NameNode.initialize()的执行逻辑在文章Hadoop源码分析之NameNode的启动与停止中已经分析了,这里就不重复了。
这部分就暂且分析到这里,接下会分析NameNode和DataNode的通信。
--EOF
Reference
《Hadoop技术内幕:深入理解Hadoop Common和HDFS架构设计与实现原理》
评论暂时关闭