JVM-SANDBOX 导致目标服务 JVM Metaspace OOM 的调查始末
背景
放火平台是公司内部研发的平台:遵循混沌工程理念通过故障注入模式对系统进行演练的实施平台,提供丰富的故障场景模拟,以可视化、自动化的方式实施故障演练。
放火平台利用字节增强框架JVM-SANDBOX对 java 类型的服务进行故障注入。在业务线使用的过程中发现在放火平台对某个 java 进程故障注入后故障没有生效
在经过排查后发现主要是开源项目JVM-SANDBOX的 bug,其中涉及到 ThreadLocal,ClassLoader,Metaspace 等相关知识,具体的分析以及优化过程可以往下看。
注入流程
放火平台注入 java 故障的流程:
在注入故障时放火平台会把故障指令下发到目标机器的 chaos-agent 上,chaos-agent 利用开源项目 jvm-sandbox 对目标 jvm 进行故障注入和故障清除。
关于 jvm-sandbox 的工作流程和原理可以参考之前的文章:透过JVM-SANDBOX源码,了解字节码增强框架的底层实现原理
排查过程
日志表现
在注入无效后登陆目标机器上观察日志,首先发现 jvm-sandbox 在 attach 目标 jvm 时失败了
其次看到更关键的日志:Metaspace 溢出了!!!
Metaspace
Metaspace 是什么,引用官方介绍
简单来说:Metapace 是一块非堆内存,用来存储类的元数据,当加载类的时候会在 Metaspace 中分配空间存储类的元数据,当某个 ClassLoader 关闭时会对应释放掉对类元数据的引用,当触发 GC 时这部分类元数据占用的空间即可在 Metaspace 中被回收掉。
在 jdk1.8 以前 jvm 中有一块区域叫永久代(PermGen)大家肯定都不陌生,永久代中有包含方法区以及常量池。不过在 1.8 的时候将永久代彻底移出 jvm,将永久代中存储的数据转移到了 Java Heap 或 Metaspace
方法区迁移到 Java Metaspace 中
字符串常量池则由永久代迁移到了 Java Heap 中
相关参数:
-XX:MetaspaceSize 是分配给类元数据空间(以字节计)的初始大小(Oracle 逻辑存储上的初始高水位,the initial high-water-mark ),此值为估计值。MetaspaceSize 的值设置的过大会延长垃圾回收时间。垃圾回收过后,引起下一次垃圾回收的类元数据空间的大小可能会变大。
-XX:MaxMetaspaceSize 是分配给类元数据空间的最大值,超过此值就会触发 Full GC,此值默认没有限制,但应取决于系统内存的大小。JVM 会动态地改变此值。
-XX:MinMetaspaceFreeRatio 表示一次 GC 以后,为了避免增加元数据空间的大小,空闲的类元数据的容量的最小比例,不够就会导致垃圾回收。
-XX:MaxMetaspaceFreeRatio 表示一次 GC 以后,为了避免增加元数据空间的大小,空闲的类元数据的容量的最大比例,不够就会导致垃圾回收。
关于 Metaspace 就不展开讲了,目前介绍的内容足够我们向下排查和分析,感兴趣的同学可以搜索相关资料。
分析原因
通过注入流程以及 Metaspace 的介绍,我们了解到在故障注入时会将 jvm-sandbox 动态的挂载(attach)到目标进程 JVM 上,在 attach 后会加载 sandbox 内部 jar 以及 sandbox 的自定义模块 jar 等,在这个过程中会加载大量的类,当加载类时会分配 Metaspace 空间存储类的元数据。
到这里思考了两个点:
第一直观感受是这个流程没毛病呀,会不会是因为业务服务 JVM 的 Metaspace 空间设置的太小?
Metaspace 的 GC 没有触发或者是有泄露导致类的元数据回收不掉?
尝试解决
登陆到目标机器上利用 jinfo 观察 jvm 的参数,发现 MaxMetaspaceSize 设置了 128M,这个值确实不大,因为 MaxMetaspaceSize 的默认是-1(无限制,受限于本地内存)。
让业务服务调整 MaxMetaspaceSize 参数改为 256M,然后重启 java 进程,再次故障注入 确实没问题了,故障正常生效了。
但实际问题没怎么简单,在连续注入多次后依然出现 Metaspace OOM 故障依旧无效。看来应该是故障清除时无法回收掉 Metaspace 中对应的空间。这个问题就比较严重了,如果不解决业务服务是不敢用放火平台进行 java 类型的故障注入了。
不过到这里 我们已经清晰知道这个问题的复现途径了,就是不断的故障注入以及故障清除在达到一定次数后就会 Metaspace OOM,本地复现接着排查吧。
本地复现
利用 JVM-SANDBOX 提供的 demo 模块做验证,看一下会不会出现 Metaspace OOM。
参数设置
启动参数设置了 MaxMetaspaceSize=30M,因为 demo 模块类非常少,其次为了快速的复现 OOM。
TraceClassLoading 和 TraceClassUnloding 参数则是为了观察 JVM-SANDBOX 在故障注入和清除时加载/卸载类的信息。
在启动服务后观察目标服务的 Metaspace 小于 7M
首次注入
看到确实加载了很多 jvm-sandbox 的类,Metaspace 也上升到了 14M,这个属于正常情况。
故障清除
在故障清除时没有看到 UnLoad 类的相关信息,Metaspace 也没有被回收还是 14M
多次注入 &清除
在多次注入以及清除的操作后,复现了线上业务出现的 Metaspace OOM,可以看到在多次注入的过程中,Metaspace 一直没有被回收过,占用空间曲线是一路上升。
问题排查
ClassLoader 关闭失败
上面的分析以及复现都可以确认 Metaspace OOM 就是因为 Metaspace 没有进行过回收,Metaspace 回收的前提是 ClassLoader 关闭,在之前的文章中介绍了 JVM-SANDBOX 在 shutdown 时会关闭 ClassLoader。
在 JVM-SANDBOX 中自定义的 ClassLoader 都是继承了 URLClassLoader,URLClassLoader 的关闭方法 官方介绍:
简单来说:当 classLoader 加载的所有类没有被引用时即可被关闭。
看到这里严重怀疑:当故障清除时 jvm-sandbox 中的类还有被引用的情况导致 classloader 关闭失败了。
验证猜想
在故障清除后,在目标服务的方法上 debug 看一下线程信息,果然在 threadLocal 中找到了两个 jvm-sandbox 的内部类(EventProcesser$Process,SandboxProtector)的引用。说明猜想是对的,问题的原因就是出现在这里了。下一步就是分析在 jvm-sandbox 执行故障清除后为什么还有对这两个类的引用。
EventProcessor
在 JVM-SANDBOX 中 EventProcessor 是事件处理器,其中包含了用户自定义的 listener 以及 listenerId,在执行增强代码时会记录调用的堆栈。记录堆栈利用 ThreadLocal 将真正的处理单元 Process 对象包装起来,目的就是每个调用都可以正确记录自己的堆栈。
这里的源码就不带大家分析了,主要是 jvm-sandbox 的代码实现有 bug,在以下两种情况会导致 processRef 的 ThreadLocal 没有及时 remove 造成泄漏
假如在执行注入故障的过程中,进行故障清除会导致泄漏。如下:
假设使用了 jvm-sandbox 的特性-流程变更(例如立即返回,立即抛出异常),本质也是 thread local 没有及时 remove,导致造成了泄漏
SandboxProtector
SandboxProtector:Sandbox 守护者,用来守护接口定义的方法,使得 sandbox 操作的事件不被响应。
在验证时我们看到 SandboxProtector 也有 ThreadLocal 泄漏的情况,这里 get=0 的时候应该及时 remove 掉。
改进验证
通过上面的分析,已经清晰的知道问题的根因,那么直接动手改代码看一下修复后的效果。
多次注入 &清除
启动参数还是相同的 MaxMetaspaceSize=30M,经过优化后多次注入和清除不会出现 Metaspace OOM。
下面是一些验证的过程:
也可以看到 Metaspace 会进行回收了
卸载类的信息也打印出来了
总结
放火平台利用开源项目 JVM-SANDBOX 对 java 类服务进行故障注入,在故障清除时由于 JVM-SANDBOX 中对 ThreadLocal 使用不当导致发生泄漏,从而引发关闭 ClassLoader 失败,最终导致 Metaspace OutOfMemoryError。
JVM-SANDBOX 社区目前已经处于不活跃状态,我们将 JVM-SANDBOX fork 到了 CNCF 沙箱项目 ChaosBlade 中,后面将独立维护。本次修改的相关代码已经提交到社区:PR
在公司中有不少项目使用了 JVM-SANDBOX,如果有类似这种多次挂载和卸载的操作,需要注意下这个问题。
版权声明: 本文为 InfoQ 作者【柠檬汁Code】的原创文章。
原文链接:【http://xie.infoq.cn/article/b186c5a7e2fddc50bbb434f68】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论