深入HDFS——NameNode启动源码

引入

前面我们已经对HDFS有了很多了解,但是光说不练假把式,今天开启深入源码的纯享模式,先来看看NameNode启动流程,在代码层面,到底是如何实现的。

首先还是得从我们的前一篇提到过的NameNode类(org.apache.hadoop.hdfs.server.namenode.NameNode)开始,既然是看启动流程,那自然是先找类里面的main方法啦。

当我们启动NameNode的时候,它就会执行NameNode类的main方法,在main方法中会创建NameNode对象,代码如下:

public static void main(String argv[]) throws Exception {
    ... ...
    // createNameNode返回NameNode对象
    NameNode namenode = createNameNode(argv, null);
    ... ...
}

在main方法里面还有个小细节,代码如下:

if (namenode != null) {
  namenode.join();
}

这个会让线程阻塞在这儿。这也就是为什么我们敲 jps 命令的时候,能一直看到 namenode的原因。

从上面源码可以看到, createNameNode 这个方法会返回NameNode对象,那我们看看它是怎么做的,代码如下:

public static NameNode createNameNode(String argv[], Configuration conf) throws 
IOException {
  LOG.info("createNameNode " + Arrays.asList(argv));
  if (conf == null)
    conf = new HdfsConfiguration();
  ... ...
  switch (startOpt) {
    // 我们如果是通过 hdfs namenode -format 命令格式化的话,就会进入这里
    case FORMAT: {
      boolean aborted = format(conf, startOpt.getForceFormat(),
          startOpt.getInteractiveFormat());
      terminate(aborted ? 1 : 0);
      return null; // avoid javac warning
    }
    // 正常启动会直接到这里
    default: {
      DefaultMetricsSystem.initialize("NameNode");
      return new NameNode(conf);
    }
  }
}

可以看到在createNameNode方法中,实际上最终返回 new NameNode(conf) 对象,NameNode构建方法如下:

public NameNode(Configuration conf) throws IOException {
    //这里第二个参数为 "NameNode"
    // NamenodeRole 枚举类里面记录着 NAMENODE  ("NameNode")
    this(conf, NamenodeRole.NAMENODE);
}

protected NameNode(Configuration conf, NamenodeRole role) throws IOException { 
    this.conf = conf;
    this.role = role;
    setClientNamenodeAddress(conf);
    String nsId = getNameServiceId(conf);
    String namenodeId = HAUtil.getNameNodeId(conf, nsId);
    this.haEnabled = HAUtil.isHAEnabled(conf, nsId);
    state = createHAState(getStartupOption(conf));
    this.allowStaleStandbyReads = HAUtil.shouldAllowStandbyReads(conf);
    this.haContext = createHAContext();
    ... ...
    initializeGenericKeys(conf, nsId, namenodeId);
    //我们去分析源码的时候,这样的关键的方法我们一定要留意。
    initialize(conf);
    ... ...
    this.started.set(true);
  }

通过上面源码可以看到,NameNode的构造方法中,会执行 initialize(conf)方法,来进行NameNode启动流程。

启动过程

下面我们重点来看下这个启动流程,进入initialize方法,可以看到其源码的主要实现如下:

protected void initialize(Configuration conf) throws IOException {
    ... ...
    //判断是NameNode角色
    if (NamenodeRole.NAMENODE == role) {
      //1.启动 NameNode  httpserver ,用户可以通过http访问WebUI
      startHttpServer(conf);
    }
    //2.加载本地文件中的镜像文件和editslog到内存中
    loadNamesystem(conf);
    ... ... 
    //3.createRpcServer 创建 NameNodeRpc服务端
    rpcServer = createRpcServer(conf);
    ... ...
    //4.启动CommoneService 进行 NameNode资源检查和安全模式检查
    startCommonServices(conf);
    ... ... 
}

给大家简单总结一下,在initialize方法中NameNode启动时,主要经过的4个过程:

  1. 启动NameNode HttpServer (方便用户通过http访问HDFS WebUI)

  2. 加载本地元数据(Fsimage和Editslog)

  3. 创建NameNodeRpcServer并启动(ClientRPCServer和ServiceRPCServer)

  4. 启动公共的服务(资源检查和安全模式检查)

是不是看到很多熟悉的关键词?别激动,我们接着分别深入这4个过程看看。

1.启动HttpServer

首先是第1个过程,startHttpServer方法主要创建HttpServer ,这样用户就可以通过WebUI来访问NameNode。

startHttpServer代码如下:

private void startHttpServer(final Configuration conf) throws IOException {
  //getHttpServerBindAddress 中绑定了NameNode的IP和端口
  httpServer = new NameNodeHttpServer(conf, this, getHttpServerBindAddress(conf));
  //启动Http server
  httpServer.start();
  httpServer.setStartupProgress(startupProgress);
}

