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失败问题分析的主要内容,如果未能解决你的问题,请参考以下文章
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