Hadoop源码学习笔记之NameNode启动场景流程五:磁盘空间检查及安全模式检查

本篇内容关注NameNode启动之前,active状态和standby状态的一些后台服务及准备工作,即源码里的CommonServices。主要包括磁盘空间检查、

可用资源检查、安全模式等。依然分为三部分:源码调用分析、伪代码核心梳理、调用关系图解。

第一部分,源码调用分析。

  接着上篇RpcServer启动之后开始梳理,进入到了initialize()方法中。

protected void initialize(Configuration conf) throws IOException {
  // 可以通过找到下面变量名的映射,在hdfs-default.xml中找到对应的配置
  if (conf.get(HADOOP_USER_GROUP_METRICS_PERCENTILES_INTERVALS) == null) {
    String intervals = conf.get(DFS_METRICS_PERCENTILES_INTERVALS_KEY);
    if (intervals != null) {
      conf.set(HADOOP_USER_GROUP_METRICS_PERCENTILES_INTERVALS,
        intervals);
    }
  }
  ......

  // 核心代码:启动HttpServer
  if (NamenodeRole.NAMENODE == role) {
    startHttpServer(conf);
  }

  this.spanReceiverHost = SpanReceiverHost.getInstance(conf);

  // 核心代码:FSNamesystem初始化
  loadNamesystem(conf);

  // 核心代码:创建一个rpc server实例
  rpcServer = createRpcServer(conf);
  
  ......
  // 核心代码:启动一些服务组件,包括rpc server等
  startCommonServices(conf);
}

  本篇的核心代码,在最后一行的startCommonServices(),我们进去看一下都start了哪些service。

