1. 看完这篇文章我奶奶都懂Opentracing了

前言

如果要基于Opentracing 开发分布式链路追踪Java 客户端工具包,首先肯定需要了解Opentracing 中的各种概念,包括但不限于SpanScope 等,其实这些概念在Opentracing官方文档中是有比较详尽的说明的,英文不好也能靠着机器翻译读得通,但是读得通不代表读得懂,从来没有接触过分布式链路追踪的人就算把官方文档通读完,整体的概念还是显得比较抽象,所以本文作为Opentracing 入门,旨在让从来没接触过分布式链路追踪的人也能理解Opentracing中的各种概念,为后续阅读相关源码和自行实现分布式链路追踪客户端工具包打好基础。

本文会从一个简单的例子入手,结合相关场景和源码实现,阐述Opentracing 中的SpanScope等概念,通过阅读本文,可以快速了解关于分布式链路追踪的相关概念,并知道有哪些扩展点我们可以利用起来进行功能扩展。

Opentracingjaeger相关版本依赖如下。

opentracing-api 版本:0.33.0
opentracing-spring-web 版本:4.1.0
jaeger-client 版本:1.8.1

正文

一. 场景演示

我们先抛开所有感情和先验知识,来看一个如下的客户端请求服务端的情况。

客户端只是简单的使用RestTemplate 向服务端发起请求,请求在服务端会先通过filterChain ,然后最终送到应用程序中提供的Controller

对于上述这样一个简单的场景,如果想要传递链路信息,我们先不考虑链路信息传递啥,我们首先确定一下链路信息放哪儿,毫无疑问,放在HTTP 请求头中是侵入最小的。对于客户端而言,可以基于ClientHttpRequestInterceptor 来为RestTemplate 客户端提供统一的拦截器,拦截器的逻辑会在请求发起前被执行,我们可以在这个时候,把链路信息放在HTTP 请求头中,对于服务端而言,可以注册一个过滤器,在过滤器中就可以从请求头里拿到链路信息,这样链路信息就从客户端传递到了服务端,下面就分别给出客户端和服务端的示例代码,我们通过这个示例,来了解如何使用基于Opentracing 定义的api来传递链路信息,从而了解这个过程中出现的各种概念。

客户端这边的pom文件如下所示。

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-parent</artifactId>
        <version>2.7.6</version>
    </parent>

    <groupId>com.learn.tracing.client</groupId>
    <artifactId>learn-tracing-client</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.opentracing</groupId>
            <artifactId>opentracing-api</artifactId>
            <version>0.33.0</version>
        </dependency>
        <dependency>
            <groupId>io.opentracing.contrib</groupId>
            <artifactId>opentracing-spring-web</artifactId>
            <version>4.1.0</version>
        </dependency>

        <dependency>
            <groupId>io.jaegertracing</groupId>
            <artifactId>jaeger-client</artifactId>
            <version>1.8.1</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

</project>

客户端这边最重要的就是为RestTemplate提供的拦截器,对应实现如下所示。

java 复制代码
public class RestTemplateTracingInterceptor implements ClientHttpRequestInterceptor {

    private final Tracer tracer;

    public RestTemplateTracingInterceptor(Tracer tracer) {
        this.tracer = tracer;
    }

    @NotNull
    public ClientHttpResponse intercept(@NotNull HttpRequest request, @NotNull byte[] body,
                                        ClientHttpRequestExecution execution) throws IOException {
        Span span = tracer.buildSpan(REST_TEMPLATE_SPAN_NAME)
                .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT)
                .start();
        span.setBaggageItem(REST_TEMPLATE_SPAN_TAG_URI, request.getURI().toString());
        tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS, new HttpHeadersCarrier(request.getHeaders()));

        try (Scope scope = tracer.activateSpan(span)) {
            return execution.execute(request, body);
        } catch (IOException e) {
            Tags.ERROR.set(span, Boolean.TRUE);
            throw e;
        } finally {
            span.finish();
        }
    }

}

上面出现了很多陌生的内容例如SpanTracerTags ,但是不慌,这些后面都会知道是啥,我们现在先把客户端和服务端搭起来。我们上述的拦截器使用了一个Tracer 对象,那么我们就继续看一下这个Tracer对象的配置类是怎么写的,如下所示。

java 复制代码
@Configuration
public class TracerConfig {

    @Autowired
    private SpanReporter spanReporter;

    @Autowired
    private Sampler sampler;

    @Bean
    public Tracer tracer() {
        return new JaegerTracer.Builder(TRACER_SERVICE_NAME)
                .withTraceId128Bit()
                .withSampler(sampler)
                .withReporter(spanReporter)
                .build();
    }

}

创建Tracer 时指定了SpanReporterSampler ,我们先不去深究SpanReporterSampler 是啥,仅先看一下这两个bean的配置类,如下所示。

java 复制代码
public class SpanReporter implements Reporter {

    public void report(JaegerSpan span) {
        // todo 打印链路日志
        // todo 暂不做任何事情
    }

    public void close() {

    }

}

@Configuration
public class ReporterConfig {

    @Bean
    public SpanReporter spanReporter() {
        return new SpanReporter();
    }

}

@Configuration
public class SamplerConfig {

    @Bean
    public Sampler sampler() {
        return new ProbabilisticSampler(DEFAULT_SAMPLE_RATE);
    }

}

到这里创建拦截器的相关内容已经全部给出,那么拦截器有了,还需要把拦截器设置给RestTemplate ,所以再看一下RestTemplate的配置类,如下所示。

java 复制代码
@Configuration
public class RestTemplateConfig {

    @Autowired
    private Tracer tracer;

    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.getInterceptors().add(new RestTemplateTracingInterceptor(tracer));
        return restTemplate;
    }

}

关于客户端最后的就是上述使用到的常量类以及一个测试的Controller,如下所示。

java 复制代码
public class Constants {

    public static final String REST_TEMPLATE_SPAN_NAME = "RestTemplateSpan";
    public static final String REST_TEMPLATE_SPAN_TAG_URI = "uri";

    public static final String TRACER_SERVICE_NAME = "TracerService";

    public static final Double DEFAULT_SAMPLE_RATE = 1.0;

}

@RestController
public class TracingClientController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/send")
    public void send(String url) {
        restTemplate.getForEntity(url, Void.class);
    }

}

客户端这边的工程目录结构如下图所示。

服务端的pom文件如下所示。

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.7.6</version>
    </parent>

    <groupId>com.learn.tracing.server</groupId>
    <artifactId>learn-tracing-server</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.opentracing</groupId>
            <artifactId>opentracing-api</artifactId>
            <version>0.33.0</version>
        </dependency>
        <dependency>
            <groupId>io.opentracing.contrib</groupId>
            <artifactId>opentracing-spring-web</artifactId>
            <version>4.1.0</version>
        </dependency>

        <dependency>
            <groupId>io.jaegertracing</groupId>
            <artifactId>jaeger-client</artifactId>
            <version>1.8.1</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

