Java 单点应用获取方法调用链路分析
本文基于 slot
本文基于 JDK1.8
方法与线程
JVM 规范中虚拟机栈为线程私有,每个方法在执行时都会在虚拟机中创建一个栈帧。每一个栈帧表示没有执行完的方法,执行完的的方法,其栈帧会被弹出栈。
如何存放方法调用链路
如果想记录应用的方法调用链路,那么首要的问题就是这些调用数据应该存在什么地方才能够被全局访问到,并且是线程隔离的。
Java 提供 ThreadLocal
来隔离各个线程的私有变量,我们只需要在 ThreadLocal
中村放各个线程的调用 Stack
即可。
链路追踪
关于链路追踪的原理可以参阅 Google 发布的一篇论文 《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》
slot 的实现较为简单还不是很成熟,只遵循了 OpenTelemetry 的部分语义,后续会实现 OpenTracing 的全部语义。
全局 traceId 控制
具体实现为 TraceContext
public class TraceContext { |
调用链控制
具体实现为 TraceManager
public class TraceManager { |
当第一个方法开始执行时,ThreadLocal 中的 Stack 是空的
+-------------------------------------------------------+
| ThreadLocal |
| Stack Stack Stack |
| +-------------+ +-------------+ +-------------+ |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| +-------------+ +-------------+ +-------------+ |
+-------------------------------------------------------+接下来我们只看一个线程,当一个方法被线程执行时,我们会调用
entrySpan()
方法在栈中压入一个 SpanId( 特殊的根方法的 SpanId 为0),如果是新的调用链的话还会生成 TraceId。+-------------------+ +-------------------+
| ThreadLocal | | ThreadLocal |
| | | |
| | | +---------------+ |
| | | | | |
| Stack | | | TraceId | |
| +-------------+ | | | | |
| | | | | +---------------+ |
| | | | +-------------------+
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | +---------+ | |
| | | SpanId | | |
| | +---------+ | |
| +-------------+ |
+-------------------+当第二个方法或更多方法被线程执行时,我们会为每一个方法(
entrySpan()
)生成一个 SpanId 并压入栈中(不存入对象是为了节约内存空间),并将当前栈顶的 SpanId 设置为当前出栈 Span 的父 id(getParentSpan()
)。+----------------------------+
| ThreadLocal |
| |
| +-------------------+
| | | |
| | | |
| Stack | | |
| +---------------+ | |
| | | | +-------+------+
| | | | || SpanId |
| | | | || |
| | | | +--------------+
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | +----v------+ | |
| | | | | |
| | | SpanId +--------------------+
| | +-----------+ | | |
| | | | ParentId |
| | +-----------+ | | |
| | | | | | |
| | | SpanId <--------------------+
| | +-----------+ | | |
| | | | |
| | +-----------+ | | ParendId |
| | | | | | |
| 0 <----+ SpanId <--------------------+
| | +-----------+ | |
| +---------------+ |
| |
+----------------------------+即使是递归方法,每次方法的调用都会生成新的 SpanId,栈的深度应该由业务系统来考虑,栈过深的话会导致 JVM 出现 StackOverflow 的错误。
exitSpan()
方法会检测每一个方法出栈时当前栈的状态,如果栈中没有数据,我们认为此次调用链结束,我们将把 TraceId 也进行弹出,做最后的收尾工作。+-------------------+ +---------------+
| ThreadLocal | | ThreadLocal |
| | | | pop
| | | +-------------->
| +-------------+ | | | |
| | Stack | | | +-----+----+ |
| | | | | | | |
| | | | | | TraceId | |
| | | | | | | |
| | | | | | | |
| | | | | +----------+ |
| | | | | |
| | | | | |
| | | | | |
| | | | | |
| | | | | |
| | | | | |
| | | | +---------------+
| | | |
| | | | pop
| | +----------------------->
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | +---+-----+ | |
| | | SpanId | | |
| | | | | |
| | +---------+ | |
| +-------------+ |
+-------------------+
总结
因为所有的方法都在线程中执行,我们使用线程私有的并且可以全局访问的 ThreadLocal
对象来追踪方法的链路调用,并使用 Stack
来组织数据的调用关系。