作者:张斌斌

介绍

ChaosBlade Java 场景性能优化,那些你不知道的事

ChaosBlade 是阿里巴巴开源的一款遵从混沌工程原理和混沌试验模型的试验注入东西,帮助企业提高分布式体系的容错才能,而且在企业上云或往云原生体系迁移进程中事务接连性保证。

现在支撑的场景有:基础资源、Java 应用、C++ 应用、Docker 容器以及 Kubernetes 平台。该项目将场景按范畴完成封装成单独的项目,不仅能够使范畴内场景标准化完成,而且十分方便场景水平缓笔直扩展,经过遵从混沌试验模型,完成 ChaosBlade cli 统一调用。

不过 Java 场景下的毛病注入现在有一些功能问题,首要体现在毛病注入时会让 CPU 的运用率大幅度颤动,严峻状况下或许会导致 CPU 的运用率 100%。这种状况对于线下服务的影响还好,可是对于线上服务就比较严峻了,由于 CPU 的运用率较高有或许会导致服务的全体功能变差,然后影响接口的耗时。

经过对 ChaosBlade Java 场景的功能优化,使 CPU 在毛病注入时的颤动得到了有效的控制,不会再呈现 CPU 运用率抵达 100% 的颤动,经过测验在线上 8C,4G,QPS 3K 左右的服务实例上注入 Dubbo 自界说抛反常的毛病,CPU 的运用率能够控制在 40% 左右的瞬时颤动规模内,功能全体提高近 2.5 倍。

本文将会详细的介绍影响功能的问题点以及是如何对这些问题进行优化的。

Java 场景

在介绍前先了解下 ChaosBlade Java 场景的注入流程。

ChaosBlade Java 场景性能优化,那些你不知道的事

Java 场景的毛病注入是基于字节码增强结构 JVM-Sandbox 完成的,注入一个毛病分为两步:

  1. ChaosBlade 履行 prepare 指令,触发 sandbox 对方针 JVM 挂载 Java agent。
  2. ChaosBlade 履行 create 指令,触发 sandbox 对方针 JVM 进行字节码增强,然后抵达毛病注入的目的。

Prepare(挂载)阶段优化

现象

本地模仿一个简单的 HTTP 服务,控制其 CPU Idle 在 50% 左右,当履行 blade prepare jvm –pid 挂载 agent 后,发现 CPU 闲暇率敏捷下降,而且下降的幅度较大。在生产中进行毛病注入有或许会直接让 Idle 掉低然后触发告警:

ChaosBlade Java 场景性能优化,那些你不知道的事

定位

经过采集 CPU profile 生成火焰图来调查在履行 blade prepare 时 CPU 的运用状况,如下图能够看到 loadPlugins 办法是资源耗费的重灾区。

ChaosBlade Java 场景性能优化,那些你不知道的事

loadPlugins办法中首要是加载 ChaosBlade Java 中支撑的全部插件,例如 dubbo,redis,kafka 等。当加载了这些插件后就能够进行毛病注入了。加载插件的进程中会对插件中界说的类以及办法进行字节码增强。

ChaosBlade Java 场景性能优化,那些你不知道的事

导致 CPU 耗费的问题就在于加载全量的插件耗时较大,而咱们毛病注入时会挑选详细某个插件进行毛病注入,显然全量加载并不是最优解

优化

优化思路:既然毛病注入时会挑选详细的插件,那么经过懒加载的办法即可处理,当咱们要针对哪一个插件毛病注入就加载哪个插件,加载的粒度变小,CPU 的耗费天然就小了:

ChaosBlade Java 场景性能优化,那些你不知道的事

中心代码:

在毛病注入阶段,经过指定的插件进行懒加载。

private void lazyLoadPlugin(ModelSpec modelSpec, Model model) throws ExperimentException {
    PluginLifecycleListener listener = ManagerFactory.getListenerManager().getPluginLifecycleListener();
    if (listener == null) {
        throw new ExperimentException("can get plugin listener");
    }
    PluginBeans pluginBeans = ManagerFactory.getPluginManager().getPlugins(modelSpec.getTarget());
    if (pluginBeans == null) {
        throw new ExperimentException("can get plugin bean");
    }
    if (pluginBeans.isLoad()) {
        return;
    }
    listener.add(pluginBean);
    ManagerFactory.getPluginManager().setLoad(pluginBeans, modelSpec.getTarget());
}