</project>

服务端这边最重要的是提供一个过滤器,如下所示。

java 复制代码
public class TracingFilter implements Filter {

    private final Tracer tracer;

    public TracingFilter(Tracer tracer) {
        this.tracer = tracer;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        SpanContext extractedSpanContext = tracer.extract(Format.Builtin.HTTP_HEADERS,
                new HttpServletRequestExtractAdapter(request));

        Span span = tracer.buildSpan(request.getMethod())
                .asChildOf(extractedSpanContext)
                .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER)
                .start();

        response.setHeader(TRACE_ID_KEY, span.context().toTraceId());

        try (Scope scope = tracer.activateSpan(span)) {
            filterChain.doFilter(servletRequest, servletResponse);
        } catch (IOException | ServletException e) {
            Tags.ERROR.set(span, Boolean.TRUE);
            throw e;
        } finally {
            span.finish();
        }
    }

}

同样的使用到了Tracer,相关配置类如下所示。

java 复制代码
@Configuration
public class TracerConfig {

    @Autowired
    private SpanReporter spanReporter;

    @Autowired
    private Sampler sampler;

    @Bean
    public Tracer tracer() {
        return new JaegerTracer.Builder(TRACER_SERVICE_NAME)
                .withTraceId128Bit()
                .withSampler(sampler)
                .withReporter(spanReporter)
                .build();
    }

}

SpanReporter 以及SpanReporterSampler的配置类如下所示。

java 复制代码
public class SpanReporter implements Reporter {

    public void report(JaegerSpan span) {
        // todo 打印链路日志
        // todo 暂不做任何事情
    }

    public void close() {

    }

}

@Configuration
public class ReporterConfig {

    @Bean
    public SpanReporter spanReporter() {
        return new SpanReporter();
    }

}

@Configuration
public class SamplerConfig {

    @Bean
    public Sampler sampler() {
        return new ProbabilisticSampler(DEFAULT_SAMPLE_RATE);
    }

}

现在还需要将TracingFilter 注册到过滤器链中,主要是基于FilterRegistrationBean来完成注册,对应配置类如下所示。

java 复制代码
@Configuration
public class ServletFilterConfig {

    @Bean
    public FilterRegistrationBean<TracingFilter> tracingFilter(Tracer tracer) {
        TracingFilter tracingFilter = new TracingFilter(tracer);
        FilterRegistrationBean<TracingFilter> filterFilterRegistrationBean
                = new FilterRegistrationBean<>(tracingFilter);
        filterFilterRegistrationBean.addUrlPatterns(ALL_URL_PATTERN_STR);
        return filterFilterRegistrationBean;
    }

}

关于服务端最后的就是上述使用到的常量类以及一个测试的Controller,如下所示。

java 复制代码
public class Constants {

    public static final String TRACER_SERVICE_NAME = "TracerService";

    public static final Double DEFAULT_SAMPLE_RATE = 1.0;

    public static final String TRACE_ID_KEY = "traceId";

    public static final String ALL_URL_PATTERN_STR = "/*";

}

@RestController
public class TracingServerController {

    @GetMapping("/receive")
    public void send() {
        System.out.println("接收请求");
    }

}

服务端的工程目录结构如下所示。

最后,在将客户端运行在8081端口,服务端运行在8082端口,并调用如下接口。

txt 复制代码
http://localhost:8081/send?url=http://localhost:8082/receive

我们在客户端这边的RestTemplate 的拦截器中,会通过Opentracing 的相关api 将请求URI 放到请求头中,然后在服务端这边的TracingFilter 中,又会从请求头中解析出客户端传递过来的URI ,这一点可以分别在客户端RestTemplate 的拦截器和服务端TracingFilter中打断点进行观察。

先看一下客户端使用RestTemplate 发请求时,经过拦截器后,HTTP请求头中的字段,如下所示。

再看一下服务端这边在TracingFilter 中解析出客户端传递过来的URI,如下所示。

也就是客户端这边通过Opentracingapi,将一些字段放在了请求头中,传递到了服务端。

那么问题就来了,我们自己不通过Opentracingapi ,其实也是可以通过HTTP 请求头来传递信息到下游,为啥要基于Opentracing 呢,其实上述示例中,客户端除了传递请求URI 到下游,还传递了在分布式链路中很重要的traceIdspanId ,前者标记一次请求链路,后者代表这次请求链路上的节点,同时Opentracing还定义了如何让链路信息在进程中传递和跨进程传递,上述示例就是跨进程传递的一个简单演示。

好了到这里示例就演示完毕了,后续我们就基于上述的示例,以及相关的源码,来阐述Opentracing中的各种概念,为后面的分布式链路工具包开发奠定基础。

二. 概念分析

1. Span和SpanContext

结合上述示例,我们从Span 开始入手来进行概念分析,但是说在最前面,Span 在不同的分布式链路实现中,其定义是不全一样的,尽管Opentracing 已经进行了概念的统一,但是具体到各家实现,有时候差异还是有的,故此,我们这里解释Span 概念时,会更倾向于Jaeger

一次请求,通常会由分布式系统中的多个工作单元组成,这些工作单元按照调用顺序串起来会形成一条链路,那么一条链路中的某个工作单元,就用Span 来表示。但工作单元具体指什么呢,可以这么理解,服务B 收到请求并开始处理,这就是一个工作单元,我们可以为这件事情创建一个Span 来表示,又比如服务B 调用了服务C ,这又是一个工作单元,我们可以为调用服务C 这件事情创建一个Span 。所有Span串联起来就组成一条分布式链路,例如下图这样。

Span 千万别想太复杂,简单点想Span 就是链路中的被调用或调用行为。通常一个Span需要包含如下的主要信息。

  • Operation Name 。可以粗略理解为Span 的名称,用于描述当前Span是做什么的;
  • StartTimestampFinishTimestamp 。分别表示Span的开始处理时间和结束处理时间;
  • Tags 。是一个Map ,用于以键值对的形式存储Span在处理业务的过程中的一些数据;
  • Logs 。最本质还是一个Map ,用于以键值对的形式存储一些数据,在Span处理结束并打印链路日志时会用到;
  • SpanContext 。表示Span 的上下文,这个说法有点抽象,下面会单独把这个SpanContext拎出来讲一下。

一个Span ,最主要的就是上述的这些内容,其中TagsLogs 有点抽象,SpanContext 较为抽象,但是不慌,首先是TagsLogs ,这两兄弟本质就是存储Span 在处理业务过程中的一些需要记录的信息,所有内容都可以记录,想记啥就记啥,想记在Tags 还是记在Logs 全凭自己心情,反正最终要用到这些数据的时候,我们都可以通过Span 来拿到,但是TagsLogs 也不是完全没有区别,先看一下Jaeger 中的Span 对象的tags字段的签名,如下所示。

java 复制代码
private final Map<String, Object> tags;

是一个简单的Map ,然后logs字段要复杂一点,字段签名如下所示。

