揭露 FileSystem 引起的线上 JVM 内存溢出问题

作者:来自 vivo 互联网大数据团队-Ye Jidong

本文主要介绍了由FileSystem类引起的一次线上内存泄漏导致内存溢出的问题分析解决全过程。

内存泄漏定义(memory leak):一个不再被程序使用的对象或变量还在内存中占有存储空间,JVM不能正常回收改对象或者变量。一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。

内存溢出(out of memory):是指在程序运行过程中,由于分配的内存空间不足或使用不当等原因,导致程序无法继续执行的一种错误,此时就会报错OOM,即所谓的内存溢出。 

一、背景

周末小叶正在王者峡谷乱杀,手机突然收到大量机器CPU告警,CPU使用率超过80%就会告警,同时也收到该服务的Full GC告警。该服务是小叶项目组非常重要的服务,小叶赶紧放下手中的王者荣耀打开电脑查看问题。

图片

图片

图1.1 CPU告警   Full GC告警

二、问题发现

2.1 监控查看

因为服务CPU和Full GC告警了,打开服务监控查看CPU监控和Full GC监控,可以看到两个监控在同一时间点都有一个异常凸起,可以看到在CPU告警的时候,Full GC特别频繁,猜测可能是Full GC导致的CPU使用率上升告警。

图片

图2.1 CPU使用率

图片

图2.2 Full GC次数  

2.2 内存泄漏

从Full Gc频繁可以知道服务的内存回收肯定存在问题,故查看服务的堆内存、老年代内存、年轻代内存的监控,从老年代的常驻内存图可以看到,老年代的常驻内存越来越多,老年代对象无法回收,最后常驻内存全部被占满,可以看出明显的内存泄漏。

图片

图2.3 老年代内存

图片

图2.4 JVM内存

2.3 内存溢出

从线上的错误日志也可以明确知道服务最后是OOM了,所以问题的根本原因是内存泄漏导致内存溢出OOM,最后导致服务不可用

图片

图2.5 OOM日志    

三、问题排查

3.1 堆内存分析

在明确问题原因为内存泄漏之后,我们第一时间就是dump服务内存快照,将dump文件导入至MAT(Eclipse Memory Analyzer)进行分析。Leak Suspects 进入疑似泄露点视图。

图片

图3.1 内存对象分析

图片

图3.2 对象链路图

打开的dump文件如图3.1所示,2.3G的堆内存   其中 org.apache.hadoop.conf.Configuration对象占了1.8G,占了整个堆内存的78.63%

展开该对象的关联对象和路径,可以看到主要占用的对象为HashMap,该HashMap由FileSystem.Cache对象持有,再上层就是FileSystem。可以猜想内存泄漏大概率跟FileSystem有关。

3.2 源码分析

找到内存泄漏的对象,那么接下来一步就是找到内存泄漏的代码。

在图3.3我们的代码里面可以发现这么一段代码,在每次与hdfs交互时,都会与hdfs建立一次连接,并创建一个FileSystem对象。但在使用完FileSystem对象之后并未调用close()方法释放连接。

但是此处的Configuration实例和FileSystem实例都是局部变量,在该方法执行完成之后,这两个对象都应该是可以被JVM回收的,怎么会导致内存泄漏呢?

图片

图3.3

(1)猜想一:FileSystem是不是有常量对象?

接下里我们就查看FileSystem类的源码,FileSystem的init和get方法如下:

图片

图片

图片

图3.4

从图3.4最后一行代码可以看到,FileSystem类存在一个CACHE,通过disableCacheName控制是否从该缓存拿对象。该参数默认值为false。也就是默认情况下会通过CACHE对象返回FileSystem。

图片

图3.5

从图3.5可以看到CACHE为FileSystem类的静态对象,也就是说,该CACHE对象会一直存在不会被回收,确实存在常量对象CACHE,猜想一得到验证。

那接下来看一下CACHE.get方法:

图片

