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

前言

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

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

Opentracing 和jaeger相关版本依赖如下。

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 ,最终才来到server 的Controller 。上述这个demo 不太好演示本文的Span模型,我们做一下修改,修改后的请求路径如下。

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

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

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

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

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

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

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

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.SpanBuilder 的createChildContext() 方法中创建父Span 的子Span ,这里的父Span有两种情况。

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

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

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.Builder 的withZipkinSharedRpcSpan() 方法即可。

总结

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

相关推荐
Wang's Blog2 小时前
Java 项目实战: 交友项目 - FastDFS:入门案例
java·开发语言·交友
一只爱撸猫的程序猿2 小时前
我看着一个 AI 引擎在 14 天里,从"记忆"走进了"搜索"
人工智能·后端·架构
apcipot_rain3 小时前
C++转Java日记(03)
java·开发语言·c++
皮皮林5513 小时前
Java实现微信扫码登录
java
陈随易5 小时前
纯 Go 写的 JS 引擎,跑分超过了 C 语言的 QuickJS
前端·后端·程序员
Yyyyyy~5 小时前
【Java】Day4Web基础知识
java·spring boot
卷无止境5 小时前
RAGFlow 项目全景报告:从文档解析引擎到 Agent 原生数据库
后端·python
小Z先生6 小时前
我用 Go + 系统 WebView 写了个纯本地 Postman:不装 Electron、不连云端
后端·github
IT_陈寒6 小时前
Redis连接池泄漏那次,我差点把生产环境搞崩了
前端·人工智能·后端
灿飞屋6 小时前
Kotlin与Java虚拟机三:控制流
android·java·开发语言·后端·python·kotlin·安卓