java 复制代码
private List<LogData> logs;

@Getter
public final class LogData {
    private final long time;
    private final String message;
    private final Map<String, ?> fields;

    LogData(long time, String message, Map<String, ?> fields) {
        this.time = time;
        this.message = message;
        this.fields = fields;
    }
}

乍一看是一个LogData 的集合,但其实LogData 本质就是对一个Map 做了包装,所以一个LogData 就是一组键值对,这就让logs 相较于tags 有一个天然的分组 的优势,例如我当前Span 某次调用下游的操作的相关数据可以存成一个LogData ,我当前Span 某次SQL 执行的相关数据可以存成一个LogData

现在再来揭开SpanContext 的面纱。首先Span 是一次请求链路中的某个工作单元的表示,那么对于某个Span 来说,有两个东西很重要,其一是标识这次请求链路的traceId ,毕竟Span 得知道自己是在哪条链路上对吧,其二是标识当前工作单元的spanId ,相当于Span 在这条链路上的身份证。既然SpanContext 作为Span 上下文,理所应当的SpanContext 就应该保存着对应的链路的traceId 以及SpanContext 所属SpanspanId ,但是仅有traceIdspanId 还不够,还得保存一个parentSpanId ,当前Span 可以通过parentSpanId 表示对应链路中自己的上一个工作单元是哪个Span ,最后,SpanContext 还有一个字段叫做baggage ,是一个Map ,可以将数据从当前Span 传递到下一个Span,即跨进程数据传递。

那么总结一下SpanContext里面的主要信息。

  • traceIdSpan所属链路的唯一标识;
  • spanIdSpan在对应链路上的唯一标识;
  • parentSpanIdSpan 在对应链路上的父Span的唯一标识;
  • baggage。以键值对的形式存储数据并进行跨进程传输。

其实无论是traceIdspanId 还是baggage ,都是需要从一个Span 传递到下一个Span 的,所以SpanContext主要就是负责跨进程数据传递。

那么到这里,Span 是什么,想必在纸面上已经是说明白了七七八八了,下面再来看一下我愿称之为Span 发射器的Tracer ,先看一下io.opentracing 包中提供的Tracer接口的注释,如下所示。

Tracer is a simple, thin interface for Span creation and propagation across arbitrary transports.

Tracer 主要干两件事情,其一是创建Span ,其二是传递Span ,创建Span 没什么好说的,传递Span 又分同一进程和跨进程传递,至于怎么传递,后面可以结合Jaeger提供的相关实现来看一下。

到此为止,我们已经基本具备去阅读JaegerOpentracing 相关实现的源码的基础了,所以现在就接合第一节中的客户端和服务端的示例代码,过一下相关源码实现,这样能更加理解Opentracing的相关概念。

首先再重温一下客户端这边的RestTemplate的拦截器的实现,如下所示。

java 复制代码
public class RestTemplateTracingInterceptor implements ClientHttpRequestInterceptor {

    private final Tracer tracer;

    public RestTemplateTracingInterceptor(Tracer tracer) {
        this.tracer = tracer;
    }

    @NotNull
    public ClientHttpResponse intercept(@NotNull HttpRequest request, @NotNull byte[] body,
                                        ClientHttpRequestExecution execution) throws IOException {
        Span span = tracer.buildSpan(REST_TEMPLATE_SPAN_NAME)
                .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT)
                .start();
        span.setBaggageItem(REST_TEMPLATE_SPAN_TAG_URI, request.getURI().toString());
        tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS, new HttpHeadersCarrier(request.getHeaders()));

        try (Scope scope = tracer.activateSpan(span)) {
            return execution.execute(request, body);
        } catch (IOException e) {
            Tags.ERROR.set(span, Boolean.TRUE);
            throw e;
        } finally {
            span.finish();
        }
    }

}

首先我们通过Tracer 创建了一个Span ,因为使用的是Jaeger ,这里的Tracer 实际是JaegerTracer ,创建出来的Span 实际是JaegerSpan 。在创建Span 时,实际调用的是JaegerTracer.SpanBuilder#start方法,这个方法实现如下所示。

java 复制代码
@Override
public JaegerSpan start() {
    JaegerSpanContext context;

    // 先检查当前线程是否已经存在Span
    if (references.isEmpty() && !ignoreActiveSpan && null != scopeManager.activeSpan()) {
        // 如果存在则新创建的Span需要作为已经存在的Span的子Span
        // 实际就是将已经存在的Span添加到SpanBuilder的references中
        asChildOf(scopeManager.activeSpan());
    }

    if (references.isEmpty() || !references.get(0).getSpanContext().hasTrace()) {
        // 如果references为空则创建新SpanContext
        // 实际就是生成traceId和spanId且置parentSpanId为0
        context = createNewContext();
    } else {
        // 如果references不为空则创建子SpanContext
        // 通常是将父SpanContext中的traceId传递到子SpanContext
        // 然后父SpanContext的spanId作为子SpanContext的parentSpanId
        // 最后子SpanContext生成一个新的spanId
        // 上述规则在Zipkin兼容开启的情况下有不同
        context = createChildContext();
    }

    long startTimeNanoTicks = 0;
    boolean computeDurationViaNanoTicks = false;

    if (startTimeMicroseconds == 0) {
        startTimeMicroseconds = clock.currentTimeMicros();
        if (!clock.isMicrosAccurate()) {
            startTimeNanoTicks = clock.currentNanoTicks();
            computeDurationViaNanoTicks = true;
        }
    } else {
        verifyStartTimeInMicroseconds();
    }

    // 创建Span并返回
    JaegerSpan jaegerSpan = getObjectFactory().createSpan(
            JaegerTracer.this,
            operationName,
            context,
            startTimeMicroseconds,
            startTimeNanoTicks,
            computeDurationViaNanoTicks,
            tags,
            references);
    if (context.isSampled()) {
        metrics.spansStartedSampled.inc(1);
    } else {
        metrics.spansStartedNotSampled.inc(1);
    }
    return jaegerSpan;
}

上面的JaegerTracer.SpanBuilder#start 方法在创建Span 对象时,关键的地方其实是创建SpanContext,创建有两种情况,如下所示。

  1. 创建新SpanContext 。在没有显式指定父Span ,并且当前线程之前没有已经存在的Span 时,就需要创建新SpanContext ,也就意味着会生成新的traceIdspanId ,对应的Span 也会作为整个链路的首个节点,相应的parentSpanId 会置为0,表示没有父Span
  2. 创建子SpanContext 。如果显式指定了父Span ,或者当前线程之前有已经存在的Span 且不能忽略这个已经存在的Span ,就需要创建子SpanContext

创建新SpanContext 的情况没什么好说的,下面重点分析一下创建子SpanContext ,对应方法为SpanBuilder#createChildContext,如下所示。

