2. 外婆都称赞的基于Jaeger的Span模型改造

662 阅读6分钟

大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈


前言

我们的目标是基于Jaeger来实现分布式链路追踪的Java客户端工具包,实际就是一个SpringbootStarter,在1. 看完这篇文章我奶奶都懂Opentracing了一文中我们详细的学习了一下Opentracing的相关概念以及阅读了相关的源码,同时特别重要的是我们还知道了Opentracing为我们造了很多轮子,这些轮子大部分时候都是可以直接用的,我们只需要指定具体的实现框架例如Jaeger和提供相应的扩展点例如Span装饰器,我们就能够造出来我们自己的轮子。

那么毫无疑问的,我们的分布式链路追踪的Java客户端工具包,会按照Opentracing规范并使用Jaeger作为具体实现来进行开发,但是这里存在一个问题,就是Jaeger中的Span模型,和我们直观的觉得一个服务就是一个Span的想法是不太一样的,具体怎么不一样,如何调整,且听下文详述。

Opentracingjaeger相关版本依赖如下。

opentracing-api版本:0.33.0
opentracing-spring-web版本:4.1.0
jaeger-client版本:1.8.1

正文

1. 看完这篇文章我奶奶都懂Opentracing了一文中我们搭建了一个示例demo,这个示例大概表示了如下的一个请求路径。

分布式链路追踪-示例过滤器和拦截器示意图

大致就是从浏览器发起请求,请求先到client,然后经过RestTemplateTracingInterceptor拦截后,由RestTemplate发送到server,进server前,先通过了TracingFilter,最终才来到serverController。上述这个demo不太好演示本文的Span模型,我们做一下修改,修改后的请求路径如下。

分布式链路追踪-改进示例过滤器和拦截器示意图

结合上面的请求路径,再结合示例demoRestTemplateTracingInterceptorTracingFilter的实现,上图可以进一步做如下的Span模型抽象。

分布式链路追踪-抽象Span模型1

我们知道一个Span通常包含traceIdspanIdparentSpanId,所以我们按照示例demoRestTemplateTracingInterceptorTracingFilter的实现的规则,再把这些信息添加上,此时Span模型抽象如下。

分布式链路追踪-抽象Span模型2

到这里可以发现,上述每个Spanid是不一样的,即在接收到请求以及发起请求时均会生成一个新的Span,那么这里就存在一个情况,即Span代表的是一次请求接收或请求发起,这无疑是符合OpentracingSpan的定义的,但是这样其实是不太好理解的,可如果说一个Span就代表一次请求经过的某一个服务实例,那么是不是一下子就好理解了,就像下面这样。

分布式链路追踪-抽象Span模型3

这样的Span才是符合我们直观的认知的,同时Span记录的时间范围,就可以表示从这个实例接收请求到这个实例响应所花的时间,那么一次请求慢在哪个实例,通过看Span的时间就清楚了,所以我们需要对之前JaegerSpan模型做一下小小的改造,下面给出改造后的Span模型示意图。

分布式链路追踪-改造后Span模型示意图

上述模型,在每次作为客户端发起请求前,还是会创建一个新的Span,但是在作为服务端接收请求时,如果客户端跨进程传递了Span,则使用客户端传递过来的Span作为当前的Span,而不再新创建Span,这样一个过程,就好比是客户端(上游)替服务端(下游)创建好了Span然后给到了服务端(下游)一样。按照这样子改造,一个Span就可以表示一次请求经过的链路中的一个服务实例,而这,也正是契合ZipkinSpan模型。

现在既然已经明确了Span模型如何改造,那么具体到代码中,要怎么改造呢,Jaeger其实给我们留了口子,下面一起来看一下。

首先我们知道,无论是在服务端接收请求时创建Span还是在客户端发起请求时创建Span,均是通过JaegerTracer.SpanBuilderstart() 方法,简化版源码如下所示。

@Override
public JaegerSpan start() {
    JaegerSpanContext context;

    ......

    if (references.isEmpty() || !references.get(0).getSpanContext().hasTrace()) {
        context = createNewContext();
    } else {
        // 上游传递了Span或者当前线程已经有激活的Span
        // 此时创建父Span的子Span
        context = createChildContext();
    }

    ......

    JaegerSpan jaegerSpan = getObjectFactory().createSpan(
            JaegerTracer.this,
            operationName,
            context,
            startTimeMicroseconds,
            startTimeNanoTicks,
            computeDurationViaNanoTicks,
            tags,
            references);

    ......

    return jaegerSpan;
}

上述关键点在于会在JaegerTracer.SpanBuildercreateChildContext() 方法中创建父Span的子Span,这里的父Span有两种情况。

  1. 客户端跨进程传递过来的Span
  2. 当前线程中已经激活的Span

现在继续跟进JaegerTracer.SpanBuildercreateChildContext() 方法,如下所示。

private JaegerSpanContext createChildContext() {
    JaegerSpanContext preferredReference = preferredReference();

    // 这里判断Tags是否有键为span.kind且值为server的键值对
    // 满足条件则表示当前是作为服务端接收请求时创建Span
    if (isRpcServer()) {
        if (isSampled()) {
            metrics.tracesJoinedSampled.inc(1);
        } else {
            metrics.tracesJoinedNotSampled.inc(1);
        }

        // 判断是否要兼容Zipkin的Span模型
        if (zipkinSharedRpcSpan) {
            // 这里返回客户端跨进程传递过来的SpanContext
            return preferredReference;
        }
    }

    return getObjectFactory().createSpanContext(
            preferredReference.getTraceIdHigh(),
            preferredReference.getTraceIdLow(),
            Utils.uniqueId(),
            preferredReference.getSpanId(),
            preferredReference.getFlags(),
            getBaggage(),
            null);
}

看到这里,就应该明白Jaeger给留的口子是什么了,即如果当前是作为服务端接收到请求要创建Span时,如果zipkinSharedRpcSpan配置为true,则不创建新的Span,而是直接使用客户端跨进程传递过来的Span,这完全就符合了我们上面的Span模型的改造思路。

那么最后一个问题,zipkinSharedRpcSpan怎么配置为true,其实就是在创建JaegerTracer时,调用一下JaegerTracer.BuilderwithZipkinSharedRpcSpan() 方法即可。

总结

Jaeger的默认实现逻辑中,一个Span通常代表一次请求接收或请求发起,如果我们想要让一个Span代表一次请求经过的链路中的某一个服务实例,则需要显式的指定JaegerSpan模型为ZipkinSpan模型,即在创建JaegerTracer时调用一下JaegerTracer.BuilderwithZipkinSharedRpcSpan() 方法即可。


大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