从这段代码中可以看出:

  1. 在Cache类内部维护了一个Map,该Map用于缓存已经连接好的FileSystem对象,Map的Key为Cache.Key对象。每次都会通过Cache.Key获取FileSystem,如果未获取到,才会继续创建的流程。

  2. 在Cache类内部维护了一个Set(toAutoClose),该Set用于存放需自动关闭的连接。在客户端关闭时会自动关闭该集合中的连接。

  3. 每次创建的FileSystem都会以Cache.Key为key,FileSystem为Value存储在Cache类中的Map中。那至于在缓存时候是否对于相同hdfs URI是否会存在多次缓存,就需要查看一下Cache.Key的hashCode方法了。

Cache.Key的hashCode方法如下:

图片

schema和authority变量为String类型,如果在相同的URI情况下,其hashCode是一致。而unique该参数的值每次都是0。那么Cache.Key的hashCode就由ugi.hashCode()决定。

由以上代码分析可以梳理得到:

  1. 业务代码与hdfs交互过程中,每次交互都会新建一个FileSystem连接,结束时并未关闭FileSystem连接。

  2. FileSystem内置了一个static的Cache,该Cache内部有一个Map,用于缓存已经创建连接的FileSystem。

  3. 参数fs.hdfs.impl.disable.cache,用于控制FileSystem是否需要缓存,默认情况下是false,即缓存。

  4. Cache中的Map,Key为Cache.Key类,该类通过schem,authority,ugi,unique 4个参数来确定一个Key,如上Cache.Key的hashCode方法。

(2)猜想二:FileSystem同样hdfs URI是不是多次缓存?

FileSystem.Cache.Key构造函数如下所示:ugi由UserGroupInformation的getCurrentUser()决定。

图片

继续看UserGroupInformation的getCurrentUser()方法,如下:

图片

其中比较关键的就是是否能通过AccessControlContext获取到Subject对象。在本例中通过get(final URI uri, final Configuration conf,final String user)获取时候,在debug调试时,发现此处每次都能获取到一个新的Subject对象。也就是说相同的hdfs路径每次都会缓存一个FileSystem对象

猜想二得到验证:同一个hdfs URI会进行多次缓存,导致缓存快速膨胀,并且缓存没有设置过期时间和淘汰策略,最终导致内存溢出。

(3)FileSystem为什么会重复缓存?

那为什么会每次都获取到一个新的Subject对象呢,我们接着往下看一下获取AccessControlContext的代码,如下:

图片

其中比较关键的是getStackAccessControlContext方法,该方法调用了Native方法,如下:

图片

该方法会返回当前堆栈的保护域权限的AccessControlContext对象。

我们通过图3.6 get(final URI uri, final Configuration conf,final String user) 方法可以看到,如下:

  • 先通过UserGroupInformation.getBestUGI方法获取了一个UserGroupInformation对象。

  • 然后在通过UserGroupInformation的doAs方法去调用了get(URI uri, Configuration conf)方法

  • 图3.7 UserGroupInformation.getBestUGI方法的实现,此处关注一下传入的两个参数ticketCachePath,user。ticketCachePath是获取配置hadoop.security.kerberos.ticket.cache.path的值,在本例中该参数未配置,因此ticketCachePath为空。user参数是本例中传入的用户名。

  • ticketCachePath为空,user不为空,因此最终会执行图3.7的createRemoteUser方法

图片

图3.6

图片

图3.7

图片

图3.8

从图3.8标红的代码可以看到在createRemoteUser方法中,创建了一个新的Subject对象,并通过该对象创建了UserGroupInformation对象。至此,UserGroupInformation.getBestUGI方法执行完成。

接下来看一下UserGroupInformation.doAs方法(FileSystem.get(final URI uri, final Configuration conf, final String user)执行的最后一个方法),如下:

图片

然后在调用Subject.doAs方法,如下:

图片

最后在调用AccessController.doPrivileged方法,如下:

图片

