Yarn LevelDb文件过大导致重启NM失败问题分析

Yarn LevelDb文件过大导致重启NM失败问题分析,第1张

Yarn LevelDb文件过大导致重启NM失败问题分析

文章目录
  • 一、问题描述
  • 二、问题分析
    • 代码分析
  • 三、解决方案
    • 1、定期重启NM
    • 2、修改源码

一、问题描述

近期滚动重启Yarn NodeMagager时(hadoop版本2.8.3),发现滚动重启NM会卡很久,然后滚动重启失败(测试了好几台,基本都滚动重启失败)

深入排查后,发现失败的原因如下:

NM在启动的时候会去加载yarn-nm-recovery下面的leveldb数据库,主要是为了恢复机器上正在运行的container的相关信息。我们发现,重启失败的NM在启动的时候一直卡在读取leveldb数据库中,之后MRS的进程健康检查脚本发现NM启动超过900s都未启动成功,就将正在启动的NM进程kill了。最终导致我们看到的滚动重启 *** 作失败。

临时解决方法:删除yarn-nm-recovery下的所有文件,NM启动的时候会自动重建leveldb数据库(代价是该机器上container都会运行失败)

二、问题分析

很明显,问题的根因在于NM读取leveldb数据库过久,后面我们看了下NM下的leveldb数据库大小,多达3.3G。正常情况下,NM使用leveldb主要存储一些正在运行的container以及application的相关信息,这些信息的量加起来一般最多就几十M。

因此我们分析了下某台NM下的leveldb,发现里面有些container的信息还是2021年4月份的。正常来说,NM对于已经运行完的container,是会从leveldb删除的,避免leveldb的大小越来越大。所以我们怀疑是yarn的代码bug,导致一些本该被删的container信息还遗留在leveldb上。

后面花一天时间跟了下Yarn StateStore的相关代码,发现在删除已完成的container信息时,确实有一些问题。

代码分析

NM在启动后,会有个 Node Status Updater 线程,这个线程主要用来定时向ResourceManager发送心跳,以及更新自身的一些状态。在这个过程中,还会进行已完成container的删除工作。

//NodeStatusUpdaterImpl.java
protected void startStatusUpdater() {
 
  statusUpdaterRunnable = new Runnable() {
    @Override
    @SuppressWarnings("unchecked")
    public void run() {
      int lastHeartbeatID = 0;
      while (!isStopped) {
        // Send heartbeat
        try {
          ...
          //getNodeStatus 会更新Node的相关状态,里面包括更新container的信息
          NodeStatus nodeStatus = getNodeStatus(lastHeartbeatID);
          ...
 
          //从context中拿到那些已完成的container,从leveldb中删除
          removeOrTrackCompletedContainersFromContext(response
                .getContainersToBeRemovedFromNM());
          ...
        } catch (ConnectException e) {
          ...
        } finally {
          synchronized (heartbeatMonitor) {
           ...
          }
        }
      }
    }
 
    private void updateMasterKeys(NodeHeartbeatResponse response) {
      ...
    }
  };
  statusUpdater =
      new Thread(statusUpdaterRunnable, "Node Status Updater");
  statusUpdater.start();
}
 
 
@VisibleForTesting
protected NodeStatus getNodeStatus(int responseId) throws IOException {
 
  NodeHealthStatus nodeHealthStatus = this.context.getNodeHealthStatus();
  nodeHealthStatus.setHealthReport(healthChecker.getHealthReport());
  nodeHealthStatus.setIsNodeHealthy(healthChecker.isHealthy());
  nodeHealthStatus.setLastHealthReportTime(healthChecker
    .getLastHealthReportTime());
  if (LOG.isDebugEnabled()) {
    LOG.debug("Node's health-status : " + nodeHealthStatus.getIsNodeHealthy()
        + ", " + nodeHealthStatus.getHealthReport());
  }
  //获取container相关信息,里面包括更新container信息的 *** 作
  List containersStatuses = getContainerStatuses();
  ResourceUtilization containersUtilization = getContainersUtilization();
  ResourceUtilization nodeUtilization = getNodeUtilization();
  List increasedContainers
      = getIncreasedContainers();
  NodeStatus nodeStatus =
      NodeStatus.newInstance(nodeId, responseId, containersStatuses,
        createKeepAliveApplicationList(), nodeHealthStatus,
        containersUtilization, nodeUtilization, increasedContainers);
 
  return nodeStatus;
}
 
 
@VisibleForTesting
protected List getContainerStatuses() throws IOException {
  List containerStatuses = new ArrayList();
  for (Container container : this.context.getContainers().values()) {
    ContainerId containerId = container.getContainerId();
    ApplicationId applicationId = containerId.getApplicationAttemptId()
        .getApplicationId();
    org.apache.hadoop.yarn.api.records.ContainerStatus containerStatus =
        container.cloneAndGetContainerStatus();
    if (containerStatus.getState() == ContainerState.COMPLETE) {
      if (isApplicationStopped(applicationId)) {
        if (LOG.isDebugEnabled()) {
          LOG.debug(applicationId + " is completing, " + " remove "
              + containerId + " from NM context.");
        }
        //从context中移除已经完成的container信息
        context.getContainers().remove(containerId);
        pendingCompletedContainers.put(containerId, containerStatus);
      } else {
        if (!isContainerRecentlyStopped(containerId)) {
          pendingCompletedContainers.put(containerId, containerStatus);
        }
      }
      //从leveldb删除这个container的相关信息
      addCompletedContainer(containerId);
    } else {
      containerStatuses.add(containerStatus);
    }
  }
  containerStatuses.addAll(pendingCompletedContainers.values());
  if (LOG.isDebugEnabled()) {
    LOG.debug("Sending out " + containerStatuses.size()
        + " container statuses: " + containerStatuses);
  }
  return containerStatuses;
}

总结下上面的代码:

  • NM中有个Context的实例,用于存储当前NM的各种信息,其中就包括正在运行的container信息
  • NM发送心跳前,会先执行****getNodeStatus方法,这个方法里面会检查已经结束的container,将其从context中移除,同时也从leveldb中删除相应记录
  • 后面发送心跳给RM后,又执行removeOrTrackCompletedContainersFromContext方法,这里又会重新检查context中已经执行结束的container,然后从context中remove掉(此处不会删除leveldb的记录)

这里就有一个问题,假设在执行getNodeStatus时,某个container还未执行完,而在NM发送心跳给RM后,再检查发现这个container结束了,因此从context中删去该container。这样,就造成了context中已经没有该container的记录,而leveldb中还有该记录的问题。这样的情况会随着NM服务的运行时间越来越多,最终导致NM下的leveldb量变的非常大。

三、解决方案 1、定期重启NM

其实NM在启动时,读取leveldb的过程中也会检查哪些container已经执行完了,然后从leveldb删除(NMLeveldbStateStoreService#loadContainersState()方法中)。因此,如果定期滚动重启NM,也可以避免leveldb大小不断增大到无法正常重启的问题。

也就是说,不能隔了太久才去滚动重启NM,不然就像我们这样(隔了半年才重启),在启动的时候由于leveldb数据过大无法正常启动。

该方法治标不治本

2、修改源码

其实问题在于removeOrTrackCompletedContainersFromContext中,只会context删除了container信息,而没考虑到leveldb里面的数据。因此,最简单的办法就是在removeOrTrackCompletedContainersFromContext中将完成的container信息也从leveldb中删除。

NodeStatusUpdaterImpl类下:

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5619525.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-15
下一篇 2022-12-15

发表评论

登录后才能评论

评论列表(0条)

保存