/** 
   * Start services common to both active and standby states
   */
  void startCommonServices(Configuration conf, HAContext haContext) throws IOException {
    ......
   try { // 创建了一个NameNodeResourceChecker对象,用来检查namenode所在机器上的磁盘空间是否足够 nnResourceChecker = new NameNodeResourceChecker(conf); // 检查可用资源是否足够:如果不够,日志打印警告信息,然后进入安全模式 checkAvailableResources(); // 判断是否进入安全模式(安全模式是否实例化),并且副本队列是否应该被同步/复制(populate怎么翻译?) assert safeMode != null && !isPopulatingReplQueues(); // 目前NameNode启动,进入到safemode阶段,处于一个等待汇报blocks的状态 StartupProgress prog = NameNode.getStartupProgress(); prog.beginPhase(Phase.SAFEMODE); prog.setTotal(Phase.SAFEMODE, STEP_AWAITING_REPORTED_BLOCKS, getCompleteBlocksTotal()); // 设置block数量 setBlockTotal(); // 启动BlockManager里面的一堆后台线程 blockManager.activate(conf); } finally { writeUnlock(); } ...... }

  这段代码里加粗的就是主要的核心逻辑,一步一步按顺序梳理。先从第一段核心代码,即new NameNodeResourceChecker()来看,这行代码实例化了

  一个NameNodeResourceChecker对象,这个NameNodeResourceChecker类从名字猜测,大概是实现一个资源检查的功能。我们点进去验证

  一下我们的猜想:

/**
 * 
 * NameNodeResourceChecker provides a method -
 * <code>hasAvailableDiskSpace</code> - which will return true if and only if
 * the NameNode has disk space available on all required volumes, and any volume
 * which is configured to be redundant. Volumes containing file system edits dirs
 * are added by default, and arbitrary extra volumes may be configured as well.
 *
 * 提供了一个hasAvailableDiskSpace()方法,
 * 这个方法仅仅在NameNode有足够的磁盘空间满足所有磁盘使用需求的时候
 * 返回true,包括文件系统的edits dir,包括配置文件内设置的dirs。
 * 否则,返回false.
 */
@InterfaceAudience.Private
public class NameNodeResourceChecker {
  ......

  // Space (in bytes) reserved per volume.
// 配置文件内的每个被配置的磁盘路径空间的默认大小 private final long duReserved;
// 一个map,用来存放所有应该检查的配置路径,在NameNodeResourceChecker实例化的时候进行初始化 private Map<String, CheckedVolume> volumes; ...... @VisibleForTesting
class CheckedVolume implements CheckableNameNodeResource { ...... @Override public boolean isResourceAvailable() { long availableSpace = df.getAvailable(); ...... // duReserved就是默认配置好的最小需要的磁盘空间 // duReserved空间默认是100Mb,默认情况下edits目录起码要有100Mb空间来写入日志, // 否则就是检查资源失败 if (availableSpace < duReserved) { LOG.warn("Space available on volume '" + volume + "' is " + availableSpace + ", which is below the configured reserved amount " + duReserved); return false; } else { return true; } } ...... } /** * Create a NameNodeResourceChecker, which will check the edits dirs and any * additional dirs to check set in <code>conf</code>. * * 构造方法。 * 创建一个NameNodeResourceChecker对象,初始化volumes Map,用来存放需要进行磁盘空间检查的路径。
* 通过将这个volumes的map传递给NameNodeResourcePolicy类进行遍历volumes Map来进行检查edits dirs
* 和一些配置文件中配置的dirs。
*/ public NameNodeResourceChecker(Configuration conf) throws IOException { this.conf = conf;
volumes = new HashMap<String, CheckedVolume>(); ......
// 从配置文件中获取DFS_NAMENODE_DU_RESERVED_KEY配置 duReserved = conf.getLong(DFSConfigKeys.DFS_NAMENODE_DU_RESERVED_KEY, DFSConfigKeys.DFS_NAMENODE_DU_RESERVED_DEFAULT); ...... } /** * Add the volume of the passed-in directory to the list of volumes to check. * If <code>required</code> is true, and this volume is already present, but * is marked redundant, it will be marked required. If the volume is already * present but marked required then this method is a no-op. * * @param directoryToCheck * The directory whose volume will be checked for available space. */ private void addDirToCheck(URI directoryToCheck, boolean required) throws IOException { ......
   CheckedVolume newVolume = new CheckedVolume(dir, required);
   CheckedVolume volume = volumes.get(newVolume.getVolume());
   if(volume == null || !volume.isRequired()){
    volumes.put(newVolume.getVolume(), newVolume);
   } }
/** * Return true if disk space is available on at least one of the configured * redundant volumes, and all of the configured required volumes. * * @return True if the configured amount of disk space is available on at * least one redundant volume and all of the required volumes, false * otherwise. * 如果磁盘空间满足所有配置要求的大小并且至少满足一个配置的冗余大小就返回true */ public boolean hasAvailableDiskSpace() { return NameNodeResourcePolicy.areResourcesAvailable(volumes.values(), minimumRedundantVolumes); } ...... }

  从NameNodeResourceChecker这个类的注释上可以清楚的得知,就是为了检查配置里的路径的磁盘空间大小是否满足使用需求的。它最核心的

  两个变量一个是duReserved,一个是volumes。下面一一来分析:

  duReserved, 这是一个long型变量,通过duReserved = conf.getLong(DFSConfigKeys.DFS_NAMENODE_DU_RESERVED_KEY,...);这行代码,

  可以溯源到hdfs-default.xml中的dfs.namenode.resource.du.reserved这个配置选项,配置文件中默认为空,但是getLong()方法有一个默认大小,

  是100M。那些待检查空间大小的路径首先会同这个duReserved进行比较,小于duReserved的话,返回false。最终会导致的结果是什么呢?先打个

  疑问,一会儿会根据梳理的代码进行解疑。

  volumes, 这是一个map,通过addDirToCheck()方法及注释可知,主要是用来存放需要进行磁盘空间检查的dirs。然后在hasAvailableDiskSpace()

  中将这个map传入到了真正负责磁盘检查逻辑的类:NameNodeResourcePolicy,这个类就是用来遍历volumes这个map进行资源检查的。

  前面提出的一个疑问:返回false结果会导致什么情况出现呢?通过跟踪hasAvailableDiskSpace()方法的被调用代码,一步一步进行梳理,最终会

  进入到FSNamesystem类的一个内部线程类中:

 /**
   * Periodically calls hasAvailableResources of NameNodeResourceChecker, and if
   * there are found to be insufficient resources available, causes the NN to
   * enter SAFE MODE. If resources are later found to have returned to
   * acceptable levels, this daemon will cause the NN to exit safe mode.
   */
  class NameNodeResourceMonitor implements Runnable  {
    boolean shouldNNRmRun = true;
    @Override
    public void run () {
      try {
        while (fsRunning && shouldNNRmRun) {
          checkAvailableResources();
          if(!nameNodeHasResourcesAvailable()) {
            String lowResourcesMsg = "NameNode low on available disk space. ";
            if (!isInSafeMode()) {
              FSNamesystem.LOG.warn(lowResourcesMsg + "Entering safe mode.");
            } else {
              FSNamesystem.LOG.warn(lowResourcesMsg + "Already in safe mode.");
            }
            // 进入到安全模式
            enterSafeMode(true);
          }
      ......
} } catch (Exception e) { FSNamesystem.LOG.error("Exception in NameNodeResourceMonitor: ", e); } }

  注释告诉我们,这个线程会周期性的调用NameNodeResourceChecker的hasAvailableRecourses()方法进行磁盘资源检查,如果一旦发现有资源

  不足的情况,会使NameNode进入安全模式。如果随后返回的状态代表资源大小到达可使用的级别,那么这个线程就使NameNode退出安全模式。

  依照这个注释,去解读run()方法的代码逻辑:在一个while循环里,首先判断资源是否可用,如果不可用,日志里就会发出一个警告信息,然后调用

  enterSafeMode();进入安全模式。

  为什么磁盘资源不足要进入安全模式呢?很简单,磁盘资源不足的情况下,任何对元数据修改所产生的日志都无法确保能够写入到磁盘,即新产生的

  edits log和fsimage都无法确保写入磁盘。所以要进入安全模式,来禁止元数据的变动以避免往磁盘写入新的日志数据。

  那么具体的进入安全模式方法里都有哪些操作,可以自己进去enterSafeMode()方法中看一下。代码就不在这儿贴出来了,主要涉及到一些日志文件的

  同步,然后在同步完成之后进行加锁处理禁止所有写的操作。

    回到startCommonServices()中来,继续下一个核心逻辑的分析,isPopulatingReplQueres();

 /**
   * Check if replication queues are to be populated
   * @return true when node is HAState.Active and not in the very first safemode
   */
  @Override
  public boolean isPopulatingReplQueues() {
    if (!shouldPopulateReplQueues()) {
      return false;
    }
    return initializedReplQueues;
  }

  private boolean shouldPopulateReplQueues() {
    if(haContext == null || haContext.getState() == null)
      return false;
    return haContext.getState().shouldPopulateReplQueues();
  }

  这个方法是用来检查副本队列是否应该被同步/复制(populate应该怎么翻译啊?大概就是这么个意思吧),返回的是副本队列是否已经初始化的状态。

  继续往下看,NameNode.getStartupProgress()这行代码会获取一个StarupProgress实例,这个类用来指示NameNode进程内各项任务的启动情况和

  进度的,比如一个任务某个阶段的开始和结束信息等。并且是线程安全的。

第二部分,伪代码核心梳理。

  NameNode.main() // 入口函数
    |——createNameNode(); // 通过new NameNode()进行实例化
      |——initialize(); // 方法进行初始化操作
        |——startHttpServer(); // 启动HttpServer
        |——loadNamesystem(); // 加载元数据
        |——createRpcServer(); // 创建并初始化rpc server实例
        |——startCommonServices();
          |——namesystem.startCommonServices(); // 启动一些磁盘检查、安全模式等一些后台服务及线程
            |——new NameNodeResourceChecker(); // 实例化一个NameNodeResourceChecker并准备出所有需要检查的磁盘路径
            |——checkAvailableResources(); // 开始磁盘空间检查
            |——NameNode.getStartupProgress(); // 获取StartupProgress实例用来获取NameNode各任务的启动信息
            |——setBlockTotal(); // 设置所有的block,用于后面判断是否进入安全模式
            |——blockManager.activate(); // 启动BlockManager里面的一堆关于block副本处理的后台线程
          |——rpcServer.start(); // 启动rpcServer
    |——join()

第三部分,调用关系图解。

  

 

  这篇内容到这儿就完事儿了,可能有些地方分析的不够深入,但是在确定的场景下,梳理主要的骨干代码,更有助于理解和把握整个场景的流程。

  用一个大师的说法叫抓大放小,即从宏观上把握主要梗概,放弃一些无关紧要的次要枝丫。

  做一下预告,下一篇内容将是NameNode启动的这个场景最后一部分内容:blockManager后台线程。

 

posted @ 2019-02-20 18:46  Boven.Qiao  阅读(868)  评论(0编辑  收藏  举报