本篇文章基于上一篇,只针对数据采集 做介绍,会提供一个SDK的实现和使用,会做实现方案的介绍,具体详细介绍下面边框加粗的部分:
一、数据采集 接着拿上一篇里的例子来说,把例子里的图贴过来:
简单回顾下上图,一次API调用,完成上面各个业务服务的调用,然后聚合所有服务的信息,然后Redis_02的调用发生瓶颈,继而影响到E、D、C三个服务,现在需要直观的展示这条链路上的瓶颈点,于是需要一个链路系统,展示成如下图的效果:
要想展示成上图中的效果,则必须要进行数据的采集和上报,那么这就牵扯到两个概念,Span
和Tracer
,抽象成数据库的设计层面,可以理解成Tracer
对Span
等于一对多
的关系,而一个Span
可能包含多个子Span
,一个Tracer
表示一次调用所经过的整个系统链路,里面包含N多Span
,每个Span
表示一次事件的触发(也就是调用),那么就用图2
来解释下这种关系:
所以上报数据最关键的地方就是要做到如下几点:
在调用之处(比如例子中API调用开始的地方),创建Tracer
,生成唯一Trace ID
;
在需要追踪的地方(比如例子中发生服务调用的地方),创建Span
,指定Trace ID
,并生成唯一Span ID
,然后按需建立父子关系,追踪结束时(比如例子中调用完成时)释放Span(即置为finished,此时计时已完成);
跨系统追踪时做好协议约定,每次跨系统调用时可以在协议头传输发起调用系统的TraceID
,以便链路可以做到跨系统顺利传输。
最终主链路执行完毕(例子中就是指API调用结束)时,推送此链路产生的所有Span
到链路系统,链路系统负责落库、数据分析和展示。
以上便是链路追踪业务SDK需要参与做到的事情。
Tracer
是个虚拟概念,负责聚合Span
使用,实际上报的数据全是Span
,下面来看下Span
的结构定义(JSON):
1 2 3 4 5 6 7 8 9 10 11 12 13 { "spanId" : 123456 , "traceId" : 1234 , "parentId" : 123455 , "title" : "getSomeThing" , "project" : "project.tree.group.project_name" , "startTime" : 1555731560000 , "endTime" : 1555731570000 , "tags" : { "component" : "rpc" , "span.kind" : "client" } }
这是一个span的基本结构定义,startTime
和endTime
可以推算出本次Span耗时(交给链路系统前端时可以用来展示时间轴的长短),title
表示的是Span本身的描述,一般是一个method
的名字,project
是当前所处项目 的全称,项目的全称可以交给链路系统前端用来搜索出该项目的所有链路信息。spanId
、traceId
、parentId
结合上面的图理解即可,tags
表示的是一些描述信息,这里有一些标准化的东西:标准的Span tag 和 log field
二、数据采集基于Java语言的实现 一般基于io.opentracing标准
实现上报SDK,下面来逐步实现一个最简单的数据收集器,首先在项目中引入io.opentracing
的jar包,然后追加两个基本类SimpleTracer
和SimpleSpan
,这里只贴出关键代码。
SimpleTracer
定义:
代码块1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 public class SimpleTracer implements Tracer { private final List finishedSpans = new ArrayList <>(); private String project; private Boolean sampled; public SimpleTracer (boolean sampled, String project) { this .project = project; this .sampled = sampled; } public SimpleTracer (String uri, String project) { this .project = project; this .sampled = PushUtils.sampled(uri); } @Override public SpanBuilder buildSpan (String operationName) { return new SpanBuilder (operationName); } public synchronized void pushSpans () { if (sampled != null && sampled) { List finished = this .finishedSpans; if (finished.size() > 0 ) { finished.stream().filter(SimpleSpan::sampled).forEach(span -> PushHandler.getHandler().pushSpan(span)); this .reset(); } } } public final class SpanBuilder implements Tracer .SpanBuilder { private final String title; private long startMicros; private List references = new ArrayList <>(); private Map<String, Object> initialTags = new HashMap <>(); SpanBuilder(String title) { this .title = title; } @Override public SpanBuilder asChildOf (SpanContext parent) { return addReference(References.CHILD_OF, parent); } @Override public SpanBuilder addReference (String referenceType, SpanContext referencedContext) { if (referencedContext != null ) { this .references.add(new SimpleSpan .Reference((SimpleSpan.SimpleSpanContext) referencedContext, referenceType)); } return this ; } @Override public SimpleSpan start () { return startManual(); } @Override public SimpleSpan startManual () { if (this .startMicros == 0 ) { this .startMicros = SimpleSpan.nowMicros(); } return new SimpleSpan (SimpleTracer.this , title, startMicros, initialTags, references); } } }
上面放了SimpleTracer
的代码片段,关键信息已标注,这个类的作用就是帮助创建span
,上面还有一个比较重要的方法,也就是sampled方法
,该方法用来生成这次链路是否上报(也就是采样率
,实际的追踪系统不可能每次的请求都上报,对于一些QPS较高的系统,会带来额外大量的存储数据,因此需要一个上报率),下面来简单看下上报率的实现:
代码块2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class PushUtils { public static final Random random = new Random (); private static final Map<String, Long> requestMap = Maps.newConcurrentMap(); public static boolean sampled (String uri) { if (Strings.isNullOrEmpty(uri)) { return false ; } Long start = requestMap.get(uri); Long end = System.currentTimeMillis(); if (start == null ) { requestMap.put(uri, end); return true ; } if ((end - start) >= 60000 ) { requestMap.put(uri, end); return true ; } else { if (random.nextInt(999 ) == 0 ) { requestMap.put(uri, end); return true ; } } return false ; } }
这种是比较适中的做法,如果1min内没有上报一次,则必定上报,如果1min内连续上报多次,则按照千分之一的概率上报,这样既保证了低QPS的系统可以有相对较多的链路数据,也可以保证高QPS的系统可以有相对较少的链路数据。
下面来看下SimpleSpan
的关键代码段:
代码块3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 public class SimpleSpan implements Span { private final SimpleTracer simpleTracer; private final long parentId; private final long startTime; private final Map<String, Object> tags; private final List references; private final List errors = new ArrayList <>(); private SimpleSpanContext context; private boolean finished; private long endTime; private boolean sampled; private String project; private String title; SimpleSpan(SimpleTracer tracer, String title, long startTime, Map<String, Object> initialTags, List refs) { this .simpleTracer = tracer; this .title = title; this .startTime = startTime; this .project = tracer.getProject(); this .sampled = tracer.isSampled(); if (initialTags == null ) { this .tags = new HashMap <>(); } else { this .tags = new HashMap <>(initialTags); } if (refs == null ) { this .references = Collections.emptyList(); } else { this .references = new ArrayList <>(refs); } SimpleSpanContext parent = findPreferredParentRef(this .references); if (parent == null ) { this .context = new SimpleSpanContext (nextId(), nextId(), new HashMap <>()); this .parentId = 0 ; } else { this .context = new SimpleSpanContext (parent.traceId, nextId(), mergeBaggages(this .references)); this .parentId = parent.spanId; } } @Nullable private static SimpleSpanContext findPreferredParentRef (List references) { if (references.isEmpty()) { return null ; } for (Reference reference : references) { if (References.CHILD_OF.equals(reference.getReferenceType())) { return reference.getContext(); } } return references.get(0 ).getContext(); } @Override public synchronized void finish (long endTime) { finishedCheck("当前span处于完成态" ); this .endTime = endTime; this .simpleTracer.appendFinishedSpan(this ); this .finished = true ; } public static final class SimpleSpanContext implements SpanContext { private final long traceId; private final Map<String, String> baggage; private final long spanId; public SimpleSpanContext (long traceId, long spanId, Map<String, String> baggage) { this .baggage = baggage; this .traceId = traceId; this .spanId = spanId; } } public static final class Reference { private final SimpleSpanContext context; private final String referenceType; public Reference (SimpleSpanContext context, String referenceType) { this .context = context; this .referenceType = referenceType; } } }
上面就是SimpleSpan
的关键实现,关键点已标注,下面来看下数据上报这里的实现:
代码块4 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class PushHandler { private static final PushHandler handler = new PushHandler (); private BlockingQueue queue; private PushHandler () { this .queue = new LinkedBlockingQueue <>(); new Thread (this ::pushTask).start(); } public static PushHandler getHandler () { return handler; } public void pushSpan (SimpleSpan span) { queue.offer(span); } private void pushTask () { if (queue != null ) { SimpleSpan span; while (true ) { try { span = queue.take(); StringBuilder sb = new StringBuilder () .append("tracerId=" ) .append(span.context().traceId()) .append(", parentId=" ) .append(span.parentId()) .append(", spanId=" ) .append(span.context().spanId()) .append(", title=" ) .append(span.title()) .append(", 耗时=" ) .append((span.endTime() / 1000000 ) - (span.startTime() / 1000000 )) .append("ms, tags=" ) .append(span.tags().toString()); System.out.println(sb.toString()); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
只是做了简单的测试,所以处理逻辑只是简单的做了打印,实际当中这里要上报链路数据(spans
)。这里使用了一个阻塞队列做数据接收的缓冲区。
这套实现是非常简单的,只进行简单的计时、推送,并没有涉及active方式
的用法,一切创建、建立父子关系均交由开发人员自己把控,清晰度也更高些。
代码完整地址:simple-trace
三、simple-trace的使用 看了上面的实现,这里利用simple-trace
来进行程序追踪,看一个简单的例子:
代码块5 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 public class SimpleTest { private SimpleTracer tracer = null ; private SimpleSpan parent = null ; @Test public void test1 () { tracer = new SimpleTracer ("test1" , "projectName" ); parent = tracer.buildSpan("test1" ) .withTag(SpanTags.COMPONENT, "http" ) .withTag(SpanTags.SPAN_KIND, "server" ) .start(); String result1 = getResult1(); System.out.println("r1 = " + result1); String result2 = getResult2(); System.out.println("r2 = " + result2); parent.finish(); tracer.pushSpans(); } public String getResult1 () { SimpleSpan currentSpan = null ; if (tracer != null && parent != null ) { SimpleSpan.SimpleSpanContext context = new SimpleSpan .SimpleSpanContext(parent.context().traceId(), parent.context().spanId(), new HashMap <>()); currentSpan = tracer.buildSpan("getResult1" ) .addReference(References.CHILD_OF, context) .withTag(SpanTags.COMPONENT, "redis" ) .withTag(SpanTags.SPAN_KIND, "client" ).start(); } try { Thread.sleep(1000L ); return "result1" ; } catch (InterruptedException e) { e.printStackTrace(); return "" ; } finally { if (currentSpan != null ) { currentSpan.finish(); } } } public String getResult2 () { SimpleSpan currentSpan = null ; if (tracer != null && parent != null ) { SimpleSpan.SimpleSpanContext context = new SimpleSpan .SimpleSpanContext(parent.context().traceId(), parent.context().spanId(), new HashMap <>()); currentSpan = tracer.buildSpan("getResult2" ) .addReference(References.CHILD_OF, context) .withTag(SpanTags.COMPONENT, "redis" ) .withTag(SpanTags.SPAN_KIND, "client" ).start(); } try { Thread.sleep(2000L ); return "result2" ; } catch (InterruptedException e) { e.printStackTrace(); return "" ; } finally { if (currentSpan != null ) { currentSpan.finish(); } } } }
运行结果:
1 2 3 4 5 r1 = result1 r2 = result2 tracerId=1507767477962777317, parentId=2107142446015091038, spanId=5095502823334701185, title=getResult1, 耗时=1555839336570 - 1555839335569 = 1001ms, tags={span.kind=client, component=redis} tracerId=1507767477962777317, parentId=2107142446015091038, spanId=9071431876337611242, title=getResult2, 耗时=1555839338572 - 1555839336571 = 2001ms, tags={span.kind=client, component=redis} tracerId=1507767477962777317, parentId=0, spanId=2107142446015091038, title=test1, 耗时=1555839338572 - 1555839334687 = 3885ms, tags={span.kind=server, component=http}
通过该实例,关于simple-trace
的基本用法已经展示出来了(创建tracer
、span
、建立关系
、tags
、finish
等),看下打印结果(打印结果就是simple-trace
推送数据时直接打印的,耗时是根据startTime
和endTime
推算出来的),父子关系建立完成,假如说这些数据已经落库完成,那么通过链路系统的API解析和前端渲染,会变成下面这样(绘图和上面测试结果不是同一次,所以图里耗时跟上面打印的耗时不一致😭):
本篇不讨论图如何生成,可以说下后端可以给前端提供的接口结构以及组装方式:首先可以根据traceId
查出来所有相关span
,然后根据parentId
进行封装层级,比如图4
的API结构大致上如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 { "spanId" : 2107142446015091038 , "traceId" : 1507767477962777317 , "parentId" : 0 , "title" : "test1" , "project" : "projectName" , "startTime" : 1555839334687 , "endTime" : 1555839338572 , "tags" : { "span.kind" : "server" , "component" : "http" } , "children" : [ { "spanId" : 5095502823334701185 , "traceId" : 1507767477962777317 , "parentId" : 2107142446015091038 , "title" : "getResult1" , "project" : "projectName" , "startTime" : 1555839335569 , "endTime" : 1555839336570 , "tags" : { "span.kind" : "client" , "component" : "redis" } , "children" : [ ] } , { "spanId" : 9071431876337611242 , "traceId" : 1507767477962777317 , "parentId" : 2107142446015091038 , "title" : "getResult2" , "project" : "projectName" , "startTime" : 1555839336571 , "endTime" : 1555839338572 , "tags" : { "span.kind" : "client" , "component" : "redis" } , "children" : [ ] } ] }
包装成上面的结构,前端根据层级关系
、startTime
、endTime
进行调用树和时间轴的渲染即可,在实际生产中,这个层级树可能更加庞大,比如图2
。
基本使用很简单,那么基于简单的例子再进行一层抽象,如果在生实际项目中,就不能单单像上面那样使用了,需要封装、解耦,那么实际项目中一般会通过怎样的方式来使用呢?跨系统的时候如何建立层级关系呢?下面针对图2中的例子,进行简单的方案设计(图2过于复杂,这里只说服务A的调用链路,其余按照服务A类推即可),下面将会采用伪代码的方式进行说明问题的解决方案,实际当中需要自己按照实现思路自行封装。
现在引入两个概念,拦截器和Context(上下文),它们属于正常业务中常用的概念,Context是指一次调用产生的上下文信息,上下文信息可以在单次程序调用中的任意位置取到,一般上下文都是利用ThreadLocal(简称TL)实现的,线程本地变量,单纯理解就是只要本次调用的信息都处于同一个线程,那么任意地方都可以通过TL对象拿到上下文对象信息,但是由于系统的复杂度越来越高,一些地方会采用线程池来进行优化业务代码,比如一次调用可能会利用CompletableFuture来进行异步任务调度来优化当前代码执行效率,这个时候单纯使用TL就办不成事儿了,而使用InheritableThreadLocal(简称ITL)又解决不了线程池传递问题,于是就有了阿里推出的TransmittableThreadLocal(简称TTL),这个可以完美解决跨线程传递上下文信息(不管是new Thread还是线程池,都可以准确传递),当然,你也可以仿照TTL的实现,简单代理线程池对象,仍然使用TL实现跨线程传递,也是可以的,TL系列文章传送门:ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal
下面是关于系统上下文的简单定义:
代码块6 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public class Context { private SimpleTracer simpleTracer; private SimpleSpan parent; public SimpleTracer getSimpleTracer () { return simpleTracer; } public void setSimpleTracer (SimpleTracer simpleTracer) { this .simpleTracer = simpleTracer; } public SimpleSpan getParent () { return parent; } public void setParent (SimpleSpan parent) { this .parent = parent; } } public class ContextHolder { private static ThreadLocal contextThreadLocal = new ThreadLocal <>(); private ContextHolder () { } public static void removeContext () { contextThreadLocal.remove(); } public static Context getContext () { return contextThreadLocal.get(); } public static void setContext (Context context) { if (context == null ) { removeContext(); } contextThreadLocal.set(context); } }
我们把链路对象和链路第一次产生的父span
放到上下文
,意味着我们可以在这次调用的任意位置通过ContextHolder
获取到当前链路对象(伪代码会出现该类),下面来结合图2
的A服务链路
,结合aop思想
,写一次从图2
API调用开始到Redis01调用结束的代码。
按照流程,API属于一次Http调用,也是链路入口,那么利用这一点,和Http服务的拦截器功能(大部分系统都会用到一个http调用的拦截器,一般上下文也是这里产生的),伪代码如下:
代码块7 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public class ApiInterceptor { public void beforeHandle (Request request) { Context context = new Context (); SimpleTracer tracer = null ; SimpleSpan parent = null ; String traceId = request.headers.get("x1-trace-id" ); String parentId = request.headers.get("x1-span-id" ); String sampled = request.headers.get.get("x1-sampled" ); if (traceId != null && parentId != null && sampled == true ) { tracer = new SimpleTracer (request.getUri, "所属项目名" ); SimpleSpan.SimpleSpanContext simpleSpanContext = new SimpleSpan .SimpleSpanContext(traceId, parentId, new HashMap <>()); parent = tracer.buildSpan(request.getUri) .addReference(References.CHILD_OF, simpleSpanContext) .withTag(SpanTags.COMPONENT, "http" ) .withTag(SpanTags.SPAN_KIND, "server" ).start(); } else { tracer = new SimpleTracer (request.getUri, "所属项目名" ); parent = tracer.buildSpan(request.getUri) .withTag(SpanTags.COMPONENT, "http" ) .withTag(SpanTags.SPAN_KIND, "server" ) .start(); } context.setSimpleTracer(tracer); context.setParent(parent); ContextHolder.setContext(context); } public void hadle () { doing(); } public void afterHandler () { SimpleTracer tracer = ContextHolder.getContext().getTracer(); SimpleSpan parent = ContextHolder.getContext().getParent(); if (tracer != null && parent != null ) { parent.finish(); tracer.pushSpans(); } } }
通过这个外部的API链路包装,可以知道的事情是上下文
在这里面充当的角色,API调用是一个系统的入口,这种入口有很多,一次系统调用都会有一个类似的入口,比如RPC调用
,跨系统后的rpcServer端
也是一个入口,这种入口级的拦截器
,before
里面做的通常都是建立Tracer
,但是代码里不是简单的创建一个Tracer对象就完事儿了,还有协议头的分析,链路系统如何实现跨系统的传输呢?这就牵扯到协议约定,比如Http请求,可以在协议头里约定几个特殊字符串来存放来源系统的tracerId
等,结合上面的例子,假如我们这个API是公司内别的系统API01
发起的http调用,API01本身也会有链路追踪,API01系统内发起对我们API的http请求,这就属于跨系统调用
,我们这次API调用相对于API01
是一个子链路
,需要建立父子关系,结合上面的例子简单画下这次调用图:
包括API的其他跨系统的调用,比如A服务的调用,也是使用同样的原理进行链路跨系统传输的(很多RPC框架
上层协议也是支持扩展协议头(即协议的元数据
信息)的,比如grpc
的上层协议就是http2
,同样有header
),那么接下来看下图中(截自图2
)标红模块对应的伪代码吧:
这块是指当前系统通过rpc client
发起对A服务
的调用,从发起调用到A服务
响应,这个过程仍然属于API这次调用的子span
(没有出系统),但是到了A服务
的触发,就牵扯到跨系统
,A服务
的链路相对于rpc client
(图6
标红的操作)的span
,是一个子span
,通过上面对跨系统的处理,这里rpc client
里一定会把自身的spanId
作为A服务
的parentId
传过去,包括traceId
等,来看下伪代码:
代码块8 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class RpcClient { public void requestRpc (RpcRequest request) { SimpleSpan span = null ; SimpleSpan parent = ContextHolder.getContext().getParent(); SimpleTracer tracer = ContextHolder.getContext().getTracer(); if (tracer != null && parent != null ) { span = tracer.buildSpan(request.getRpcMethod).asChildOf(parent) .withTag(SpanTags.COMPONENT, "grpc" ) .withTag(SpanTags.PEER_SERVICE, request.getRpcMethod) .withTag(SpanTags.SPAN_KIND, "client" ) .start(); request.setHeader("x1-rpc-span-id" , span.context().spanId()); request.setHeader("x1-rpc-trace-id" , span.context().traceId()); request.setHeader("x1-rpc-sampled" , span.sampled()); } rpcServerRequest(request); if (span != null ){ span.finish(); } } }
这样就完成了图6
中红线部分的span
,然后来看下被调用的服务A内部
是怎么处理的(其实很像上面http入口的处理方式):
代码块9 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 public class RpcServerInterceptor { public void beforeHandle (RpcRequest request) { Context context = new Context (); SimpleTracer tracer = null ; SimpleSpan parent = null ; String traceId = request.headers.get("x1-rpc-trace-id" ); String parentId = request.headers.get("x1-rpc-span-id" ); String sampled = request.headers.get.get("x1-rpc-sampled" ); if (traceId != null && parentId != null && sampled == true ) { tracer = new SimpleTracer (request.getMethod, "所属项目名" ); SimpleSpan.SimpleSpanContext simpleSpanContext = new SimpleSpan .SimpleSpanContext(traceId, parentId, new HashMap <>()); parent = tracer.buildSpan(request.getMethod) .addReference(References.CHILD_OF, simpleSpanContext) .withTag(SpanTags.COMPONENT, "rpc" ) .withTag(SpanTags.SPAN_KIND, "server" ).start(); } else { tracer = new SimpleTracer (request.getMethod, "所属项目名" ); parent = tracer.buildSpan(request.getMethod) .withTag(SpanTags.COMPONENT, "rpc" ) .withTag(SpanTags.SPAN_KIND, "server" ) .start(); } context.setSimpleTracer(tracer); context.setParent(parent); ContextHolder.setContext(context); } public void rpcServerHadle () { doing(); } public void afterHandler () { SimpleTracer tracer = ContextHolder.getContext().getTracer(); SimpleSpan parent = ContextHolder.getContext().getParent(); if (tracer != null && parent != null ) { parent.finish(); tracer.pushSpans(); } } }
可以看到,client
发起调用时传递的协议字段,在服务端这里被解析了,建立好父子关系后,A服务再去处理自己的逻辑和链路。
没有牵扯到跨系统的链路追踪,如对redis
、memcached
、mysql
等DB的调用,可以简单在调用元方法上搞个aop代理
,然后通过通过上下文对象里的Tracer
和parent
建立父子关系,结束时finish
即可,而pushSpans
这个动作通常发生在一次系统调用执行完毕的时候发生,比如API的调用结束时、A服务调用结束时,都是pushSpans
的触发点。
到这里基本上关于链路追踪的介绍算结束了,因为系统级的实现方式想要完整的展现在一篇文章里不太现实,所以在使用simple-trace sdk
的时候使用了伪代码
,便于说明问题,文章没有针对整个链路系统作说明,主要是针对数据采集、数据跨系统追踪做了描述,因为数据采集
这一环算是比较重要的一环,也是跟业务开发人员息息相关的一环,如果想要完整搞一个链路追踪系统,可以参考之前的架构搭建一套,以完成采集、上报、落库、解析、展示整个流程。