该方法为Native方法,该方法会使用指定的AccessControlContext来执行PrivilegedExceptionAction,也就是调用该实现的run方法。即FileSystem.get(uri, conf)方法。

至此,就能够解释在本例中,通过get(final URI uri, final Configuration conf,final String user) 方法创建FileSystem时,每次存入FileSystem的Cache中的Cache.key的hashCode都不一致的情况了。

小结一下:

  1. 在通过get(final URI uri, final Configuration conf,final String user)方法创建FileSystem时,由于每次都会创建新的UserGroupInformationSubject对象。

  2.  在Cache.Key对象计算hashCode时,影响计算结果的是调用了UserGroupInformation.hashCode方法。

  3. UserGroupInformation.hashCode方法,计算为:System.identityHashCode(subject)。即如果Subject是同一个对象则返回相同的hashCode,由于在本例中每次都不一样,因此计算的hashCode不一致。

  4. 综上,就导致每次计算Cache.key的hashCode不一致,便会重复写入FileSystem的Cache。

(4)FileSystem的正确用法

从上述分析,既然FileSystem.Cache都没有起到应起的作用,那为什么要设计这个Cache呢。其实只是我们的用法没用对而已。

在FileSystem中,有两个重载的get方法:

public static FileSystem get(final URI uri, final Configuration conf, final String user)
public static FileSystem get(URI uri, Configuration conf)

图片

我们可以看到 FileSystem get(final URI uri, final Configuration conf, final String user)方法最后是调用FileSystem get(URI uri, Configuration conf)方法的,区别在于FileSystem get(URI uri, Configuration conf)方法于缺少也就是缺少每次新建Subject的的操作。

图片

图3.9

没有新建Subject的的操作,那么图3.9 中Subject为null,会走最后的getLoginUser方法获取loginUser。而loginUser是静态变量,所以一旦该loginUser对象初始化成功,那么后续会一直使用该对象。UserGroupInformation.hashCode方法将会返回一样的hashCode值。也就是能成功的使用到缓存在FileSystem的Cache。

图片

图片

图3.10

四、解决方案

经过前面的介绍,如果要解决FileSystem 存在的内存泄露问题,我们有以下两种方式:

(1)使用public static FileSystem get(URI uri, Configuration conf):

  • 该方法是能够使用到FileSystem的Cache的,也就是说对于同一个hdfs URI是只会有一个FileSystem连接对象的。

  • 通过System.setProperty("HADOOP_USER_NAME", "hive")方式设置访问用户。

  • 默认情况下fs.automatic.close=true,即所有的连接都会通过ShutdownHook关闭。

(2)使用public static FileSystem get(final URI uri, final Configuration conf, final String user):

  • 该方法如上分析,会导致FileSystem的Cache失效,且每次都会添加至Cache的Map中,导致不能被回收。

  • 在使用时,一种方案是:保证对于同一个hdfs URI只会存在一个FileSystem连接对象。

  • 另一种方案是:在每次使用完FileSystem之后,调用close方法,该方法会将Cache中的FileSystem删除。

图片

图片

图片

基于我们已有的历史代码最小改动的前提下,我们选择了第二种修改方式。在我们每次使用完FileSystem之后都关闭FileSystem对象。

五、优化结果

对代码进行修复发布上线之后,如下图一所示,可以看到修复之后老年代的内存可以正常回收了,至此问题终于全部解决。

图片

图片

六、总结

内存溢出是 Java 开发中最常见的问题之一,其原因通常是由于内存泄漏导致内存无法正常回收引起的。在我们这篇文章中,详细介绍一次完整的线上内存溢出的处理过程。

总结一下我们在碰到内存溢出时候的常用解决思路:

(1)生成堆内存文件

在服务启动命令添加

 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base

让服务在发生oom时自动dump内存文件,或者使用 jamp 命令dump内存文件。

