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

Posted 疯狂哈丘

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Yarn LevelDb文件过大导致重启NM失败问题分析相关的知识,希望对你有一定的参考价值。

一、问题描述

近期滚动重启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<ContainerStatus> containersStatuses = getContainerStatuses();
  ResourceUtilization containersUtilization = getContainersUtilization();
  ResourceUtilization nodeUtilization = getNodeUtilization();
  List<org.apache.hadoop.yarn.api.records.Container> increasedContainers
      = getIncreasedContainers();
  NodeStatus nodeStatus =
      NodeStatus.newInstance(nodeId, responseId, containersStatuses,
        createKeepAliveApplicationList(), nodeHealthStatus,
        containersUtilization, nodeUtilization, increasedContainers);
 
  return nodeStatus;

 
 
@VisibleForTesting
protected List<ContainerStatus> getContainerStatuses() throws IOException 
  List<ContainerStatus> containerStatuses = new ArrayList<ContainerStatus>();
  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类下:

以上是关于Yarn LevelDb文件过大导致重启NM失败问题分析的主要内容,如果未能解决你的问题,请参考以下文章

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

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

Oracle 监听器日志文件过大导致监听异常报ORA-12514 TNS 错误

Failed to start NodeManager caused by "/var/lib/hadoop-yarn/yarn-nm-recovery/yarn-nm-state/LOCK

对Hadoop2.7.2文档的学习-Yarn部分RM Restart/RM HA/Timeline Server/NM Restart

YARN 重启失败原因之一