Java 调用链跟踪
何为调用链?
方法调用的链路,就是调用链;很显然这里的链不仅仅适合于 本地方法调用,远程方法调用(RPC)也是同样适用的。
例如一个方法test(),里面调用了test1()和test2(); test1()里面调用了test01(),test01()里面调用了test001(), 即如下如下所示: test()-> test1()-> test01()-> test001() test2() 很显然调用链可能会非常复杂并且嵌套很深,而这种复杂性给监控带来 一些挑战和难点。
对于性能敏感的应用,很希望能够分析出整个调用链的执行情况,例如 每个方法的执行顺序、耗时等等,这样才能方便诊断性能瓶颈所在。
分析
可以分析出,这里最核心的点是调用顺序和深度。如果能将这个结构梳理 清晰,问题也就迎刃而解。
-
调用链标识:什么才算一个完整的调用链?这一点是比较灵活的,一般 取决于开发者实际情况。最上一层需要监控的方法称之为root,root方法 与其它方法的不同之处在于其需要生成一个独一无二的token,来唯一确定 此次调用链,否则调用链的数据将无从分析。token产生后必须放在线程的 本地存储(ThreadLocal)中。每次方法执行日志,都需要将此token拿到 并输出。
-
调用顺序:即平级的方法调用的顺序,例如上面的test1和test2, 每一次平级调用,顺序值加1.
- 调用深度:即嵌套的方法调用,如上面的test()->test1(),每嵌套 一次,调用深度加1。
因此可以抽象出下面的数据结构:
public class TraceToken {
private int depth = 0;
private Map<Integer, Integer> seqMap = Maps.newHashMap();
public void enter() {
this.depth = this.depth + 1;
if (!seqMap.containsKey(depth)) {
this.seqMap.put(this.depth, 0);
return;
}
int lastSeq = this.seqMap.get(this.depth);
this.seqMap.put(this.depth, lastSeq + 1);
}
public void leave() {
this.depth = this.depth - 1;
}
}
其中,depth是当前嵌套深度,seqMap是在该层嵌套深度上的调用顺序值。 对于任何一次方法调用,在调用开始前触发enter(),将当前depth 对应的seq自增1,并更新执行顺序;在调用结束后触发leave(),恢复深度。
剩下的问题就很简单了,只要拦截方法的执行情况即可。例如常用的Spring AOP+AspectJ,可以很方便拦截指定的方法调用。在ProceedingJoinPoint 处理完毕后,将调用链数据输出到日志中供后面分析使用。这些数据非常适合 用hadoop类的工具做处理。