本文内容主要翻译(意译)自Yurishkuro大神的 opentracing-tutorial java ,加了一些补充说明,方便理解,习惯看英文的也可以看原文。总共4篇,本文是第3篇。如果你还没接触过OpenTracing,建议先读这篇文章《 OpenTracing概念术语介绍 》和 官方文档 。
学习如何:
Inject
和 Extract
方法在服务间传递span上下文(SpanContext)信息 为了演示跨服务做链路跟踪,我们先来构建几个服务:
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
然后看下生成的链路图:
可以看到新生成的链路包含了3个服务,共5个span。点击查看链路详情:
从右侧可以清楚的看出调用关系,左侧可以看出耗时。然后再看下每个服务的一些详细信息:
Tags包含了各个span的一些关键信息。
本文主要展示了如何跨进程/服务传递SpanContext,下一篇介绍另外一种传递信息的方式,也是SpanContext中非常重要的一部分:Baggage。