软件测试其他零散知识点Java 开发

Opentracing 链路追踪实战

2021-10-12  本文已影响0人  殷天文

链路追踪的作用

当系统架构从单机转变为微服务后,我们的一次后端请求,可能历经了多个服务才最终响应到客户端。如果请求按照预期正确响应还好,万一在调用链的某一环节出现了问题,排查起来是很麻烦的。但是如果有链路追踪的话,就容易很多了。

可以通过链路埋点,记录请求链中所有重要的步骤,例如与哪些数据库做了交互,调用了哪些下游服务,下游服务又与哪些数据库做了交互,又调用了哪些下游服务...

下图是 jaeger UI 为例,每次链路追踪都会产生一个唯一的 TraceId,通过该 Id 可以查看请求链路的状态

当有了链路追踪之后。我们可以清楚的看到问题出在哪。

什么是 Opentracing

Opentracing 制定了一套链路追踪的 API 规范,支持多种编程语言。虽然OpenTracing不是一个标准规范,但现在大多数链路跟踪系统都在尽量兼容OpenTracing

使用 Opentracing 时,还需要集成实现该规范的链路追踪系统,例如我们的项目正在使用 Jaeger,本文也同样以 Jaeger 为例

Opentracing 核心接口

快速开始

首先,我们先部署一个 jaeger 服务。关于 jaeger 服务的更多细节,这里不多说了,各位读者可以自行去 jaeger 官网阅读

执行如下命令,启动 jaeger 服务
docker run -d --name jaeger -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 -p 5775:5775/udp -p 6831:6831/udp -p 6832:6832/udp -p 5778:5778 -p 16686:16686 -p 14268:14268 -p 14250:14250 -p 9411:9411 jaegertracing/all-in-one:1.23

引入 maven 依赖

<!-- guava 依赖,与 jaeger 无关 -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.1.1-jre</version>
</dependency>

<dependency>
    <groupId>io.opentracing</groupId>
    <artifactId>opentracing-api</artifactId>
    <version>0.33.0</version>
</dependency>

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

记录一个简单的链路

import com.google.common.collect.ImmutableMap;
import io.jaegertracing.Configuration;
import io.jaegertracing.internal.JaegerTracer;
import io.opentracing.Span;
import io.opentracing.Tracer;

import java.time.LocalDateTime;

public class GettingStarter {

    public static void main(String[] args) {
        // 指定服务名,初始化 Tracer
        Tracer tracer = initTracer("starter-service");
        // 指定 Span 的 operationName
        Span span = tracer.buildSpan("")
                // 指定当前 Span 的 Tag, key value 格式
                .withTag("env", "local")
                .start();

        span.setTag("system", "windows");

        // log 也是 key value 格式,默认 key 为 event
        span.log("create first Span");
        // 传入一个 Map
        span.log(ImmutableMap.of("currentTime", LocalDateTime.now().toString()));

        // 输出当前 traceId
        System.out.println(span.context().toTraceId());

        // 结束并上报 span
        span.finish();
    }

    public static JaegerTracer initTracer(String service) {
        Configuration.SamplerConfiguration samplerConfig = Configuration.SamplerConfiguration.fromEnv().withType("const").withParam(1);
        Configuration.ReporterConfiguration reporterConfig = Configuration.ReporterConfiguration.fromEnv().withLogSpans(true);
        Configuration config = new Configuration(service).withSampler(samplerConfig).withReporter(reporterConfig);
        return config.getTracer();
    }

}

jaeger UI 查询链路,访问 http://localhost:16686/search,右上角输入 traceId 搜索。结果如下

image.png

可以看到我们刚刚在代码中的 Tag 和 Log 都记录在链路上了

线程传递 Span

Span 之间是可以建立父子关系的,使用

Span parent= tracer.buildSpan("say-hello").start();
tracer.buildSpan("son")
                .asChildOf(parent)
                .start();

效果如图

