前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >低版本skywalking与LinkAgent不兼容怎么办?记一次详细的解决过程

低版本skywalking与LinkAgent不兼容怎么办?记一次详细的解决过程

原创
作者头像
数列科技
发布2022-03-18 18:33:03
1.3K0
发布2022-03-18 18:33:03
举报
文章被收录于专栏:Takin应用

​我们在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 方法
代码语言:javascript
复制
public static void premain(String agentArgument, Instrumentation instrumentation) throws Exception
  1. 在 JVM 启动后 Attach,通过 Attach API 进行加载,这种方式会在 agent 加载以后执行 agentmain 方法
代码语言:javascript
复制
public static void agentmain(String agentArgument, Instrumentation instrumentation) throws Exception

这两个方法都有两个参数

第一个 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中的所有类名。代码地址🔗

代码语言:javascript
复制
@SpringBootApplication
public 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$tTwQs5Cscom.example.demo.TestController$auxiliary$rZrClpy4 两个类。 在retransform的时候抛出了java.lang.UnsupportedOperationException retransform之后又多生成了三个匿名内部类。

第三步:给出合理的解决方案

1.添加jvm的启动参数

skywalking官方8.1.0以后的版本可以通过添加jvm的启动参数来解决这个问题。

代码语言:javascript
复制
-Dskywalking.agent.is_cache_enhanced_class=true 
-Dskywalking.agent.class_cache_mode=MEMORY

也可以通过 -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的。

代码语言:javascript
复制
public byte[] transform(ClassLoader classLoader,
                        String internalTypeName,
                        Class<?> classBeingRedefined,
                        ProtectionDomain protectionDomain,
                        byte[] binaryRepresentation) {
    // ... 忽略实现
}
  • 对切点进行字节码增强
代码语言:javascript
复制
    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
代码语言:javascript
复制
/**
 * @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来采用不同的缓存策略

代码语言:javascript
复制
/**
 * @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);
}
  • 定义内存缓存器
代码语言:javascript
复制
/**
 * @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 方法进行切入,但这样就会导致缓存数据过多,有些根本不会出现问题的数据也被缓存起来了导致资源浪费。 所以还是得找到最核心的问题点进行修改,确保这次改动的影响面是最小的。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • agent是什么
  • agent能干什么
  • agent 的两种使用方式
  • 第一步:问题分析
  • 第二步:本地复现
    • 1.不加skywalking直接启动
      • 2.指定skywalking启动
      • 第三步:给出合理的解决方案
        • 1.添加jvm的启动参数
          • 2.新写额外的agent来实现 skywalking cache功能
          • 一点个人的思考
            • 1、可插拔、不侵入代码
              • 2、最小改动
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档