(2)堆内存分析:使用内存分析工具帮助我们更深入地分析内存溢出问题,并找到导致内存溢出的原因。以下是几个常用的内存分析工具:

  • Eclipse Memory Analyzer:一款开源的 Java 内存分析工具,可以帮助我们快速定位内存泄漏问题。

  • VisualVM Memory Analyzer:一个基于图形化界面的工具,可以帮助我们分析java应用程序的内存使用情况。

(3)根据堆内存分析定位到具体的内存泄漏代码。

(4)修改内存泄漏代码,重新发布验证。

内存泄漏是内存溢出的常见原因,但不是唯一原因。常见导致内存溢出问题的原因还是有:超大对象、堆内存分配太小、死循环调用等等都会导致内存溢出问题。

在遇到内存溢出问题时,我们需要多方面思考,从不同角度分析问题。通过我们上述提到的方法和工具以及各种监控帮助我们快速定位和解决问题,提高我们系统的稳定性和可用性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/574979.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

无人机生态环境监测、图像处理与 GIS 数据分析

原文链接:无人机生态环境监测、图像处理与 GIS 数据分析https://mp.weixin.qq.com/s?__bizMzUzNTczMDMxMg&mid2247602414&idx6&sn950b55bc2cc4812c838c66af2118d74e&chksmfa821109cdf5981f2af51bd27e459a1c46dd783cdceba5aa3693461260bbf7b0101ac8…

Vim学习笔记01~04

第01章: 遁入空门,模式当道 1.什么是vim Vim是一个高效的文本编辑工具,并且可以在编程开发过程中发挥越来越重要的作用。 事实上,有不少编程高手使用他们来进行代码的开发,并且对此赞不绝口。 2.本系列目的 但是让…

Java作业7-Java异常处理

异常处理这块有些不太理解,看看Bz网课-异常 编程1-计算器输入异常 题目 计算器输入异常 在实验三中实现的命令行计算器已有功能基础上,添加异常处理机制,当用户输入的操作数为非整数时,利用异常处理机制显示错误提示信息。如&a…

网易云热评加密函数逆向(Jsrpc)