详细代码 PR:github.com/ChaosBlade-…

改进后作用

CPU Idle 下降幅度下降

ChaosBlade Java 场景性能优化,那些你不知道的事

火焰图中的 CPU 运用率简直“消失”

ChaosBlade Java 场景性能优化,那些你不知道的事

Create(注入)阶段优化

在实践运用中发现毛病注入导致 CPU Idle 跌底的状况比较多,跌底的持续时刻是比较时刻短的根本都在 20S 左右,有一些状况是和方针服务的事务代码有联系或许是和方针服务的 jvm 参数设置有关,本文只介绍由 ChaosBlade 导致的或直接导致的 CPU Idle 跌底问题。

ChaosBlade Java 场景性能优化,那些你不知道的事

CPU Idle 跌底:这儿指的是 CPU 闲暇率下降为 0,一起意味着 CPU 运用率抵达了 100%。

Dubbo 毛病优化

  • 问题描绘

ChaosBlade 中支撑对 dubbo provider 或许 consumer 进行毛病注入(例如抛反常),当一个服务既是 provider 又是 consumer 的时候,假如对 provider 毛病注入则会触发 bug,有或许会导致 CPU Idle 跌底。

正常状况:一个既是 provider 又是 consumer 的服务,它的恳求处理流程是流量会首要进入到 provider 经过处理后交由事务逻辑履行,最后经过 consumer 将恳求转发出去。

ChaosBlade Java 场景性能优化,那些你不知道的事

针对 consumer 毛病注入:当运用 ChaosBlade 对 consumer 进行毛病注入时,流量抵达 consumer 就会抛出反常,不会将流量真实的转发出去,然后抵达一个模仿毛病发生的作用。

ChaosBlade Java 场景性能优化,那些你不知道的事

针对 provider 毛病注入:当运用 ChaosBlade 对 provider 进行毛病注入时,流量抵达 provider 就会抛出反常,不会将流量向下转发。

ChaosBlade Java 场景性能优化,那些你不知道的事

上面说的都是预期作用,实践上 ChaosBlade 无论是对 provider 或许 consumer 进行毛病注入时,都会一起 provider 以及 consumer 一起进行毛病注入,这就有或许形成额定的资源糟蹋。

  1. 字节码增强的类变的多了

  2. 例如当注入 provider 毛病时,咱们期望流量不要经过事务逻辑,由于一旦是在 consumer 也抛出了反常,流量回来时天然要经过事务逻辑的反常处理(例如打印 error 日志,重试等),这就有或许由于事务逻辑的处理问题导致 CPU Idle 下降。

ChaosBlade Java 场景性能优化,那些你不知道的事

问题原因:由于 ChaosBlade 的字节码增强逻辑是依照插件的粒度进行的,例如 dubbo 就属于一个插件,不过像 dubbo 和 kafka 这种既有针对 provider 又有针对 consumer 毛病注入的插件就会一起对 provider 和 consumer 都注入毛病了。

  • 优化

在加载插件的时候,依据详细加载的插件名按需加载,例如履行指令:

./blade create dubbo throwCustomException --provider --exception Java.lang.Exception --service org.apache.dubbo.UserProvider --methodname GetUser

代表实践要针对 dubbo 的 provider 注入毛病,那么就只加载 provider 插件进行字节码增强。

修改的中心代码:

private void lazyLoadPlugin(ModelSpec modelSpec, Model model) throws ExperimentException {
    // ...... 省掉
    for (PluginBean pluginBean : pluginBeans.getPluginBeans()) {
        String flag = model.getMatcher().get(pluginBean.getName());
        if ("true".equalsIgnoreCase(flag)) {
            listener.add(pluginBean);
            break;
        }
        listener.add(pluginBean);
    }
    // ...... 省掉
}
}

相关 PR:github.com/ChaosBlade-…

自界说脚本毛病优化

  • 问题描绘