java 复制代码
private JaegerSpanContext createChildContext() {
    // 通常是拿到父SpanContext
    JaegerSpanContext preferredReference = preferredReference();

    // 判断当前要创建的Span是否包含键为span.kind且值为server的Tag
    // 通常包含键为span.kind且值为server的Tag的Span代表被调用方
    if (isRpcServer()) {
        if (isSampled()) {
            metrics.tracesJoinedSampled.inc(1);
        } else {
            metrics.tracesJoinedNotSampled.inc(1);
        }

        // Zipkin兼容开启的情况下直接将父SpanContext作为子SpanContext返回
        if (zipkinSharedRpcSpan) {
            return preferredReference;
        }
    }

    // 正常情况下会走到这里
    return getObjectFactory().createSpanContext(
            // 父SpanContext传递traceId给到子SpanContext
            preferredReference.getTraceIdHigh(),
            preferredReference.getTraceIdLow(),
            // 子SpanContext的spanId是新生成的
            Utils.uniqueId(),
            // 父SpanContext的spanId作为子SpanContext的parentSpanId
            preferredReference.getSpanId(),
            preferredReference.getFlags(),
            getBaggage(),
            null);
}

在创建子SpanContext 时,Jaeger 在满足一定条件下会开启Zipkin 的兼容模式,这个时候父SpanContext 会直接作为子SpanContext 返回,至于这么做有什么用,其实质就是改变了Span 的理论模型,但我们这里不必深究,知道有这么回事即可。在更多的时候,创建子SpanContext 时其实就是父SpanContexttraceId 传递给子SpanContext ,然后父SpanContextspanId 作为子SpanContextparentSpanId ,最后子SpanContext 会新生成一个自己的spanId

至此,我们已经了解了Tracer 如何创建出Span ,那么现在再来看一下io.opentracing 中定义的Span 接口长什么样,这决定了我们可以基于Span做一些什么操作。

java 复制代码
public interface Span {

    // 拿到Span的SpanContext
    SpanContext context();

    // 设置Tags键值对
    Span setTag(String key, String value);

    // 设置Tags键值对
    Span setTag(String key, boolean value);

    // 设置Tags键值对
    Span setTag(String key, Number value);

    // 设置Tags键值对
    <T> Span setTag(Tag<T> tag, T value);

    // 添加Logs键值对
    Span log(Map<String, ?> fields);

    // 添加Logs键值对
    Span log(long timestampMicroseconds, Map<String, ?> fields);

    // 添加Logs键值对
    // span.log(Collections.singletonMap("event", event));
    Span log(String event);

    // 添加Logs键值对
    // span.log(timestampMicroseconds, Collections.singletonMap("event", event));
    Span log(long timestampMicroseconds, String event);

    // 设置Baggage键值对
    Span setBaggageItem(String key, String value);

    // 根据键获取Baggage值
    String getBaggageItem(String key);

    // 覆盖OperationName
    Span setOperationName(String operationName);

    // 将当前时间作为Span结束时间并记录Span
    // 所谓记录Span通常就是完成日志打印
    void finish();

    // 指定时间作为Span结束时间并记录Span
    void finish(long finishMicros);
}

基本就是Span 持有什么,我们就能操作什么,比如在客户端的RestTemplate 拦截器的示例代码中,我们就手动添加了Baggage 键值对,而我们知道,Baggage 是存在于SpanContext 中并用于跨进程传输的,那么这里要如何做到跨进程传输呢,这里就要引出Opentracing 中另外一套很重要的概念,即InjectExtract

2. Inject和Extract

要了解InjectExtract ,首先需要看一下Tracer接口中的如下两个方法。

java 复制代码
// 将SpanContext注入到carrier里面
<C> void inject(SpanContext spanContext, Format<C> format, C carrier);

// 从carrier中提取出SpanContext
<C> SpanContext extract(Format<C> format, C carrier);

上述的inject() 方法和extract() 方法乍一看有点抽象,但是如果用第一节示例代码来解释应该就比较好理解,即客户端这边通过inject() 方法可以将SpanContext 中的内容,注入到HTTP 请求头中,怎么注入,就是formatcarrier 配合完成,然后服务端这边通过extract() 方法可以从HTTP 请求头中提取出SpanContext ,怎么提取,也是formatcarrier 配合完成。在大概知道inject() 方法和extract() 方法的作用后,我们再来看一下formatcarrier ,通过方法签名,我们可以知道carrier 的类型是由format 来决定的,所以formatcarrier 肯定是要配合使用的,这里以示例代码中的客户端的RestTemplate拦截器的实现举例,先看如下这一段示例代码。

java 复制代码
tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS, new HttpHeadersCarrier(request.getHeaders()));

先看一下Format.Builtin.HTTP_HEADERS是什么,如下所示。

java 复制代码
public final static Format<TextMap> HTTP_HEADERS = new Builtin<TextMap>("HTTP_HEADERS");

上述能说明两个事情:第一就是Format.Builtin.HTTP_HEADERS 的名称叫做HTTP_HEADERS ,表明这个Format 和注入信息到HTTP 请求头有关,第二就是和Format.Builtin.HTTP_HEADERS 配合使用的carrier 的类型是TextMap 。不过如果仔细看一下Format.Builtin.HTTP_HEADERS ,发现其是一个Builtin对象,这个对象是一个很简单的对象,如下所示。

java 复制代码
final class Builtin<C> implements Format<C> {
    private final String name;

    private Builtin(String name) {
        this.name = name;
    }

    public final static Format<TextMap> TEXT_MAP = new Builtin<TextMap>("TEXT_MAP");

    public final static Format<TextMapInject> TEXT_MAP_INJECT = new Builtin<TextMapInject>("TEXT_MAP_INJECT");

    public final static Format<TextMapExtract> TEXT_MAP_EXTRACT = new Builtin<TextMapExtract>("TEXT_MAP_EXTRACT");

    public final static Format<TextMap> HTTP_HEADERS = new Builtin<TextMap>("HTTP_HEADERS");

    public final static Format<Binary> BINARY = new Builtin<Binary>("BINARY");

    public final static Format<BinaryInject> BINARY_INJECT = new Builtin<BinaryInject>("BINARY_INJECT");

    public final static Format<BinaryExtract> BINARY_EXTRACT = new Builtin<BinaryExtract>("BINARY_EXTRACT");

    @Override
    public String toString() {
        return Builtin.class.getSimpleName() + "." + name;
    }
}

那反正我是无论如何都无法想明白Builtin 对象怎么和TextMap 扯上关系,直到我发现其实每一个Format 都有一个配套的Injector 以及Extractor ,例如默认情况下,Format.Builtin.HTTP_HEADERS 配套的InjectorExtractor 都是TextMapCodec ,所以下面就分析一下TextMapCodec,先看一下类图,如下所示。

首先TextMapCodec 分别实现了InjectorExtractor接口,对应的实现方法签名如下所示。

java 复制代码
public void inject(JaegerSpanContext spanContext, TextMap carrier)

public JaegerSpanContext extract(TextMap carrier)