getHttpServerBindAddress(conf)中进行了NameNode节点IP和端口绑定,并返回InetSocketAddress对象,getHttpServerBindAddress(conf)源码如下:

protected InetSocketAddress getHttpServerBindAddress(Configuration conf) {
  //getHttpServerAddress 绑定NameNode IP及端口
  InetSocketAddress bindAddress = getHttpServerAddress(conf);

  // If DFS_NAMENODE_HTTP_BIND_HOST_KEY exists then it overrides the
  // host name portion of DFS_NAMENODE_HTTP_ADDRESS_KEY.
  //获取 NameNode host主机
  final String bindHost = conf.getTrimmed(DFS_NAMENODE_HTTP_BIND_HOST_KEY);
  if (bindHost != null && !bindHost.isEmpty()) {
    bindAddress = new InetSocketAddress(bindHost, bindAddress.getPort());
  }

  return bindAddress;
}

以上源码中getHttpServerAddress会绑定节点IP和端口。

接着往下看,httpServer.start()具体源码如下:

void  start() throws IOException {
    ... ...
    //Hadoop中封装了自己的Httpserver,形成自己的Httpserver2
    HttpServer2.Builder builder = DFSUtil.httpServerTemplateForNNAndJN(conf,
        httpAddr, httpsAddr, "hdfs",
        DFSConfigKeys.DFS_NAMENODE_KERBEROS_INTERNAL_SPNEGO_PRINCIPAL_KEY,
        DFSConfigKeys.DFS_NAMENODE_KEYTAB_FILE_KEY);
    ... ...
    //servlet越多,支持的功能就越多
    setupServlets(httpServer, conf);
    //启动 httpServer 服务,对外开放绑定的端口
    httpServer.start();
    ... ...
}

startHttpServer方法中的httpServer.start()方法中,HDFS进行了HttpServer2封装,Hadoop中使用了自己的Httpserver进行Kerberos认证,最后通过HttpServer2.Builder.build()方法创建了hdfs自己的httpserver并调用start方法进行启动。

2.加载元数据

再看第2个过程,loadNamesystem(conf) 中会加载本地Fsimage和Editslog,具体源码如下:

protected void loadNamesystem(Configuration conf) throws IOException {
  //从磁盘中加载editslog和fsimage
  this.namesystem = FSNamesystem.loadFromDisk(conf);
}

loadFromDisk源码如下:

static FSNamesystem loadFromDisk(Configuration conf) throws IOException {
    ... ...
    // 封装FSImage对象
    FSImage fsImage = new FSImage(conf,
        FSNamesystem.getNamespaceDirs(conf),
        FSNamesystem.getNamespaceEditsDirs(conf));
    // 创建 FSNamesystem 对象,并对该对象中fsimage 属性赋值fsimage
    FSNamesystem namesystem = new FSNamesystem(conf, fsImage, false);
    ... ...
    // HDFS这命名挺清晰的,这个代码看名字就知道去加载元数据的
    namesystem.loadFSImage(startOpt);
    .... ...
}

继续往里走,loadFSImage 源码如下:

private void loadFSImage(StartupOption startOpt) throws IOException {
    final FSImage fsImage = getFSImage();

    // format before starting up if requested
    // 参数配置里有格式化的要求,就会在这进行格式化元数据
    if (startOpt == StartupOption.FORMAT) {
      fsImage.format(this, fsImage.getStorage().determineClusterId());// reuse current id
      startOpt = StartupOption.REGULAR;
    }
    ... ...
    final boolean staleImage
                //fsimage + editLog = new FSimage
                //这里会合并元数据
          = fsImage.recoverTransitionRead(startOpt, this, recovery);
    // 打印日志
    LOG.info("Need to save fs image? " + needToSave
          + " (staleImage=" + staleImage + ", haEnabled=" + haEnabled
          + ", isRollingUpgrade=" + isRollingUpgrade() + ")");
    //(2)把合并出来的新的fsimage写到我们的磁盘上面。
    fsImage.saveNamespace(this);
    ... ...
    //(3)打开一个新的editLog开始写日志
    fsImage.openEditLogForWrite();
    ... ...
    imageLoadComplete();
}

可以从源码看到,当HDFS重启时,会将FsImage内容映射到内存中,然后再一条条执行Editslog文件中的操作,从而恢复到NameNode重启前的状态。

这也进一步验证了我们之前所说的内容。

3.创建NameNodeRpcServer并启动

接着是第3个过程,也就是我们很熟悉的RPC啦。

NameNodeRPCserver里面有两个主要的RPC服务:

  1. ClientRPCServer:hdfs的客户端(用户)去操作HDFS的方法
  2. ServiceRPCServer:服务之间互相进行的方法的调用(注册,心跳等)

