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

1,058 阅读34分钟

大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈


前言

如果要基于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

正文

一. 场景演示

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

分布式链路追踪-简单ClientServer场景

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

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

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

<?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提供的拦截器,对应实现如下所示。

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对象的配置类是怎么写的,如下所示。

@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的配置类,如下所示。

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的配置类,如下所示。

@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,如下所示。

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);
    }

}

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

示例Client的目录结构

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

<?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>

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

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,相关配置类如下所示。

@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的配置类如下所示。

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来完成注册,对应配置类如下所示。

@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,如下所示。

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("接收请求");
    }

}

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

示例Server的目录结构

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

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就是链路中的被调用或调用行为。通常一个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字段的签名,如下所示。

private final Map<String, Object> tags;

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

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的拦截器的实现,如下所示。

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方法,这个方法实现如下所示。

@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,如下所示。

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做一些什么操作。

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接口中的如下两个方法。

// 将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拦截器的实现举例,先看如下这一段示例代码。

tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS, new HttpHeadersCarrier(request.getHeaders()));

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

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对象,这个对象是一个很简单的对象,如下所示。

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类图

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

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方法,如下所示。

@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方法干了什么,如下所示。

@Override
public void put(String key, String value) {
    httpHeaders.add(key, value);
}

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

@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对象后,后续执行了如下这么一行代码。

Scope scope = tracer.activateSpan(span)

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

Scope activateSpan(Span span);

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

public interface Scope extends Closeable {

    @Override
    void close();

}

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

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

@Override
public Scope activateSpan(Span span) {
    return scopeManager().activate(span);
}

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

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的构造函数中,如下所示。

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() 方法的签名如下所示。

// 获取到当前线程激活的Span
Span activeSpan();

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

@Override
public Span activeSpan() {
    return this.scopeManager.activeSpan();
}

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

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

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

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

@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的如下两个方法。

<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的如下两个方法。

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() 方法,该方法签名如下所示。

// 设置Span完成时间并记录Span
void finish();

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

@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方法,如下所示。

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方法的实现,如下所示。

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

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

@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接口,如下所示。

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() 方法如下所示。

@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() 方法如下所示。

@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,就像下面这样。

@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,如下所示。

@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中的若干重要概念,这里用于一张图进行总结吧。

分布式链路追踪-Opentracing概念总结图


大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