那么现在就逐渐清晰明了了,我们之前困惑format 对象怎么和carrier 扯上关系,其实就是首先根据format 对象拿到与之配对的InjectorExtractor ,然后在InjectorExtractor 中会调用到carrier 对象来完成注入和提取的操作,那么具体到我们的示例里,format 对象是Format.Builtin.HTTP_HEADERS ,所以对应的carrier 类型是TextMap ,且其配对的InjectorExtractor 均是TextMapCodec ,然后TextMapCodec 实现的inject()extract() 的入参carrier 的类型也均是TextMap ,所以Format.Builtin.HTTP_HEADERS 就可以处理类型是TextMapcarrier

那么示例代码中,客户端RestTemplate 拦截器中,使用的formatFormat.Builtin.HTTP_HEADERScarrierHttpHeadersCarrier ,所以如何将SpanContext 的内容写到HTTP 请求头,需要分析一下TextMapCodec#inject方法,如下所示。

java 复制代码
@Override
public void inject(JaegerSpanContext spanContext, TextMap carrier) {
    // 以键值对的形式将SpanContext注入到carrier
    // 这里的键是uber-trace-id
    carrier.put(contextKey, contextAsString(spanContext));
    for (Map.Entry<String, String> entry : spanContext.baggageItems()) {
        // 以键值对的形式将baggage注入到carrier
        carrier.put(keys.prefixedKey(entry.getKey(), baggagePrefix), encodedValue(entry.getValue()));
    }
}

我们知道上述方法中的carrier 实际类型为HttpHeadersCarrier ,所以看一下HttpHeadersCarrier#put方法干了什么,如下所示。

java 复制代码
@Override
public void put(String key, String value) {
    httpHeaders.add(key, value);
}

真相大白了,就是HttpHeadersCarrier#put 方法其实就是往HTTP 请求头里塞键值对嘛,那么如法炮制,在示例代码服务端的TracingFilter 中,使用的formatFormat.Builtin.HTTP_HEADERScarrierHttpServletRequestExtractAdapter ,所以其实最终会调用到HttpServletRequestExtractAdapter#iterator方法,如下所示。

java 复制代码
@Override
public Iterator<Map.Entry<String, String>> iterator() {
    return new MultivaluedMapFlatIterator<>(headers.entrySet());
}

我感觉我又懂了,这肯定就是外层会for 循环HttpServletRequestExtractAdapter ,从而HttpServletRequestExtractAdapteriterator() 方法就把HTTP 请求头的迭代器返回出去了,然后外层就能拿到从客户端传递过来的数据了,包括SpanContextBaggage中的内容都能拿到,从而完成了跨进程传递链路信息。

现在知道如何跨进程传递链路信息了,但是我们站在服务端的视角来审视一下,服务端收到一个携带链路信息的请求,然后这个请求在送到业务代码前,先经过了TracingFilter ,我们在TracingFilter 中通过formatcarrier 配合,将客户端传递过来的SpanContext 获取了出来,然后基于SpanContext 创建出来了当前这一个工作单元的Span ,那么后续就应该进入到业务代码中了对吧,但是这个热乎乎的Span 对象怎么传递下去呢,如果是通过方法参数的形式传递下去,那侵入性可太大了,例如Go 语言,可能真的只能把Span 放在context 里面,然后把context 当传家宝一样的逐层转递下去,但若是Java 语言,我们应该想到一个很好使的东西,叫做ThreadLocal ,没错,Span 在线程中的传递,本质是依赖ThreadLocal,我们接着来分析是怎么一回事。

3. Scope和ScopeManager

现在来到示例的服务端TracingFilter 的代码,注意到在获取到Span对象后,后续执行了如下这么一行代码。

java 复制代码
Scope scope = tracer.activateSpan(span)

好的,目标出现了,上述代码通过TraceractivateSpan() 方法传入Span 并得到了一个ScopeSpan 我们已经很熟了,但是Scope 又是什么妖魔鬼怪呢,我们先看一下Tracer 接口中activateSpan() 方法的签名。

java 复制代码
Scope activateSpan(Span span);

Tracer 接口中的activateSpan() 方法用于在当前线程中激活一个Span ,这里激活其实可以理解为绑定,即将一个Span 绑定到当前线程上,然后得到一个Scope 对象,那么再继续看一下Scope接口长啥样,如下所示。

java 复制代码
public interface Scope extends Closeable {

    @Override
    void close();

}

只有一个close() 方法,这里其实就是反激活一个Span ,即将一个Span 从当前线程取消绑定,那么到这里,Scope 基本可以理解为控制一个Span 在线程中的活动时间段,当基于某个Span 创建得到一个Scope 起,这个Span 在当前线程就开始活跃了,也就是当前线程任何一个地方都可以操作这个Span ,当调用Scopeclose() 方法时,对应Span 就不活跃了,也就是当前线程不再能够任意操作这个Span了。

基本了解Scope 的概念后,还是得结合具体的实现来加深理解,我们还是以Jaeger 的实现进行举例,看一下JaegerTracer#activateSpan方法的实现,如下所示。

java 复制代码
@Override
public Scope activateSpan(Span span) {
    return scopeManager().activate(span);
}

上述scopeManager() 方法默认情况下会返回一个ThreadLocalScopeManager ,先简单将ScopeManager 理解为管理Scope 的管理器即可,下面先看一下ThreadLocalScopeManager的实现。

java 复制代码
public class ThreadLocalScopeManager implements ScopeManager {
    final ThreadLocal<ThreadLocalScope> tlsScope = new ThreadLocal<ThreadLocalScope>();

    @Override
    public Scope activate(Span span) {
        return new ThreadLocalScope(this, span);
    }

    @Override
    public Span activeSpan() {
        ThreadLocalScope scope = tlsScope.get();
        return scope == null ? null : scope.span();
    }
}

我突然感觉自己就悟了,我们传进来的Span 会被封装为一个ThreadLocalScope 对象,同时ThreadLocalScopeManager 中又有一个类型为ThreadLocal <ThreadLocalScope >的tlsScope 字段,所以创建出来的ThreadLocalScope 对象肯定就是放在了tlsScope 中,这样就完成和当前线程绑定了嘛,但是上面的activate() 方法好像没有看到那里有将创建出来的ThreadLocalScope 对象往tlsScope 放,所以玄机肯定就在ThreadLocalScope的构造函数中,如下所示。

java 复制代码
ThreadLocalScope(ThreadLocalScopeManager scopeManager, Span wrapped) {
    this.scopeManager = scopeManager;
    this.wrapped = wrapped;
    // 将之前激活的Span获取出来以备后续恢复
    this.toRestore = scopeManager.tlsScope.get();
    // 将当前的Span进行激活
    scopeManager.tlsScope.set(this);
}

ThreadLocalScope 在构造函数中做了两件事情,其一是将之前激活的Span 获取出来以备后续恢复,也就是从ThreadLocal 中把之前绑定的Span 获取出来保存起,其二是将当前的Span 放到ThreadLocal 中,这会覆盖之前放的Span ,这就表明同一线程同一时刻只能同时激活一个Span