创建NameNodeRpcServer的代码如下:

//3.createRpcServer 创建 NameNodeRpc服务端和客户端
rpcServer = createRpcServer(conf);

createRpcServer源码如下:

protected NameNodeRpcServer createRpcServer(Configuration conf) throws IOException {
  return new NameNodeRpcServer(conf, this);
}

可以看到, new NameNodeRpcServer(conf, this) 方法会创建并返回一个nameNodeRpcServer对象,而这个对象里面,又会创建前面我们提到的,Rpc服务端和客户端的RpcServer。

具体实现源码如下:

public NameNodeRpcServer(Configuration conf, NameNode nn) throws IOException {
    ... ...
    // 这个服务起来是用来监听DataNode发送过来的请求的
    serviceRpcServer = new RPC.Builder(conf)
        .setProtocol(
            org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolPB.class)
        .setInstance(clientNNPbService)
        .setBindAddress(bindHost)
        .setPort(serviceRpcAddr.getPort())
        .setNumHandlers(serviceHandlerCount)
        .setVerbose(false)
        .setSecretManager(namesystem.getDelegationTokenSecretManager())
        .build();
    ... ...
    // 这个服务是主要服务于客户端发送过来的请求的
    clientRpcServer = new RPC.Builder(conf)
        .setProtocol(
            org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolPB.class)
        .setInstance(clientNNPbService)
        .setBindAddress(bindHost)
        .setPort(rpcAddr.getPort())
        .setNumHandlers(handlerCount)
        .setVerbose(false)
        .setSecretManager(namesystem.getDelegationTokenSecretManager())
        .setAlignmentContext(stateIdContext)
        .build();
    ... ...
}

这里只是创建,NameNode serviceRpcServer和clientRpcServer,会在下一个过程启动。

4.启动公共的服务

经过前面3个过程后,就会开始启动一些公共的服务,比如上面刚刚提到的,NameNode的RPC服务,不过在启动前还会进行两个检查:

  1. 资源检查,检查是否有磁盘足够存储元数据
  2. 安全模式检查,检查是否可以退出安全模式。

下面我们来看看源码怎么实现的:

//4.启动CommoneService 进行 NameNode资源检查和安全模式检查
startCommonServices(conf);

startCommonServices方法实现如下:

private void startCommonServices(Configuration conf) throws IOException {
    ... ...
    // 启动服务 检测磁盘空间和安全模式
    namesystem.startCommonServices(conf, haContext);
    ... ...
    // RPC服务端启动起来了
    rpcServer.start();
    ... ...
}

以上代码中

  • startCommonServices(conf, haContext) 主要负责磁盘空间和安全模式检测;
  • rpcServer.start() 则主要进行NameNode serviceRpcServer和clientRpcServer的启动。

startCommonServices(conf, haContext)方法的具体源码如下:

void startCommonServices(Configuration conf, HAContext haContext) throws IOException {
    ... ...
    //nnResourceChecker 对象用于后续检查editslog 目录空间是否足够
    nnResourceChecker = new NameNodeResourceChecker(conf);
    //检查是否有足够磁盘空间存储数据
    checkAvailableResources();
    assert !blockManager.isPopulatingReplQueues();
    StartupProgress prog = NameNode.getStartupProgress();
    //开始进入安全模式
    prog.beginPhase(Phase.SAFEMODE);
    //获取所有可用的block
    long completeBlocksTotal = getCompleteBlocksTotal();
    //设置安全模式
    prog.setTotal(Phase.SAFEMODE, STEP_AWAITING_REPORTED_BLOCKS,completeBlocksTotal);
    //启动块服务并对DataNode 心跳超时进行判断
    blockManager.activate(conf, completeBlocksTotal);
    ... ...
}

以上代码中 nnResourceChecker = new NameNodeResourceChecker(conf); 中会设置磁盘空间最小阈值100M,然后执行 checkAvailableResources(); 方法进行检查节点磁盘空间是充足。

new NameNodeResourceChecker(conf) 源码如下:

public NameNodeResourceChecker(Configuration conf) throws IOException {
    ... ...
    // duReserved 默认为100M
    duReserved = conf.getLongBytes(DFSConfigKeys.DFS_NAMENODE_DU_RESERVED_KEY,
        DFSConfigKeys.DFS_NAMENODE_DU_RESERVED_DEFAULT);
    ... ...
}

checkAvailableResources()源码如下:

void checkAvailableResources() {
    ... ...
    //判断磁盘资源是否够用
    hasResourcesAvailable = nnResourceChecker.hasAvailableDiskSpace();
    ... ...
}

其中 hasAvailableDiskSpace() 方法实现如下:

