一步一步写APM(四)--Jedis插件、线程内链的构造

一点修改和准备工作

  1. 在tomcat测试项目中添加jedis相关代码(需要本地开一个redis),代码省略
  2. 提取Enhancer公共部分
public abstract class PluginEnhancer {
    public abstract String getEnhanceClassName();
    public abstract String getInterceptorClassName();
    public abstract ElementMatcher getInstanceMethodInterceptorMatch();

    public DynamicType.Builder<?> doIntercept(DynamicType.Builder<?> builder){
        try {
            ClassInstanceMethodInterceptor classInstanceMethodInterceptor =(ClassInstanceMethodInterceptor) Class.forName(getInterceptorClassName()).newInstance();
            builder = builder.method(not(isStatic()).and(getInstanceMethodInterceptorMatch())).intercept(MethodDelegation.to(classInstanceMethodInterceptor));
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return builder;
    }
}

具体Enhancer只做少量工作 * 获取要拦截的类 * 获取要拦截的函数 * 获取Interceptor类名

public class TomcatEnhancer extends PluginEnhancer {

    public static String ENHANCE_CLASSNAME = "org.apache.catalina.core.StandardWrapperValve";
    public static String INTERCEPTOR_CLASSNAME = "younian.apmsample.agent.plugin.tomcat.TomcatInterceptor";

    @Override
    public String getEnhanceClassName() {
        return ENHANCE_CLASSNAME;
    }

    @Override
    public String getInterceptorClassName() {
        return INTERCEPTOR_CLASSNAME;
    }

    @Override
    public ElementMatcher getInstanceMethodInterceptorMatch() {
        return  named("invoke");
    }
}

运行一下tomcat:

TomcatInterceptor before...
服务正在运行: PONG
JedisInterceptor before..
JedisInterceptor after..
JedisInterceptor before..
JedisInterceptor after..
value of key:1519712852893
TomcatInterceptor after...

现在tomcat以及jedis都能够拦截到了。阶段Tag:添加jedis插件

详细的拦截信息

现在拦截到的信息从控制台来看基本是无意义且混乱的,那我们要如何处理呢?

  1. 请求链从tomcat转到jedis,如果构成一条链?

    如何将当前上下文中的信息进行临时存储,并保证线程安全呢?这一点可以借助ThreadLocal来完成,发起创建Trace信息时,往ThreadLocal中写入记录,当前请求过程中再发起新的请求时,从ThreadLocal中获取Trace信息继续往下传递,等信息可以提交归档的时候,从ThreadLocal读取,并清除ThreadLocal中的信息。

  2. 组件请求如何开始结束?--比如jedis get操作。 一个组件的一个操作定义为一个Span,比如一个jedis.ping(),一个jedis.get(),Span是调用链中的最小单元节点。我们定义一个Span,每个span有自己的id,另外包含一个parentSpanId,标识上级spanid,如果是tomcat没有上级span我们可以定义parentSpanId=0;这样一条链就串起来了。
public class Span {
    private int spanId;
    private int parentSpanId;
    private long startTime;
    private long endTime;
    private String operationName;
}

这里有一点需要说明:比如tomcat测试例子中的操作,用户请求“/”,然后在Controller中有jedis.ping()、jedis.set()、jedis.get()这四个操作我们分别设置id是1 2 3 4 的话,那么 * 1的parent是0 * 2的parent是1 * 但是3的parent不是2,而是1,因为2和3是并列操作,没有父子关系,没有2也可以有3。 所以我们使用栈的数据结构来对span进行存储,正确流程是:1入栈->2入栈->2出栈->3入栈->3出栈->4入栈->4出栈->1出栈。知道栈空了为止说明这一条链已经结束了。

操作

  1. 定义ContextManager用于Interceptor操作Span以及ThreadLocal变量保存。
public class ContextManager {
    private final static ThreadLocal<TraceContext> CONTEXT = new ThreadLocal<>();
    public static Span createSpan(String operationName){
        return get().createSpan(operationName);
    }

    public static Span stopSpan(){
        return  get().stopSpan();
    }

    public static TraceContext get(){
        return CONTEXT.get();
    }
}
  1. 定义TraceContext作为Span的栈容器。
public class TraceContext {

    private static LinkedList<Span> activeSpanStak = new LinkedList<>();

    private static int spanIdGenerator;

    static Span createSpan(String operationName) {
        Span parentSpan =  peek();
        Span span = null;
        if (parentSpan == null) {
            span = new Span(spanIdGenerator++, System.currentTimeMillis(), operationName);
        } else {
            span = new Span(spanIdGenerator++, parentSpan.getSpanId(), System.currentTimeMillis(), operationName);
        }
        TraceContext.push(span);
        return span;
    }

    static Span stopSpan() {
        Span span =  pop();
        span.setEndTime(System.currentTimeMillis());
        return span;
    }

    static void push(Span span) {
        activeSpanStak.push(span);
    }

    static Span pop() {
        return activeSpanStak.pop();
    }

    static Span peek() {
        return activeSpanStak.peek();
    }
}
  1. 运行tomcat查看控制台:
服务正在运行: PONG
Span{spanId=10, parentSpanId=9, startTime=1519716542175, endTime=1519716542176, operationName='set'}
Span{spanId=11, parentSpanId=9, startTime=1519716542176, endTime=1519716542176, operationName='get'}
value of key:1519716542175
Span{spanId=9, parentSpanId=0, startTime=1519716542174, endTime=1519716542177, operationName='invoke'}

可以看到多个span的信息已经展示出来。可以看到span的起止时间以及操作,以及父span。 阶段Tag:线程内链的构造

另外,如果想要将agent放到tomcat的pom文件中,就会出现 http://www.younian.me/archives/%E6%96%B0%E7%89%88tomcat%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6%E6%94%B9%E5%8F%98%E5%AF%BC%E8%87%B4%E7%9A%84%E4%B8%8D%E5%90%8C%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8%E5%8A%A0%E8%BD%BD%E5%90%8C%E4%B8%80%E4%B8%AA%E7%B1%BB%E7%9A%84%E5%AE%9E%E4%BE%8B%E8%B5%8B%E5%80%BC%E5%87%BA%E9%94%99 这里出现的问题。看来依然避免不了,有时间可以研究一下。

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×