到这里就清晰明了了,我们要激活的Span 其实是被包装成了一个ThreadLocalScope 对象,然后所谓的激活,就是把ThreadLocalScope 对象设置到ThreadLocalScopeManager 持有的一个ThreadLocal中。

现在Span 是激活了,那么我们要怎么获取到激活的Span 呢,这就得看一下TraceractiveSpan() 方法,Tracer 接口中activeSpan() 方法的签名如下所示。

java 复制代码
// 获取到当前线程激活的Span
Span activeSpan();

直接看一下JaegerTraceractiveSpan() 方法的实现,如下所示。

java 复制代码
@Override
public Span activeSpan() {
    return this.scopeManager.activeSpan();
}

继续跟进ThreadLocalScopeManageractiveSpan() 方法,如下所示。

java 复制代码
@Override
public Span activeSpan() {
    ThreadLocalScope scope = tlsScope.get();
    return scope == null ? null : scope.span();
}

其实就是从ThreadLocal 中把激活的Span获取出来。

最后再来分析一下ThreadLocalScopeclose() 方法,如下所示。

java 复制代码
@Override
public void close() {
    if (scopeManager.tlsScope.get() != this) {
        return;
    }

    scopeManager.tlsScope.set(toRestore);
}

因为每个Span 在激活时,都会把之前已经激活的Span 保存下来,而如果当前Span 对应的ThreadLocalScopeclose() 方法被调用,则表明当前Span 已经结束活动,那么此时就应该将保存下来的之前Span 再重新与当前线程绑定,所以ThreadLocalScopeclose() 方法干的就是这么一个事情,而且ThreadLocalScopeclose() 方法一定要记得调用,否则会引发内存泄漏。

4. Tracer

其实前文有提到Tracer 是什么,之所以在这里又重新分析一次Tracer ,是因为只有在知道SpanSpanContextInjectExtract ,以及ScopeScopeManager 之后,才能明白Tracer 是干什么的。在io.opentracing 中定义的Tracer接口上,有如下这么一段注释。

Tracer is a simple, thin interface for Span creation and propagation across arbitrary transports.

也就是Tracer 主要完成两件事情,其一是创建Span ,其二是传递Span

对于创建Span 来说,Tracer 主要是通过buildSpan() 方法得到一个SpanBuilder ,然后基于SpanBuilder 可以设置Span 的父SpanTags 等信息,最后通过SpanBuilderstart() 方法创建出来Span 。创建Span 的相关源码,在本节第1小节介绍SpanSpanContext时已经进行了分析,这里就不重复分析了。

对于传递Span 来说,又分两种情况,其一是跨进程传递,其二是线程中传递。对于跨进程传递,主要就是依赖Tracer的如下两个方法。

java 复制代码
<C> void inject(SpanContext spanContext, Format<C> format, C carrier);

<C> SpanContext extract(Format<C> format, C carrier);

跨进程传递Span ,传递的主体是SpanSpanContextinject() 方法可以把SpanContext 注入到跨进程传递的介质中,例如HTTP 请求头,具体一点就是利用format 配套的Injector ,把SpanContext 注入给carrier 中的传递介质例如HTTP 请求头,然后extract() 方法可以从跨进程传递介质中把SpanContext 提取出来,具体一点就是利用format 配套的Extractor ,从carrier 中的传递介质例如HTTP 请求头中提取出SpanContext 。跨进程传递Span 的相关源码,在本节第2小节介绍InjectExtract时已经进行了分析,这里就不重复分析了。

对于线程中传递Span ,主要是依赖Tracer的如下两个方法。

java 复制代码
Scope activateSpan(Span span);

Span activeSpan();

activateSpan() 方法意为激活Span ,实际就是将Span 和当前线程绑定,activeSpan() 方法意为拿到激活的Span ,实际也就是拿到和当前线程绑定的Span ,所以通过activateSpan() 方法和activeSpan() 方法,就能实现Span 在线程中的传递,并且在Java 语言中,绑定一个东西到一个线程,天然的适合使用ThreadLocal 来使用,所以通常Span 就是放在ThreadLocal 中进行线程中传递的,具体的源码实现在本节第3小节介绍ScopeScopeManager时已经进行了分析,这里就不重复分析了。

那么至此,Tracer 是干啥的,其实是比较清楚的了,称之为Span发射器,不为过吧。

5. Jaeger中的Reporter

可以注意到,Span 接口有定义一个finish() 方法,该方法签名如下所示。

java 复制代码
// 设置Span完成时间并记录Span
void finish();

由上可知finish() 方法主要完成的事情就是设置Span 的完成时间并记录Span ,至于如何记录,取决于各家实现,本小节主要是分析一下Jaeger 中的SpanReporter ,首先看一下JaegerSpan#finish方法的实现,如下所示。

java 复制代码
@Override
public void finish() {
    if (computeDurationViaNanoTicks) {
        // 计算Span的持续时间
        long nanoDuration = tracer.clock().currentNanoTicks() - startTimeNanoTicks;
        finishWithDuration(nanoDuration / 1000);
    } else {
        finish(tracer.clock().currentTimeMicros());
    }
}

Span 的持续时间就等于当前时间减去startTimeNanoTicksstartTimeNanoTicks 是在Span 被创建的时候完成赋值的,现在继续跟进一下JaegerSpan#finishWithDuration方法,如下所示。

java 复制代码
private void finishWithDuration(long durationMicros) {
    // 设置Span的持续时间
    if (durationMicrosUpdater.compareAndSet(this, null, durationMicros)) {
        if (context.isSampled()) {
            // 设置成功则记录Span
            tracer.reportSpan(this);
        }
    } else {
        log.warn("Span has already been finished; will not be reported again.");
    }
}

上述方法实际就是基于CAS 的方式来更新JaegerSpandurationMicroseconds 字段,更新成功则调用TracerreportSpan() 方法来记录Span ,更新失败则表示Span 已经被完成过,而Span 是不能重复被完成的。现在继续看一下JaegerTracer#reportSpan方法的实现,如下所示。

java 复制代码
void reportSpan(JaegerSpan span) {
    // 调用到reporter来记录Span
    reporter.report(span);
    metrics.spansFinished.inc(1);
}

上述方法中会使用JaegerTracer 持有的reporter 字段(类型是Reporter )来记录Span ,而JaegerTracer 持有的reporter 是在创建JaegerTracer 时设置的,回顾一下示例代码中的TracerConfig,如下所示。

java 复制代码
@Configuration
public class TracerConfig {

    @Autowired
    private SpanReporter spanReporter;

    @Autowired
    private Sampler sampler;

    @Bean
    public Tracer tracer() {
        return new JaegerTracer.Builder(TRACER_SERVICE_NAME)
                .withTraceId128Bit()
                .withSampler(sampler)
                .withReporter(spanReporter)
                .build();
    }

}

