堆外内存泄露排查经历

优质博文:IT-BLOG-CN

一、问题描述

淘宝后台应用从今年某个时间开始docker oom的量突然变多,确定为堆外内存泄露。

后面继续按照上一篇对外内存分析方法的进行排查(jemallocpmapmalloc+pmap/maps+NMT+jstack+gdb),但都没有定位到问题。至于为什么没有定位到问题,后面会根据问题的特点进行分析。

至此,回到原点。其实也不是原点,最起码已经确定了是堆外内存off-heap,而不是Native Memory(JVM自身所用内存)泄露。所以你看,很多时候问题排查其实是排除法(苦涩的笑)。

二、排查过程

堆外内存off-heap的泄露,不外乎有以下几个原因:
【1】流没有关闭;
【2】Unsafe.allocateMemory内存没释放;
【3】jni内存没有释放;

其中流没有关闭是最常见的,而23出现的概率是比较低的,所以先排查流没有关闭的可能。

2.1 走寻常路

对于流没有关闭导致泄露的定位,一般来说有以下4种方式:
【1】看代码;
【2】用jemalloc分析;
【3】分析堆内存找小尾巴;

2.1.1 看代码

流未关闭的话,一般来说都是因为没有显式地调用close()方法或没有使用try-with-resource的方式管理流。比如下面这段代码就存在流未关闭的情形:
在这里插入图片描述

像下面的这段代码,流会被try-with-resource机制去关闭,正常情况下不会出现内存泄露。而存在泄露的应用,恰恰就是用try-with-resource机制去管理流,所以排除这里的嫌疑。
在这里插入图片描述

2.1.2 用jemalloc分析

这里用到jemalloc主要是利用它的heap dump以及它的jeprof命令来分析java进程的内存分配情况。注意这里的heap不是jvm堆内存,而是操作系统视角的内存布局,比如heapstackBSS、数据段、代码段,这里不是本文的重点,就不展开描述了……

使用jemalloc分析内存分配的过程很多文章都有描述,这里也不展开了,结果是通过jeprof生成的pdf文件,依然没有发现导致流未关闭的场景,只能作罢。

2.1.3 分析堆内存

通过看代码的方式以及jemalloc都没法定位到流未关闭的情形,考虑代码走查难免有遗漏,同时应用使用了大量的第三方组件,第三方组件会不会存在流未关闭的可能呢?但很显然,如果去分析第三方组件的代码会累吐血。联想到流没有关闭的情形,一般会在堆内存里面留一些引用的痕迹,于是开始dump java堆内存。

内存dump下来后,通过MAT查找java.lang.ref.FinalizerInputStreamOutputStream相关的对象,依然一无所获,这时开始怀疑内存的泄露跟流未关闭没有关系。

2.1.4 内部工具分析

公司内部提供了一个跟踪内存分配的工具,通过扩展malloc方法获取到分配内存的调用线程和内存地址,通过jstack打印线程栈,结合gdbpmap等方式获取可疑内存段,以定位内存泄露源头。通过这种方式依然没有找到任何线索,同时jstack的方式会导致应用出现短暂的停顿safepoint而影响性能,所以这种方式也放弃了。

2.2 走了弯路

在暂时排除了流未关闭的嫌疑后,这时转向分析直接用Unsafe.allocateMemory分配的内存。有些组件不会基于java.nio.DirectByteBuffer(int cap)申请堆外内存,而是直接用unsafe.allocateMemory方法申请内存,这时候MaxDirectMemorySize是限制不住堆外内存的用量的,当然基于DirectByteBuffer申请的堆外内存,最终也是基于unsafe.allocateMemory方法申请内存,所以这里只要分析unsafe.allocateMemory申请的内存即可。到这里,前面提到的神器async-profiler就粉墨登场了。

async-profiler的安装步骤这里就不介绍了,可以自行安装。安装完毕使用以下脚本就可以分析Unsafe_AllocateMemory0的内存分配情况了。

sudo -u deploy /tmp/async-profiler-2.9-linux-x64/profiler.sh -e Unsafe_AllocateMemory0 -d 1200 -f /tmp/unsafe_alloc-$(pgrep java)-$(date +'%y%m%d%H%M').html $(pgrep java) 

这里-e代表要分析的事件,-d代表分析的时长,以秒为单位。生成的结果是一张火焰图,你可以下载下来在浏览器上查看哪块用到了Unsafe_AllocateMemory0来分配内存。

比较悲催的是,通过Unsafe_AllocateMemory0分配的内存比较少,所以这里的嫌疑也被排除了。所以分析Unsafe_AllocateMemory0这一步算是走了弯路。

2.3 柳暗花明

前面所有的手段都用尽之后,已经快一个星期过去了。在前面的手段都用尽之后,尝试分析jni的内存分配情况。其实这时候有点死马当作活马医的味道了。

jni(Java Native Interface),简单说就是Java调用c/c++写的程序,实现更强的功能。c写的程序,要分配内存,一般是通过malloc()方法向操作系统申请内存。在malloc的实现中,一般分配大块内存 128KB会使用mmap分配内存空间。而async-profiler可以通过分析linux perf_event中的perf_event_mmap_page来追踪内存分配情况的。想到这里,便尝试通过下面的命令来追踪系统层面malloc情况:

sudo -u deploy /tmp/async-profiler-2.9-linux-x64/profiler.sh --loop 1h -e malloc -f /tmp/malloc-$(pgrep java)-%t.html $(pgrep java) 

这个命令中的–loop参数是能够以1个小时间隔不间断跟踪内存分配情况,如果你想长时间进行问题定位,可以尝试使用一下这个参数,profiler会每隔1个小时生成一个html文件,是不是很方便?

-e malloc就是告诉async-profiler去追踪perf_event_mmap_page的内存分配。

运行了1个小时后,就得到了下面的这个内存分配火焰图:
在这里插入图片描述

从图中可以看出,zstd-jni这个组件分配了大量的内存。因为在之前我们通过review代码排查流没有关闭的场景时,是看过这段代码的,但当时没有发现什么问题。但从火焰图中看到分配的内存量,总感觉不对劲。这时候忽然想到,能不能从日志中找到什么蛛丝马迹呢?于是开始扒日志,这时,一个broken pipe的异常引起了我的注意:
在这里插入图片描述

这种broken pipe的异常其实蛮常见的,尤其在有一方断开连接时,很容易就出现这种异常。但顺着调用栈往下看,顿时眼前一亮,其中有ZstdOutputStream的调用。流里面的异常那是很容易泄露的,于是进入到ZstdOutputStream.java 178行看代码,发现了zstd-jni 1.3.x版本存在的bug:当ZstdOutputStream关闭流的时候,会尝试把剩余的数据发送出去。但这时候如果连接已经关闭了,它就咯咯了,导致流关闭不掉,jni的内存也释放不掉。
在这里插入图片描述

这个bug,在1.4.4-11版本中就修复了,我们可以看到作者用try-finally捕获了out.write的异常,这样不管zstd依赖的流的状态如何,它最终都会释放自己使用的资源。
在这里插入图片描述

定位到问题之后就好办多了,将zstd-jni的版本升级到1.4.9-5之后的版本,这个问题就不存在了,下面是修复后RSS的情况,可以看到RSS很平稳了:
在这里插入图片描述

三、总结

这个case从开始排查到最终定位到问题,花费了一个星期的时间,成本巨大,回过头看看排查的步骤,貌似也没什么问题,但终究是走了一些弯路:

3.1 忽略了异常信息

如果最开始就重视异常信息的话,那么这个问题可能很早就定位到了。但这个应用自己不是直接责任人,而且在看到broken pipe的时候犯了经验性错误,没有往影响流关闭的角度想,导致方向错误,浪费了大量的时间。