在运用 ChaosBlade 注入自界说脚本的毛病时导致 CPU Idle 跌底,自界说脚本是 ChaosBlade jvm 毛病中支撑的一种办法,指的是用户能够编写恣意一段 Java 代码,然后将这段代码注入到对应的方针类和办法上,这样的办法灵活度十分高,经过 ChaosBlade 的自界说脚本注入毛病能够做许多事情。

ChaosBlade 指令:

./blade c jvm script --classname com.example.xxx.HelloController --methodname Hello --script-content .....
  • 问题排查

咱们抓取了毛病注入时的火焰图以及 jstack 日志,经过 jstack 打印的线程仓库发现了一些问题:

  1. 在毛病注入后线程数量会突然上升

  2. 有部分线程是 blocked 状况

毛病注入前:

ChaosBlade Java 场景性能优化,那些你不知道的事

毛病注入后:

ChaosBlade Java 场景性能优化,那些你不知道的事

BLOCKED 的线程仓库:

Stack Trace is:
Java.lang.Thread.State: RUNNABLE
at Java.util.zip.ZipFile.getEntryTime(Native Method)
at Java.util.zip.ZipFile.getZipEntry(ZipFile.Java:586)
at Java.util.zip.ZipFile.access$900(ZipFile.Java:60)
at Java.util.zip.ZipFile$ZipEntryIterator.next(ZipFile.Java:539)
- locked <0x00000006c0a57670> (a sun.net.www.protocol.jar.URLJarFile)
at Java.util.zip.ZipFile$ZipEntryIterator.nextElement(ZipFile.Java:514)
at Java.util.zip.ZipFile$ZipEntryIterator.nextElement(ZipFile.Java:495)
at Java.util.jar.JarFile$JarEntryIterator.next(JarFile.Java:258)
at Java.util.jar.JarFile$JarEntryIterator.nextElement(JarFile.Java:267)
at Java.util.jar.JarFile$JarEntryIterator.nextElement(JarFile.Java:248)
at com.alibaba.ChaosBlade.exec.plugin.jvm.script.Java.JavaCodeScriptEngine$InMemoryJavaFileManager.processJar(JavaCodeScriptEngine.Java:421)
at com.alibaba.ChaosBlade.exec.plugin.jvm.script.Java.JavaCodeScriptEngine$InMemoryJavaFileManager.listUnder(JavaCodeScriptEngine.Java:401)
at com.alibaba.ChaosBlade.exec.plugin.jvm.script.Java.JavaCodeScriptEngine$InMemoryJavaFileManager.find(JavaCodeScriptEngine.Java:390)
at com.alibaba.ChaosBlade.exec.plugin.jvm.script.Java.JavaCodeScriptEngine$InMemoryJavaFileManager.list(JavaCodeScriptEngine.Java:375)
at com.sun.tools.Javac.api.ClientCodeWrapper$WrappedJavaFileManager.list(ClientCodeWrapper.Java:231)
at com.sun.tools.Javac.jvm.ClassReader.fillIn(ClassReader.Java:2796)
at com.sun.tools.Javac.jvm.ClassReader.complete(ClassReader.Java:2446)
at com.sun.tools.Javac.jvm.ClassReader.access$000(ClassReader.Java:76)
at com.sun.tools.Javac.jvm.ClassReader$1.complete(ClassReader.Java:240)
at com.sun.tools.Javac.code.Symbol.complete(Symbol.Java:574)
at com.sun.tools.Javac.comp.MemberEnter.visitTopLevel(MemberEnter.Java:507)
at com.sun.tools.Javac.tree.JCTree$JCCompilationUnit.accept(JCTree.Java:518)
at com.sun.tools.Javac.comp.MemberEnter.memberEnter(MemberEnter.Java:437)
at com.sun.tools.Javac.comp.MemberEnter.complete(MemberEnter.Java:1038)
at com.sun.tools.Javac.code.Symbol.complete(Symbol.Java:574)
at com.sun.tools.Javac.code.Symbol$ClassSymbol.complete(Symbol.Java:1037)
at com.sun.tools.Javac.comp.Enter.complete(Enter.Java:493)
at com.sun.tools.Javac.comp.Enter.main(Enter.Java:471)
at com.sun.tools.Javac.main.JavaCompiler.enterTrees(JavaCompiler.Java:982)
at com.sun.tools.Javac.main.JavaCompiler.compile(JavaCompiler.Java:857)
at com.sun.tools.Javac.main.Main.compile(Main.Java:523)
at com.sun.tools.Javac.api.JavacTaskImpl.doCall(JavacTaskImpl.Java:129)
at com.sun.tools.Javac.api.JavacTaskImpl.call(JavacTaskImpl.Java:138)
at com.alibaba.ChaosBlade.exec.plugin.jvm.script.Java.JavaCodeScriptEngine.compileClass(JavaCodeScriptEngine.Java:149)
at com.alibaba.ChaosBlade.exec.plugin.jvm.script.Java.JavaCodeScriptEngine.compile(JavaCodeScriptEngine.Java:113)
at com.alibaba.ChaosBlade.exec.plugin.jvm.script.base.AbstractScriptEngineService.doCompile(AbstractScriptEngineService.Java:82)
at com.alibaba.ChaosBlade.exec.plugin.jvm.script.base.AbstractScriptEngineService.compile(AbstractScriptEngineService.Java:69)
at com.alibaba.ChaosBlade.exec.plugin.jvm.script.model.DynamicScriptExecutor.run(DynamicScriptExecutor.Java:74)
at com.alibaba.ChaosBlade.exec.common.injection.Injector.inject(Injector.Java:73)
at com.alibaba.ChaosBlade.exec.common.aop.AfterEnhancer.afterAdvice(AfterEnhancer.Java:46)
at com.alibaba.ChaosBlade.exec.common.plugin.MethodEnhancer.afterAdvice(MethodEnhancer.Java:47)
at com.alibaba.ChaosBlade.exec.bootstrap.jvmsandbox.AfterEventListener.onEvent(AfterEventListener.Java:93)
at com.alibaba.jvm.sandbox.core.enhance.weaver.EventListenerHandler.handleEvent(EventListenerHandler.Java:116)
at com.alibaba.jvm.sandbox.core.enhance.weaver.EventListenerHandler.handleOnEnd(EventListenerHandler.Java:426)
at com.alibaba.jvm.sandbox.core.enhance.weaver.EventListenerHandler.handleOnReturn(EventListenerHandler.Java:363)