我们在创建JaegerTracer 时,设置了一个我们自己写的SpanReporterJaegerTracer ,我们自己写的SpanReporter 实现了Reporter接口,如下所示。

java 复制代码
public class SpanReporter implements Reporter {

    public void report(JaegerSpan span) {
        // todo 打印链路日志
        // todo 暂不做任何事情
    }

    public void close() {

    }

}

所以在最终记录Span 时,每一个Span 都会送到我们写的SpanReporter 中来,那这个时候所谓的记录Span,就很有可操作性了。

已知一条分布式链路中,实际就是由一个又一个的Span 串起来的,每个Span 都有一个相同的traceId ,表明大家都是同一个分布式链路上的工作单元,同时每个Span 都有spanIdparentSpanId ,这样可以通过某个SpanspanIdparentSpanId 明确这个Span 在这条分布式链路中所处的位置,所以我们在记录Span 时,其实就是需要读取出SpantraceIdspanIdparentSpanIddurationMicroseconds 等参数,然后将这些参数组装成一定格式的数据,最后将Span 的格式化后的数据送到分布式链路追踪系统的服务端,后续分布式链路追踪系统的服务端就可以结合指定的格式,解析出一个又一个Span的链路信息,最终给用户呈现出一条又一条的分布式链路。

我们在示例的SpanReporter 中,没有对Span 做任何操作,这主要是为了简化示例代码,但实际上,我们可以通过各种方式来发送Span 数据给到我们的分布式链路追踪系统的服务端,例如发送Kafka,再例如打印日志再进行日志采集,总之可操作性是很强的。

三. 扩展点分析

通过前面的分析我们了解到,Opentracing对分布式链路追踪中的各种概念进行了统一的定义,某种程度上,已经成为分布式链路追踪的规范。

Java 语言中,Opentracing 定义了诸如SpanTracer 等概念对应的接口,不同的分布式链路实现方需要结合具体的实现方案来提供相应实现,例如本文选择的Jaeger ,其提供的JaegerSpan 实现了Span 接口,JaegerTracer 实现了Tracer接口等。

现在接口定义已经有了,具体的实现也有了,该怎么用起来呢。在本文的示例中,具体的使用案例就是我们提供的RestTemplate 拦截器,以及过滤器TracingFilter ,那么问题就来了,为什么我知道可以这么用,是因为我比较聪明吗,那必然不是,当然是Opentracing 告诉我该这么用,所以我才这么用,既然Opentracing 定义好了接口,还告诉了用户该怎么用,那么有没有一种可能,Opentracing 来提供RestTemplate 拦截器,来提供过滤器TracingFilter 呢,那完全是有可能的,Opentracing也正是这么做的。

OpentracingRestTemplate 提供了一个拦截器叫做TracingRestTemplateInterceptor ,也提供了一个过滤器叫做TracingFilter ,好吧,到这里我就不装了,示例中的RestTemplate 拦截器和过滤器TracingFilter ,其实就是抄的Opentracing 的,不过我没抄全,毕竟我只是需要搭建一个演示demo,所以官方的很多为了提升扩展性的扩展点,我都给砍掉了,而这些扩展点,正是我们基于已有的轮子造更好的轮子的基础,也正是本节的分析重点。

1. ServletFilterSpanDecorator和RestTemplateSpanDecorator

我们先看一下io.opentracing.contrib.web.servlet.filter.TracingFilter 中有哪些扩展点。OpentracingServlet 提供了一个专门服务于分布式链路追踪的过滤器TracingFilter ,其实现了javax.servlet.Filter 接口,关键的doFilter() 方法如下所示。

java 复制代码
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
        throws IOException, ServletException {

    HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
    HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;

    // 基于正则来判断当前请求URI是否不需要记录链路信息
    if (!isTraced(httpRequest, httpResponse)) {
        chain.doFilter(httpRequest, httpResponse);
        return;
    }

    if (servletRequest.getAttribute(SERVER_SPAN_CONTEXT) != null) {
        chain.doFilter(servletRequest, servletResponse);
    } else {
        // 使用Extractor从HTTP请求头中提取出SpanContext
        SpanContext extractedContext = tracer.extract(Format.Builtin.HTTP_HEADERS,
                new HttpServletRequestExtractAdapter(httpRequest));

        // 创建Span并将其作为提取出来的Span的子Span
        final Span span = tracer.buildSpan(httpRequest.getMethod())
                .asChildOf(extractedContext)
                .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER)
                .start();

        httpRequest.setAttribute(SERVER_SPAN_CONTEXT, span.context());

        // 在请求的一开始使用装饰器来装饰Span
        // 这里的装饰器是很重要的扩展点
        for (ServletFilterSpanDecorator spanDecorator: spanDecorators) {
            spanDecorator.onRequest(httpRequest, span);
        }

        // 将创建出来的Span激活
        try (Scope scope = tracer.activateSpan(span)) {
            chain.doFilter(servletRequest, servletResponse);
            if (!httpRequest.isAsyncStarted()) {
                for (ServletFilterSpanDecorator spanDecorator : spanDecorators) {
                    spanDecorator.onResponse(httpRequest, httpResponse, span);
                }
            }
        } catch (Throwable ex) {
            // 在请求异常时使用装饰器来装饰Span
            for (ServletFilterSpanDecorator spanDecorator : spanDecorators) {
                spanDecorator.onError(httpRequest, httpResponse, ex, span);
            }
            throw ex;
        } finally {
            if (httpRequest.isAsyncStarted()) {
                // 异步Servlet场景下添加监听器
                httpRequest.getAsyncContext()
                        .addListener(new AsyncListener() {
                            @Override
                            public void onComplete(AsyncEvent event) throws IOException {
                                HttpServletRequest httpRequest = (HttpServletRequest) event.getSuppliedRequest();
                                HttpServletResponse httpResponse = (HttpServletResponse) event.getSuppliedResponse();
                                // 异步操作完成时使用装饰器装饰Span
                                for (ServletFilterSpanDecorator spanDecorator: spanDecorators) {
                                    spanDecorator.onResponse(httpRequest,
                                            httpResponse,
                                            span);
                                }
                                span.finish();
                            }

                            @Override
                            public void onTimeout(AsyncEvent event) throws IOException {
                                HttpServletRequest httpRequest = (HttpServletRequest) event.getSuppliedRequest();
                                HttpServletResponse httpResponse = (HttpServletResponse) event.getSuppliedResponse();
                                // 异步操作超时时使用装饰器装饰Span
                                for (ServletFilterSpanDecorator spanDecorator : spanDecorators) {
                                    spanDecorator.onTimeout(httpRequest,
                                            httpResponse,
                                            event.getAsyncContext().getTimeout(),
                                            span);
                                }
                            }

                            @Override
                            public void onError(AsyncEvent event) throws IOException {
                                HttpServletRequest httpRequest = (HttpServletRequest) event.getSuppliedRequest();
                                HttpServletResponse httpResponse = (HttpServletResponse) event.getSuppliedResponse();
                                // 异步操作异常时使用装饰器装饰Span
                                for (ServletFilterSpanDecorator spanDecorator: spanDecorators) {
                                    spanDecorator.onError(httpRequest,
                                            httpResponse,
                                            event.getThrowable(),
                                            span);
                                }
                            }

                            @Override
                            public void onStartAsync(AsyncEvent event) throws IOException {
                            }
                        });
            } else {
                // 请求完成时设置完成时间并记录Span
                span.finish();
            }
        }
    }
}

