本文内容主要翻译(意译)自Yurishkuro大神的opentracing-tutorial java,加了一些补充说明,方便理解,习惯看英文的也可以看原文。总共4篇,本文是第3篇。如果你还没接触过OpenTracing,建议先读这篇文章《OpenTracing概念术语介绍》和官方文档

  1. 第1篇:单span的trace创建
  2. 第2篇:多span的trace创建(进程内SpanContext传递)。
  3. 第3篇:跨服务(进程)传递SpanContext本文)。
  4. 第4篇:Baggage介绍

目标

学习如何:

  • 跨服务做链路跟踪
  • 使用InjectExtract方法在服务间传递span上下文(SpanContext)信息
  • 使用一些OpenTracing推荐的tags

开发流程

构建Hello-World微服务

为了演示跨服务做链路跟踪,我们先来构建几个服务:

  • Hello.java :基于上一节的代码,修改了部分代码,增加了HTTP请求代码。
  • Formatter.java:基于Dropwizard-based的HTTP服务器,提供这样的一个接口:发送GET 'http://localhost:8081/format?helloTo=Bryan',返回"Hello, Bryan!" 字符串。
  • Publisher.java:类似Formatter.java,提供这样一个接口:发送GET 'http://localhost:8082/publish?helloStr=hi%20there'请求,就往标准输出打印一个"hi there" 字符串。

先把后面两个HTTP Server运行起来:

// terminal tab 1
$ ./run.sh lesson03.exercise.Formatter server
// terminal tab 2
$ ./run.sh lesson03.exercise.Publisher server

然后发送一个HTTP请求:

$ curl 'http://localhost:8081/format?helloTo=Bryan'
Hello, Bryan!

如果出现上面打印,说明我们的服务已经OK了。

最后我们像前一篇文章一样,继续运行Hello服务:

./run.sh lesson03.solution.Hello Bryan 

进程/服务间链路信息传递

虽然我们的Hello中做了两个RPC请求(HTTP也是RPC的一种),但运行之后会发现链路图和之前的一样:产生了一个包含三个span的链路,都是hello-world这个服务产生的。我们当然希望链路可以展示出这个调用中的涉及的所有服务,这个时候就需要实现在服务间(即跨进程)传递链路信息。链路信息一般包装在上下文中,这个上下文称之为SpanContext:一般至少包含链路的状态信息(比如traceID、spanID等)和Baggage信息。Baggage信息下篇文章介绍。所以链路信息的传递就是传递这个SpanContext。OpenTracing提供了一个抽象,在Tracer接口中定义了两个接口:

  • inject(spanContext, format, carrier) :用于将SpanContext注入到RPC请求中;
  • extract(format, carrier):用于从RPC请求中获取SpanContext。

按照OpenTracing API定义,format参数表示SpanContext的编码格式(或者说传递方式吧),需要为以下三个编码之一:

  • TEXT_MAP:将SpanContext编码为key-value形式
  • BINARY:编码为字节流;
  • HTTP_HEADERS:和TEXT_MAP一样,区别在于key的信息必须能够安全的放在HTTP headers里面(即不能包含一些http头不支持的特殊字符)。经常把HTTP_HEADERS归到TEXT_MAP类。

carrier是基于底层RPC框架做的一层抽象,用于传递SpanContext。比如TEXT_MAP格式对应的carrier接口允许tracer实例通过put(key, value)方法将key-value格式的数据写入到请求中。同理,BINARY格式的就是ByteBuffer

下面我们看如何通过inject和extract来实现进程间的链路上下文信息传递。

客户端增强

首先需要在客户端发送HTTP请求前将SpanContext注入进去,发送给服务端。现在的HTTP请求是封装在 Hello#getHttp()中的,所以在这里加:

import io.opentracing.propagation.Format;
import io.opentracing.tag.Tags;

Tags.SPAN_KIND.set(tracer.activeSpan(), Tags.SPAN_KIND_CLIENT);
Tags.HTTP_METHOD.set(tracer.activeSpan(), "GET");
Tags.HTTP_URL.set(tracer.activeSpan(), url.toString());
tracer.inject(tracer.activeSpan().context(), Format.Builtin.HTTP_HEADERS, new RequestBuilderCarrier(requestBuilder));

这里是以TEXT_MAP(HTTP_HEADERS)编码SpanContext的,所以需要实现TextMap类:

import java.util.Iterator;
import java.util.Map;

import okhttp3.Request;

public class RequestBuilderCarrier implements io.opentracing.propagation.TextMap {
    private final Request.Builder builder;

    RequestBuilderCarrier(Request.Builder builder) {
        this.builder = builder;
    }

    @Override
    public Iterator<Map.Entry<String, String>> iterator() {
        throw new UnsupportedOperationException("carrier is write-only");
    }

    @Override
    public void put(String key, String value) {
        builder.addHeader(key, value);
    }
}

tracer会调用put方法将SpanContext中的信息以key-value的形式加到HTTP头中。这里的信息主要是我们写的一些跟请求想关的Tags信息。

