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

相关推荐
拖孩5 分钟前
AI 帮我写的小程序「厨菜记」上线满月了,挣了两百块!!!
前端·后端·微信小程序
流烟默10 分钟前
SSE 实战:在 SpringBoot + Vue 项目中零依赖实现轻量级实时消息推送
vue.js·spring boot·后端·sse
upala15 分钟前
【仓颉语言入门 · 第二篇 第 9 课】
后端
奇牙coding23 分钟前
Codex CLI 升级后多文件编辑静默失效:config.yaml 枚举变更排查与修复指南
java·前端·javascript·ai
小羊没烦恼!30 分钟前
Ajax与JSON的一些总结
android·java·前端·数据库·mysql·算法·okhttp
Lhappy嘻嘻36 分钟前
读懂 JVM:内存模型、类加载机制、分代回收
java·开发语言·jvm
黄俊懿37 分钟前
【架构师从入门到进阶】第六章:服务集群优化——第八节:线程数公式
java·服务器·数据库·分布式·性能优化·架构·系统架构
Rain的Java大神之路43 分钟前
别再死磕 MySQL 了:向量数据库与 RAG 生产落地实录(Java 工程师专场)
java·后端·面试
七夜zippoe44 分钟前
Spring AI 是什么?Java 工程师的 AI 开发第一站
java·人工智能·spring·ai·开发
雨中飘荡的记忆1 小时前
SDD × Claude Code:先写清楚,再让 AI 写代码
后端