jvm-Sandbox内存溢出排查记录

事发描述

在10月1日下午某服务线上5台机器出现严重问题,资源消耗打满并最终无法正常提供服务。

服务器监控
堆dump

  1. 堆使用内存占满,从12点开始不停上升到18点,内存从2G升到8G最大值
  2. Full GC可以触发
  3. 频繁GC,18点和20点GC count均有高峰,但没有带来内存回收效果
  4. 同一集群同一时间只有一台服务有问题

怀疑方向

  1. 服务器出现内存泄漏
  2. 该应用内置的主从策略引起选举为主的服务器出现问题
  3. 选举为主的服务器单独运行的方法触发sandbox懒加载机制,使得该现象发生在运行期

需要解释的问题:

  1. 为什么有内存泄漏
  2. 什么时候触发的内存泄漏
  3. 为什么只有一台有问题,而且是运行中发生

    解决过程:

根据之前拿到的分析结果,可以看到SandboxClassLoader里有一个GaLRUCache对象发生了泄漏,使用OQL语句进行查询发现发生内存泄漏确实是发生在该对象上,该map扩展到了1048576个Entry,且总大小达到了8388632;


查看源码,改对象操作发生在ClassStructureImplByAsm类中,以ClassLoader和ClassName为唯一标识对类结构进行缓存

1
2
3
4
5
6
7
8
9
private static final GaLRUCache<Pair, ClassStructure> classStructureCache = new GaLRUCache(1024);


Pair pair = new Pair(new Object[]{this.loader, javaClassName});
if (classStructureCache.containsKey(pair)) {
return (ClassStructure)classStructureCache.get(pair);
} else {
classStructureCache.put(xxxxx)
}

该Map存在的并发问题:GaLRUCache直接继承的LinkedHashMap,containKey方法没有重写,导致不断触发put所在的代码块,对这个map进行并发操作,而LinkedHashMap是一个线程不安全的map,并发操作导致该map的size对象变得无效,无法触发超过size阈值自动移除首位,使得该Map可以无限扩容


同时在堆dump中发现两个比较特殊的现象:

指标名称 指标数据
字节总数 9,239,723,959
类总数 160,616
实例总数 56,294,016
类加载器 136,976
垃圾回收根节点 5,268
等待结束的暂挂对象数 0

类总数和类加载器数量过多,在堆dump里发现基本上是GaLRUCache里缓存的Value,也就是ASMClassLoader对象

回归问题(第一次):

  1. 为什么有内存泄漏(sandbox源码问题)
  2. 如何解决类加载器过多带来的额外占用
  3. 什么时候触发的内存泄漏(sandbox插件将发现的类和插件需要监控的类进行匹配的时候)
  4. 为什么只有一台有问题,而且是运行中发生

    尝试复现(第一次):

    我写了很多AdviceListener用来watch,使用正则表达式*来匹配所有类,并不断启停插件,希望通过不断触发类匹配复现该问题

未解决,但是发现额外问题:

类只会装载不会卸载,元数据区有泄漏风险

回归问题(第二次):

  1. 为什么有内存泄漏(sandbox源码问题)
  2. 如何解决类加载器过多带来的额外占用
  3. 什么时候触发的内存泄漏(sandbox插件将发现的类和插件需要监控的类进行匹配的时候)
  4. 为什么只有一台有问题,而且是运行中发生(怀疑是路由策略导致一台机器比较明显)
  5. 不停启停的场景下元数据区有内存泄漏风险

因为想看一下ASMClassLoader为什么是多例的,搜了一下这个ClassLoader相关的文章,发现这个类是FastJson插件的类,用于在序列化/反序列化时生成临时类来处理json,通过这种方式提高性能,并且在某些容器下有类泄漏的先例,猜想会不会是fastjson的serialize方法动态生成Class导致Sandbox不停的认为发现了新的类,然后才触发的sandbox Map并发问题?在当时的堆Dump里也有大量Class来自ASMClassLoader

与作者沟通,获得到的答案:是

尝试复现(第二次)

在本地环境重复调用使用 JSON.parseObject接口,进行验证

失败

尝试复现(第三次)

在特殊的生成环境重复调用使用 JSON.parseObject接口,验证目标同第二次,排除jvm启动参数带来的影响
复现成功

复现出的服务器监控
复现出的堆dump

回归问题(第三次):

  1. 为什么有内存泄漏(sandbox源码问题,Fastjson和sandbox兼容不好的问题)
  2. 如何解决类加载器过多带来的额外占用(修改sandbox源码,将原来匹配API扩展,在入口处拦截更多的类)
  3. 什么时候触发的内存泄漏(sandbox插件将发现的类和插件需要监控的类进行匹配的时候)
  4. 为什么只有一台有问题,而且是运行中发生(这是个错误条件,最后发现所有机器都有问题,只是其他机器没有宕机)
  5. 不停启停的场景下元数据区有内存泄漏风险

遗留问题:

  1. 不停启停的场景下元数据区有内存泄漏风险(我们远没有达到泄漏风险的要求,暂时无需关注,等待开发者维护该问题)

如何规避:

  1. 上线前:在特殊的生产机器部署一套sandbox和目标应用项目,通过复现功能进行压力测试之后再应用到生产,如果扩展插件功能,需要进行全插件功能的压测
  2. 上线后:针对如何快速关闭有问题的服务,需要提供一套关闭方案(不方便透露,有疑问可以通过github信息找到我)