在YARN(Yet Another Resource Negotiator)架构中,内存泄漏是指应用程序或系统组件持续占用内存却未能正确释放的现象。这种问题在长期运行的大规模集群中尤为常见,会导致资源逐渐耗尽,最终引发容器(Container)崩溃或节点(NodeManager)失效。理解其成因、表现和影响,是后续进行堆转储分析和GC日志定位的基础。
YARN作为Hadoop的资源调度核心,通过容器化机制分配内存和CPU资源。每个容器运行一个独立的任务(如MapReduce任务或Spark Executor),其内存管理依赖JVM的垃圾回收机制。当应用程序存在对象引用未释放、缓存未清理或线程池未关闭等问题时,即使任务完成,占用的堆内存也无法被回收。这种“隐形资源占用”会逐渐累积,表现为:
java.lang.OutOfMemoryError
,尤其是堆内存不足错误;根据实际案例和社区反馈,YARN内存泄漏通常源于以下场景:
HashMap
)持续添加数据而未清理,常见于缓存或状态跟踪场景。yarn.nodemanager.resource.memory-mb
设置过高,导致物理内存不足时触发交换(Swap),加剧GC压力。-XX:+HeapDumpOnOutOfMemoryError
等参数,导致OOM时无法自动生成堆转储文件。内存泄漏的负面影响会从单容器扩散至整个集群:
reduceByKey
操作中未合并的中间数据堆积,多次触发容器崩溃,最终任务超时失败。通过监控指标可早期发现泄漏迹象:
yarn logs -applicationId
查看日志)。这一现象为后续章节的堆转储分析和GC日志定位提供了明确的问题边界。例如,当发现老年代内存持续增长时,需结合MAT(Memory Analyzer Tool)分析对象留存路径;而频繁Full GC则需检查垃圾回收器的配置是否匹配应用特性。
当YARN Container出现内存泄漏时,堆内存转储(Heap Dump)分析是最直接的诊断手段。以下是完整的分析流程和技术要点:
yarn container -signal <container_id> DUMP_HEAP
生成的dump文件默认保存在NodeManager的logs/userlogs/<application_id>/<container_id>
目录下,文件名格式为heap-dump-<pid>-<timestamp>.hprof
。
container-executor.cfg
中配置JVM参数,使OOM时自动生成dump: <property>
<name>yarn.nodemanager.container-monitor.java.opts</name>
<value>-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dumps</value>
</property>
jmap -dump:format=b,file=/tmp/container_heap.hprof <pid>
Leak Suspects Report
:自动识别可疑对象Histogram
:按类统计对象数量及内存占用Path to GC Roots
:追踪对象引用链Eclipse MAT界面展示
Dominator Tree
视图定位内存瓶颈: // 示例:发现某个CacheManager持有80%内存
org.apache.hadoop.yarn.server.resourcemanager.CacheManager @ 0x7fe4a8d8
|- 1,234,567 bytes (82.3%) by 450 instances
|- com.google.common.cache.LocalCache$Segment @ 0x7fe4b1a0
Path to GC Roots
(排除软/弱引用),典型泄漏模式:static HashMap
)mapreduce.jobhistory
配置)ByteBuffer.allocateDirect()
调用栈sun.misc.Unsafe
分配记录jmap -heap
与OS进程内存的差值org.apache.hadoop.io.Writable
对象占75%内存Comparator
将中间结果缓存到静态TreeMapCompare Basket
功能对比不同时间点的dump: # 生成基线dump
jmap -dump:format=b,file=heap1.hprof <pid>
# 执行可疑操作后生成对比dump
jmap -dump:format=b,file=heap2.hprof <pid>
SELECT * FROM java.util.HashMap WHERE size > 1000
Scheduler Load Simulator
复现泄漏场景: <property>
<name>yarn.resourcemanager.scheduler.monitor.enable</name>
<value>true</value>
</property>
LeakHunter
插件加速分析-keep_unreachable_objects
参数yarn-site.xml
中添加内存监控: <property>
<name>yarn.nodemanager.container-monitor.interval-ms</name>
<value>3000</value>
</property>
在YARN环境中,有效获取GC日志是排查内存泄漏的首要步骤。对于NodeManager和ApplicationMaster等关键组件,需要在JVM启动参数中添加以下关键配置:
-Xloggc:/var/log/hadoop-yarn/containers/application_xxx/container_xxx/gc.log
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-XX:+PrintTenuringDistribution
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=5
-XX:GCLogFileSize=10M
其中PrintTenuringDistribution
参数特别重要,它能显示对象在Survivor区的年龄分布,这对识别"过早晋升"现象(即对象未经过足够次数的Young GC就被提升到老年代)具有决定性作用。对于使用G1垃圾收集器的场景,建议额外添加-XX:+PrintAdaptiveSizePolicy
参数以获取堆自适应调整信息。
在生产环境中,这些日志通常位于YARN容器的工作目录下,路径格式为/yarn/nm/usercache/[user]/appcache/application_[appid]/container_[containerid]/
。通过YARN CLI命令yarn logs -applicationId <appid>
可以集中获取所有容器的日志文件。
当出现内存泄漏时,GC日志会呈现以下特征模式:
1. 老年代占用持续增长:每次Full GC后,PSOldGen
或G1 Old Generation
的回收量显著小于回收前占用空间,例如:
[Full GC (Ergonomics) [PSYoungGen: 8192K->0K(9216K)] [ParOldGen: 31744K->31568K(32768K)] 39936K->31568K(41984K)
上述日志显示老年代仅释放了176KB内存,说明大部分对象无法被回收。
2. Full GC频率加速:初期可能每小时1-2次Full GC,随着泄漏加剧逐渐变为每分钟数次,此时FGC
计数会呈现非线性增长。
3. GC效率下降:通过jstat -gcutil
观察到的FGCT
(Full GC总耗时)与GCT
(GC总耗时)比值持续上升,典型内存泄漏场景下该比值可能超过70%。
健康的应用应保持对象主要在年轻代回收。若日志中出现以下情况则需警惕:
[GC (Allocation Failure)
[PSYoungGen: 1572864K->1572800K(1572864K)]
1572864K->1572800K(3145728K), 0.123456 secs]
这种几乎无效的Young GC表明Eden区对象直接晋升老年代,通常由以下原因导致:
-XX:SurvivorRatio
调整)对于JDK8+环境,需特别关注Metaspace的使用情况:
Metaspace used 34567K, capacity 45678K, committed 56789K, reserved 67890K
如果used
接近committed
且持续增长,可能存在类加载器泄漏,这在动态生成类的框架(如Spark SQL)中较为常见。
原始GC日志可借助GCViewer等工具进行可视化分析,重点关注:
Total heap
和Young heap
的差值计算对象晋升速率在怀疑存在泄漏时,使用以下命令进行实时监控:
jstat -gc <pid> 5s 10 # 每5秒采样一次,共10次
输出中的OU
(老年代使用量)和MU
(元空间使用量)列应重点关注。若连续采样中OU
的增量超过Young GC触发阈值(通常为Eden区的80%),则存在对象过早晋升。
GC日志中的触发原因字段包含重要线索:
System.gc()
:非必要的显式GC调用Metadata GC Threshold
:元空间扩容触发Ergonomics
:基于JVM启发式策略触发Allocation Failure
:分配失败(最常见)对于YARN容器,频繁出现Allocation Failure
且伴随Heap Dump
的GC事件往往指向内存泄漏。
当出现Container killed by YARN for exceeding memory limits
错误时,需对比GC日志中的MaxHeapFreeRatio
(默认70%):
YARN的ResourceManager
与NodeManager
通过心跳传递内存使用数据。当GC日志显示堆使用正常但YARN报告OOM时,可能原因是:
-XX:MaxDirectMemorySize
)fs.local.cache.size
)jstat -gcnewcapacity
计算对象晋升到老年代的速率-XX:+HeapDumpOnOutOfMemoryError
在临界状态保存快照通过以上多维度的GC日志分析,可以准确定位内存泄漏的源头,为后续的堆转储分析提供明确方向。在典型YARN场景中,约60%的内存泄漏问题可通过GC日志分析直接定位到具体组件或代码段。
在YARN集群运行过程中,内存泄漏问题往往表现为资源管理器(ResourceManager)或节点管理器(NodeManager)的内存使用量随时间持续增长,最终导致服务崩溃或频繁重启。通过大量实践案例和源码分析,我们总结了以下几种典型的内存泄漏模式及其解决方案。
这是YARN中最常见的内存泄漏场景之一,典型案例是AsyncDispatcher组件中的BlockingQueue事件队列无限增长。当事件生产者(如Container状态上报)速度远高于消费者(如TimelineServer数据写入)时,未处理的事件会堆积在内存中。某生产环境曾出现eventQueue尺寸突破千万级的极端情况,直接导致ResourceManager内存耗尽。
优化方案包括:
yarn.system-metrics-publisher.dispatcher.pool-size
参数增加事件处理线程数(默认10个),建议根据集群规模线性调整yarn.timeline-service.enabled=false
彻底关闭时间线服务在YARN 2.9.1版本中曾出现RMNodeImpl对象中的completedContainers集合持续增长的问题。每个RMNodeImpl对象可能包含超过14万个已完成容器记录,单个对象内存占用达14MB。其根本原因是AM(ApplicationMaster)未及时拉取容器完成状态,导致containersToBeRemovedFromNM
清理机制失效。
解决方案需要双管齐下:
yarn.resourcemanager.max-completed-applications
限制历史记录保留数量在动态加载组件的场景中,未正确移除的事件监听器会导致内存中保留大量无用对象引用。这类问题在自定义服务中尤为常见,表现为Old Gen区域持续增长却找不到明显的大对象。
预防措施包括:
public class EventComponent implements Closeable {
private List<EventHandler> handlers = new ArrayList<>();
public void register(EventHandler handler) {
dispatcher.register(handler);
handlers.add(handler);
}
@Override
public void close() {
handlers.forEach(dispatcher::unregister);
}
}
YARN的容器启动过程中,镜像缓存、依赖库缓存等如未设置合理的淘汰策略,会导致缓存无限增长。特别是当使用第三方资源本地化方案时,可能产生GB级的内存泄漏。
优化建议实施三级缓存策略:
yarn.nodemanager.localizer.cache.cleanup.interval-ms
定期清理yarn.nodemanager.localizer.cache.target-size-mb
控制磁盘占用 <!-- 推荐G1GC配置 -->
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=45
-XX:G1ReservePercent=20
sun.misc.SharedSecrets.getJavaNioAccess().getDirectBufferPool().getMemoryUsed()
yarn.nodemanager.vmem-pmem-ratio
防止虚拟内存溢出 Runtime.getRuntime().addShutdownHook(new Thread(() -> {
containerManager.cleanupContainers();
}));
对于已部署的生产系统,建议建立三级防御体系:实时监控(Prometheus+Grafana)、定期堆分析(MAT工具自动化巡检)、关键操作前后内存快照对比。某电商平台通过这套体系将内存泄漏导致的故障率降低了82%。
某电商平台大数据集群在促销活动期间频繁出现NodeManager节点宕机,监控系统显示多个Container内存使用量持续增长直至超出YARN分配的内存上限。通过YARN ResourceManager UI观察到以下异常特征:
运维团队首先执行基础检查:
# 检查YARN配置
yarn.nodemanager.resource.memory-mb=64GB
yarn.scheduler.maximum-allocation-mb=8GB
mapreduce.map.memory.mb=2048
mapreduce.reduce.memory.mb=4096
# 查看异常Container的GC情况
yarn logs -applicationId application_123456789_0001 | grep GC
发现Full GC频率从每小时1次逐渐增加到每分钟3次,且每次GC后老年代内存释放不足30%。
使用YARN命令获取问题Container的堆转储文件:
# 获取运行中Container的堆dump
yarn container -signal <container_id> QUIT
# 或通过jmap直接获取(需NodeManager白名单)
jmap -dump:live,format=b,file=/tmp/container_heap.hprof <pid>
使用Eclipse Memory Analyzer(MAT)分析堆转储文件时,发现三个关键现象:
org.apache.hadoop.mapred.MapTask
实例持有45%的堆内存byte[]
数组存在超过200万个实例java.util.concurrent.ConcurrentHashMap$Node
存在异常增长进一步检查发现业务代码中存在未关闭的缓存机制:
// 问题代码片段
public class DataProcessor {
private static final Map<String, byte[]> CACHE = new ConcurrentHashMap<>();
public void process(Record record) {
byte[] compressed = compress(record.data()); // 压缩后的数据约50KB
CACHE.put(record.key(), compressed); // 未设置缓存淘汰策略
}
}
该静态Map持续累积压缩数据,且由于YARN Container长期运行(处理千万级记录),最终导致堆内存耗尽。
通过配置JVM参数获取详细GC日志:
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintTenuringDistribution
-XX:+HeapDumpOnOutOfMemoryError
关键GC事件序列:
2024-05-20T14:25:03.123+0800: [Full GC (Allocation Failure)
[PSYoungGen: 1024K->0K(1536K)]
[ParOldGen: 3072K->4095K(4096K)] 4096K->4095K(5632K),
[Metaspace: 256K->256K(1056768K)], 0.123456 secs]
[Times: user=0.23 sys=0.01, real=0.12 secs]
异常模式识别:
结合GC日志与堆转储分析,确认问题本质为:
实施分阶段修复方案:
第一阶段:紧急缓解
// 引入LRU缓存机制
public class DataProcessor {
private static final int MAX_CACHE_SIZE = 1000;
private static final Map<String, byte[]> CACHE =
Collections.synchronizedMap(new LinkedHashMap<String, byte[]>() {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_CACHE_SIZE;
}
});
}
第二阶段:长期优化
<property>
<name>yarn.nodemanager.container-monitor.threshold</name>
<value>0.8</value> <!-- 内存使用超80%触发告警 -->
</property>
-XX:MaxDirectMemorySize=512m
-XX:SoftRefLRUPolicyMSPerMB=50
验证效果:
该案例揭示的深层问题包括:
yarn.nodemanager.pmem-check-enabled=false
通过arthas工具进一步发现的隐藏问题:
# 监控JVM内存分布
memory -d 60 -n 5
输出显示Native Memory持续增长,定位到JNI调用的本地库存在未释放的分配。
[1] : https://blog.51cto.com/u_16213325/10573647
[2] : https://www.jianshu.com/p/f6801cfce1bd
[3] : https://www.freesion.com/article/67101601224/
[4] : https://blog.csdn.net/qq_26442553/article/details/146458331