HDFS的StartupProgress启动跟踪分析

来源:互联网 发布:js document 编辑:程序博客网 时间:2024/05/22 01:29

前言


想必HDFS集群的起停操作对于HDFS的使用者来说绝对不是一件陌生的事情.一般情况下我们重启集群服务是出于这2点原因:1).集群新增配置项,需要重启集群服务才能生效.2).对集群相关jar包程序进行了更新,需要重启服务来运行最新的jar包.于是我们重启了集群,一般的我们看它是否启动成功,一定会关注它的输出日志.比如说在HDFS中,我们会比较关注NameNode的启动日志.当然这的确是一个有效的办法,但是我们是否有其他更方便快捷的方法呢?比如说我不想登到NameNode所在节点上去看日志.最后的答案是有的.至于这里面到底是什么一个过程和原理呢?这就是本文将要重点阐述的主题:HDFS的StartupProgress.

HDFS的StartupProgress


这里可以把StartupProgress理解为Startup,Startup就是启动的意思.HDFS的Startup启动过程主要指的是NameNode的启动.在hdfs server端的project下有专门的package目录来存放这部分的代码.正如StartupProgress名称所展示的那样,此模块的功能并不是一个HDFS启动控制的角色,而是说是一种启动跟踪,并进行进度汇报的一个角色.因为HDFS的StartupProgress包下的代码并不多,我们可以做一个详细的分析.相关类文件如下图所示:

这里写图片描述

在上图中,我总共将此包下的相关类分成了3大类:

第一大类,StartupProgress启动进度信息核心类:此大类中包括了以StartupProgress为核心的类以及相关周边类, StartupProgressView和StartupProgressMetrics.前者是StartupProgress类的一个”瞬时数据拷贝”,后者是StartupProgress的统计计数类.
第二大类,Phase阶段类:在NameNode的启动过程中,会经历很多个阶段,而每个阶段的执行情况就是由这里的Phase类所控制.
第三大类,Set阶段步骤类:Set步骤相比于Phase阶段而言,是更细粒度一级的单位,用一句话来概况就是每个Phase阶段内包含了一个或多个Step.

背景知识: NameNode的启动过程


在学习了解HDFS StartupProgress内部代码之前,我们有必要事先了解NameNode的启动过程.下面是NameNode启动过程的简要描述:

1.NameNode启动开始,首先加载fsImage镜像文件,里面将会花比较多的时间解析这个文件.
2.FsImage镜像文件加载完毕之后,apply应用editlog日志文件到NameNode的内存中,如果editlog非常多,也会消耗一定的时间.
3.EditLog应用完毕,NameNode会做一次checkpoint操作.此时会生成一个新的fsImage镜像文件,也就是说,此时的fsImage文件已经是最新的元数据了.
4.以上文件数据全部加载完毕之后,NameNode将会进入SafeMode安全模式,等待DataNode块的上报.上报块数达到安全模式所规定的阈值要求,安全模式自然就会退出,随后NameNode正式启动完毕.

StartupProgress内的阶段和步骤


上节NameNode的4个启动过程在StartupProgress的Phase类都有相对应的声明,代码如下:

public enum Phase {  /**   * The namenode is loading the fsimage file into memory.   */  LOADING_FSIMAGE("LoadingFsImage", "Loading fsimage"),  /**   * The namenode is loading the edits file and applying its operations to the   * in-memory metadata.   */  LOADING_EDITS("LoadingEdits", "Loading edits"),  /**   * The namenode is saving a new checkpoint.   */  SAVING_CHECKPOINT("SavingCheckpoint", "Saving checkpoint"),  /**   * The namenode has entered safemode, awaiting block reports from data nodes.   */  SAFEMODE("SafeMode", "Safe mode");  ...

在每个阶段内,由更细粒度的Step构成.StartupProgress包下的StepType类中总共定义了如下几类的Step:

public enum StepType {  /**   * The namenode has entered safemode and is awaiting block reports from   * datanodes.   */  AWAITING_REPORTED_BLOCKS("AwaitingReportedBlocks", "awaiting reported blocks"),  /**   * The namenode is performing an operation related to delegation keys.   */  DELEGATION_KEYS("DelegationKeys", "delegation keys"),  /**   * The namenode is performing an operation related to delegation tokens.   */  DELEGATION_TOKENS("DelegationTokens", "delegation tokens"),  /**   * The namenode is performing an operation related to inodes.   */  INODES("Inodes", "inodes"),  /**   * The namenode is performing an operation related to cache pools.   */  CACHE_POOLS("CachePools", "cache pools"),  /**   * The namenode is performing an operation related to cache entries.   */  CACHE_ENTRIES("CacheEntries", "cache entries");  ...

Phase与Set之间是包含与被包含的关系.每个Phase与Set在执行的过程中总共会经历以下3个状态的变化:

