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

10次阅读

共计 9186 个字符,预计需要花费 23 分钟才能阅读完成。

咱们在 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 中的所有类名。

@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$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 办法进行切入,但这样就会导致缓存数据过多,有些基本不会呈现问题的数据也被缓存起来了导致资源节约。所以还是得找到最外围的问题点进行批改,确保这次改变的影响面是最小的。

正文完
 0