collie
使用Java实现一个分布式调用链追踪系统
现在项目已经开源,欢迎提pr和star,项目地址:分布式调用链追踪系统Collie
项目系列博客地址:
柠檬好酸啊:用Java实现一个分布式调用链追踪系统 (一)聊聊自己的想法
柠檬好酸啊:用java实现一个分布式调用链追踪系统(二)项目搭建过程中的一些注意事项
柠檬好酸啊:用java实现一个分布式调用链追踪系统(三)最核心的实现之Javassist
柠檬好酸啊:用java实现一个分布式调用链追踪系统(四)项目具体实现
柠檬好酸啊:用java实现一个分布式调用链追踪系统(五)总结
创建项目以及这个过程中的一些考虑
实现这个分布式调用链追踪系统,需要完成上一篇文章中说的那些目标和想法。首要的问题是什么呢,应该是把项目工程搭建起来,我在搭建的过程中遇到了很多值得思考的事情,在这里分享给大家。项目工程搭建的过程中需要考虑两个方面,一个是考虑这是一个java agent工程,所以需要搭建一个agent项目,这一个很简单就能搭建起来。难就难在了第二个方面,就是需要考虑我们的代码需要和应用隔离,也就是说这两个子工程不能使用同一个类加载器加载。
项目结构和jar包隔离
那么为什么需要和应用隔离并且使用另一个类加载器就可以了呢?
大家可以想象一下这种情况,那就是如果我们的这个agent,也就是Collie引用了一个spring的jar包,版本是4.x,但是使用这个agent的代码引用的是5.x版本的,那么这两个jar包可能会出现冲突,导致应用代码出现影响,这个是我们万万不想要的结果,那么解决这个代码污染的途径就是jar包隔离,方法就是使用自己定义的类加载器来加载我们写的类。
大家可以看看我之前整理的一篇文章,介绍了java类加载机制,可以复习一下:https://www.dzhh.top/Tiny-Java/2021/10/24/28.html
简单说来,业务代码是由应用类加载器加载,我们的代码由自定义的加载器加载,一个类加载器加载的类只能引用同一个加载器加载的类和其父加载器加载的类。所有自定义加载器加载的类不会影响到业务代码。Tomcat就是采用了这种方式,来隔离不同的应用,我们知道Tomcat本身是一个java应用,本身需要引用一些jar包,而且一个Tomcat可以部署多个web应用,能够实现这些项目不互相干扰的方式就是类加载器隔离。下图就是Tomcat使用的类加载器层次结构:
代码结构和Spy
我们这种字节码插桩的组件是肯定的需要进行代码隔离的,否则就会污染线上的代码。所以我们至少需要两个jar包,一个是agent,一个是core,然后在agent中使用自定义的加载器加载core,但是这样出现了一个问题,那就是,我们需要在业务代码中插入的代码,我们这里就是一个函数,调用这个函数,把时间参数等传过来,我们在这个函数里实现我们想要的逻辑,但是这样的话由于这个代码是自定义加载器加载的,应用类加载器访问不到,会报ClassNotFoundException。所以一个解决方法就是业务代码中调用的方法拆出来封装成一个jar包叫做spy,这个jar包由Bootstrap类加载器加载。这个jar包中最关键的类就是一个类:
public class SpyAPI {
public static final AbstractSpy NOPSPY = new NopSpy();
private static volatile AbstractSpy spyInstance = NOPSPY;
public static volatile boolean INITED;
public static AbstractSpy getSpy() {
return spyInstance;
}
public static void setSpy(AbstractSpy spy) {
spyInstance = spy;
}
public static void setNopSpy() {
setSpy(NOPSPY);
}
public static boolean isNopSpy() {
return NOPSPY == spyInstance;
}
public static void init() {
INITED = true;
}
public static boolean isInited() {
return INITED;
}
public static void destroy() {
setNopSpy();
INITED = false;
}
public static void atEnter(Class<?> clazz, String methodInfo, Object target, Object[] args) {
spyInstance.atEnter(clazz, methodInfo, target, args);
}
public static void atExit(Class<?> clazz, String methodInfo, Object target, Object[] args,
Object returnObject) {
spyInstance.atExit(clazz, methodInfo, target, args, returnObject);
}
public static void atExceptionExit(Class<?> clazz, String methodInfo, Object target,
Object[] args, Throwable throwable) {
spyInstance.atExceptionExit(clazz, methodInfo, target, args, throwable);
}
public static void atFrameworkEnter(String traceId, String spanId, String parentSpanId) {
spyInstance.atFrameworkEnter(traceId, spanId, parentSpanId);
}
public static void atFrameworkExit(String info) {
spyInstance.atFrameworkExit(info);
}
public static abstract class AbstractSpy {
public abstract void atEnter(Class<?> clazz, String methodInfo, Object target,
Object[] args);
public abstract void atExit(Class<?> clazz, String methodInfo, Object target, Object[] args,
Object returnObject);
public abstract void atExceptionExit(Class<?> clazz, String methodInfo, Object target,
Object[] args, Throwable throwable);
public abstract void atFrameworkEnter(String traceId, String spanId, String parentSpanId);
public abstract void atFrameworkExit(String info);
}
static class NopSpy extends AbstractSpy {
@Override
public void atEnter(Class<?> clazz, String methodInfo, Object target, Object[] args) {
}
@Override
public void atExit(Class<?> clazz, String methodInfo, Object target, Object[] args,
Object returnObject) {
}
@Override
public void atExceptionExit(Class<?> clazz, String methodInfo, Object target, Object[] args,
Throwable throwable) {
}
@Override
public void atFrameworkEnter(String traceId, String spanId, String parentSpanId) {
}
@Override
public void atFrameworkExit(String info) {
}
}
}
这个类里提供了两个方法,其实就是提供了一个拓展接口,这个类由启动类加载器加载,可以由应用和agent访问到,在业务代码中可以调用这个方法,而在agent里可以通过反射来把相应的实现函数赋值给spy,这样业务代码调用这个函数就能实现我们想要的逻辑了。
具体怎么加载这个jar包可以在agent中有一个函数叫做instrumentation.appendToBootstrapClassLoaderSearch(new JarFile(agentSpyFile));
通过这种方式加载的类就可以实现应用隔离了。最终类加载器的关系如下图所示:
由于我们的一些多重的考虑,导致一共分成了三个子项目,一开始我觉得core和spy包的路径需要通过agent传过去,导致启动的方式非常的丑陋,但是如果我们把这两个jar包和agent包放到一个目录下面就可以解决这个问题了。
对于这种方式,其实我之前的调研也发现了,阿里开源的组件arthas和jvm-sandbox就是通过这种方式来进行字节码插桩的,感兴趣的也可以参考一下他们的代码。
下一篇我们介绍一下我们这个项目核心技术之一-javassist。
如果你喜欢,欢迎点赞关注加收藏,后续我还会继续做一些类似的有意思的java玩具。