brave-zipkin的日志源码分析

其实在zipkin的日志里面作为发送端日志两个,sr,ss,这个日志是servlet产生的;接收端日志是四个,分别是cr,sr,ss,cs;cr和cs分别是上游的日志的信息;ss和sr是接收端输出的日志;
这些信息都是分别达到zipkin里面,拼接形式是由zipkin做的。都是span start开始计时并记录信息;span.finish达到缓冲区,到时候一并发送到zipkin的服务端。
 
zipkin内部判断两个span是否是一此调用链就看traceId,至于层级关系就看parentId,有了这些信息都是上游的restTemplate或者httpClient在发送请求的时候把这些信息添加到了HttpHeader里面的。如下:
=== MimeHeaders ===
content-type = application/json; charset=utf-8
content-length = 0
content-encoding = UTF-8
host = localhost:9080
connection = Keep-Alive
user-agent = Apache-HttpClient/4.5.2 (Java/1.7.0_79)
accept-encoding = gzip,deflate
x-b3-traceid = c933260d145a7d60
x-b3-spanid = 7041add2ef70a0c3
x-b3-parentspanid = c933260d145a7d60
x-b3-sampled = 1
因为之前一直想搞明白parentId是怎么传递过来的。为了解释清楚这个我们首先看一张图:
brave servlet的流程分为两套,首先网站初始化,通过读取web.xml获取到了DelegatingTracingFilter(brave-instrumentation-spring-webmvc.jar),进行创建,调用其init的时候,看到DelegatingTracingFilter的主要作用其实就是一个代理(所以称之为delegateFilter)内部没做什么事情,主要就是init初始化了一个自己代理的Filter:TracingFilter(brave-instrumentation-servlet.jar);
在第二套流程里面处理http请求流程中走DelegatingTracingFilter的doFilter其实是委托给了TracingFilter的doFilter方法;下面就是TracingFilter的对于请求的处理了。Filter是servlet的拦截器,当一个请求过来,brave的Filter要创建一个span,span是brave的日志内容实体,向zipkin服务器发送日志内容(JSON)就是从span中获取的;这件事情是由TracingFilter里面的HttpServerHandler做的。
HttpServerHandler(brave-instrumentation-http.jar)十分常用重要的类,因为其实拦截器里面servlet前后的处理全在这个类里面做的,里面核心方法就是一个handleReceive和一个handleSend,顾名思义,就是在调用servlet的execute前后做的处理。我们把话题再拉回来,创建span就是在handlerReceive中做的,填充span信息主要就是通过读取request来做的,那么这部分处理就是封装在HttpServerParse里面,这个类非常简单:
 1 public class HttpServerParser extends HttpParser {
 2  
 3 /**
 4 * Customizes the span based on the request received from the client.
 5 *
 6 * <p>{@inheritDoc}
 7 */
 8 @Override public <Req> void request(HttpAdapter<Req, ?> adapter, Req req, SpanCustomizer customizer) {
10     super.request(adapter, req, customizer);
11 }
12  
13 /**
14 * Customizes the span based on the response sent to the client.
15 *
16 * <p>{@inheritDoc}
17 */
18 @Override public <Resp> void response(HttpAdapter<?, Resp> adapter, @Nullable Resp res, @Nullable Throwable error, SpanCustomizer customizer) {
20     super.response(adapter, res, error, customizer);
21 }
22 }
但是其实本质处理都是在HttpParser 里面做的,读取request里面的属性来进行填充;但是,这里并没有做parentId以及traceId的信息。只是填充annotation里面的信息。我关心的parent、traceId在哪里呢?
在HttpServerHandler里面处理handlerReceive:
 1 public <C> Span handleReceive(TraceContext.Extractor<C> extractor, C carrier, Req request) {
 2     Span span = nextSpan(extractor.extract(carrier), request);
 3     if (span.isNoop()) return span;
 4  
 5     // all of the parsing here occur before a timestamp is recorded on the span
 6     span.kind(Span.Kind.SERVER);
 7     parseRequest(request, span);
 8  
 9     return span.start();
10 }

   在加粗部分里面的nextSpan函数内部那个提取器extractor将会提取request(carrier)里面的header信息,然后填充到span:

1 Span nextSpan(TraceContextOrSamplingFlags extracted, Req request) {
2     if (extracted.sampled() == null) { // Otherwise, try to make a new decision
3         extracted = extracted.sampled(sampler.trySample(adapter, request));
4   }
5     return extracted.context() != null ? tracer.joinSpan(extracted.context()) : tracer.nextSpan(extracted);
8 }    

 

我们在看到日志里面的输出cr,cs;有的是cs, sr, ss, cr,在链的顶端(首节点),没有trace信息,走的是tracer.nextSpan(extracted);在链的其他节点,走tracer.joinSpan(extracted.context())。join,顾名思义,就是加入一个Trace。
这段代码中有一个非常重要的类:Tracer(brave-XX.jar),Tracer在调用中是一个全局的对象的概念,代表一次调用链跟踪(traceId就是用来标志它的)。可以通过Tracer对象来创建span对象,尽管每个span都是独立的,但是他们都是从属于一个Tracer(通过traceId关联);这些可以通过看Tracer的类级别注释了解。tracer里面有个一个属性叫noop,设置了这个属性就没有了日志输出;可以在创建的Tracer的时候处理。
对于http.method以及http.path是HttpParse通过customerize.tag写入了日志中binaryAnnotations节点;
 
span有点混淆,在brave.jar中有个span,他的实现类是AutoValue_RealSpan(也在brave.jar中);类似的有个SpanCustomizer,实现类是AutoValue_RealSpanCustomizer在zipkin.jar中也有一个span,最终走的其实zipkin中的span。
连个span,有点混淆了;基本流程是这样的,首先创建的RealSpan,RealSpan在创建的时候将会创建一个RealSpanCustomizer(其实是AutoValue_RealSpanCustomizer),其实使用路径是通过brave的span对象获取customizer,然后通过customize获取recorder,其实所有的操作都是最后都是委托给了Record写入内容,Record里面维护一个MutableSpan的Map,其实委托给Record的写操作都委托给了Record;然后在RealSpan在finish的时候,将会被把record里面获取MutableSpan,MutableSpan里面包含了zipkin的spanBuilder,可以转换为zipkin的span进行发送。
为什么要在Record来封装MutableSpanMap呢?因为要增加是否有noop设置,如果有则不设置span值。这里注意是两个span,一个brave的span,一个zipkin的span。
为什么要这么处理?
(未解)
 
 
现在我知道了在zipkin看到的日志内容其实是多个发送拼接,三段内容拼接,首先是上游应用的servlet接收到http请求,servlet拦截完事后调用record.finish向zipkin发送日志内容(sr,ss);上游的httpClient/restTemplate的拦截器(cs,cr),拦截完事了向zipkin发送日志;最后是下游应用的servlet拦截完事后发送的日志(sr,ss);通过zipkin页面看到的内容其实是三段内容组合在了一起。
 
下面就是brave的httpclient的拦截机制。
HttpBuilder是HttpClient.jar里面的类,这个类里面有两个抽象函数,分别是decorateProtocalExec以及decorateMainExex,分别是让子类设置对应的实现类。XXDecorateProtocalExe.executec是第一个执行的拦截器(调用handler的handleSend来跟踪处理client.execute之前),主要是创建了span;XXDecorateMainExec.execute是最后一个拦截器(调用handler的handleReceive来处理client.execute之后),主要用于启动span。这两个类的处理本质上都是调用HttpClientHandler,handler里面连个函数:handlerReceive被TracingProtocalExec消费,handlerSend则被TracingPMainExec消费。
这两个Exec都共同引用了一个对象clientExeChain,这个是在创建的时候通过参数注入进去的,是client.execute的整个调用(拦截)链,就是通过每层都调用这个链的execute来执行下一个链。
 

posted on 2018-04-22 22:51  张叫兽的技术研究院  阅读(2563)  评论(0编辑  收藏  举报

导航