10

OpenTracing Java Library教程(3)——跨服务传递SpanContext

 4 years ago
source link: https://niyanchun.com/opentracing-java-library-tutorial-3.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

本文内容主要翻译(意译)自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)
extract(format, carrier)

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

TEXT_MAP
BINARY
HTTP_HEADERS

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 函数,这个函数实现的功能如下:

extract
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

然后看下生成的链路图:

Vb6rqi2.png!web

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

FJJRny.png!web

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

fuMvAvz.png!web

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK