针对后台服务内存溢出问题分析
发生背景
系统后端查询服务工程生产环境多次不固定的出现了内存上涨溢出导致页面瘫痪,有时实在程序发布后不久,有时是过了好多天后出现溢出,对此进行了一次内存分析
初步分析
通过云服务容器的Grafana监控页面,如图1.1和1.2对比图,可以发现后台容器内存在短时间内发生了飙升,可以判断内存泄漏原因可能不是长期定时任务导致,可能是短时间触发某个方法或者接口导致内存压力飙升。
图1.1
图1.2
准备工具分析
Java内存分析工具
VisualVM 2.17
Java 自带分析工具,jdk 1.8之后 这个就被单独移出来后续jdk版本中不在自带这个,若是有1.8 jdk 可以直接去bin 目录下使用 如下图
后续版本请用下方链接官网自行下载
下载链接: https://visualvm.github.io/documentation.html
如下图,通过生成的堆栈文件载入可分析(注意:若堆栈文件过大会导致该工具分析内存占用很高)
MemoryAnalyzer 1.11.0
Eclipse 出的分析工具,有可视化的图表分析,还会对你当前的分析出内存溢出可能出现的位置
下载链接:https://eclipse.dev/mat/previousReleases.php
如下图加载堆栈文件
功能上比VisualVM多了很多维度上的分析但是对于基本功扎实的人,两者没有什么区别因人而异去选择使用,市面上还有很多分析工具
也要注意若堆文件过大分析时会报工具内存相关错,可以如下图调整工具的内存占用大小
Java包相关命令
JPS 命令 可以查看当前进程端口号
jmap -dump:format=b,live,file=dump.hprof 37 可以生成当前堆文件
注意这个命令执行时一定要考虑,执行该命令时会做一次FullGC他会让当前程序所有动作停止,若堆存储文件过大,这个时间可能会很长,对于生产环境是致命的,如果项目允许可以尝试将jvm内存调小,在做导出方便后续排查。
也可以在启动命令时配置这些东西溢出报错时自动生成堆信息(该方法暂未尝试不知效果)
-XX:+HeapDumpOnOutOfMemoryError 设置当首次遭遇内存溢出时导出此时堆中相关信息
-XX:HeapDumpPath=/tmp/heapdump.hprof 指定导出堆信息时的路径或文件名
Kubernetes连接工具
由于生产环境是由Kubernetes 容器管理的所以若想将堆文件下载下来需要将堆文件挂载到生产集群上防止容器重启后文件丢失,然后远程工具连接k8s集群将堆文件下载下来进行分析。
这里使用的是kubectl 工具来进行连接
下载地址:
windows : https://dl.k8s.io/release/版本号/bin/windows/amd64/kubectl.exe
mac: https://dl.k8s.io/release/版本号/bin/darwin/amd64/kubectl
linux: https://dl.k8s.io/release/版本号/bin/linux/amd64/kubectl
注:版本号尽量和k8s版本保持一致,我是云服务器的k8s 直接有版本号
https://dl.k8s.io/release/v1.18.0/bin/windows/amd64/kubectl.exe
环境变量配置方便命令执行
上述环境变量中有一个yml文件里面配置的是你当前连接集群信息,入下图配置文件信息
clusters 是当前集群列表信息 你可以配置多个k8s集群server信息,name 是一个别名用于和下方的contexts做关联
contexts是当前你配置的集群信息用于连接的用户信息和集群信息,你可以拥有多个context来对不同的集群进行连接操作cluster就是上述配置的集群地址信息 user 就是下面配置的用户连接时的认证信息
current-context 是指代当前使用kubectl命令时指定的集群
users 是集群连接用户认证信息列表 token 是k8s Apiservier token
APIServer 地址是我们的地址若有多个可以选择其中一个即可,复制令牌就是我们的token
使用 kubectl config view 命令查看当前配置文件是否被识别
kubectl cp -n ${k8s_namespace} ${k8s_podname}: ${container_path} ./
使用拷贝命令对远程的k8s集群文件拷贝到本地
分析过程
当上述工具和堆存储文件已经全部备好后可以使用工具分析来进行问题定位,这里我的堆文件比较大有9G,使用的是 VisualVM , 打开和一些往下分析基本上加上其他的软件把我电脑的16G吃满了。
成功打开后可以很直观的看到string 实例和 byte 实例 非常的大基本上是占满了,点开String 继续往下查看
打开其中一个追溯他的上级发现是一个数组 (若是第一次打开这个references 会花费很长时间,因为程序需要分析,这个过程长短取决于你堆文件和自身内存大小)
右键追溯他的进程进一步的查看
可以发现有三处属于后台工程方法,去工程中对这个三处的行数进行逐一排查
最终发现在这个公共方法中发现了潜在的隐藏问题若调用该方法的接口传递的startDate > endDate 那么就会导致程序出现死循环,在对他的这个调用的接口进行了场景模拟复现了内存溢出的点,在于接口的入参上,这也解释了一开始我看到的内存是在某一瞬间暴涨上去的而不是持续一点点的涨上去,而且内存溢出的时间都是不固定的有时在程序启动没多久,有的时候是在好几天后。最终也是做了校验和修改,解决了该问题
总结
针对内存分析过程这只是我的一种方式,对于其他人并不一定适用,或者有其他更高效的方法,已上的分析都是基于我的生产环境在云服务器上,其他人的则因人而异。
主要难点在于生产环境大多数场景都是容器化的部署, jvm内存分配的很多,这种情况内存溢出会导致在生成堆栈文件的时间变长,你将面临这客户的投诉和程序恢复需要一定时间的双重压力,因为容器重启意味着你的堆栈文件将不完全,这种情况你的分析工具一般都无法对一个不完全的或者损坏的堆文件进行分析。
所以说如何保存当前容器的信息和程序恢复就是你最需要解决的,我这里是提前做了一个备份的后端服务,重启前端修改nginx 的转发服务地址。你当然也可以通过负载均衡来解决这个问题,但是对于k8s来说这个方法不会成功因为你这个只是jvm的内存溢出他本质上不会造成容器的异常停止,对于k8s的负载来说他认为该容器还是正常的请求也会继续根据负载策略进行分配,除非说是这个容器停止了,不过k8s会自动重启容器,当重启之后你的文件还没有导出来一切都晚了。
所以根据场景的不同,你需要使用不同方式来处理,这时你将面临时间飞速,知识匮乏等等,其他的压力袭来,这才是真正的考验,学无止境,结果是必然的,但过程多样的,希望下次遇到的时候不再手忙脚乱,而是坦然自若的解决。