经过线程仓库能够看到线程首要是在解压 jar 文件是阻塞了,为什么会阻塞到这儿呢?

其实是在 ChaosBlade 注入自界说脚本时,自界说脚本(Java 代码)仅仅被当作一段字符串来处理,当真实的激活插件时会把这段字符串解析,然后变成 Java 代码让 jvm 进行加载编译并履行这段代码。

ChaosBlade Java 场景性能优化,那些你不知道的事

问题就在这儿,当毛病注入时外部流量也是在连绵不断的调用当时服务的。那么依照上面说的逻辑就有或许在激活插件时,由于外部流量也在不断调用,导致很多恳求都来解析自界说脚本,这样的话就形成了线程被 blocked,由于解析自界说脚本到正确的让 jvm 加载它,这个进程是相对复杂且缓慢的,而且有的地方是要保证线程安全的。

其实 ChaosBlade 也做了缓存,只需自界说脚本被编译过一次,后边的恳求就会直接履行这个脚本了,但这样的缓存在并发恳求的场景下编译作用并不好

  • 优化

经过上面的排查,其实应该能够想到优化手段了,那便是要让自界说脚本的加载时刻提早。

ChaosBlade 注入毛病分为两步,第一步挂载 agent 时拿不到自界说脚本信息,那么就在第二步激活插件前进行加载(由于一旦插件被激活后就有流量会履行到毛病注入的埋点办法然后触发脚本的编译了)

ChaosBlade Java 场景性能优化,那些你不知道的事

这个优化思路不仅仅适用于自界说脚本毛病,例如自界说抛反常毛病也是能够的。

在自界说抛反常的毛病履行中,也是当流量过来时才会依据用户输入的反常类字符进行反射加载,类的加载(classloader)底层也是需求加锁的,所以也有或许形成线程 blocked.

优化内容:添加毛病前置履行接口,针对需求在毛病注入前,履行某些动作的插件能够去完成它。

