用java实现一个分布式调用链追踪系统(四)项目具体实现

collie

使用Java实现一个分布式调用链追踪系统

现在项目已经开源,欢迎提pr和star,项目地址:分布式调用链追踪系统Collie

项目系列博客地址:
柠檬好酸啊:用Java实现一个分布式调用链追踪系统 (一)聊聊自己的想法
柠檬好酸啊:用java实现一个分布式调用链追踪系统(二)项目搭建过程中的一些注意事项
柠檬好酸啊:用java实现一个分布式调用链追踪系统(三)最核心的实现之Javassist
柠檬好酸啊:用java实现一个分布式调用链追踪系统(四)项目具体实现
柠檬好酸啊:用java实现一个分布式调用链追踪系统(五)总结

项目创建

我们的项目分为三个部分,分别是spy、agent和core,项目目录如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j9tRM5rF-1637399360000)(/Users/dongzhonghua03/Library/Application Support/typora-user-images/image-20211115234735325.png)]

其中spy在第二节里面已经介绍过了,很简单就一个文件。今天重点介绍一下agent和core的实现。

agent 的实现

agent应用最重要的是要在menifest文件中指定Premain-Class和Agent-Class,我这里的设置如下:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>single</goal>
            </goals>
            <phase>package</phase>
            <configuration>
                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
                <archive>
                    <manifestEntries>
                        <!-- 注册premain的class -->
                        <Premain-Class>xyz.dsvshx.collie.agent.CollieAgent</Premain-Class>
                        <!-- 注册agentmain的class -->
                        <Agent-Class>xyz.dsvshx.collie.agent.CollieAgent</Agent-Class>
                        <Can-Redefine-Classes>true</Can-Redefine-Classes>
                        <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        <Specification-Title>${project.name}</Specification-Title>
                        <Specification-Version>${project.version}</Specification-Version>
                        <Implementation-Title>${project.name}</Implementation-Title>
                        <Implementation-Version>${project.version}</Implementation-Version>
                        <Boot-Class-Path>javassist-3.27.0-GA.jar</Boot-Class-Path>
                    </manifestEntries>
                </archive>
            </configuration>
        </execution>
    </executions>
</plugin>

agent入口类最重要的两个函数如下:

private static final String USER_HOME = System.getProperty("user.home");
private static final String COLLIE_CORE_JAR = USER_HOME + "/.collie/collie-core.jar";
private static final String COLLIE_SPY_JAR = USER_HOME + "/.collie/collie-spy.jar";
private static final String TRANSFORMER = "xyz.dsvshx.collie.core.instrumentation.CollieClassFileTransformer";

private static ClassLoader collieClassLoader;
private static ScheduledExecutorService EXECUTOR_SERVICE;

/**
 * jvm启动时运行这个函数
 */
public static void premain(String agentOps, Instrumentation instrumentation) {
    main(agentOps, instrumentation);
}

public static void agentmain(String agentOps, Instrumentation instrumentation) {
    main(agentOps, instrumentation);
}

main方法具体的实现如下:

private static synchronized void main(String agentOps, Instrumentation instrumentation) {
    try {
        log.info(">>>>>>>collie agent 启动...");
        // 使用启动类加载器load spy
        File agentSpyFile = new File(COLLIE_SPY_JAR);
        if (!agentSpyFile.exists()) {
            System.out.println("Spy jar file does not exist: " + agentSpyFile);
            return;
        }
        // 使用启动类加载器加载agentSpy
        instrumentation.appendToBootstrapClassLoaderSearch(new JarFile(agentSpyFile));
        // load core
        File agentCoreFile = new File(COLLIE_CORE_JAR);
        if (!agentCoreFile.exists()) {
            System.out.println("Agent jar file does not exist: " + agentCoreFile);
            return;
        }
        // 使用自定义的类加载器加载core包
        ClassLoader collieClassLoader = getClassLoader(instrumentation, agentCoreFile);
        // addTransformer 使用transformer来进行插桩
        Class<?> collieClassFileTransformer = collieClassLoader.loadClass(TRANSFORMER);
        Constructor<?> transform = collieClassFileTransformer.getDeclaredConstructor(String.class);
        instrumentation.addTransformer((ClassFileTransformer) transform.newInstance(COLLIE_SPY_JAR));
        // jvm信息,暂时关闭
        // processJvmInfo();
    } catch (Throwable throwable) {
        throwable.printStackTrace();
    }
}