所以,系统中任何的异常,都要重视起来,避免产生更严重的问题。

3.2 jemalloc失效

jemalloc在分析内存持续泄露方面比较方便,但对于非稳定复现的场景,如果采样间隔过久,有可能会导致错过问题点。而如果你将采样间隔调短,又会造成生成大量的dump文件,在用jeprof生成分析报告的时候,可能会导致too many arguments的错误而无法生成分析报告。

3.3 内部工具失效

内部工具,能够把可疑的内存段内容用strings命令查看,某些场景是能够发现蛛丝马迹的,为什么这个case就不行了呢?这里猜测是因为zstd对数据做了压缩,用strings看到的全是乱码,没法发现数据的特征;

综上,问题排查很多时候真的像排雷一样,一个个的去排除。这需要的是耐心和毅力,当你最终定位到问题的时候,那种如释重负的感觉会让自己觉得一切都是值得的。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据检索是什么意思?数据检索包括哪几个

不少用户会提出这样的疑问,数据检索是什么意思?数据检索即把数据库中存储的数据根据用户的需求提取出来,选择适合的数据库检索方式需要根据具体的需求和场景来进行判断。数据检索的结果会生成一个数据表,既可以放回数据库&#xf…

DimensionX:单图生成任意的3d/4d视图

DimensionX:单图生成任意的3d/4d视图 通俗易懂的来说 在我们的方法中,关键是如何从一张图片生成动态的3D和4D场景。我们使用一个叫做ST-Director的工具,它可以分开处理空间(3D)和时间(4D)两个方面。想象一…

接口测试和单元测试

🍅 点击文末小卡片 ,免费获取软件测试全套资料,资料在手,涨薪更快 接口测试的本质:就是通过数据驱动,测试类里面的函数。 单元测试的本质:通过代码级别,测试函数。 单元测试的框架…

第5篇 寻找最大数___ARM汇编语言<一>

Q:如何设计一段ARM处理器汇编语言子程序并调用来寻找一组数中的最大数呢? A:基本原理与基于Nios II处理器的汇编语言子程序一样,使用子程序LARGE实现找到列表中最大数的功能。主程序通过寄存器将列表的条目数和起始地址作为参数传…

JavaWeb之综合案例

前言 这一节讲一个案例 1. 环境搭建 然后就是把这些数据全部用到sql语句中执行 2.查询所有-后台&前台 我们先写后台代码 2.1 后台 2.2 Dao BrandMapper: 注意因为数据库里面的名称是下划线分割的,我们类里面是驼峰的,所以要映射 …

【LeetCode每日一题】——746.使用最小花费爬楼梯

文章目录 一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目示例】六【题目提示】七【解题思路】八【时空频度】九【代码实现】十【提交结果】 一【题目类别】 数组 二【题目难度】 简单 三【题目编号】 746.使用最小花费爬楼梯 四【题目描述】 给你一…

记录下jekins新建个前端部署配置项

1 新建个item 2 输入项目名称,选择个新的工程或 或者搜个已存在的现有模板 3 添加一些描述 4 (可选)配置下构建历史保存情况 5 限制下构建节点和选择gitlab或者github 6 写下git仓库地址、账号密码以及分支 7 选择构建工具node以及版本 8 构建…

设计模式之 状态模式

