- 一、问题描述
- 二、问题分析
- 代码分析
- 三、解决方案
- 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信息的 *** 作 ListcontainersStatuses = 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类下:
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)