  • 1.PENDING
  • 2.RUNNING
  • 3.COMPLETE

这3个状态的归属将由Phase或Step的开始时间和结束时间所决定.Phase与Set的相关结构图如下:

这里写图片描述

在NameNode的启动过程中,会经历以上Phase和Step的执行,它们各自的执行状况由对应的跟踪类负责记录.Phase对应的是PhaseTracking,Step对应的是StepTracking.它们都继承于AbstractTracking.最底层的跟踪类包含了2个最基本的变量信息:开始时间和结束时间.如下:

abstract class AbstractTracking implements Cloneable {  long beginTime = Long.MIN_VALUE;  long endTime = Long.MIN_VALUE;  ...}

在2类跟踪器内部是如何跟踪此过程的进度状况呢?首先我们来看PhaseTracking类:

final class PhaseTracking extends AbstractTracking {  String file;  long size = Long.MIN_VALUE;  // Step步骤集合以及对应的跟踪器类  final ConcurrentMap<Step, StepTracking> steps =    new ConcurrentHashMap<Step, StepTracking>();  ...

从上面的代码可以看出,Phase与Step的确是包含与被包含的关系,Phase的跟踪监控需要依赖于每个子Step的跟踪监控.

下面是StepTracking的跟踪类:

final class StepTracking extends AbstractTracking {  // 计数值  AtomicLong count = new AtomicLong();  // 总数值  long total = Long.MIN_VALUE;  ...

在此跟踪类中,有2个指标变量:计数值和总数值.而且计数值变量count特意设计为AtomicLong类型,这表明了此过程的计数更新过程将会存在并发竞争的问题,使用AtomicLong能保证原子的更新.

StartupProgress的原理与调用


上节详细阐述了Phase和Step的结构构造之后,下面我们要看看在NameNode的Startup启动过程中到底是如何调用这些对象的.这部分的逻辑在核心类StartupProgress中.我们找到这个类的声明:

public class StartupProgress {  // 包含了各个Phase阶段  // package-private for access by StartupProgressView  final Map<Phase, PhaseTracking> phases =    new ConcurrentHashMap<Phase, PhaseTracking>();  ...

我们马上可以看到StartupProgress类中包含了各个阶段的集合.我们可以猜想这个phases将会在后面NameNode的启动过程中陆续加入各个Phase.那么StartupProgress是在何时被创建的呢?答案在下面所示的代码中,总共分为3个相关的调用:

