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() 方法,简化版源码如下所示。

java 复制代码
@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() 方法,如下所示。

java 复制代码
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() 方法即可。

相关推荐
一颗花生米。22 分钟前
深入理解JavaScript 的原型继承
java·开发语言·javascript·原型模式
问道飞鱼22 分钟前
Java基础-单例模式的实现
java·开发语言·单例模式
奔跑吧邓邓子3 小时前
大数据利器Hadoop:从基础到实战,一篇文章掌握大数据处理精髓!
大数据·hadoop·分布式
ok!ko4 小时前
设计模式之原型模式(通俗易懂--代码辅助理解【Java版】)
java·设计模式·原型模式
2401_857622664 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php
2402_857589364 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
吾爱星辰5 小时前
Kotlin 处理字符串和正则表达式(二十一)
java·开发语言·jvm·正则表达式·kotlin
哎呦没6 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
_.Switch6 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
编程、小哥哥6 小时前
netty之Netty与SpringBoot整合
java·spring boot·spring