其中,core包需要用自定义类加载器来加载,自定义类加载器的逻辑为:

@Slf4j
public class CollieClassLoader extends URLClassLoader {
    public CollieClassLoader(URL[] urls) {
        super(urls, getSystemClassLoader().getParent());
    }

    @Override
    protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

        final Class<?> loadedC = findLoadedClass(name);
        if (loadedC != null) {
            return loadedC;
        }
        // 优先从parent(SystemClassLoader)里加载系统类,避免抛出ClassNotFoundException
        if (name != null && (name.startsWith("sun.") || name.startsWith("java."))) {
            return super.loadClass(name, resolve);
        }
        if (name != null && name.contains("xyz.dsvshx.collie.point")) {
            return super.loadClass(name, resolve);
        }
        try {
            Class<?> loadedClass = findClass(name);
            if (loadedClass != null) {
                if (resolve) {
                    resolveClass(loadedClass);
                }
                return loadedClass;
            }
        } catch (ClassNotFoundException ignored) {
        }
        return super.loadClass(name, resolve);
    }
}

在agent中有这个一条语句:

instrumentation.addTransformer((ClassFileTransformer) transform.newInstance(COLLIE_SPY_JAR));

这条语句的意思是给instrumentation添加一个transformer,在transformer里就可以使用javassist对加载的类做一次转换,插入我们想要的埋点。

埋点主要分为两个部分:

一个部分是针对方法进行埋点,采用的方式是使用javassist。

第二个是针对框架进行适配,也就是说需要把spanId和traceId写入ThreadLocal,那样在最后的时候就可以从ThreadLocal中获取并且传递到下一个组件中。这里作为例子我适配的是我自己写的一个简单的spring的框架summer。当然针对spring和其他的rpc等框架适配也是可以的。

Spy的实现类