这样,客户端已经通过inject将SpanContext加入到请求中了。接下来看服务端收到请求后,如何使用extract取出这些信息。

服务端增强

服务端增强和客户端类似,先参照客户端创建一个Tracer实例。这部分一样,就略过了,重点看如何取出SpanContext信息。

这里封装一个startServerSpan函数,这个函数实现的功能如下:

  • 从收到的请求中读取头信息(SpanContext信息包含在里面)
  • 使用extract方法解析出SpanContext,并基于此创建了一个新的Span,同时增加了一些服务端的信息
public static Span startServerSpan(Tracer tracer, javax.ws.rs.core.HttpHeaders httpHeaders, String operationName) {
    // format the headers for extraction
    MultivaluedMap<String, String> rawHeaders = httpHeaders.getRequestHeaders();
    final HashMap<String, String> headers = new HashMap<String, String>();
    for (String key : rawHeaders.keySet()) {
        headers.put(key, rawHeaders.get(key).get(0));
    }

    Tracer.SpanBuilder spanBuilder;
    try {
        SpanContext parentSpanCtx = tracer.extract(Format.Builtin.HTTP_HEADERS, new TextMapAdapter(headers));
        if (parentSpanCtx == null) {
            spanBuilder = tracer.buildSpan(operationName);
        } else {
            spanBuilder = tracer.buildSpan(operationName).asChildOf(parentSpanCtx);
        }
    } catch (IllegalArgumentException e) {
        spanBuilder = tracer.buildSpan(operationName);
    }
    return spanBuilder.withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER).start();
}

Formatter和Publisher两个服务都需要做这个事情。有了这个span,就可以使用了,这里展示一下Formatter代码的代码:

@GET
public String format(@QueryParam("helloTo") String helloTo, @Context HttpHeaders httpHeaders) {
    // 调用封装的startServerSpan函数,基于客户端传递过来SpanContext的创建一个新的span,并在tags中加入服务端的一些信息
    Span span = startServerSpan(tracer, httpHeaders, "format");
    try (Scope scope = tracer.scopeManager.activate(span)) {
        String helloStr = String.format("Hello, %s!", helloTo);
        span.log(ImmutableMap.of("event", "string-format", "value", helloStr));
        return helloStr;
    } finally {
      span.finish();
    }
}

至此,服务端的增强也实现好了,是时候见证奇迹了。

见证奇迹

重新运行Formatter、 Publisher和Hello服务(我没有改时区,所以日志中的时间差了8小时,实际现在是周日早晨8点):

// terminal tab 1:启动Formatter服务
$ ./run.sh lesson03.exercise.Formatter server
// 省略了部分日志
INFO  [2020-07-12 00:57:48,181] io.jaegertracing.internal.reporters.LoggingReporter: Span reported: 2b20ca6e8ddc6547:2eb6a1fbef6e9789:8a92a88a65fb4776:1 - format
127.0.0.1 - - [12/Jul/2020:00:57:48 +0000] "GET /format?helloTo=Bryan HTTP/1.1" 200 13 "-" "okhttp/3.9.0" 4

// terminal tab 2:启动Publisher服务
$ ./run.sh lesson03.exercise.Publisher server
// 省略了部分日志
Hello, Bryan!
INFO  [2020-07-12 00:57:48,440] io.jaegertracing.internal.reporters.LoggingReporter: Span reported: 2b20ca6e8ddc6547:93916ee579078535:75065f170bf15bff:1 - publish
127.0.0.1 - - [12/Jul/2020:00:57:48 +0000] "GET /publish?helloStr=Hello,%20Bryan! HTTP/1.1" 200 9 "-" "okhttp/3.9.0" 137

// terminal tab 3:启动Hello,启动后会分别调用Formatter服务和Publisher服务
-> % ./run.sh lesson03.solution.Hello Bryan 
// 省略了部分日志
08:57:48.206 [main] INFO io.jaegertracing.internal.reporters.LoggingReporter - Span reported: 2b20ca6e8ddc6547:8a92a88a65fb4776:2b20ca6e8ddc6547:1 - formatString
08:57:48.468 [main] INFO io.jaegertracing.internal.reporters.LoggingReporter - Span reported: 2b20ca6e8ddc6547:75065f170bf15bff:2b20ca6e8ddc6547:1 - printHello
08:57:48.468 [main] INFO io.jaegertracing.internal.reporters.LoggingReporter - Span reported: 2b20ca6e8ddc6547:2b20ca6e8ddc6547:0:1 - say-hello

然后看下生成的链路图:

lesson-03-1.png

可以看到新生成的链路包含了3个服务,共5个span。点击查看链路详情:

lesson-03-2

从右侧可以清楚的看出调用关系,左侧可以看出耗时。然后再看下每个服务的一些详细信息:

lesson-03-3

Tags包含了各个span的一些关键信息。

总结

本文主要展示了如何跨进程/服务传递SpanContext,下一篇介绍另外一种传递信息的方式,也是SpanContext中非常重要的一部分:Baggage。