public interface PreActionExecutor {
    /**
     * Pre run executor
     *
     * @param enhancerModel
     * @throws Exception
     */
    void preRun(EnhancerModel enhancerModel) throws ExperimentException;
}
private void applyPreActionExecutorHandler(ModelSpec modelSpec, Model model)
        throws ExperimentException {
    ActionExecutor actionExecutor = modelSpec.getActionSpec(model.getActionName()).getActionExecutor();
    if (actionExecutor instanceof PreActionExecutor) {
        EnhancerModel enhancerModel = new EnhancerModel(EnhancerModel.class.getClassLoader(), model.getMatcher());
        enhancerModel.merge(model);
        ((PreActionExecutor) actionExecutor).preRun(enhancerModel);
    }
}

相关 PR:

github.com/ChaosBlade-…

日志打印优化

  • 问题描绘

日志打印导致的 CPU Idle 跌底问题首要有两方面:

  1. 事务体系内部自身的日志结构,例如运用 log4j/logback 同步日志打印,假如在注入毛病后(例如抛反常)很有或许由于事务体系处理反常并打印日志导致线程大面积被 blocked。由于同步日志打印是需求加锁处理,而且反常仓库是相对内容较多的打印也相对耗时,然后当 QPS 较高时或许会导致很多线程被阻塞。
- locked <0x00000006f08422d0> (a org.apache.log4j.DailyRollingFileAppender)
at org.apache.log4j.helpers.AppenderAttachableImpl.appendLoopOnAppenders(AppenderAttachableImpl.Java:66)
at org.apache.log4j.Category.callAppenders(Category.Java:206)
- locked <0x00000006f086daf8> (a org.apache.log4j.Logger)
at org.apache.log4j.Category.forcedLog(Category.Java:391)
at org.apache.log4j.Category.log(Category.Java:856)
at org.slf4j.impl.Log4jLoggerAdapter.log(Log4jLoggerAdapter.Java:601)

ChaosBlade Java 场景性能优化,那些你不知道的事

  1. ChaosBlade 自身的日志打印,每次毛病注入规则匹配成功时都会输出 info 日志:
LOGGER.info("Match rule: {}", JsonUtil.writer().writeValueAsString(model));

在输出日志的时候都会将毛病模型运用 jackson 序列化输出,这会触发类加载(加锁操作)当有很多恳求时或许会导致很多线程阻塞。

Java.lang.Thread.State: RUNNABLE
at Java.lang.String.charAt(String.Java:657)
at Java.io.UnixFileSystem.normalize(UnixFileSystem.Java:87)
at Java.io.File.<init>(File.Java:279)
at sun.net.www.protocol.file.Handler.openConnection(Handler.Java:80)
- locked <0x00000000c01f2740> (a sun.net.www.protocol.file.Handler)
at sun.net.www.protocol.file.Handler.openConnection(Handler.Java:72)
- locked <0x00000000c01f2740> (a sun.net.www.protocol.file.Handler)
at Java.net.URL.openConnection(URL.Java:979)
at sun.net.www.protocol.jar.JarFileFactory.getConnection(JarFileFactory.Java:65)
at sun.net.www.protocol.jar.JarFileFactory.getPermission(JarFileFactory.Java:154)
at sun.net.www.protocol.jar.JarFileFactory.getCachedJarFile(JarFileFactory.Java:126)
at sun.net.www.protocol.jar.JarFileFactory.get(JarFileFactory.Java:81)
- locked <0x00000000c00171f0> (a sun.net.www.protocol.jar.JarFileFactory)
at sun.net.www.protocol.jar.JarURLConnection.connect(JarURLConnection.Java:122)
at sun.net.www.protocol.jar.JarURLConnection.getInputStream(JarURLConnection.Java:152)
at Java.net.URL.openStream(URL.Java:1045)
at Java.lang.ClassLoader.getResourceAsStream(ClassLoader.Java:1309)
......
at Java.lang.reflect.Method.invoke(Method.Java:498)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.Java:689)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.Java:755)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.Java:178)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.Java:728)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.Java:755)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.Java:178)
at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.Java:480)
at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.Java:319)
at com.fasterxml.jackson.databind.ObjectWriter$Prefetch.serialize(ObjectWriter.Java:1516)
at com.fasterxml.jackson.databind.ObjectWriter._writeValueAndClose(ObjectWriter.Java:1217)
at com.fasterxml.jackson.databind.ObjectWriter.writeValueAsString(ObjectWriter.Java:1086)
at com.alibaba.ChaosBlade.exec.common.injection.Injector.inject(Injector.Java:69)
at com.alibaba.ChaosBlade.exec.common.aop.AfterEnhancer.afterAdvice(AfterEnhancer.Java:46)
at com.alibaba.ChaosBlade.exec.common.plugin.MethodEnhancer.afterAdvice(MethodEnhancer.Java:47)
at com.alibaba.ChaosBlade.exec.bootstrap.jvmsandbox.AfterEventListener.onEvent(AfterEventListener.Java:93)
at com.alibaba.jvm.sandbox.core.enhance.weaver.EventListenerHandler.handleEvent(EventListenerHandler.Java:116)
at com.alibaba.jvm.sandbox.core.enhance.weaver.EventListenerHandler.handleOnEnd(EventListenerHandler.Java:426)
at com.alibaba.jvm.sandbox.core.enhance.weaver.EventListenerHandler.handleOnReturn(EventListenerHandler.Java:363)
at Java.com.alibaba.jvm.sandbox.spy.Spy.spyMethodOnReturn(Spy.Java:192)
  • 优化

