咱们在github上收到社区用户的问题反馈:

用户原先利用曾经接入skywalking,须要再接入数列的LinkAgent时启动会抛java.lang.UnsupportedOperationException,导致利用启动失败。

 也就是说在不批改代码的状况下如果须要启动利用,skywalking和LinkAgent只能存在一个,两者不能同时存在。skywalking与LinkAgent不兼容该如何解决?本文将围绕这个问题的具体开展。 skywalking是分布式系统的应用程序性能监督工具,大家绝对相熟,可有的人并不理解Agent,这里略微科普一下:

agent是什么

介绍javaagent之前也要介绍另一个概念JVMTI。 JVMTI是JDK提供的一套用于开发JVM监控, 问题定位与性能调优工具的通用编程接口(API)。 通过JVM TI,咱们能够开发各式各样的JVMTI Agent。这个Agent的表现形式是一个以C/C++语言编写的动静共享库。 javaagent能够帮忙咱们疾速应用JVMTI的性能,又不须要重写编写C/C++的底层库。

javaagent是依赖java底层提供的一个叫instrument的JVMTI Agent。这个agent又叫JPLISAgent(Java Programming Language Instrumentation Services Agent)

简略来说,javaagent是一个JVM的“插件”。 在java运行命令中 javaagent是一个参数,用来指定agent。

agent能干什么

  • 能够在加载class文件之前进行拦挡并把字节码做批改。
  • 能够在运行期对已加载类的字节码做变更,然而这种状况下会有很多的限度。
  • 还有其余一些小众的性能:

    • 获取所有曾经加载过的类
    • 获取所有曾经初始化过的类(执行过 clinit 办法,是下面的一个子集)
    • 获取某个对象的大小
    • 将某个jar退出到bootstrap classpath里作为高优先级被bootstrapClassloader 加载
    • 将某个jar退出到classpath里供AppClassloard去加载
    • 设置某些native办法的前缀,次要在查找native办法的时候做规定匹配

总的来说能够让JVM依照咱们的预期逻辑去执行。 最次要的也是应用最广的性能就是对字节码的批改。通过对字节码的批改咱们就能够实现对JAVA底层源码的重写,也正好能够满足我之前的需要。 咱们还能够做:

  • 齐全非侵入式的进行代码埋点,进行系统监控
  • 批改JAVA底层源码,进行JVM自定义
  • 实现AOP动静代理

agent 的两种应用形式

1.在 JVM 启动的时候加载,通过 javaagent 启动参数 java -javaagent:myagent.jar MyMain,这种形式在程序 main 办法执行之前执行 agent 中的 premain 办法

2.在 JVM 启动后 Attach,通过 Attach API 进行加载,这种形式会在 agent 加载当前执行 agentmain 办法

这两个办法都有两个参数

第一个 agentArgument 是 agent 的启动参数,能够在 JVM 启动命令行中设置,比方java -javaagent:<jarfile>=appId:agent-demo,agentType:singleJar test.jar的状况下 agentArgument 的值为 “appId:agent-demo,agentType:singleJar”。

第二个 instrumentation 是 java.lang.instrument.Instrumentation 的实例,能够通过 addTransformer 办法设置一个 ClassFileTransformer。

第一步:问题剖析

异样信息是说在从新定义某个类的时候,原先的父类或者接口类产生了扭转,导致从新定义失败。可是在没有应用skywalking的时候,数列LinkAgent与其余的一些agent并没有呈现过相似的兼容性问题。 在github上搜寻发现发现有人提过skywalking和arthas的兼容性问题。链接

 问题起因skywalking官网也给出了回答: 当 Java 应用程序启动时,SkyWalking 代理应用 ByteBuddy 转换类。 ByteBuddy 每次都会生成具备不同随机名称的辅助类。 当另一个 Java 代理从新转换同一个类时,它会触发 SkyWalking 代理再次加强该类。 因为 ByteBuddy 从新生成了字节码,批改了字段和导入的类名,JVM 对类字节码的验证失败,因而从新转换类将不胜利。

 所以问题还是由ByteBuddy产生的,而数列agent底层应用的是ASM不会产生对应的问题。

第二步:本地复现

从后面的剖析曾经得悉skywalking与LinkAgent的不兼容问题背地的起因,可要想无效解决就得先本地复现这个问题,编写DemoApplication手动的去触发retransform,并且在retransform前后打印jvm中的所有类名。

