背景:
通过钉钉和短信提醒得知,有几台阿里云的服务器内存超过阈值触发了报警,当时因为怕影响接口的访问,于是重启了出现问题的容器,通过监控大屏观察,当时并没有任何一个容器发生OOM。
1.使用命令进行排查:
1.1 堆内存排查
过了几天之后开始排查问题,由于没有发生oom,只能一步一步排查,先排查一下堆内存, 进入容器内部ps -ef 直接可以看下运行配置:
说明有堆内存设置,于是顺手看了一下堆内存中的对象,线上使用的是openjdk8,有些命令是没有的,这里使用了jmap -histo <pid> | head -10 命令看了一下占用堆内存前10的对象:
可以看出,堆内存最多也就占2-3g左右(此处有伏笔,当时没想着去看元空间的情况,如果当时去看了一下元空间gc情况,到这里应该就知道原因了)
1.2堆外内存排查
因为之前就有引用了第三方jar包导致OOM的,排查后发现是无限创建了线程,所以先查看了一下线程数是否正常,直接使用命令cat /proc/pid/status查看linux的进程转储文件:
查看了一下jvm参数配置,发现没有配置线程栈大小,说明每个线程的线程栈是默认的1M,那么这里也是正常的。 剩下其他几个内存区域排查,当时最先想的是用jdk自带的NativeMemoryTracking,因为arthas在容器内部署是需要改造的,一时半会可能无法立即在线上使用,但是经过尝试 配置了NativeMemoryTracking,发现线上使用的openjdk没有这个功能,这个就没办法了,只能去使用arthas了
2.使用arthas进行排查:
2.1 direct 区域
过了一段时间后,在线上部署了arthas,因为服务中有使用了dubbo,怀疑有可能是堆外内存泄漏,首先使用memory命令看了一下direct区域的大小,发现是正常的:
2.2 元空间
当时还是没仔细元空间这块区域,但是在查看classloader时发现了异常:
这里居然加载了8万多个类,元空间直接占了2-3g,还是挺恐怖的(由于服务还没运行多久,时间长了这种内存泄漏肯定会导致内存报警和OOM),到这肯定了是fastjson的问题了。但是arthas只能一次导出所有类,在线上执行肯定有风险。于是这里先把这个服务切到其他集群了,留了占用内存最大的两个容器进行加载类的导出:
这里发现很多类都是重复加载,根据关键词应该是fastjson内部维通过AsmClassLoader生成class
3 问题解决:
3.1 问题分析
首先笔者就去看了一下项目的fastjson版本和github上fastjson的issue,没有找到满意的答案,于是就开始看源码分析,线上主要项目版本的都是1.2.83版本的,先找到了是在哪里创建的这些类(直接通过在arthas中看到的关键词查询):
这个反序列化器工厂应该就是创建这个class的地方,根据源码可以看出这里只是计了个数,并没有找到有去做缓存的地方(每次都会去loadclass导致在元空间生成大量类元信息):
这里使用的classloader是fastjson自定义的AsmClassloader,AsmClassloader倒是有缓存的,但是缓存的居然是写死的几个类!?
到这里其实就明白为什么fastjson会创建那么多重复的class
3.2 问题解决
首先想到的办法是升级版本或者直接更换为spirng使用的jackson,但是后来这两种方法都被笔者否决了。
1.为什么不升级fastjson版本?
fastjson之前风评一直不是太好,各种漏洞,没有jackson来的稳定,就算是新出的fastjson2都一堆问题
2.为什么不换成jackson?
不得不承认,fastjson虽然称为bugson,但是确实用法比较简单,项目使用fastjson时间较长了,有些代码耦合了fastjson相关功能,换成jackson比较麻烦
3.能不能直接把asm关了?
答案是可以的,这里笔者也是想了很久才走到这一步,因为一开始怕关了asm是有风险的,但是不得不走这一步。需要找到fastjson是否有这样的配置,首先找到其中一个配置类,搜索一下asm相关内容:
好巧不巧第一个就是,这里给了一个setter方法可以对是否开启asm进行设置。 设置如下关闭asm:
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAsmEnable(false);
SerializeConfig.getGlobalInstance().setAsmEnable(false);
SpringApplication springApplication = new SpringApplication(AppStarter.class);
springApplication.setAllowBeanDefinitionOverriding(true);
springApplication.run(args);
}
笔者这里把配置加在启动类里了,至此问题解决。这里验证一个笔者非常关心的问题,关闭asm后,fastjson应该会走反射,那么性能真的会如fastjson介绍里下降很多嘛?(测试结果仅供参考)
public static void main(String[] args) {
//ParserConfig.getGlobalInstance().setAsmEnable(false);
//SerializeConfig.getGlobalInstance().setAsmEnable(false);
BaseModel baseModel = new BaseModel();
baseModel.setCreationdate(new Date());
baseModel.setIsactive("Y");
baseModel.setOwnerid(888L);
baseModel.setModifiername("asdasdasd");
long start = System.currentTimeMillis();
//System.out.println("开始:" + start);
for (int i = 0; i < 1000; i++) {
JSONObject jsonObject = (JSONObject) JSONObject.toJSON(baseModel);
String json = jsonObject.toJSONString();
}
long end = System.currentTimeMillis();
System.out.println("结束:" + (end - start));
}
循环了1000次发现不开启asm反而快(默认开启),大概40ms-50ms之间。开启了asm反而需要130-150ms之间,这里没有继续仔细去研究了,总之不降低太多是可以接受的。 最后回归了一下,目前线上运行了几周后,没有出现过报警:
说明正常了
总结:
本文先尝试通过命令去排查堆内存、线程是否存在问题,然后使用arthas排查了direct和元空间区域是否有问题,找到是fastjson的问题后,通过分析部分源码找到解决方案。最后,奉劝各位读者选择中间件的时候还是得深入考察一下,否则后期改造相当困难。