public boolean hasAvailableDiskSpace() {
  return NameNodeResourcePolicy.areResourcesAvailable(volumes.values(),
      minimumRedundantVolumes);
}

这个方法如果返回true,表示至少有一个配置的磁盘空间满足使用。

方法中的 areResourcesAvailable 实现源码如下:

static boolean areResourcesAvailable(
    Collection resources,
    int minimumRedundantResources) {
    ... ...
    //检查资源是否充足
    for (CheckableNameNodeResource resource : resources) {
      if (!resource.isRequired()) {
        redundantResourceCount++;
        // isResourceAvailable 实现类为 NameNodeResourceChecker.CheckedVolume中的isResourceAvailable 方法
        if (!resource.isResourceAvailable()) {
          disabledRedundantResourceCount++;
        }
      } else {
        requiredResourceCount++;
        if (!resource.isResourceAvailable()) {
          // Short circuit - a required resource is not available.
          return false;
        }
      }
    }
    ... ...
}

其中resource.isResourceAvailable()中判断磁盘是否满足最低的100M,返回true表示满足,返回false表示不满足。

具体判断源码如下:

public boolean isResourceAvailable() {
... ...
//如果磁盘空间小于100M 返回fasle
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;
}
... ...
}

检测完磁盘可用空间后,进入安全模式,并进行可用block的检测,进而判断是否退出NameNode安全模式,具体源码在FSNmaesystem.startCommonServices中,如下:

... ...
//开始进入安全模式
prog.beginPhase(Phase.SAFEMODE);
//获取所有可用的block
long completeBlocksTotal = getCompleteBlocksTotal();
//设置安全模式
prog.setTotal(Phase.SAFEMODE, STEP_AWAITING_REPORTED_BLOCKS,
    completeBlocksTotal);
//检测DataNode状态及是否退出安全模式
blockManager.activate(conf, completeBlocksTotal);
... ...

以上代码中blockManager.activate(conf, completeBlocksTotal)会进行block块检测,查看正常可用block数是否满足总block的99.9% 可用。

active(conf,completeBlocksTotal)具体源码如下:

public void activate(Configuration conf, long blockTotal) {
    ... ...
    //datanodeManager对象对周期检查DataNode连接情况
    datanodeManager.activate(conf);

    ... ...
    //检测 正常 block 情况
    bmSafeMode.activate(blockTotal);
    ... ...
}

datanodeManager.activate(conf)主要进行DataNode节点是否宕机,默认经过10分钟+30s一个DataNode没有向NameNode汇报心跳信息,则认为该DataNode宕机。(还记得核心设计里面提到的计算公式吗?)

datanodeManager.activate(conf)实现源码如下:

void activate(final Configuration conf) {
  datanodeAdminManager.activate(conf);
  //与DataNode心跳检测
  heartbeatManager.activate();
}

heartbeatManager.activate()方法最终调用到Monitor线程的run方法进行DataNode状态监测。

bmSafeMode.activate(blockTotal)进行是否退出安全模式检查,实现源码如下:

void activate(long total) {
... ...
//设置正常可用block,并设置正常退出安全模式阈值为0.999f
setBlockTotal(total);
if (areThresholdsMet()) {//判断是否可以退出安全模式,block和datanode阈值都满足退出
  boolean exitResult = leaveSafeMode(false);
  Preconditions.checkState(exitResult, "Failed to leave safe mode.");
} else {//进入安全模式
  // enter safe mode
  status = BMSafeModeStatus.PENDING_THRESHOLD;
  initializeReplQueuesIfNecessary();
  reportStatus("STATE* Safe mode ON.", true);
  lastStatusReport = monotonicNow();
}
... ...
}

以上代码中:

  • setBlockTotal(total) 设置正常可用block的阈值
  • areThresholdsMet() 进行可用block是否满足阈值

areThresholdsMet()实现如下:

private boolean areThresholdsMet() {
//如果block和datanode阈值都满足,则为True,否则返回false
... ...
synchronized (this) {
  boolean isBlockThresholdMet = (blockSafe ]]>= blockThreshold);
  boolean isDatanodeThresholdMet = true;
  if (isBlockThresholdMet && datanodeThreshold ]]> 0) {
    int datanodeNum = blockManager.getDatanodeManager().
            getNumLiveDataNodes();
    isDatanodeThresholdMet = (datanodeNum ]]>= datanodeThreshold);
  }
  return isBlockThresholdMet && isDatanodeThresholdMet;
}

总结

今天通过一步步梳理NameNode启动的源码细节,进一步深入理解NameNode的设计与实现思路,同时还串联了前面我们提到的知识点。

感兴趣的小伙伴可以跟着文章的思路,再捋一遍源码,会更有收获!

你可能感兴趣的:(大数据基础,#,深入HDFS,hdfs,hadoop,大数据)