image.png

parent Span 肯定不能一直在调用栈中传递下去,这对集成 opentracing 的程序来说侵入性太大了。回顾一下上面我们提到了一个 ScopeManager ,来看下其核心方法

ThreadLocalScopeManager

Jaeger 中默认的 ScopeManager,该类由 opentracing 提供。

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();
    }
}
public class ThreadLocalScope implements Scope {
    private final ThreadLocalScopeManager scopeManager;
    private final Span wrapped;
    private final ThreadLocalScope toRestore;

    ThreadLocalScope(ThreadLocalScopeManager scopeManager, Span wrapped) {
        this.scopeManager = scopeManager;
        this.wrapped = wrapped;
        this.toRestore = scopeManager.tlsScope.get();
        scopeManager.tlsScope.set(this);
    }

    @Override
    public void close() {
        if (scopeManager.tlsScope.get() != this) {
            // This shouldn't happen if users call methods in the expected order. Bail out.
            return;
        }

        scopeManager.tlsScope.set(toRestore);
    }

    Span span() {
        return wrapped;
    }
}

ThreadLocalScopeManager#activate:直接调用了new ThreadLocalScope。在 ThreadLocalScope 构造方法中,首先将从 ThreadLocal 中获取到目前上下文中的 ThreadLocalScope 赋值给 toRestore,然后将 this (ThreadLocalScope 对象) set 到 ThreadLocal。

ThreadLocalScope 中存储着当前的 Span。后续的代码如果想要获取 Span,只需要调用 ScopeManager#activeSpan 就可以(ScopeManager 可以在 Tracer 对象中拿到)

在执行 close 时(Scope 继承了 Closeable),将之前的 Span 重新放回到上下文中

线程传递 Span 演示

Span parentSpan = tracer.buildSpan("parentSpan").start();

try (Scope scope = tracer.activateSpan(parentSpan)) {
    xxxMethod();
} finally {
    parentSpan.finish();
}

public void xxxMethod() {
    // 这里并不需要手动从 ScopeManager 中取出上下文中的 Span
    // start 方法中已经做了
    // 如果 ScopeManager.activeSpan() != null 会自动调用 asChildOf
    tracer.buildSpan("sonSpan").start();
}

链路中数据共享

如果需要和链路的下游共享某些数据,使用如下方法

// 写
span.setBaggageItem("key", "value");

// 读
span.getBaggageItem("key");

只要保证在同一条链路中,即使下游 Span 在不同的进程,依然可以通过 getBaggageItem 读到数据

跨进程链路追踪

opentracing 中提供了实现跨进程追踪的规范

Tracer 接口中提供了如下两个方法

简单举个例子解释下,例如我们使用 Http 协议访问下游服务,inject 可以将 SpanContext 注入到 HttpHeaders 中。
下游服务再从 HttpHeaders 中按照链路中的约定取出有特殊标识的 header 来构建 SpanContext。这样一来就实现了链路的跨进程

再回到代码层面,通过接口方法的声明我们可以看出来,Format 决定了 Carrier 的类型。下面来看看实际代码中如何实现

跨进程链路追踪演示

public class TracingRestTemplateInterceptor implements ClientHttpRequestInterceptor {
    private static final String SPAN_URI = "uri";

    private final Tracer tracer;

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

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        ClientHttpResponse httpResponse;
        // 为当前 RestTemplate 调用,创建一个 Span
        Span span = tracer.buildSpan("RestTemplate-RPC")
                .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT)
                .withTag(SPAN_URI, request.getURI().toString())
                .start();
        // 将当前 SpanContext 注入到 HttpHeaders
        tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS,
                new HttpHeadersCarrier(request.getHeaders()));

        try (Scope scope = tracer.activateSpan(span)) {
            httpResponse = execution.execute(request, body);
        } catch (Exception ex) {
            TracingError.handle(span, ex);
            throw ex;
        } finally {
            span.finish();
        }
        return httpResponse;
    }

}