@SpringBootApplicationpublic class DemoApplication {    public static void main(String[] args) throws InterruptedException, UnmodifiableClassException {        SpringApplication.run(DemoApplication.class, args);        test();    }    public static void test() throws InterruptedException, UnmodifiableClassException {        Instrumentation instrumentation = ByteBuddyAgent.install();        System.err.println("before =============");        printAllTestControllerClasses(instrumentation);        reTransform(instrumentation);        reTransform(instrumentation);        reTransform(instrumentation);        System.err.println("after =============");        printAllTestControllerClasses(instrumentation);    }    public static void reTransform(Instrumentation instrumentation) throws UnmodifiableClassException {        ClassFileTransformer transformer = new ClassFileTransformer() {            @Override            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,                ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {                return null;            }        };        try {            instrumentation.addTransformer(transformer, true);            try {                instrumentation.retransformClasses(TestController.class);            } catch (Throwable e) {                e.printStackTrace();            }        } finally {            instrumentation.removeTransformer(transformer);        }    }    public static void printAllTestControllerClasses(Instrumentation instrumentation) {        Class<?>[] classes = instrumentation.getAllLoadedClasses();        for (Class<?> clazz : classes) {            if (clazz.getName().startsWith(TestController.class.getName())) {                System.out.println(clazz.getName());            }        }    }

1.不加skywalking间接启动

后果如下:在retransform前后都是有 com.example.demo.TestController

2.指定skywalking启动

通过 -javaagent:${path}/apache-skywalking-apm-6.4.0-bin/agent/skywalking-agent.jar 启动参数来运行我的项目。 发现在retransform之前多生成了 com.example.demo.TestController$auxiliary$tTwQs5Cs和com.example.demo.TestController$auxiliary$rZrClpy4 两个类。 在retransform的时候抛出了java.lang.UnsupportedOperationException retransform之后又多生成了三个匿名外部类。

第三步:给出正当的解决方案

1.增加jvm的启动参数

skywalking官网8.1.0当前的版本能够通过增加jvm的启动参数来解决这个问题。

也能够通过 -Dskywalking.agent.class_cache_mode=MEMORY 或 -Dskywalking.agent.class_cache_mode=FILE 命令来指定是通过内存缓存还是文件缓存。 留神:然而这些参数在8.1.0当前的skywalking中才有,低于8.1.0版本的skywalking还是无奈解决上述问题。

2.新写额定的agent来实现 skywalking cache性能

低于 8.1.0 版本的skywalking能够新写一个额定的agent来实现 skywalking cache的性能。 问题起因是skywalking从新retransform的时候从新生成了匿名外部类导致的问题,所以只须要在skywalking对应的transformer进行 retransform的时候使其走缓存即可解决这个问题。

确定切点

通过debug发现skywalking是由 org.apache.skywalking.apm.dependencies.net.bytebuddy.agent.builder.AgentBuilder$Default$ExecutingTransformer 的transform办法进行retransform的。

public byte[] transform(ClassLoader classLoader,                        String internalTypeName,                        Class<?> classBeingRedefined,                        ProtectionDomain protectionDomain,                        byte[] binaryRepresentation) {    // ... 疏忽实现}

对切点进行字节码加强

    public static void premain(final String agentArgs, final Instrumentation instrumentation) throws Exception {        System.err.println("====== skywalking-byte-buddy-agent ======");        // 预处理启动参数        AgentConfig.instance().initConfig();        if (AgentConfig.enable) {            System.err.println("=== begin start skywalking-byte-buddy-agent ===");            System.out.println("=== cacheMode is " + AgentConfig.cacheMode + " ===");            AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> builder                    // 拦挡transform办法                    .method(ElementMatchers.hasMethodName("transform")                                    .and(ElementMatchers.takesArguments(5))                    )                    // 委托                    .intercept(MethodDelegation.to(CacheInterceptor.class));            new AgentBuilder                    .Default()                    // 指定须要拦挡的类                    .type(ElementMatchers.named("org.apache.skywalking.apm.dependencies.net.bytebuddy.agent.builder.AgentBuilder$Default$ExecutingTransformer"))                    .transform(transformer)                    .installOn(instrumentation);            System.err.println("=== end start skywalking-byte-buddy-agent ===");        } else {            System.err.println("=== enable is false, not start skywalking-byte-buddy-agent ===");        }    }

自定义Interceptor

/** * @Description 缓存拦截器 * @Author ocean_wll * @Date 2021/8/5 11:53 上午 */public class CacheInterceptor {    @RuntimeType    public static Object intercept(@Origin Method method, @AllArguments Object[] args,                                   @SuperCall Callable<?> callable) {        Object returnObj = null;        try {            // 校验参数            if (checkArgs(args)) {                ClassLoader classLoader = (ClassLoader) args[0];                String className = (String) args[1];                // 获取缓存中的value                byte[] bytes = Cache.getClassCache(classLoader, className);                if (bytes != null) {                    return bytes;                }                // 调用原有办法                returnObj = callable.call();                if (returnObj != null) {                    // 如果缓存中没有,并且原办法执行后果不为null,则放入缓存中                    Cache.putClassCache(classLoader, className, (byte[]) returnObj);                }            } else {                // 会呈现classloader为null的状况,但还是须要去执行transform                returnObj = callable.call();            }            return returnObj;        } catch (Exception e) {            e.printStackTrace();        }        return returnObj;    }    /**     * 因为拦挡的办法是五个参数,jvm中类的唯一性是依据classloader和className来确定的,所以进行加强前对办法参数进行一次校验防止办法加强谬误     * <p>     * 须要加强的办法     * public byte[] transform(ClassLoader classLoader,     * String internalTypeName,     * Class<?> classBeingRedefined,     * ProtectionDomain protectionDomain,     * byte[] binaryRepresentation) {     * if (circularityLock.acquire()) {     * try {     * return AccessController.doPrivileged(new AgentBuilder.Default.ExecutingTransformer.LegacyVmDispatcher(classLoader,     * internalTypeName,     * classBeingRedefined,     * protectionDomain,     * binaryRepresentation), accessControlContext);     * } finally {     * circularityLock.release();     * }     * } else {     * return NO_TRANSFORMATION;     * }     * }     *     * @param args 办法入参     * @return true校验通过,false校验失败     */private static boolean checkArgs(Object[] args) {        // 先校验参数个数        if (args.length == 5) {            // 校验第一个参数,第一个参数类型是classLoader            boolean arg0IsTrue = args[0] != null && args[0] instanceof ClassLoader;            // 校验第二个参数,第二个参数示意的是类名,类型为String            boolean agr1IsTrue = args[1] != null && args[1] instanceof String;            return arg0IsTrue && agr1IsTrue;        }        return false;    }}

定义ClassCacheResolver

通过不同的ClassCacheResolver来采纳不同的缓存策略**

/** * @Description cacheResolver接口 * @Author ocean_wll * @Date 2021/8/5 4:02 下午 */public interface ClassCacheResolver {    /**     * 获取class缓存     *     * @param loader    ClassLoader     * @param className 类名     * @return byte数组     */byte[] getClassCache(ClassLoader loader, String className);    /**     * 寄存class缓存     *     * @param loader          ClassLoader     * @param className       类名     * @param classfileBuffer 字节码数据     */void putClassCache(ClassLoader loader, String className, byte[] classfileBuffer);}

定义内存缓存器

/** * @Description 内存缓存解析器 * @Author ocean_wll * @Date 2021/8/5 4:03 下午 */public class MemoryCacheResolver implements ClassCacheResolver {    /**     * key为 classloader+className,value为 字节码     */private final Map<String, byte[]> classCacheMap = new ConcurrentHashMap<>();    @Override    public byte[] getClassCache(ClassLoader loader, String className) {        String cacheKey = getCacheKey(loader, className);        return classCacheMap.get(cacheKey);    }    @Override    public void putClassCache(ClassLoader loader, String className, byte[] classfileBuffer) {        String cacheKey = getCacheKey(loader, className);        classCacheMap.put(cacheKey, classfileBuffer);    }    /**     * 获取缓存key ClassLoaderHash(loader) + "@" + className     *     * @param loader    ClassLoader     * @param className 类名     * @return 缓存key     */private String getCacheKey(ClassLoader loader, String className) {        return Cache.getClassLoaderHash(loader) + "@" + className;    }}

验证后果

在skywalking javaagent参数前 加上 -javaagent:${jarPath}/skywalking-byte-buddy-cache-agent-1.0.0.jar 确保在skywalking agent启动之前曾经对skywalking的类进行加强了。

 能够看到加了自定义的agent当前屡次retransform并不会抛出java.lang.UnsupportedOperationException,并且retransform前后也没有产生新的匿名外部类了。

一点集体的思考

1、可插拔、不侵入代码

这个问题其实skywalking官网曾经给出了解决方案,然而官网的解决方案只对 8.1.0及以上版本才会失效。对于无奈降级skywalking版本还在应用低版本的用户来说就须要另辟蹊径了。 第一种办法:批改低版本的skywalking的源码,从新打包。然而必须非常理解skywalking源码的人才能去干,否则免不齐又会引入什么新的问题。实现难度十分高。 第二种办法:本人写一个agent,批改字节码。这种形式灵便不便,即不干涉原来的代码,又能够依据本人想要的进行调整。实现难度个别。 所以当前在相似的问题上,能不批改原有代码就尽量不批改原有代码,除非你十分理解原来的业务逻辑,不然在不分明的状况下随便批改危险太大了。

2、最小改变

在这个问题里我能够对所有的 transformer 的 transform 办法进行切入,但这样就会导致缓存数据过多,有些基本不会呈现问题的数据也被缓存起来了导致资源节约。 所以还是得找到最外围的问题点进行批改,确保这次改变的影响面是最小的。