前言
如果要基于Opentracing开发分布式链路追踪Java客户端工具包,首先肯定需要了解Opentracing中的各种概念,包括但不限于Span和Scope等,其实这些概念在Opentracing的官方文档中是有比较详尽的说明的,英文不好也能靠着机器翻译读得通,但是读得通不代表读得懂,从来没有接触过分布式链路追踪的人就算把官方文档通读完,整体的概念还是显得比较抽象,所以本文作为Opentracing入门,旨在让从来没接触过分布式链路追踪的人也能理解Opentracing中的各种概念,为后续阅读相关源码和自行实现分布式链路追踪客户端工具包打好基础。
本文会从一个简单的例子入手,结合相关场景和源码实现,阐述Opentracing中的Span和Scope等概念,通过阅读本文,可以快速了解关于分布式链路追踪的相关概念,并知道有哪些扩展点我们可以利用起来进行功能扩展。
Opentracing和jaeger相关版本依赖如下。
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 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();
}
}
}
上面出现了很多陌生的内容例如Span,Tracer和Tags,但是不慌,这些后面都会知道是啥,我们现在先把客户端和服务端搭起来。我们上述的拦截器使用了一个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时指定了SpanReporter和Sampler,我们先不去深究SpanReporter和Sampler是啥,仅先看一下这两个bean的配置类,如下所示。
public class SpanReporter implements Reporter {
public void report(JaegerSpan span) {
}
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);
}
}
客户端这边的工程目录结构如下图所示。
服务端的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以及SpanReporter和Sampler的配置类如下所示。
public class SpanReporter implements Reporter {
public void report(JaegerSpan span) {
}
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("接收请求");
}
}
服务端的工程目录结构如下所示。
最后,在将客户端运行在8081端口,服务端运行在8082端口,并调用如下接口。
http://localhost:8081/send?url=http://localhost:8082/receive
我们在客户端这边的RestTemplate的拦截器中,会通过Opentracing的相关api将请求URI放到请求头中,然后在服务端这边的TracingFilter中,又会从请求头中解析出客户端传递过来的URI,这一点可以分别在客户端RestTemplate的拦截器和服务端TracingFilter中打断点进行观察。
先看一下客户端使用RestTemplate发请求时,经过拦截器后,HTTP请求头中的字段,如下所示。
再看一下服务端这边在TracingFilter中解析出客户端传递过来的URI,如下所示。
也就是客户端这边通过Opentracing的api,将一些字段放在了请求头中,传递到了服务端。
那么问题就来了,我们自己不通过Opentracing的api,其实也是可以通过HTTP请求头来传递信息到下游,为啥要基于Opentracing呢,其实上述示例中,客户端除了传递请求URI到下游,还传递了在分布式链路中很重要的traceId和spanId,前者标记一次请求链路,后者代表这次请求链路上的节点,同时Opentracing还定义了如何让链路信息在进程中传递和跨进程传递,上述示例就是跨进程传递的一个简单演示。
好了到这里示例就演示完毕了,后续我们就基于上述的示例,以及相关的源码,来阐述Opentracing中的各种概念,为后面的分布式链路工具包开发奠定基础。