通过上述代码,可以发现有一个满脸长着我是扩展点的东西,就是ServletFilterSpanDecorator ,其主要负责在过滤器链执行之前,之后以及发生异常时对Span 进行装饰,怎么理解这里的装饰呢,其实就是往Span 添加一些东西或者修改一些东西,举个例,在过滤器链执行前,往SpanTags 中添加本次请求的URI ,然后在过滤器链执行后,往SpanTags 中添加本次请求的响应码,等等这些需求,都可以在ServletFilterSpanDecorator 中完成,这其实就赋予了我们对Span极高的可操作性。

装饰器除了在TracingFilter 中有被使用,同样也在Opentracing 提供的TracingRestTemplateInterceptor 中被使用,其intercept() 方法如下所示。

java 复制代码
@Override
public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] body,
                                    ClientHttpRequestExecution execution) throws IOException {
    ClientHttpResponse httpResponse;

    Span span = tracer.buildSpan(httpRequest.getMethod().toString())
            .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT)
            .start();
    tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS,
            new HttpHeadersCarrier(httpRequest.getHeaders()));

    for (RestTemplateSpanDecorator spanDecorator : spanDecorators) {
        try {
            spanDecorator.onRequest(httpRequest, span);
        } catch (RuntimeException exDecorator) {
            log.error("Exception during decorating span", exDecorator);
        }
    }

    try (Scope scope = tracer.activateSpan(span)) {
        httpResponse = execution.execute(httpRequest, body);
        for (RestTemplateSpanDecorator spanDecorator : spanDecorators) {
            try {
                spanDecorator.onResponse(httpRequest, httpResponse, span);
            } catch (RuntimeException exDecorator) {
                log.error("Exception during decorating span", exDecorator);
            }
        }
    } catch (Exception ex) {
        for (RestTemplateSpanDecorator spanDecorator : spanDecorators) {
            try {
                spanDecorator.onError(httpRequest, ex, span);
            } catch (RuntimeException exDecorator) {
                log.error("Exception during decorating span", exDecorator);
            }
        }
        throw ex;
    } finally {
        span.finish();
    }

    return httpResponse;
}

上述实现中,在请求前,请求后以及报错时使用了RestTemplateSpanDecorator 来装饰Span ,所以RestTemplateSpanDecorator也是一个重要的扩展点,具体如何使用,在后续的文章中会逐步进行演示。

2. Injector和Extractor

InjectorExtractor 分别用来处理SpanContext 的注入和提取操作,以Jaeger 为例,在创建JaegerTracer 时,可以按照键值对的方式,向JaegerTracer 注册InjectorExtractor,就像下面这样。

java 复制代码
@Configuration
public class TracerConfig {

    @Autowired
    private SpanReporter spanReporter;

    @Autowired
    private Sampler sampler;

    @Bean
    public Tracer tracer(MyHttpHeadersInjector myHttpHeadersInjector,
                         MyHttpHeadersExtractor myHttpHeadersExtractor) {
        return new JaegerTracer.Builder(TRACER_SERVICE_NAME)
                .withTraceId128Bit()
                .withSampler(sampler)
                .withReporter(spanReporter)
                .registerInjector(Format.Builtin.HTTP_HEADERS, myHttpHeadersInjector)
                .registerExtractor(Format.Builtin.HTTP_HEADERS, myHttpHeadersExtractor)
                .build();
    }

}

键是Format ,可以自己定义,也可以使用Opentracing 为我们定义好的,例如Format.Builtin#HTTP_HEADERS ,值就是InjectorExtractor 的实现类,那么我们就可以自己提供InjectorExtractor 的实现类来扩展SpanContext的注入和提取操作。

3. ScopeManager

大多数情况下,Opentracing 提供的ThreadLocalScopeManager 能满足我们的使用需求,但如果是异步链路追踪的场景,ThreadLocal 就无法满足使用需求,此时需要使用InheritableThreadLocal ,我们就可以基于InheritableThreadLocal 来提供一个ScopeManager 接口的实现类,并在创建Tracer 时指定我们要使用的ScopeManager 。还是以Jaeger 为例,在创建JaegerTracer 时,可以指定要使用的ScopeManager,如下所示。

java 复制代码
@Configuration
public class TracerConfig {

    @Autowired
    private SpanReporter spanReporter;

    @Autowired
    private Sampler sampler;

    @Bean
    public Tracer tracer(ScopeManager scopeManager) {
        return new JaegerTracer.Builder(TRACER_SERVICE_NAME)
                .withTraceId128Bit()
                .withSampler(sampler)
                .withReporter(spanReporter)
                .withScopeManager(scopeManager)
                .build();
    }

}

具体如何使用,以及如何基于InheritableThreadLocal 来实现适用于异步链路追踪的ScopeManager,这里就不再深入,这一块儿将在后续的文章中进行分析和演示。

4. 其它扩展点

扩展点还有很多,限于本文篇幅,这里就不再一一介绍,后面在实现分布式链路工具包的时候,用到了,自然会进行说明。

总结

一不小心,又扯了这么多,本文其实重点就是聚焦于Opentracing中的若干重要概念,这里用于一张图进行总结吧。

相关推荐
m0_748230212 分钟前
【Spring Boot】Spring AOP中的环绕通知
spring boot·后端·spring
潘多编程4 分钟前
动态定时任务在Spring Boot中集成Quartz的实践
java·spring boot·后端
2401_857636395 分钟前
Spring Boot OA系统:企业办公自动化的创新实践
java·spring boot·后端
SiMmming23 分钟前
从0到1部署Tomcat和添加servlet(IDEA2024最新版详细教程)
java·ide·tomcat·intellij-idea·web
gaogao_jack25 分钟前
[Leetcode小记] 3233. 统计不是特殊数字的数字数量
java·算法·leetcode
可乐加.糖29 分钟前
RabbitMQ和RocketMQ相关面试题
java·rabbitmq·rocketmq·ruby·java-rabbitmq
小菜日记^_^33 分钟前
Maven高级篇
java·spring boot·后端·spring·maven·intellij-idea·mybatis
小杨4041 小时前
高级并发编程系列七(锁入门)
java·后端·性能优化
计算机毕设指导61 小时前
基于SpringBoot的教学辅助平台系统【附源码】
java·spring boot·后端·mysql·spring·tomcat·mybatis
两眼墨黑1 小时前
规则引擎aviatorEvaluator注意点
java