首先创建一个AbstactSpy的实现类:public class SpyImpl extends AbstractSpy,拿方法入口来说,方法中插入的埋点最终会调用这个函数,然后再执行自定义的MethodAspect。对于框架的适配器来说,道理是一样的。

    @Override
    public void atEnter(Class<?> clazz, String methodInfo, Object target, Object[] args) {
        try {
            if (SamplingRate.needSampling()) {
                String[] methodInfos = splitMethodInfo(methodInfo);
                String methodName = methodInfos[0];
                String methodDesc = methodInfos[1];
                for (MethodAspect methodAspect : methodAspects) {
                    methodAspect.before(clazz, methodName, methodDesc, target, args);
                }
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
    @Override
    public void atFrameworkEnter(String traceId, String spanId, String parentSpanId) {
        for (FrameworkAspect frameworkAspect : frameworkAspects) {
            try {
                frameworkAspect.entry(traceId,spanId,parentSpanId);
            } catch (Throwable throwable) {
                //
            }
        }
    }

方法切面

方法切面分为两个部分,第一部分是实现方法切面的字节码插桩,就是将我们的埋点在类加载的时候通过修改字节码的方式植入进去。我们的方法埋点的方式如下所示:

public byte[] modifyClass(String className, byte[] classfileBuffer, String spyJarPath) {
    try {
        ClassPool classPool = ClassPool.getDefault();
        // 必须要有这个,否则会报point找不到,搞了大半天 https://my.oschina.net/xiaominmin/blog/3153685
        classPool.appendClassPath(spyJarPath);
        String clazzname = className.replace("/", ".");
        CtClass ctClass = classPool.get(clazzname);
        // 排除掉注解,接口,枚举
        if (!ctClass.isAnnotation() && !ctClass.isInterface() && !ctClass.isEnum()) {
            // 针对所有函数操作
            for (CtBehavior ctBehavior : ctClass.getDeclaredMethods()) {
                addMethodAspect(clazzname, ctBehavior, false);
            }
        }
        return ctClass.toBytecode();
    } catch (Exception e) {
        e.printStackTrace();
        return classfileBuffer;
    }
}

private static void addMethodAspect(String clazzname, CtBehavior ctBehavior, boolean isConstructor)
        throws Exception {
    if (isNative(ctBehavior)
            || isAbstract(ctBehavior)
            || "toString".equals(ctBehavior.getName())
            || "getClass".equals(ctBehavior.getName())
            || "equals".equals(ctBehavior.getName())
            || "hashCode".equals(ctBehavior.getName())) {
        return;
    }
    // 方法前增强
    // 如果是基本数据类型的话,传参为Object是不对的,需要转成封装类型
    // 转成封装类型的话非常方便,使用$w就可以,不影响其他的Object类型
    String methodName = isConstructor ? ctBehavior.getName() + "#" : ctBehavior.getName();
    String methodInfo = methodName + "|" + ctBehavior.getMethodInfo().getDescriptor();
    String target = isStatic(ctBehavior) ? "null" : "this";
    ctBehavior.insertBefore(
            String.format("{xyz.dsvshx.collie.point.SpyAPI.atEnter(%s, \"%s\", %s, %s);}",
                    "$class", methodInfo, target, "($w)$args")
    );
    // 方法后增强
    ctBehavior.insertAfter(
            String.format("{xyz.dsvshx.collie.point.SpyAPI.atExit(%s, \"%s\", %s, %s, %s);}",
                    "$class", methodInfo, target, "($w)$args", "($w)$_")
    );
    // 异常出增强
    ctBehavior.addCatch(
            String.format("{xyz.dsvshx.collie.point.SpyAPI.atExceptionExit(%s, \"%s\", %s, %s, %s);"
                            + "throw $e;}",
                    "$class", methodInfo, target, "($w)$args", "$e"),
            ClassPool.getDefault().get("java.lang.Throwable")
    );
}

上面是把埋点插入了calss,当真正调用的时候就会出发Spy中的方法,最终会调用我们自定义的实现类。

然后我们来定义一个方法切面接口,它的实现类就是我们自己想实现的逻辑,比如打印日志或者将日志发送到kafka或者其他数据库。

public interface MethodAspect {

    void before(Class<?> clazz, String methodName, String methodDesc, Object target, Object[] args) throws Throwable;

    void after(
            Class<?> clazz, String methodName, String methodDesc, Object target, Object[] args, Object returnObject)
            throws Throwable;

    void error(
            Class<?> clazz, String methodName, String methodDesc, Object target, Object[] args, Throwable throwable)
            throws Throwable;

}

框架适配

框架的适配也包含两个部分,第一个部分是需要在一个框架入口处在ThreadLocal中埋入traceId和spanId等信息。第二个部分是需要插桩并进一步实现一些自己的逻辑,比如打印日志等。

第一部分,在入口处插入trace信息的代码如下:

private final static ThreadLocal<TransactionInfo> TRANSACTION_INFO_THREAD_LOCAL = new ThreadLocal<>();
private static TransactionInfo TRANSACTION_INFO;

public void entry(String traceId, String spanId, String parentSpanId) {
    try {
        if (traceId == null || traceId.trim().length() == 0) {
            traceId = UUID.randomUUID().toString();
        }
        if (spanId == null || spanId.trim().length() == 0) {
            spanId = UUID.randomUUID().toString();
        }
        if (parentSpanId == null || parentSpanId.trim().length() == 0) {
            parentSpanId = "-1";
        }
        TRANSACTION_INFO = new TransactionInfo(traceId, spanId, parentSpanId);
        TRANSACTION_INFO_THREAD_LOCAL.set(TRANSACTION_INFO);
        System.out.printf(">>>>>>>>>Thread %s set, transaction info :%s%n", Thread.currentThread().getName(),
                TRANSACTION_INFO);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

第二部分,也有一些值得注意的地方。前面说过,我们的traceId之类的参数需要一直在不同的中间件里传递下去,其中的方法就是使用ThreadLocal,我们可以在一个框架开始的地方吧traceId插入到ThreadLocal中,最后读出来传递到下一个中间件中。我这里做适配的是一个框架,是我自己模仿Spring的一个小工具,我叫他Summer。

public  byte[] modifyClass(String className, byte[] classBytes, String spyJarPath) {
    try {
        if (SUMMER_ADAPTOR_CLASS.equals(className)) {
            ClassPool classPool = ClassPool.getDefault();
            classPool.appendClassPath(spyJarPath);
            String clazzname = className.replace("/", ".");
            CtClass ctClass = classPool.get(clazzname);
            CtMethod doHandlerMethod = ctClass.getDeclaredMethod("doHandler");
            // 没想到这么简单就成了?
            doHandlerMethod.insertBefore("{"
                    + "String traceId = fullHttpRequest.headers().get(\"collie-trace-id\");"
                    + "String parentSpanId = fullHttpRequest.headers().get(\"collie-span-id\");"
                    + "xyz.dsvshx.collie.point.SpyAPI.atFrameworkEnter(traceId, \"\", parentSpanId);"
                    + "}");
            doHandlerMethod.insertAfter("{"
                    + "String traceId = fullHttpRequest.headers().get(\"collie-trace-id\");"
                    + "String parentSpanId = fullHttpRequest.headers().get(\"collie-span-id\");"
                    + "xyz.dsvshx.collie.point.SpyAPI.atFrameworkExit(traceId + \"|\" + parentSpanId);"
                    + "}");
            return ctClass.toBytecode();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return classBytes;
}

通过上面说的过程,最终的一个方法会被转化成这个样子:

public static <T> T getInstance(Class<T> clz, Constructor<T> constructor, Object[] args) {
    try {
        SpyAPI.atEnter(Desc.getClazz("xyz.dsvshx.ioc.util.BeanUtils"), "getInstance|(Ljava/lang/Class;Ljava/lang/reflect/Constructor;[Ljava/lang/Object;)Ljava/lang/Object;", (Object)null, new Object[]{clz, constructor, args});

        Object var10000;
        try {
            var10000 = constructor == null ? clz.newInstance() : constructor.newInstance(args);
        } catch (Exception var7) {
            var7.printStackTrace();
            log.error("创建对象失败");
            var10000 = null;
        }

        Object var5 = var10000;
        SpyAPI.atExit(Desc.getClazz("xyz.dsvshx.ioc.util.BeanUtils"), "getInstance|(Ljava/lang/Class;Ljava/lang/reflect/Constructor;[Ljava/lang/Object;)Ljava/lang/Object;", (Object)null, new Object[]{clz, constructor, args}, var5);
        return var5;
    } catch (Throwable var8) {
        SpyAPI.atExceptionExit(Desc.getClazz("xyz.dsvshx.ioc.util.BeanUtils"), "getInstance|(Ljava/lang/Class;Ljava/lang/reflect/Constructor;[Ljava/lang/Object;)Ljava/lang/Object;", (Object)null, new Object[]{clz, constructor, args}, var8);
        throw var8;
    }
}

对一个框架的改造最终的结果是这样:

public static FullHttpResponse doHandler(WebApplicationContext webApplicationContext, FullHttpRequest fullHttpRequest) {
    String var2 = fullHttpRequest.headers().get("collie-trace-id");
    String var3 = fullHttpRequest.headers().get("collie-span-id");
    SpyAPI.atFrameworkEnter(var2, "", var3);
    HttpMethod method = fullHttpRequest.method();
    FullHttpResponse var10000;
    if (HttpMethod.GET.equals(method)) {
        var10000 = handleGet(webApplicationContext, fullHttpRequest);
    } else if (HttpMethod.POST.equals(method)) {
        var10000 = handlePost(webApplicationContext, fullHttpRequest);
    } else {
        log.error("不支持该方法");
        var10000 = null;
    }

    FullHttpResponse var5 = var10000;
    String var6 = fullHttpRequest.headers().get("collie-trace-id");
    String var7 = fullHttpRequest.headers().get("collie-span-id");
    SpyAPI.atFrameworkExit(var6 + "|" + var7);
    return var5;
}

到这里,最核心的部分已经介绍完了,下一篇是本系列的最后一篇,介绍几个遗留的问题,然后再介绍一下项目如何运行以及结果的展示。

如果你喜欢,欢迎点赞关注加收藏,后续我还会继续做一些类似的有意思的java玩具。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值