状态模式(State Pattern)是一种行为型设计模式,它允许一个对象在其内部状态改变时,改变其行为。这种模式将状态的转换和行为的变化解耦,将不同状态的行为封装到独立的状态类中,而通过上下文(Con…

uni-app 界面TabBar中间大图标设置的两种方法

一、前言 最近写基于uni-app 写app项目的时候,底部导航栏 中间有一个固定的大图标,并且没有激活状态。这里记录下实现方案。效果如下(党组织这个图标): 方法一:midButton的使用 官方文档:ta…

IText创建加盖公章的pdf文件并生成压缩文件

第一、前言 此前已在文章:Java使用IText根据pdf模板创建pdf文件介绍了Itex的基本使用技巧,本篇以一个案例为基础,主要介绍IText根据pdf模板填充生成pdf文件,并生成压缩文件。 第二、案例 以下面pdf模板为例,生成一个p…

合法三元数量计算

问题描述 小C、小U 和小R 三个好朋友喜欢做一些数字谜题。这次他们遇到一个问题&#xff0c;给定一个长度为n的数组a&#xff0c;他们想要找出符合特定条件的三元组 (i, j, k)。具体来说&#xff0c;三元组要满足 0 < i < j < k < n&#xff0c;并且 max(a[i], a[…

【AI系统】GPU 架构回顾(从2018年-2024年)

Turing 架构 2018 年 Turing 图灵架构发布&#xff0c;采用 TSMC 12 nm 工艺&#xff0c;总共 18.6 亿个晶体管。在 PC 游戏、专业图形应用程序和深度学习推理方面&#xff0c;效率和性能都取得了重大进步。相比上一代 Volta 架构主要更新了 Tensor Core&#xff08;专门为执行…

【高阶数据结构】图论

> 作者&#xff1a;დ旧言~ > 座右铭&#xff1a;松树千年终是朽&#xff0c;槿花一日自为荣。 > 目标&#xff1a;了解什么是图&#xff0c;并能掌握深度优先遍历和广度优先遍历。 > 毒鸡汤&#xff1a;有些事情&#xff0c;总是不明白&#xff0c;所以我不会坚持…

日期(练习)

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title></title> </head> <body></body> <script>// 定义一个函数&#xff0c;实现格式化日期对象&#xff0c;返回yyyy-MM-dd…

【IDEA】解决总是自动导入全部类(.*)问题

文章目录 问题描述解决方法 我是一名立志把细节说清楚的博主&#xff0c;欢迎【关注】&#x1f389; ~ 原创不易&#xff0c; 如果有帮助 &#xff0c;记得【点赞】【收藏】 哦~ ❥(^_-)~ 如有错误、疑惑&#xff0c;欢迎【评论】指正探讨&#xff0c;我会尽可能第一时间回复…

企业使用知识管理工具与技术的好处(举例说明)

我们都知道“知识就是力量”这句老话&#xff0c;无论是在工作还是个人生活中&#xff0c;我们每一天都越来越认识到这句话的真谛。近年来&#xff0c;不可否认的是&#xff0c;全球范围内我们都在某种程度上缺乏对于许多企业和大型公司至关重要的高端技术技能。 当然&#xf…

机器学习系列-决策树

文章目录 1. 决策树原理决策树的构建流程 2. 案例步骤 1&#xff1a;计算当前节点的熵步骤 2&#xff1a;对每个特征计算分裂后的熵(1) 按“天气”分裂数据集(2) 计算分裂后的加权熵 步骤 3&#xff1a;计算分裂依据信息增益信息增益率GINI系数&#xff08;二叉树&#xff09; …

resnet50,clip,Faiss+Flask简易图文搜索服务

一、实现 文件夹目录结构&#xff1a; templates -----upload.html faiss_app.py 前端代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widt…

爬虫重定向问题解决

一&#xff0c;问题 做爬虫时会遇到强制重定向的链接&#xff0c;此时可以手动获取重定向后的链接 如下图情况 第二个链接是目标要抓取的&#xff0c;但它是第一个链接重定向过去的&#xff0c;第一个链接接口状态也是302 二&#xff0c;解决方法 请求第一个链接&#xff0c…

一个小的可编辑表格问题引起的思考

11.21工作中遇到的问题 预期&#xff1a;当每行获取红包金额的时候若出现错误&#xff0c;右侧当行会出现提示 结果&#xff1a;获取红包金额出现错误&#xff0c;右侧对应行并没有出现错误提示 我发现&#xff0c;当我们设置readonly的时候&#xff0c;其实render函数依旧是…