  // 1.初始化StartupProgress实例子  private static final StartupProgress startupProgress = new StartupProgress();
  protected void initialize(Configuration conf) throws IOException {    ...    // 2.注册StartupProgress Metric统计    StartupProgressMetrics.register(startupProgress);
  private void startHttpServer(final Configuration conf) throws IOException {    httpServer = new NameNodeHttpServer(conf, this, getHttpServerBindAddress(conf));    httpServer.start();    // 3.赋值startupProgress到NameNodeHttpServer中,这个将会与StartupProgressView相关    httpServer.setStartupProgress(startupProgress);  }

在初始化实例的时候,将会加入所有的Phase对象,也就是之前提到的4个阶段.

  public StartupProgress() {    for (Phase phase: EnumSet.allOf(Phase.class)) {      phases.put(phase, new PhaseTracking());    }  }

那么现在问题来了,StartupProgress对象是如何标记Phase,Set运行的开始结束?我们以LOAD_FSIMAGE阶段为例子.

  private boolean loadFSImage(FSNamesystem target, StartupOption startOpt,      MetaRecoveryContext recovery)      throws IOException {    ...    // NameNode中获取StartupProgress实例    StartupProgress prog = NameNode.getStartupProgress();    // 开始LOADING_FSIMAGE阶段    prog.beginPhase(Phase.LOADING_FSIMAGE);    File phaseFile = imageFiles.get(0).getFile();    // 设置此阶段用到的文件路径以及大小    prog.setFile(Phase.LOADING_FSIMAGE, phaseFile.getAbsolutePath());    prog.setSize(Phase.LOADING_FSIMAGE, phaseFile.length());    boolean needToSave = inspector.needToSave();    ...

继续往里看beginPhase方法

  public void beginPhase(Phase phase) {    // 判断启动过程是否结束    if (!isComplete()) {      // 如果没有结束,设置此阶段的开始时间为当前时间      phases.get(phase).beginTime = monotonicNow();    }  }

阶段开始执行了之后,将会经历内部Step的执行,比如在load fsimage开始之后,途中会经历inodes的加载,代码如下:

    public void load(File curFile) throws IOException {      checkNotLoaded();      assert curFile != null : "curFile is null";      // 获取StartupProgress对象实例      StartupProgress prog = NameNode.getStartupProgress();      // 新建INODES的Step对象      Step step = new Step(StepType.INODES);      // 开始此Step      prog.beginStep(Phase.LOADING_FSIMAGE, step);      ...

我们继续进入beginStep方法,

  public void beginStep(Phase phase, Step step) {    // 同样判断启动过程是否已完成    if (!isComplete()) {      // 设置开始时间      lazyInitStep(phase, step).beginTime = monotonicNow();    }  }

这部分代码与之前的beginStep方法极为相似,但是这里调用的是lazyInitStep方法,我们进入此方法来近一步查看其中的逻辑:

  private StepTracking lazyInitStep(Phase phase, Step step) {    // 获取此阶段的Step集合    ConcurrentMap<Step, StepTracking> steps = phases.get(phase).steps;    // 如果不存在目标step,则加入    if (!steps.containsKey(step)) {      steps.putIfAbsent(step, new StepTracking());    }    return steps.get(step);  }

在Phase和Step结束的时候,对应的会执行endPhase/endStep方法,同时会设置相关计数值.

prog.endPhase(Phase.LOADING_FSIMAGE);
    ...        // Step结束之后,结束此step        prog.endStep(Phase.LOADING_FSIMAGE, step);        // Now that the step is finished, set counter equal to total to adjust        // for possible under-counting due to reference inodes.        // 同时设置Counter        prog.setCount(Phase.LOADING_FSIMAGE, step, numFiles);    ...

其他的Phase与Step的执行过程完全类似,这里就不一一展开描述了.但是有一个Step比较特殊,我要额外提一下,就是LOADING_EDITS阶段的Step.这阶段的每个step做的事情是apply editlog文件数据到内存中.但是这类Step类型并没有事先存在,而是直接根据editlog文件名new出来的,代码如下:

  long loadFSEdits(EditLogInputStream edits, long expectedStartingTxId,      StartupOption startOpt, MetaRecoveryContext recovery) throws IOException {    StartupProgress prog = NameNode.getStartupProgress();    // 根据edits文件输入流创建新的step    Step step = createStartupProgressStep(edits);    // 开始此step    prog.beginStep(Phase.LOADING_EDITS, step);    ...

Startup过程内的Phase与Set的对应关系如下图所示:

这里写图片描述

StartupProgress的”瞬时映像”: StartupProgressView


上面花了一定的篇幅介绍了StartupProgress内部的进度追踪原理以及过程,那么有什么办法可以使用户获取这些数据信息呢?HDFS的设计者早已经帮我们考虑到这样的需求点了.答案是通过StartupProgressView.正如小标题所描述的那样,StartupProgressView是StartupProgress类的一个”瞬时数据拷贝”.这个时间点是这个状态数据,5分钟后又会变成另外的状态数据.在这里我们主要关注以下2个方面:

  • StartupProgressView如何对StartupProgress启动过程的数据做拷贝同步.
  • StartupProgressView如何将自身的数据展示给外部用户.

首先第一个方面,StartupProgressView在对外展示进度数据之前,需要对当前StartupProgress的进度数据做拷贝.相关代码如下:

  StartupProgressView(StartupProgress prog) {    phases = new HashMap<Phase, PhaseTracking>();    for (Map.Entry<Phase, PhaseTracking> entry: prog.phases.entrySet()) {      // 对StartupProgress中的每个Phase的跟踪数据做拷贝      phases.put(entry.getKey(), entry.getValue().clone());    }  }

在每个PhaseTracking的拷贝过程中,会近一步进行StepTracking的数据拷贝.拷贝完毕之后,用户如何能够访问到其数据呢?HDFS内部为我们提供了对此相应的http请求服务,换句话说,我们可以通过http请求的方式获取StartupProgressView的数据,从而我们也可以通过web界面直接查看HDFS启动进度数据.Http请求服务处理代码如下:

public class StartupProgressServlet extends DfsServlet {  ...  @Override  protected void doGet(HttpServletRequest req, HttpServletResponse resp)      throws IOException {    resp.setContentType("application/json; charset=UTF-8");    // 获取StartupProgress实例    StartupProgress prog = NameNodeHttpServer.getStartupProgressFromContext(      getServletContext());    // 获取此刻的一份数据拷贝    StartupProgressView view = prog.createView();    // 将拷贝数据以json格式返回    JsonGenerator json = new JsonFactory().createJsonGenerator(resp.getWriter());    try {      json.writeStartObject();      json.writeNumberField(ELAPSED_TIME, view.getElapsedTime());      json.writeNumberField(PERCENT_COMPLETE, view.getPercentComplete());      json.writeArrayFieldStart(PHASES);      ...      }      json.writeEndArray();      json.writeEndObject();    } finally {      IOUtils.cleanup(LOG, json);    }  }

下面是通过NameNode的Web界面查看的StartupProgress启动进度信息图,可以与之前提到的Phase和Step相对照,完全是吻合的.

这里写图片描述
这里写图片描述

以上就是本文所要阐述的关于HDFS Startup方面的相关内容,希望能给大家带来收获.

0 0