关于事务体系的日志打印引发的线程 block,不在 ChaosBlade 优化的规模内,咱们有遇到相似状况能够自行处理。

处理的思路:

  1. 日志同步打印改为异步打印

  2. ChaosBlade 自界说抛反常时的错误仓库能够尽量疏忽,削减日志输出的内容。

关于 ChaosBlade 打印日志的优化就比较简单了,只需求将 match rule 序列化毛病模型的部分替换掉即可。将 Model 完成 toString,打印时直接打印 Model 即可。

LOGGER.info("Match rule: {}", model);
@Override
public String toString() {
    return "Model{" +
            "target='" + target + ''' +
            ", matchers=" + matcher.getMatchers().toString() +
            ", action=" + action.getName() +
            '}';
}

相关 PR:

github.com/ChaosBlade-…

Metaspace OOM 优化

Metaspace 是什么,引证官方介绍:

PMetaspace is a native (as in: off-heap) memory manager in the hotspot.
It is used to manage memory for class metadata. Class metadata are allocated when classes are loaded.
Their lifetime is usually scoped to that of the loading classloader – when a loader gets collected, all class metadata it accumulated are released in bulk.

简单来说:Metapace 是一块非堆内存,用来存储类的元数据,当加载类的时候会在 Metaspace 中分配空间存储类的元数据,当某个 ClassLoader 封闭时会对应释放掉对类元数据的引证,当触发 GC 时这部分类元数据占用的空间即可在 Metaspace 中被收回掉。

现象

  • 日志体现

在运用 ChaosBlade 注入无效后,登陆方针机器上调查日志,首要发现 jvm-sandbox 在 attach 方针 jvm 时失利

ChaosBlade Java 场景性能优化,那些你不知道的事

其次看到更要害的日志:Metaspace 溢出了!!!

ChaosBlade Java 场景性能优化,那些你不知道的事

定位

在文章开端介绍了ChaosBlade注入Java毛病的流程,知道在毛病注入时会将 jvm-sandbox 动态的挂载(attach)到方针进程 JVM 上,在 attach 后会加载 sandbox 内部 jar 以及 sandbox 的自界说模块 jar 等,在这个进程中会加载很多的类,当加载类时会分配 Metaspace 空间存储类的元数据。

这儿有两个考虑点:

1. 会不会是由于事务服务 JVM 的 Metaspace 空间设置的太小?

2. Metaspace 的 GC 没有触发或许是有泄露导致类的元数据收回不掉?

登陆到方针机器上运用 jinfo 调查 jvm 的参数,发现 MaxMetaspaceSize 设置了 128M,这个值的确不大,由于 MaxMetaspaceSize 的默许是 -1(无限制,受限于本地内存)。

让事务服务调整 MaxMetaspaceSize 参数改为 256M,然后重启 Java 进程,再次毛病注入 的确没问题了,毛病正常生效了。

但实践问题没怎么简单,在接连注入屡次后仍然呈现 Metaspace OOM 毛病仍旧无效。看来应该是毛病铲除时无法收回掉 Metaspace 中对应的空间。

  • 本地复现

由于 ChaosBlade Java 毛病注入实质是 jvm-sandbox 的一个插件,类加载,字节码增强等中心逻辑都在 jvm-sandebox 上,所以咱们直接将问题定位在 jvm-sandbox 上,运用 jvm-sandbox 供给的 demo 项目进行复现。

发动参数设置了 MaxMetaspaceSize=30M,由于 demo 模块类十分少,其次为了快速的复现 OOM。

TraceClassLoading 和 TraceClassUnloding 参数则是为了调查 JVM-SANDBOX 在毛病注入和铲除时加载/卸载类的信息。

ChaosBlade Java 场景性能优化,那些你不知道的事

在屡次注入以及铲除的操作后,复现了线上事务呈现的 Metaspace OOM,能够看到在屡次注入的进程中,Metaspace 一向没有被收回过,占用空间曲线是一路上升。

ChaosBlade Java 场景性能优化,那些你不知道的事

Metaspace OOM 是由于 Metaspace 没有进行过收回,Metaspace 收回的条件是 ClassLoader 封闭,而 JVM-SANDBOX 在 shutdown 时会封闭 ClassLoader。JVM-SANDBOX 中自界说的 ClassLoader 都是继承了 URLClassLoader,URLClassLoader 的封闭办法 官方介绍:

How to Close a URLClassLoader?
The URLClassLoader close() method effectively eliminates the problem of how to support updated implementations of the classes and resources loaded from a particular codebase, and in particular from JAR files. In principle, once the application clears all references to a loader object, the garbage collector and finalization mechanisms will eventually ensure that all resources (such as the JarFile objects) are released and closed.

简单来说:当 classLoader 加载的所有类没有被引证时即可被封闭。

  • 猜测

当毛病铲除时 jvm-sandbox 中的类还有被引证的状况导致 classloader 封闭失利了。

  • 验证猜测

在毛病铲除后,在方针服务的办法上 debug 看一下线程信息,果然在 threadLocal 中找到了两个 jvm-sandbox 的内部类的引证。说明猜测是对的,问题的原因便是呈现在这儿了

内部类的引证:

EventProcesser$Process

SandboxProtector

ChaosBlade Java 场景性能优化,那些你不知道的事

jvm-sandbox 源码在这儿就不带咱们分析了,感兴趣的能够检查这篇文章。首要是 jvm-sandbox 的代码完成有 bug,在以下两种状况会导致 processRef 的 threadLocal 没有及时 remove 形成走漏

  1. 假如在履行注入毛病的进程中,进行毛病铲除会导致走漏。如下:

ChaosBlade Java 场景性能优化,那些你不知道的事

  1. 假设运用了 jvm-sandbox 的特性-流程改变(例如当即回来,当即抛出反常),实质也是 thread local 没有及时 remove,导致形成了走漏

优化

由于jvm-sandbox项目已经不在活跃了,咱们将jvm-sandbox项目fork到了ChaosBlade中。

优化后的相关pr:

github.com/ChaosBlade-…

改进后作用

发动参数还是相同的 MaxMetaspaceSize=30M,经过优化后屡次注入和铲除不会呈现 Metaspace OOM,Metaspace 能够被收回了。

ChaosBlade Java 场景性能优化,那些你不知道的事

卸载类的信息也打印出来了

ChaosBlade Java 场景性能优化,那些你不知道的事

再次优化

虽然咱们处理了 JVM-Sandbox 的 ThreadLocal 走漏问题,可是由于 Metaspace 的内存分配以及收回机制还是有或许导致 OOM!!!

关于 Metaspace 的内存分配以及收回的相关内容能够参考文章:

www.javadoop.com/post/metasp…

上面的优化基础上还需求在每一次毛病注入前触发一次 full gc,目的是让上一次 jvm-sandbox 占用的元空间强制释放掉。

改动点:

public static void agentmain(String featureString, Instrumentation inst) {
    System.gc();
    LAUNCH_MODE = LAUNCH_MODE_ATTACH;
    final Map<String, String> featureMap = toFeatureMap(featureString);
    writeAttachResult(
            getNamespace(featureMap),
            getToken(featureMap),
            install(featureMap, inst)
    );
}

这样的改动的虽然能处理一部分场景下的 Metaspace OOM,可是也有坏处,这样会导致每一次毛病注入挂载 agent 时都会触发一次 full GC,到现在为止还没有更好的处理办法,后边能够考虑将这个 full gc 做成装备,经过 sandbox 脚原本进行敞开,让用户按需挑选是否要在注入前强制 full gc 一次。

相关 PR:

github.com/ChaosBlade-…

那么如何彻底处理 Metaspace OOM 问题呢?先说定论:不能彻底处理,由于在运用反射的状况下会主动生成一些,所以在事务代码中很难去封闭,那就导致 DelegatingClassLoader 会一向存活,然后引发 Metaspace 碎片化的问题,最终导致 Metaspace 空间无法被正确的收回(这部分内容比较复杂,一言两语很难描绘清楚)

主动生成:

sun.reflect.DelegatingClassLoader

考虑

关于 Metaspace OOM 的问题,其实优化是一方面,换个角度想也许是咱们运用的办法不正确。在咱们的事务场景下是会频繁的对一个服务进行毛病注入&卸载,每次的注入点不同。

如下图:相当于每次都是重复 1-4 步骤,那么实践上咱们并不需求这么做,由于在第一步时 sandbox 初始化会加载很多的类,填充 metaspace。而咱们每次注入仅仅毛病点不同,agent 不需求从头挂载,所以只需求重复的进行第二步和第三步即可。在第二步和第三步中仅仅触发 sandbox 的激活和冻住事情,成本十分小。

后边咱们会依据这个思路,对整个毛病注入流程进行优化,信任会有更多的提高。

ChaosBlade Java 场景性能优化,那些你不知道的事

JIT(及时编译)导致 CPU 颤动

问题描绘

在 Java 中编译器首要分为三类:

  1. 前端编译器:JDK 的 Javac,即把*.Java 文件转变成*.class 文件的进程

  2. 即时编译器:HotSpot 虚拟机的 C1,C2 编译器,Graal 编译器,JVM 运行期把字节码转变成本地机器码的进程

  3. 提早编译器:JDK 的 Jaotc,GNU Compiler for the Java(GCJ)等

在经过 ChaosBlade 进行毛病注入后,实质是运用 jvm-sandbox 对方针类和放火进行了字节码增强。然后也会触发 JVM 的即时编译(JIT- Just In Time)。

JVM 的即时编译目的是让字节码转换为机器码,然后能够更高效的履行。可是在 JVM 即时编译的进程中是会耗费资源的,最典型的场景便是 Java 的服务 在刚发动时 CPU 的运用率都会相对较高,一段时刻后逐渐康复平稳,呈现这种现象部分状况下是由于即时编译的介入导致的。

关于即时编译的内容能够参考文章:

xie.infoq.cn/article/dac…

对于即时编译引发的 CPU 运用率升高是正常现象,假如遇到 JIT 占用的 CPU 运用率特别高,咱们需求特别重视下即时编译的参数即可。例如是否启用了分层编译,编译的线程数量等等。

总结

ChaosBlade 支撑丰富的毛病注入场景,尤其是在Java 生态中支撑很多的插件。对于Java 场景的毛病注入优势比较明显。

经过对上面介绍的问题进行优化,运用ChaosBlade进行Java场景的毛病注入不会再导致CPU Idle跌底,即使在线上运行的服务进行毛病注入也会将CPU的颤动控制在一个较小的动摇规模。

但由于JVM JIT的问题在毛病注入时CPU的瞬时颤动还是无法避免,假如咱们有什么好的办法/想法也欢迎提交 issue/pr 来一起沟通~

相关链接

ChaosBlade 官方网址

chaosblade.io/

ChaosBlade Github

github.com/chaosblade-…

ChaosBlade 钉钉社区沟通群 :23177705

作者简介:

张斌斌(Github账号:binbin0325)ChaosBlade Committer , Nacos PMC ,Apache Dubbo-Go Committer, Sentinel-Golang Committer 。现在首要重视于混沌工程、中间件以及云原生方向。