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

前言

我们的目标是基于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通常包含traceIdspanIdparentSpanId,所以我们按照示例demoRestTemplateTracingInterceptorTracingFilter的实现的规则,再把这些信息添加上,此时Span模型抽象如下。

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

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

    
    
    if (isRpcServer()) {
        if (isSampled()) {
            metrics.tracesJoinedSampled.inc(1);
        } else {
            metrics.tracesJoinedNotSampled.inc(1);
        }

        
        if (zipkinSharedRpcSpan) {
            
            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() 方法即可。
原文:https://juejin.cn/post/7330331064370774054

  • 34
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值