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() 方法即可。

相关推荐
程柯梦想14 分钟前
Maven修改默认编码格式UTF-8
java·maven
涛ing15 分钟前
【5. C++ 变量作用域及其深入探讨】
java·linux·c语言·开发语言·c++·ubuntu·vim
大秦王多鱼21 分钟前
Kafka ACL(访问控制列表)介绍
运维·分布式·安全·kafka·apache
码农小旋风22 分钟前
Hive分区和分桶
后端
字节全栈_mMD1 小时前
Flink Connector 写入 Iceberg 流程源码解析_confluent icebergsinkconnector
java·大数据·flink
轩情吖1 小时前
二叉树-堆(补充)
c语言·数据结构·c++·后端·二叉树··排序
SomeB1oody2 小时前
【Rust自学】19.2. 高级trait:关联类型、默认泛型参数和运算符重载、完全限定语法、supertrait和newtype
开发语言·后端·rust
小园子的小菜2 小时前
RocketMQ中的NameServer主要数据结构
java·中间件·rocketmq·java-rocketmq
平凡君2 小时前
缓存的今生今世
java·spring·缓存
40岁的系统架构师2 小时前
17 一个高并发的系统架构如何设计
数据库·分布式·系统架构