PDF书籍《手写调用链监控APM系统-Java版》第6章 链路的架构(Trace+TraceSe ...

打印 上一主题 下一主题

主题 835|帖子 835|积分 2505

本人阅读了 Skywalking 的大部分焦点代码,也了解了相关的文献,对此深有感悟,特此借助巨人的思想自己手动用JAVA语言实现了一个 “调用链监控APM” 系统。本书采用边讲解实现原理边编写代码的方式,看本书时一定要跟着敲代码。
作者已经将过程写成一部书籍,怎样没有钱发表,如果您知道渠道可以联系本人。一定重谢。
本书涉及到的焦点技能与思想

JavaAgent , ByteBuddy,SPI服务,类加载器的定名空间,增强JDK类,kafka,插件思想,切面,链路栈等等。实际上远不止这么多,差不多贯通了整个java体系。
适用人群

自己公司要实现自己的调用链的;写架构的;深入java编程的;阅读Skywalking源码的;
版权

本书是作者费尽心血亲自编写的代码,不经同意切勿拿出去商用,否则会追究其责任。
原版PDF+源码请见:

本章涉及到的工具类也在下面:

PDF书籍《手写调用链监控APM系统-Java版》第1章 开篇介绍-CSDN博客

第6章 链路的架构(Trace+TraceSegment+Span)

经过前面章节的洗礼,能对峙读到这里的已是难能可贵,也证明你离胜利已经不远了。
本章介绍的是在前面章节的切面围绕方法举行链路埋点, 将一系列方法请求调用的信息抽象成链路(Trace),然后发送到后端OAP。请求调用包含 “http调用”,“DB调用”,“MQ调用”,“Cache调用”,“RPC调用”,“本地方法调用”。复杂的微服务系统就是由以上6种调用组成错综复杂的链路。调用链监控系统就是收罗N条链路然后举行监控。
一条链路我们又可以抽象出TraceSegment和Span的概念,下面我们来详细讲解下链路的知识。
5.1 链路的理论知识

5.1.1 Trace的介绍

Trace就是一条链路,是指一个请求或者一个操纵从开始到结束的完整路径。Trace结束后会被立马发送到后端。
比如浏览器访问下单接口,起首请求到达网关,此时一条链路就开始了,会分配一个唯一taceId作为标识,直到这个下单接口的网关返回给浏览器了,这条trace便结束,然后被立马发送到kafka。
实际上我们反面编写代码是没有Trace这个类的,是一个抽象概念,只有traceId这个实际的字符串存在。
5.1.2 TraceSegment的介绍

一个Trace由很多的TraceSegment组成。TraceSegment是包含了JVM线程,或进程的一些列操纵,也就是说,如果方法实行时开辟了新的线程,就会新天生一个TraceSegment记录调用信息, 如果时发起了rpc请求(跨进程)也会天生一个TraceSegment。
比如下单接口进入到了下单服务,下单服务起首调用本地的一个校验参数方法,由于没有跨线程或进程,到这里都只构建一个TraceSegment。当发起RPC调用查询库存服务库存是否充足时,此时就会产生一个新的TraceSegment,新的TraceSegment会持有一个TraceSegmentRef,指向前一个TraceSegment信息 ,如许就把调用串联起来了。
TraceSegment也有自己的id,同时也有traceId ,一条链路的所有TraceSegment里面的traceId都是相同的。后端分析时,只须要查询出traceId相同的TraceSegment集合,就可以分析一条链路了。
5.1.3 Span的介绍

TraceSegment下面还有Span,Span就是最小粒度了,代表一个详细的操纵, 也就是上面提到过的“http调用”,“DB调用”,“MQ调用”,“Cache调用”,“RPC调用”,“本地方法调用”。
TraceSegment 就是由很多详细的span组成,在反面代码中,TraceSegment 类里面会有一个List集合,里面就是存储的当前所属的span。
Span里面包含很多关于调用的信息,都是上报到后端的信息。
第一个是operationName:当前的操纵的名称,如果是http接口的话,这个就是请求的url地址。
第二个是component:当前是哪个组件,比如Tomcat,Mysql等。
第三个是tag信息:tag是一个map数组,记录一些额外信息,比如当前是HTTP请求,tag就会记录http.method=GET|POST 等。
第四个是spanLayer:layer是一个罗列,里面包含:DB(1), RPC_FRAMEWORK(2), HTTP(3), MQ(4), CACHE(5);
第五个是log:记录的是发生错误时的错误堆栈信息。
第六个是time:包含span的创建时间和结束时间,结束就代表发起了另一个span或者请求结束。
每个span也有自己的整型spanId,在一个TraceSegment里面的所有的span的ID都是自增长的,从0开始,也就是说第一个span的id为0 ,span里面也有parentSpanId用于指向前一个span的id。第一个span的parentSpanId为-1 。
通过对调用的总结与抽象,我们可以将Span分为三大类型:EntrySpan,LocalSpan,ExitSpan。
EntrySpan
代表一个请求入口的类型的span, 比如一个下单请求进入了下单服务的controller,起首请求会进入tomcat框架层,此时就会被tomcat的插桩插件拦截处理,创建EntrySpan ,而且设置好operationName,layer,tag等信息。
但是请求反面还会进入springmvc ,然后在到我们的controller接口, 进入springmvc插桩插件时不会创建新的EntrySpan , 而是复用这个tomcat层创建的EntrySpan ,但是会覆盖之前的operationName,layer,tag。 这种复用节省资源的操纵是最合理的计划。所以如果是连着两个都是Entry Span的就会产生复用逻辑,且信息记录的是靠近反面的信息。
值得留意的是,如果是调用查询redis或者mysql时,就不是创建EntrySpan, 而是反面要说的ExitSpan 。
LocalSpan
这个就很简单了,代表的是一个本地方法的调用,留意不是native方法,就是普通方法的调用。
ExitSpan
是链路中一个退出的点或者离开的Span,可以简单理解为离开当前线程或进程的操纵。
拿我们之前的下单接口的例子来讲,请求进到tomcat,然后进入springmvc,然后进入到我们的下单方法,下单方法里面又有一个redis的调用和mysql的调用。
当请求redis时,由于是离开了当前进程,会创建一个ExitSpan , 然后请求mysql,又会创建一个ExitSpan ,这个ExitSpan 和请求redis创建的两者之前没有任何关系。
如果下单接口里又通过feign调用了库存服务,这时会有ExitSpan 的复用逻辑 起首通过feign时,会被feign的插桩插件新建一个ExsitSpan,而且设置好operationName,layer,tag等信息。
然后feign通过httpclient发起http调用。此时httpclient的插桩插件会复用feign创建的ExsitSpan , 而且不会覆盖之前的operationName,layer,tag等信息,这个正好与EntrySpan相反。

写到这里,不知道读者能不能大概明白了链路的基本模型。可以反复几次读上面的理论知识,信任你一定能理解清晰,理解后,我们反面的编码会轻松很多。
5.2 链路TraceSegment,Span的编码实现

5.2.1 TraceSegment的实现

经过前面理论知识的洗礼,我们很轻易写出TraceSegment的实现,在apm-agent-core 模块下,新建类:
com.hadluo.apm.agentcore.trace.TraceSegment
  1. public class TraceSegment {
  2.     // 指向上一个 segment
  3.     @Setter
  4.     private TraceSegmentRef ref ;
  5.     // 当前 segment的 所有 span
  6.     private List<AbstractSpan> spanList = new ArrayList<>();
  7.     // segment的 id
  8.     private String traceSegmentId ;
  9.     // 一条跟踪链路的唯一id
  10.     @Setter
  11.     private String traceId ;
  12.     // 创建时间
  13.     private long createTime;
  14.     public TraceSegment() {
  15. // 工具类生成唯一id
  16.         this.traceSegmentId = GlobalIdGenerator.generate();
  17.         this.createTime = System.currentTimeMillis();
  18.         this.traceId = GlobalIdGenerator.generate() ;
  19.     }
  20.     // 添加一个span 到 当前segment
  21.     public void addSpan(AbstractSpan span) {
  22.         this.spanList.add(span);
  23.     }
  24. }
复制代码
TraceSegment有指向上一个的ref,还有装span的集合,以及traceId,traceSegmentId 等。这里traceId在构造函数中赋值了,不是说一条链路的traceId都一样吗? 这里我们的traceId 字段上面还有一个@Setter,这个是lombok注解,提供了set重新设置这个traceId 的方法,所以会包管traceId 都是同一个的。
GlobalIdGenerator为工具类,在apm-commons里面。
TraceSegmentRef 须要建立在apm-commons项目下,反面会公用到,在apm-commons项目新建类:
com.hadluo.apm.commons.trace.TraceSegmentRef
  1. public class TraceSegmentRef {
  2.     public enum SegmentRefType {
  3.         /*是跨进程产生的 trace segment*/
  4.         CROSS_PROCESS,
  5.         /*跨线程产生的新 trace segment*/
  6.         CROSS_THREAD
  7.     }
  8.     private SegmentRefType type;
  9.     private String traceId;
  10.     private String traceSegmentId;
  11.     // trace segment里面最后一个span id
  12.     private int spanId;
  13.     // 当前的服务名称
  14.     private String parentServiceName;
  15.     // 实例名称
  16.     private String parentServiceInstance ;
  17.     public TraceSegmentRef(ContextCarrier carrier) {
  18.         this.type = SegmentRefType.CROSS_PROCESS;
  19.         this.traceId = carrier.getTraceId();
  20.         this.traceSegmentId = carrier.getTraceSegmentId();
  21.         this.spanId = carrier.getSpanId();
  22.         this.parentServiceName = carrier.getParentServiceName();
  23.         this.parentServiceInstance = carrier.getParentServiceInstance();
  24.     }
  25. }
复制代码
这个TraceSegmentRef 就相称于TraceSegment。
之前我们讲到过,一个线程或进程里面操纵就是一个TraceSegment, 如果产生一个TraceSegment必定就是跨线程或者进程操纵,所以会有SegmentRefType 作为标识。其余的都是id的基本信息和服务名称等。
ContextCarrier 为跨线程或者进程传输的直接载体类,相称于一个bean对象。 当发生HTTP跨进程调用时,会把当前链路信息像traceId等,设置到http 的请求头里面,收到的服务就会剖析天生ContextCarrier ,然后天生TraceSegmentRef ,就将TraceSegment举行了串联。
在apm-commons中新建类:
com.hadluo.apm.commons.trace.ContextCarrier
  1. @Data
  2. public class ContextCarrier {
  3.     private String traceId;
  4.     private String traceSegmentId;
  5.     // 最后一个span id
  6.     private int spanId;
  7.     private String parentServiceName;
  8.     private String parentServiceInstance;
  9.     public boolean isEmpty() {
  10.         return traceId == null || traceId.isEmpty();
  11.     }
  12. }
复制代码
isEmpty 方法可以判断出上游是否有携带数据。 想象一下,如果是请求刚进入下单接口网关,此时是没有ContextCarrier 的,到下一层之前,会把ContextCarrier 里面的数据,设置到http请求头中,然后举行跨进程传递,下一层就会构造出isEmpty为false的ContextCarrier ,从而就得到了正确的TraceSegmentRef 。
5.2.2 span的实现

Span相对复杂一点,因为有三种类型,起首我们定义一个抽象span基类,在apm-commons项目下新建类:
com.hadluo.apm.commons.trace.AbstractSpan
  1. public interface AbstractSpan {
  2.     // 设置 当前span 的操作 的插件名称, 比如 :tomcat插件,mysql插件等
  3.     AbstractSpan setComponent(String component);
  4.     // 插件的分层 : DB层 , cache缓存层, rpc层,  http层 , mq层
  5.     AbstractSpan setLayer(SpanLayer spanLayer);
  6.     // 设置操作名称
  7.     AbstractSpan setOperationName(String operationName);
  8.     // 设置 一些 tag 值
  9.     AbstractSpan setTag(String key, String value);
  10.     // 开启
  11.     AbstractSpan start();
  12.     // 结束
  13.     AbstractSpan finish();
  14.     // 获取父亲spanid
  15.     int getParentSpanId();
  16.     // 获取当前的spanId
  17.     int getSpanId();
  18.     // 设置 当前span 所在trace segment 的前一个 ref
  19.     void ref(TraceSegmentRef ref);
  20.     // 记录错误
  21.     AbstractSpan log(Throwable t);
  22. }
复制代码
以上方法都是操纵span基本属性的方法,只有start和finish比较特殊。这两个方法标记span的开始和结束的一些动作。
SpanLayer 为一个罗列类, 在apm-commons目录下新建类:
com.hadluo.apm.commons.trace.SpanLayer
  1. public enum SpanLayer {
  2.     DB(1), RPC_FRAMEWORK(2), HTTP(3), MQ(4), CACHE(5);
  3.     private int code;
  4.     SpanLayer(int code) {
  5.         this.code = code;
  6.     }
  7.     public int getCode() {
  8.         return code;
  9.     }
  10. }
复制代码
然后新建一个抽象基本功能的实现类,在apm-agent-core模块新建类:
com.hadluo.apm.agentcore.trace.AbstractTracingSpan
  1. public abstract class AbstractTracingSpan implements AbstractSpan {
  2.     // 当前 spanId
  3.     @Setter
  4.     private int spanId;
  5.     // 上一级 的spanId
  6.     private int parentSpanId;
  7.     // 当前span操作
  8.     private String operationName;
  9.     private String componentName;
  10.     // tag
  11.     private final Map<String, String> tag = new HashMap<String, String>();
  12.     // 当前span所在的segment的 前一个segment , 当是批量线程调用时,就会是多个
  13.     protected final List<TraceSegmentRef> refs = new ArrayList<>();
  14.     // 当前span操作的分层: DB,CACHE,RPC,HTTP,MQ
  15.     private SpanLayer spanLayer;
  16.     // 开始时间
  17.     private long startTime;
  18.     // 结束时间
  19.     private long endTime;
  20.     // 错误堆栈
  21.     private Map<String, String> log = new HashMap<>();
  22.     protected AbstractTracingSpan(int spanId, int parentSpanId, String operationName) {
  23.         this.operationName = operationName;
  24.         this.spanId = spanId;
  25.         this.parentSpanId = parentSpanId;
  26.     }
  27.     @Override
  28.     public AbstractSpan setComponent(String component) {
  29.         this.componentName = component;
  30.         return this;
  31.     }
  32.     @Override
  33.     public AbstractSpan setLayer(SpanLayer spanLayer) {
  34.         this.spanLayer = spanLayer;
  35.         return this;
  36.     }
  37.     @Override
  38.     public AbstractTracingSpan setOperationName(String operationName) {
  39.         this.operationName = operationName;
  40.         return this;
  41.     }
  42.     @Override
  43.     public AbstractSpan setTag(String key, String value) {
  44.         tag.put(key, value);
  45.         return this;
  46.     }
  47.     @Override
  48.     public AbstractSpan finish() {
  49.         this.endTime = System.currentTimeMillis();
  50.         return this;
  51.     }
  52.     @Override
  53.     public AbstractSpan start() {
  54.         this.startTime = System.currentTimeMillis();
  55.         return this;
  56.     }
  57.     @Override
  58.     public int getParentSpanId() {
  59.         return parentSpanId;
  60.     }
  61.     @Override
  62.     public int getSpanId() {
  63.         return spanId;
  64.     }
  65.     @Override
  66.     public void ref(TraceSegmentRef ref) {
  67.         refs.add(ref);
  68.     }
  69.     @Override
  70.     public AbstractSpan log(Throwable t) {
  71.         log.put("time", System.currentTimeMillis() + "");
  72.         log.put("message", t.getMessage());
  73.         // 取4000长度
  74.         log.put("stack", Logs.convert2String(t, 4000));
  75.         return this;
  76.     }
  77. }
复制代码
AbstractTracingSpan抽象类很简单,就是实现了对span基本属性的设置操纵,span的基本属性在前面讲解理论也提到过,这里不在赘述。
然后开始实现详细的EntrySpan和ExitSpan,我们前面理论知识提到过,一个请求进入tomcat然后到springmvc,这两个插件的EntrySpan要被复用,只有一个,而且oprationName,tag,layer等记录的是SpringMVC层的信息,而ExitSpan的模式正好相反。
这里我们通过一个模拟的栈来实现上述功能。我们假象EntrySpan,ExitSpan 都有一个stackDepth栈深度属性,EntrySpan还有一个maxStackDepth最大栈深的属性。
EntrySpan的模拟
当请求到tomcat构建出EntrySpan时,stackDepth栈深度和maxStackDepth都从0加到1,进入springmvc层又都加1,设置oprationName,tag,layer信息时,判断stackDepth==maxStackDepth时,才设置,是不是调用越深,记录的就是靠近最里层的信息,当springmvc方法返回时,stackDepth减1,但是maxStackDepth不变,如许stackDepth就不会等于maxStackDepth,就不会覆盖oprationName,tag,layer信息。
ExitSpan 的模拟
ExitSpan 只有一个stackDepth栈深度的属性,当stackDepth==1时,记录oprationName,tag,layer信息,如许就包管了记录第一次的信息。
由于两者都有一个stackDepth栈深度属性,所以还可以抽象一层基于栈的抽象类,在apm-agent-core模块新建类:
com.hadluo.apm.agentcore.trace.StackBasedTracingSpan
  1. public abstract class StackBasedTracingSpan extends AbstractTracingSpan {
  2.     // 当前栈深度
  3.     int stackDepth;
  4.     protected StackBasedTracingSpan(int spanId, int parentSpanId, String operationName) {
  5.         super(spanId, parentSpanId, operationName);
  6.     }
  7. // 创建span的方法返回时会调用
  8.     @Override
  9.     public AbstractSpan finish() {
  10.         if (--stackDepth == 0) {
  11.             // 减到0代表栈为空了
  12.             super.finish();
  13.         }
  14.         return this;
  15.     }
  16. }
复制代码
这个finish方法并不是代表span结束,只有stackDepth 减到0时,才代表当前span结束,前面提到过,请求从tomcat到springmvc,stackDepth 的值会加到2,在springmvc方法返回时,会调用finish,stackDepth 的值减到1,此时并不是span结束,当tomcat层的方法返回时,stackDepth 的值减到0,span才会结束,然后调用父类的finish,记录结束时间, 此时这个span就真正结束了,就该加入到trace segment里面归档了。
末了一层就是EntrySpan代码,在apm-agent-core模块新建类:
com.hadluo.apm.agentcore.trace.EntrySpan
  1. public class EntrySpan extends StackBasedTracingSpan {
  2.     // 最大栈深,只增不减
  3.     private int maxStackDepth;
  4.     protected EntrySpan(int spanId, int parentSpanId, String operationName) {
  5.         super(spanId, parentSpanId, operationName);
  6.     }
  7.     @Override
  8.     public AbstractSpan start() {
  9.         // 当前栈深加1
  10.         stackDepth = stackDepth + 1;
  11.         // 赋值给最大栈深
  12.         maxStackDepth = stackDepth;
  13.         if(stackDepth == 1){
  14.             // 第一次进来
  15.             super.start();
  16.         }
  17.         return this;
  18.     }
  19.     @Override
  20.     public AbstractSpan setTag(String key, String value) {
  21.         // 比如:一个请求先进到Tomcat插件,然后进入到SpringMVC插件
  22.         // 进到 Tomcat 时,创建了entry span调用 start方法,stackDepth=1 , maxStackDepth=1 , 记录tag
  23.         // 在进入到SpringMvc时, 会复用span,但是会调用start方法,stackDepth=2 , maxStackDepth=2, 覆盖tag
  24.         // 出来时,调 finish,stackDepth减1 ,maxStackDepth不变, tag值不变
  25.         // 所以就记录的是 SpringMvc时的tag信息,也就是靠近里层的信息
  26.         if(maxStackDepth == stackDepth){
  27.             return super.setTag(key, value);
  28.         }
  29.         return this;
  30.     }
  31.     @Override
  32.     public AbstractSpan setLayer(SpanLayer spanLayer) {
  33.         // 同理 setTag
  34.         if(maxStackDepth == stackDepth){
  35.             return super.setLayer(spanLayer);
  36.         }
  37.         return this;
  38.     }
  39.     @Override
  40.     public AbstractTracingSpan setOperationName(String operationName) {
  41.         // 同理 setTag
  42.         if(maxStackDepth == stackDepth){
  43.             return super.setOperationName(operationName);
  44.         }
  45.         return this;
  46.     }
  47. }
复制代码
EntrySpan的实现关键就在于 maxStackDepth 和stackDepth的管理,以及判断 maxStackDepth 和stackDepth相等时,才设置有用的信息。
ExitSpan 代码,在apm-agent-core模块新建类:
com.hadluo.apm.agentcore.trace.ExitSpan
  1. public class ExitSpan extends StackBasedTracingSpan {
  2.     protected ExitSpan(int spanId, int parentSpanId, String operationName) {
  3.         super(spanId, parentSpanId, operationName);
  4.     }
  5.     @Override
  6.     public AbstractSpan start() {
  7.         stackDepth = stackDepth + 1;
  8.         if (stackDepth == 1) {
  9.             super.start();
  10.         }
  11.         return this;
  12.     }
  13.     @Override
  14.     public AbstractSpan setLayer(SpanLayer spanLayer) {
  15.         if(stackDepth == 1){
  16.             // 只有第一次会记录
  17.             return super.setLayer(spanLayer);
  18.         }
  19.         return this;
  20.     }
  21.     @Override
  22.     public AbstractSpan setTag(String key, String value) {
  23.         if(stackDepth == 1){
  24.             // 只有第一次会记录
  25.             return super.setTag(key, value);
  26.         }
  27.         return this;
  28.     }
  29.     @Override
  30.     public AbstractTracingSpan setOperationName(String operationName) {
  31.         if(stackDepth == 1){
  32.             // 只有第一次会记录
  33.             return super.setOperationName(operationName);
  34.         }
  35.         return this;
  36.     }
  37. }
复制代码
ExitSpan 的实现关键就在于 stackDepth的管理,以及判断 stackDepth == 1时,才设置有用的信息。
Span的类型还缺一个LocalSpan , 这个比较简单,没有复用的栈逻辑。LocalSpan 代码,在apm-agent-core模块新建类:
com.hadluo.apm.agentcore.trace.LocalSpan
  1. public class LocalSpan extends AbstractTracingSpan {
  2.     protected LocalSpan(int spanId, int parentSpanId, String operationName) {
  3.         super(spanId, parentSpanId, operationName);
  4.     }
  5. }
复制代码
其实为了逻辑流程的通用性,我们还须要一个忽略的Span类型,比如,当我们的采样率控制服务判断链路不须要采样时,为了流程的通用性,我们照旧要构建一个Span,只是这个类型为一个忽略的Span类型,当发送到OAP时,我们判断这个Span类型就过滤掉就好。
忽略的Span类型代码,在apm-agent-core模块新建类:
com.hadluo.apm.agentcore.trace.LoopSpan
  1. public class LoopSpan implements AbstractSpan {
  2. // 里面的实现方法都是空实现,没有逻辑
  3. }
复制代码
5.3 链路上下文

上小节部分我们实现了TraceSegment和Span, 但是我们还须要一个context去管理它们。
前面说到了span内部为了包管EntrySpan记录最深一层信息,和ExitSpan记录第一层信息计划了一个雷同栈的结构。其实我们的TraceSegment里面的所有Span也是一个真实的栈结构,这个栈不同于上面的说的栈,两者没有关系。
请求调用本地方法,或者rpc方法,都是一个TraceSegment里面的Span的入栈和出栈的操纵,当栈为空时,证明这个TraceSegment完结,就可以发送到OAP后端了。这些Span的管理,和入栈出栈就交给我们的链路管理的上下文(AbstraceTraceContext )。这里一个AbstraceTraceContext 对应一个TraceSegment。
固然AbstraceTraceContext还有一些其他的功能,我们直接编写AbstraceTraceContext代码,在apm-commons模块新建类:
com.hadluo.apm.commons.trace.AbstraceTraceContext
  1. public abstract class AbstraceTraceContext {
  2.     // 创建 entry span
  3.     public abstract AbstractSpan createEntrySpan(String operationName);
  4.     // 创建 local span
  5.     public abstract AbstractSpan createLocalSpan(String operationName);
  6.     // 创建 exit span
  7.     public abstract AbstractSpan createExitSpan(String operationName, String remotePeer);
  8.     /***
  9.      * 跨进程调用时, 将ContextCarrier设置到当前trace segment 的ref上
  10.      * @param carrier
  11.      */
  12.     public abstract void extract(ContextCarrier carrier);
  13.     /**
  14.      * 创建 entry span的 链路方法 结束时调用
  15.      */
  16.     public abstract void stopSpan();
  17.     /**
  18.      * 获取栈顶的span
  19.      * @return
  20.      */
  21.     public abstract AbstractSpan acviveSpan();
  22.     /**
  23.      * 当前span栈是否为空
  24.      * @return
  25.      */
  26.     public abstract boolean isEmpty() ;
  27. }
复制代码
它的实现类代码,在apm-agent-core模块新建类:
com.hadluo.apm.agentcore.trace.TracingContext
  1. public class TracingContext extends AbstraceTraceContext {
  2.     // 对应的 trace segment
  3.     private final TraceSegment traceSegment ;
  4.     // span 栈
  5.     private final LinkedList<AbstractSpan> spanStack = new LinkedList<>();
  6.     // span id 自增器
  7.     private final AtomicInteger spanIdGenerator = new AtomicInteger(0);
  8.     // kafka发送服务
  9.     private final KafkaProducerManager kafkaProducerManager ;
  10.     public TracingContext(){
  11.         this.traceSegment = new TraceSegment() ;
  12.         kafkaProducerManager = ServiceManager.INSTANCE.getService(KafkaProducerManager.class);
  13.     }
  14.     // 出栈,但栈内元素不变
  15.     private AbstractSpan pop(){
  16.         try {
  17.             return spanStack.getLast() ;
  18.         }catch (NoSuchElementException e){
  19.             return null ;
  20.         }
  21.     }
  22.     // 入栈
  23.     private void push(AbstractSpan span){
  24.         spanStack.addLast(span);
  25.     }
  26.     public AbstractSpan createEntrySpan(String operationName) {
  27.     }
  28.     public AbstractSpan createLocalSpan(String operationName) {
  29.     }
  30.     @Override
  31.     public AbstractSpan createExitSpan(String operationName, String remotePeer) {
  32.     }
  33.     @Override
  34.     public void extract(ContextCarrier carrier) {
  35.     }
  36.     @Override
  37.     public void stopSpan() {
  38.     }
  39.     @Override
  40.     public AbstractSpan acviveSpan() {
  41.         return pop();
  42.     }
  43.     public boolean isEmpty(){
  44.         return spanStack.isEmpty();
  45.     }
  46. }
复制代码
TracingContext的焦点就是维护span栈,通过LinkedList实现栈。一个TracingContext对应一个TraceSegment, 其实这个Context就是TraceSegment的辅助类。还有几个重要方法未实现单独提出来讲。
createEntrySpan方法代码如下:
  1. public AbstractSpan createEntrySpan(String operationName) {
  2.     // 获取栈顶, 不弹出栈元素
  3.     AbstractSpan parent = pop() ;
  4.     AbstractSpan entrySpan;
  5.     // 设置 父span的id, 没有parent就 为-1, 否则就是parent的id
  6.     int parentSpanId = (parent == null?-1:parent.getSpanId()) ;
  7.     if(parent != null && parent instanceof EntrySpan){
  8.         // 这里很重要, 要复用span,两个相邻的span都是entry span 就要发生复用
  9.         parent.setOperationName(operationName);
  10.         entrySpan = parent;
  11.         return entrySpan.start();
  12.     }
  13.     // 真正创建
  14.     entrySpan = new EntrySpan( spanIdGenerator.getAndIncrement()  , parentSpanId,operationName ) ;
  15.     // 入栈
  16.     push(entrySpan);
  17.     return entrySpan.start();
  18. }
复制代码
上述代码关键就在于entry span的复用,因为之前举了接口先到tomcat然后在到springmvc , 两个插件内实行时都要创建entry span ,这就是相邻两个都是entry span的场景, 要复用tomcat插件创建的entry span,而信息设置的是springmvc 插件的方法信息。
如果不是相邻的entry span,就要创建一个新的,然后入栈。
createLocalSpan方法代码如下:
  1. public AbstractSpan createLocalSpan(String operationName) {
  2.     AbstractSpan parent = pop() ;
  3. // 设置 父span的id, 没有parent就 为-1, 否则就是parent的id
  4.     int parentSpanId = (parent == null?-1:parent.getSpanId()) ;
  5.     LocalSpan localSpan = new LocalSpan( spanIdGenerator.getAndIncrement()  , parentSpanId,operationName ) ;
  6.     // 入栈
  7.     push(localSpan);
  8.     return localSpan;
  9. }
复制代码
LocalSpan没有复用的逻辑, 直接创建一个新的,然后入栈。
createExitSpan方法代码如下:
  1. @Override
  2. public AbstractSpan createExitSpan(String operationName, String remotePeer) {
  3.     AbstractSpan parent = pop() ;
  4.     int parentSpanId = (parent == null?-1:parent.getSpanId()) ;
  5.     if(parent instanceof ExitSpan){
  6.         // 要复用这个span
  7.         parent.start();
  8.         return parent;
  9.     }
  10.     ExitSpan exitSpan = new ExitSpan( spanIdGenerator.getAndIncrement()  , parentSpanId,operationName ) ;
  11.     exitSpan.start();
  12. // 将远端地址设置到tag里面
  13.     exitSpan.setTag("remotePeer" , remotePeer) ;
  14.     //入栈
  15.     push(exitSpan);
  16.     return exitSpan;
  17. }
复制代码
前面提到过ExitSpan理论,当feign插件调用httpclient插件发起接口请求时,这种属于方法的嵌套(前面方法还没返回),也是相邻复用的逻辑。
如果是调redis然后调mysql,这种是不存在复用的逻辑的,因为调redis方法返回了然后在调的mysql,不属于嵌套关系,当反面用调mysql时,之前调redis的ExitSpan已经在redis方法返回时出栈了,所以parent不大概是ExitSpan。
ExitSpan还有一个特殊属性就是远端地址,比如:发起redis调用,会创建一个ExitSpan,远端地址就是redis集群的地址,然后设置到tag里面。
extract方法代码如下:
  1. @Override
  2. public void extract(ContextCarrier carrier) {
  3.     if(carrier.isEmpty()){
  4.         return ;
  5.     }
  6.     TraceSegmentRef ref = new TraceSegmentRef(carrier);
  7.     this.traceSegment.setRef(ref);
  8.     this.traceSegment.setTraceId(carrier.getTraceId());
  9.     AbstractSpan span = pop();
  10.     if (span instanceof EntrySpan) {
  11.         span.ref(ref);
  12.     }
  13. }
复制代码
extract 就是借助跨进程传递的ContextCarrier 对象信息天生TraceSegmentRef , 然后设置到当前TraceSegment的ref字段上,如果是entry span入口类型的,还须要添加到span的ref上。其实就是上一个trace segment的信息传递。
setTraceId这个方法相称重要,标记了一条链路上的所有trace segment的traceId一样。
stopSpan方法代码如下:
  1. @Override
  2. public void stopSpan() {
  3.     AbstractSpan span = pop();
  4.     // span 的结束
  5.     span.finish();
  6.     // 将span加到segment 中
  7.     this.traceSegment.addSpan(span);
  8.     // 移除栈顶
  9.     spanStack.removeLast();
  10.     if(spanStack.isEmpty()){
  11.         // 将 segment 发送到 后端
  12.         kafkaProducerManager.send(this.traceSegment.transtorm());
  13.     }
  14. }
复制代码
一个span结束后, 要将span归档到trace segment里面 , 当span栈为空时,代表这个trace segment结束,须要将数据发送到后端,但是发送的对象并不是原生的TraceSegment, 而是通过transtorm方法复制的新对象。下面我们实现下真正发送到kafka的数据对象和transtorm方法。
TraceSegment的transtorm方法代码如下:
  1. public Segment transtorm(){
  2.     // 转换成 kafka发送的数据
  3.     Segment  segment = new Segment();
  4.     segment.setTraceSegmentId(traceSegmentId);
  5.     segment.setTraceId(traceId);
  6.     segment.setSpans(new ArrayList<>());
  7.     spanList.forEach(item->segment.getSpans().add(item.transtform()));
  8.     segment.setMsgTypeClass(Segment.class.getName());
  9.     segment.setServiceName(Config.Agent.serviceName);
  10.     segment.setServiceInstance(Config.Agent.serviceInstance);
  11.     return segment ;
  12. }
复制代码
封装span时,又调用了span的transtorm,代码如下:
  1. public Segment.Span transtform() {
  2.     Segment.Span span = new Segment.Span();
  3.     span.setSpanId(spanId);
  4.     span.setParentSpanId(parentSpanId);
  5.     span.setStartTime(startTime);
  6.     span.setEndTime(endTime);
  7.     span.setOperationName(operationName);
  8.     if (this instanceof EntrySpan) {
  9.         span.setSpanType("Entry");
  10.     } else if (this instanceof LocalSpan) {
  11.         span.setSpanType("Local");
  12.     } else {
  13.         span.setSpanType("Exit");
  14.     }
  15.     if(spanLayer != null){
  16.         span.setSpanLayer(spanLayer.toString());
  17.     }
  18.     span.setComponent(componentName);
  19.     span.setLogs(log);
  20.     span.setRefs(new ArrayList<>());
  21.     this.refs.forEach(item -> span.getRefs().add(item.transform()));
  22.     span.setTags(tag);
  23.     return span;
  24. }
复制代码
还须要在AbstractSpan上添加transtform,我就不写了。还调用了TraceSegmentRef的transform,代码如下:
  1. public Segment.SegmentReference transform(){
  2.     Segment.SegmentReference reference = new Segment.SegmentReference();
  3.     reference.setRefType(type.toString());
  4.     reference.setTraceId(traceId);
  5.     reference.setParentTraceSegmentId(traceSegmentId);
  6.     reference.setParentSpanId(spanId);
  7.     reference.setParentService(parentServiceName);
  8.     reference.setParentServiceInstance(parentServiceInstance);
  9.     return reference ;
  10. }
复制代码
然后在apm-commons 新建kafka发送的实体,新建类:
com.hadluo.apm.commons.kafka.Segment
  1. @Data
  2. public class Segment extends BaseMsg{
  3.     private String traceId ;
  4.     private String traceSegmentId;
  5.     private List<Span> spans ;
  6.     @Data
  7.     public static class Span {
  8.         private int spanId;
  9.         private int parentSpanId;
  10.         private long startTime;
  11.         private long endTime;
  12.         private List<SegmentReference> refs ;
  13.         private String operationName;
  14.         private String peer;
  15.         private String spanType;
  16.         //    DB(1), RPC_FRAMEWORK(2), HTTP(3), MQ(4), CACHE(5);
  17.         private String spanLayer ;
  18.         private String component;
  19.         private Map<String , String> tags ;
  20.         private Map<String , String> logs ;
  21.     }
  22.     @Data
  23.     public static class SegmentReference {
  24.         private String refType;
  25.         private String traceId;
  26.         private String parentTraceSegmentId;
  27.         private int parentSpanId ;
  28.         private String parentService;
  29.         private String parentServiceInstance;
  30.         private String networkAddressUsedAtPeer;
  31.     }
  32. }
复制代码
到此链路上下文,我们基本实现,后续还会添加新方法。固然,雷同前面的LoopSpan忽略的Span逻辑,我们同样也有一个忽略的Context,在apm-agent-core项目下新建类:
com.hadluo.apm.agentcore.trace.LoopTraceContext:
  1. public class LoopTraceContext extends AbstraceTraceContext {
  2.     private final LoopSpan INSTANCE = new LoopSpan();
  3.     @Override
  4.     public AbstractSpan createEntrySpan(String operationName) {
  5.         return INSTANCE;
  6.     }
  7.     @Override
  8.     public AbstractSpan createLocalSpan(String operationName) {
  9.         return INSTANCE;
  10.     }
  11.     @Override
  12.     public AbstractSpan createExitSpan(String operationName, String remotePeer) {
  13.         return INSTANCE;
  14.     }
  15.     @Override
  16.     public AbstractSpan acviveSpan() {
  17.         return INSTANCE;
  18.     }
  19. }
复制代码
方法也都是空的,入栈和出栈都是LoopSpan一个实例。
5.4 链路上下文管理器服务

前面说到一个上下文对应着一个TraceSegment,而TraceSegment是一个线程的所有Span操纵。所以上下文跟线程绑定,这里我们把上下文放入到一个ThreadLocal中举行管理,于是我们又计划了上下文管理器(TraceContextManager) , 用来管理上下文,这个管理器照旧一个BootService服务。
在apm-commoms模块中,新建类:
com.hadluo.apm.commons.trace.TraceContextManager
  1. public class TraceContextManager implements BootService {
  2.     // 采样服务
  3.     private SamplingService samplingService;
  4.     // 持有 上下文 的 ThreadLocal
  5.     private ThreadLocal<AbstraceTraceContext> CONTEXT = new ThreadLocal<>();
  6.     // 从ThreadLocal中取 上下文
  7.     private AbstraceTraceContext getOrCreate(boolean passed) {
  8.         if (CONTEXT.get() == null) {
  9.             if(!passed){
  10.                 try {
  11.                     CONTEXT.set((AbstraceTraceContext) Class.forName("com.hadluo.apm.agentcore.trace.LoopTraceContext").newInstance());
  12.                 } catch (Exception e) {
  13.                     throw new RuntimeException(e);
  14.                 }
  15.             }else{
  16.                 try {
  17.                     CONTEXT.set((AbstraceTraceContext) Class.forName("com.hadluo.apm.agentcore.trace.TracingContext").newInstance());
  18.                 } catch (Exception e) {
  19.                     throw new RuntimeException(e);
  20.                 }
  21.             }
  22.         }
  23.         return CONTEXT.get();
  24.     }
  25.     public AbstractSpan createEntrySpan(String operationName, ContextCarrier contextCarrier) {
  26.         AbstraceTraceContext context  ;
  27.         if (contextCarrier == null || contextCarrier.isEmpty()) {
  28.             //携带参数为空, 前面没有链路的调用
  29.             context =  getOrCreate(samplingService.trySampling());
  30.         }else {
  31.             // 前面的调用链路是已经采样了, 后续的调用 也必须要采样
  32.             context =  getOrCreate(true);
  33.         }
  34.         AbstractSpan span =  context.createEntrySpan(operationName);
  35.         context.extract(contextCarrier);
  36.         return span;
  37.     }
  38.     public AbstractSpan createLocalSpan(String operationName) {
  39.         return getOrCreate(true).createLocalSpan(operationName);
  40.     }
  41.     public AbstractSpan createExitSpan(String operationName, String remotePeer){
  42.         return getOrCreate(true).createExitSpan(operationName,remotePeer);
  43.     }
  44.     public void stopSpan(){
  45.         AbstraceTraceContext context = CONTEXT.get();
  46.         context.stopSpan();
  47.         if(context.isEmpty()){
  48.             // 栈已经是空的了,需要将线程变量移除
  49.             CONTEXT.remove();
  50.         }
  51.     }
  52.     public AbstractSpan activeSpan(){
  53.         return CONTEXT.get().acviveSpan();
  54.     }
  55.     @Override
  56.     public void prepare() throws Throwable {
  57.         this.samplingService = ServiceManager.INSTANCE.getService(SamplingService.class);
  58.     }
  59. }
复制代码
TraceContextManager 维护了线程ThreadLocal ,署理了上下文的创建span的几个方法。
createEntrySpan 须要单独说明下,ContextCarrier 参数是跨进程或跨线程传递的上一级TraceSegment的信息,在之前实现TraceSegment的时候已经实现过。判断如果是携带ContextCarrier 参数的,代表前面的TraceSegment已经存在,则反面的TraceSegment必须要采样(链路不能断掉)。
context.extract方法就是将ContextCarrier的值设置到 TraceSegmentRef ,这个 TraceSegmentRef 就是指向上一级的TraceSegment信息,之前也提到过。
5.5 本章小结

本章是整个链路监控的数据结构焦点,通过对链路的抽象将链路划分为详细的TraceSegment,Span等。他们的结构如图(图片摘抄于网络):


本章还介绍了EntrySpan的雷同栈计划,通过stackDepth(当前栈深度)和maxStackDepth(最大栈深度)灵活的控制了怎么包管记录靠近调用内测的信息。
本章还介绍了链路的上下文AbstraceTraceContext,一个上下文对应一个TraceSegment, 通过这个上下文对所属的所有Span也举行了栈管理,这是一个真实先进后出的栈,值得留意的是入栈时,须要包管EntrySpan和ExitSpan的复用对象逻辑。出栈时,当栈为空,表现这个TraceSegment已经结束须要归档发送到kafka。
由于TraceSegment属于线程里面的操纵,所以还创作了一个基于ThreadLocal的下文的管理器服务TraceContextManager,这个服务通过线程ThreadLocal来管理链路的上下文AbstraceTraceContext。
本章没有举行代码测试,因为大概一些细节后续还要修改,等反面介绍详细的插桩插件如何调用TraceContextManager的创建Span方法,传递怎样的参数,再举行修改美满代码和测试。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

tsx81429

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表