今天给大家来个jsrpc实战教程,让大家继续加深对jsrpc的理解和认识。 1、因为网易云音乐热评的加密并不在cookie上,而是参数加密,所以这里就不需要进行hook住cookie了。 2、之前就知道网易云音乐热评的加密存在之地是在下图的位置,是那个函数window.asrsea(JSON.stringify(…

项目实战 | 责任链模式 (下)

案例二:工作流,费用报销审核流程 同事小贾最近刚出差回来,她迫不及待的就提交了费用报销的流程。根据金额不同,分为以下几种审核流程。报销金额低于1000元,三级部门管理者审批即可,1000到5000元除了三级部…

免费分享一套SpringBoot+Vue家政服务管理平台管理系统,帅呆了~~

大家好,我是java1234_小锋老师,看到一个不错的SpringBootVue家政服务管理平台管理系统,分享下哈。 项目视频演示 【免费】SpringBootVue家政服务管理平台系统 Java毕业设计_哔哩哔哩_bilibili【免费】SpringBootVue家政服务管理平台系统 Ja…

coverage,一个有趣的 Python 库!

更多Python学习内容:ipengtao.com 大家好,今天为大家分享一个有趣的 Python 库 - coveragepy。 Github地址:https://github.com/nedbat/coveragepy 在软件开发中,测试是确保代码质量和稳定性的关键步骤之一。而代码覆盖率则是衡量…

2024新版计算机网络视频教程65集完整版(视频+配套资料)

今日学计算机网络,众生皆叹难理解。 却见老师神乎其技,网络通畅如云烟。 协议层次纷繁复杂,ARP、IP、TCP、UDP。 路由器交换机相连,数据包穿梭无限。 网络安全重于泰山,防火墙、加密都来添。 恶意攻击时刻存在&#xf…

深圳证券交易所Binary行情数据接口规范

对接深圳证券交易所Binary行情数据接口其实并不难,你需要具备以下知识。 1、需要了解Binary报文设计结构,消息头消息体消息尾。 消息体: 如果是纯map结构的比较简单,字段平铺开来即可。如{"id":"1","…

与AI对话:探索最佳国内可用的ChatGPT网站

与AI对话:探索最佳国内可用的ChatGPT网站 🌐 链接: GPTGod 点击可注册 🏷️ 标签: GPT-4 支持API 支持绘图 Claude 📝 简介:GPTGod 是一个功能全面的平台,提供GPT-4的强大功能&…

多线程(安全 同步 线程池)

线程安全问题 多线程给我们的程序带来了很大性能上的提升,但是也可能引发线程安全问题线程安全问题指的是当多个线程同时操作同一个共享资源的时候,可能会出现的操作结果不符预期问题 取钱的线程安全问题 线程安全问题出现的原因? 存在多线…

idea创建完项目如何隐藏不重要的文件

如果您不打算直接使用这些脚本,而是更倾向于通过IDEA的内置工具来运行Maven命令,那么您可以选择隐藏这些文件。但是,隐藏这些文件并不会影响它们的功能,只是在项目视图中不再显示它们。 1.转到 File > Settings(Wi…

时间,空间复杂度讲解——夯实根基

前言:本节内容属于数据结构的入门知识——算法的时间复杂度和空间复杂度。 时间复杂度和空间复杂度的知识点很少, 也很简单。 本节的主要篇幅会放在使用具体例题来分析时间复杂度和空间复杂度。本节内容适合刚刚接触数据结构或者基础有些薄弱的友友们哦。…

【threejs教程7】threejs聚光灯、摄影机灯和汽车运动效果

【图片完整效果代码位于文章末】 在上一篇文章中我们实现了汽车模型的加载,这篇文章主要讲如何让汽车看起来像在运动。同时列出聚光灯和摄像机灯光的加载方法。 查看上一篇👉【threejs教程6】threejs加载glb模型文件(小米su7)&…

详解23种设计模式——单例模式

单例模式 | CoderMast编程桅杆单例模式 单例模式是最常用的设计模式之一,他可以保证在整个应用中,某个类只存在一个实例化对象,即全局使用到该类的只有一个对象,这种模式在需要限制某些类的实例数量时非常有用,通常全局…

The Clock and the Pizza [NeurIPS 2023 oral]

本篇文章发表于NeurIPS 2023 (oral),作者来自于MIT。 文章链接:https://arxiv.org/abs/2306.17844 一、概述 目前,多模态大语言模型的出现为人工智能带来新一轮发展,相关理论也逐渐从纸面走向现实,影响着人们日常生活…

WebAssembly学习记录

1.WebAssembly 1.1 指令集 概念:二进制编码集合。 依据计算机组成原理和计算机概论,指令集是一组二进制编码。 作用:控制硬件。 这些二进制指令直接作用于硬件电路,控制硬件完成指定操作。 例如:控制数据进入某个寄存…

如何通过质构分析仪客观评价面包的硬度、咀嚼性等口感指标

如何通过质构分析仪客观评价面包的硬度、咀嚼性等口感指标 一、引言:面包口感品质的重要性 面包作为日常生活中常见的食品之一,其口感品质直接影响到消费者的购买决策和食用体验。其中,硬度和咀嚼性是衡量面包口感品质的重要指标。因此&…

车企如何利用数据技术,指导汽车全生命周期的业务运营?

引言:数据正作为重点,为行业提供不可或缺的指导 《汽车数据发展研究报告(2023)》指出,汽车行业正由传统硬件制造向“电动化、智能化、网联化”方向转变。德勤预测,到 2025 年,汽车行业 20%的利…

小程序AI智能名片S2B2C商城系统:四大主流商业模式深度解析与实战案例分享

在私域电商迅速崛起的大背景下,小程序AI智能名片S2B2C商城系统以其独特的商业模式和强大的功能,正成为品牌商们争相探索的新领域。在这一系统中,拼团模式、会员电商、社区团购和KOC营销等四种主流模式,为品牌商提供了多样化的营销…