TracingRestTemplateInterceptor 实现了 RestTemplate 的拦截器,用于在 Http 调用之前,将 SpanContext 注入到 HttpHeaders 中。

Format.Builtin.HTTP_HEADERS 决定了当前的 Carrier 类型必须 TextMap (源码中可以看到,这里我没有列出)

public class HttpHeadersCarrier implements TextMap {
    private final HttpHeaders httpHeaders;

    public HttpHeadersCarrier(HttpHeaders httpHeaders)  {
        this.httpHeaders = httpHeaders;
    }

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

    @Override
    public Iterator<Map.Entry<String, String>> iterator() {
        throw new UnsupportedOperationException("Should be used only with tracer#inject()");
    }
}

tracer.inject 内部会调用 TextMap 的 put 方法,这样就将 SpanContext 注入到 HttpHeaders 了。

下面再来看看下游怎么写

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 httpRequest = (HttpServletRequest) servletRequest;
        HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
        // 通过 HttpHeader 构建 SpanContext
        SpanContext extractedContext = tracer.extract(Format.Builtin.HTTP_HEADERS,
                new HttpServletRequestExtractAdapter(httpRequest));

        String operationName = httpRequest.getMethod() + ":" + httpRequest.getRequestURI();
        Span span = tracer.buildSpan(operationName)
                .asChildOf(extractedContext)
                .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER)
                .start();

        httpResponse.setHeader("TraceId", span.context().toTraceId());

        try (Scope scope = tracer.activateSpan(span)) {
            filterChain.doFilter(servletRequest, servletResponse);
        } catch (Exception ex) {
            TracingError.handle(span, ex);
            throw ex;
        } finally {
            span.finish();
        }
    }
}

TracingFilter 实现了 Servlet Filter,每次请求访问到服务器时创建 Span,如果可以抽取到 SpanContext,则创建的是 Child Span

public class HttpServletRequestExtractAdapter implements TextMap {
    private final IdentityHashMap<String, String> headers;

    public HttpServletRequestExtractAdapter(HttpServletRequest httpServletRequest) {
        headers = servletHeadersToMap(httpServletRequest);
    }

    @Override
    public Iterator<Map.Entry<String, String>> iterator() {
        return headers.entrySet().iterator();
    }

    @Override
    public void put(String key, String value) {
        throw new UnsupportedOperationException("This class should be used only with Tracer.inject()!");
    }

    private IdentityHashMap<String, String> servletHeadersToMap(HttpServletRequest httpServletRequest) {
        IdentityHashMap<String, String> headersResult = new IdentityHashMap<>();

        Enumeration<String> headerNamesIt = httpServletRequest.getHeaderNames();
        while (headerNamesIt.hasMoreElements()) {
            String headerName = headerNamesIt.nextElement();

            Enumeration<String> valuesIt = httpServletRequest.getHeaders(headerName);
            while (valuesIt.hasMoreElements()) {
                // IdentityHashMap 判断两个 Key 相等的条件为 k1 == k2
                // 为了让两个相同的字符串同时存在,必须使用 new String
                headersResult.put(new String(headerName), valuesIt.nextElement());
            }

        }

        return headersResult;
    }

}

tracer.extract 内部会调用 HttpServletRequestExtractAdapter iterator 方法用于构建 SpanContext

如果你看完了这些还是对于跨进程链路追踪有疑惑的,可以下载一下我写的 Demo,通过 Debug 来更进一步了解

https://github.com/TavenYin/taven-springcloud-learning/tree/master/jaeger-mutilserver

Demo 中的代码参考了 opentracing 的实现,做了相应的简化,诸位可以放心食用

实际使用

opentracing 已经实现了一些常用 api 的链路埋点,在没有什么特殊需求的时候,我们可以直接使用这些代码。具体参考

上一篇下一篇

猜你喜欢

热点阅读