NYC's Blog - OpenTracing http://niyanchun.com/tag/opentracing/ zh-CN Sun, 12 Jul 2020 22:04:00 +0800 Sun, 12 Jul 2020 22:04:00 +0800 OpenTracing概念术语介绍 http://niyanchun.com/opentracing-introduction.html http://niyanchun.com/opentracing-introduction.html Sun, 12 Jul 2020 22:04:00 +0800 NYC 本来计划接着上文介绍Skywalking的架构,但是我想了一下,觉得还是有必要先把链路跟踪里面涉及的一些基础概念术语介绍一下,介绍这些Skywalking并不是一个比较好的选择。原因一方面是Skywalking只是众多APM实现之一,里面有些设计并不适合其它APM,另一方面Skywalking提供的比较好的探针多时Java的,而且是字节码注入的,不利于观察学习。当然最重要的是有一个更合适的选择:OpenTracing。

OpenTracing介绍

分布式请求/链路跟踪(Distributed Request Tracing)最早是Google内部在用,后来相对成熟以后,2010年对外发布了一篇论文:Dapper, a Large-Scale Distributed Systems Tracing Infrastructure,但没有将系统开源。接着就有一些公司和社区开始基于Dapper实现自己的链路跟踪系统,比较有名的有(这里只列举开源的):Twitter的Zipkin、韩国的PinPoint、大众点评的CAT,以及一些后起之秀:Uber的Jaeger,Apache Skywalking等。

在百花齐放的时候,出现了OpenTracing,关于它的介绍官方是这样说的:

What is OpenTracing?

It is probably easier to start with what OpenTracing is NOT.

  • OpenTracing is not a download or a program. Distributed tracing requires that software developers add instrumentation to the code of an application, or to the frameworks used in the application.
  • OpenTracing is not a standard. The Cloud Native Computing Foundation (CNCF) is not an official standards body. The OpenTracing API project is working towards creating more standardized APIs and instrumentation for distributed tracing.

OpenTracing is comprised of an API specification, frameworks and libraries that have implemented the specification, and documentation for the project. OpenTracing allows developers to add instrumentation to their application code using APIs that do not lock them into any one particular product or vendor.

我简单概括一下,OpenTracing制定了一些链路跟踪的API规范,并且提供了一些框架和库,这些框架和库实现了它制定的那些API规范。而且它是一个独立开放的项目,现在已经是云原生基金会(Cloud Native Computing Foundation, CNCF)的项目了。任何组织和个人都可以贡献符合API规范的库/框架。虽然OpenTracing不是一个标准规范,但现在大多数链路跟踪系统都在尽量兼容OpenTracing。

需要重点说明的是OpenTracing提供的框架和库只是采集最原始的链路数据,并不做分析。如果放到Skywalking的架构中,它只实现了探针部分。也就是OpenTracing并不是一个完备的链路系统,所以我们无法单独使用,必须配合兼容OpenTracing规范的系统使用,比如Jaeger、LightStep、Apache Skywalking、Elastic APM等。

最新的进展是OpenTracing已经和CNCF的另外一个项目OpenTelemetry合并了。

题外话:虽然APM各个系统号称兼容OpenTracing,但兼容性到底如何,其实还是参差不齐的,比如Jaeger就比Skywalking兼容性好。

OpenTracing 术语介绍

说明:

  • 基本上所有的链路跟踪系统以及OpenTracing里面术语大多都是来自于Dapper论文里面的。下面介绍的规范都是OpenTracing定义的一些规范
  • 为了方便理解和交流,概念术语等使用英文,不做翻译。

分布式链路跟踪系统的数据模型:

Traces(一般翻译为链路):一起请求从发出,然后经过多个模块(这个模块可能是函数或者系统,或者都有),最终得到请求回复,整个请求按照调用时间和关系串起来就是一个trace。

Span则是组成trace的最基本单元,它一般代表分布式系统中一个独立的工作单元。有点抽象,没关系,后面看一些例子就懂了。一个Span包含如下几部分:

  • 操作名称:一般用于展示、过滤、聚合
  • 开始和结束时间戳:用于计算耗时
  • 由key-value组成的Tags:用于添加一些时间无关的信息(可选)
  • 由key-value组成并包含时间戳的Logs:用于添加一些时间相关的信息(可选)
  • span上下文,即SpanContext。一般包含两部分数据:(1)span的状态数据,比如traceID和spanID(2)Baggage Items。Baggage是链路跟踪提供的一个通用的跨进程/服务传递数据的方式,格式也是key-value形式的。

Trace就是由若干个span组成的有向无环图,图中的每个节点就是Span,连接节点的边称之为References。每个trace有一个唯一标识符traceID,每个span也有一个唯一标识符spanID。一个链路中的所有span的traceID是相同的,但spanID各不相同。一个链路中span典型的调用关系图如下:

Causal relationships between Spans in a single Trace


        [Span A]  ←←←(the root span)
            |
     +------+------+
     |             |
 [Span B]      [Span C] ←←←(Span C is a `ChildOf` Span A)
     |             |
 [Span D]      +---+-------+
               |           |
           [Span E]    [Span F] >>> [Span G] >>> [Span H]
                                       ↑
                                       ↑
                                       ↑
                         (Span G `FollowsFrom` Span F)

对应的时间维度为:

Temporal relationships between Spans in a single Trace


––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time

 [Span A···················································]
   [Span B··············································]
      [Span D··········································]
    [Span C········································]
         [Span E·······]        [Span F··] [Span G··] [Span H··]

其实很简单,就是个调用关系。需要说明的是一个trace的span间有两种可能的关系:

  • ChildOf:即父子关系,也是最常见的关系。比如上图中的Span A和Span B、Span C就是父子关系,表示只有Span B和Span C(包括Span A和Span B的所有子Span)都完成了,Span A才能完成,类似于同步调用(仅仅是类似于,并不完全一样)。
  • FollowsFrom:其实也是父子关系,子Span是由父Span调用产生的,但父Span是否完成不依赖于子Span。比如图中的Span F和Span G就属于FollowsFrom关系。Span G由Span F调用创建,但Span F是否完成不依赖于Span G,有点类似于异步调用。

最后需要介绍的一个概念就是“active span”。一个线程里面可以包含多个span,但同一时刻只能有一个span处于工作状态,这个span称之为ActiveSpan。Span可以有这么几个状态:

  • Started
  • Not Finished
  • Not "active"
  • Active

Span的状态由ScopeManager管理,但是否实现由开发者决定。另外OpenTracing定义了Inject和Extract接口来简化SpanContext跨进程传递。

如果你是第一次了解分布式链路跟踪,看了上面这些,我相信你还是一头雾水,心里有很多疑问。没事,理论结合实践是掌握知识最佳的手段,先对这些概念有个大概理解和印象,然后看下面的几篇实战文章:

说明:这4篇文章内容主要翻译(意译)自Yurishkuro大神的opentracing-tutorial java,加了一些补充说明以及在Jaeger UI上面的展示,方便理解,习惯看英文的也可以看原文,代码自行从GitHub拉取。

通过这几篇文章,对于分布式链路跟踪基本概念和原理应该可以理解的比较好了。后面会介绍一些SDK如何写,以及一些具体的APM。

]]>
2 http://niyanchun.com/opentracing-introduction.html#comments http://niyanchun.com/feed/tag/opentracing/
OpenTracing Java Library教程(4)——Baggage介绍 http://niyanchun.com/opentracing-java-library-tutorial-4.html http://niyanchun.com/opentracing-java-library-tutorial-4.html Sun, 12 Jul 2020 21:59:00 +0800 NYC 本文内容主要翻译(意译)自Yurishkuro大神的opentracing-tutorial java,加了一些补充说明,方便理解,习惯看英文的也可以看原文。总共4篇,本文是第4篇。如果你还没接触过OpenTracing,建议先读这篇文章《OpenTracing概念术语介绍》和官方文档

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

目标

学习:

  • 理解分布式上下文的传递
  • 使用baggage来传递数据

开发流程

在上一节中看到,SpanContext是如何在不同的服务之间传递的。我们可以将这种机制通用化,相当于实现了一个在分布式环境中进行信息传递的通道,这样就可以在服务间传递一些自定义数据了。

这就是OpenTracing定义的Baggage:baggage是SpanContext的一部分,也是以key-value形式存在的。和tags、logs的差别在于,baggage是全局传递的。父Span里面的baggage信息会自动被所有子span继承,这就是所谓的全局性。也就是你能在调用链上任意地方读取到该信息。下面我们看个具体的例子。

在Client中增加Baggage

代码还是在上一篇文章的代码之上修改。我们再增加一个命令行参数,然后将这个参数放到baggage里面。修改Hello的main方法:

public static void main(String[] args) {
    if (args.length != 2) {
        throw new IllegalArgumentException("Expecting two arguments, helloTo and greeting");
    }
    String helloTo = args[0];
    String greeting = args[1];
    Tracer tracer = Tracing.init("hello-world");
    new Hello(tracer).sayHello(helloTo, greeting);
}

然后将第二个参数放到baggage里面:

private void sayHello(String helloTo, String greeting) {
    Span span = tracer.buildSpan("say-hello").start();
    try (Scope scope = tracer.scopeManager().activate(span)) {
        span.setTag("hello-to", helloTo);
        // 把第二个参数信息放到baggage里面
        span.setBaggageItem("greeting", greeting);

        String helloStr = formatString(helloTo);
        printHello(helloStr);
    } finally {
        span.finish();
    }
}

其它地方都不用修改。

在Formatter服务中读取Baggage

@GET
public String format(@QueryParam("helloTo") String helloTo, @Context HttpHeaders httpHeaders) {
    Span span = Tracing.startServerSpan(tracer, httpHeaders, "format");
    try (Scope scope = tracer.scopeManager().activate(span)) {
        // 读取Baggage
        String greeting = span.getBaggageItem("greeting");
        if (greeting == null) {
            greeting = "Hello";
        }
        String helloStr = String.format("%s, %s!", greeting, helloTo);
        span.log(ImmutableMap.of("event", "string-format", "value", helloStr));
        return helloStr;
    } finally {
        span.finish();
    }
}

Baggage注意点

首先,上面实现的功能也可以通过在接口中增加参数来实现。但服务比较多的时候比较麻烦,而且接口改动是一个比较大的改动,一般定了以后不能随意更改。而Baggage是不需要改动接口的,基本对于服务自身是透明的。这也是Baggage的意义所在。这里列举一些Baggage的使用场景:

  • 多租户系统的租户信息
  • 底层调用者的标识信息
  • 混沌工程中传递一些错误注入指令
  • passing request-scoped dimensions for other monitoring data, like separating metrics for prod vs. test traffic

但需要主要的是Baggage是全局传递的,所以数据量不能太大,否则可能会产生性能问题。一些库/框架会在实现层限制这个大小。

总结

Baggage是链路跟踪给出的一个通用的分布式数据传输机制,可以根据场景合理利用。

]]>
0 http://niyanchun.com/opentracing-java-library-tutorial-4.html#comments http://niyanchun.com/feed/tag/opentracing/
OpenTracing Java Library教程(3)——跨服务传递SpanContext http://niyanchun.com/opentracing-java-library-tutorial-3.html http://niyanchun.com/opentracing-java-library-tutorial-3.html Sun, 12 Jul 2020 21:58:00 +0800 NYC 本文内容主要翻译(意译)自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。

]]>
0 http://niyanchun.com/opentracing-java-library-tutorial-3.html#comments http://niyanchun.com/feed/tag/opentracing/
OpenTracing Java Library教程(2)——进程间传递SpanContext http://niyanchun.com/opentracing-java-library-tutorial-2.html http://niyanchun.com/opentracing-java-library-tutorial-2.html Sun, 12 Jul 2020 21:57:00 +0800 NYC 本文内容主要翻译(意译)自Yurishkuro大神的opentracing-tutorial java,加了一些补充说明,方便理解,习惯看英文的也可以看原文。总共4篇,本文是第2篇。如果你还没接触过OpenTracing,建议先读这篇文章《OpenTracing概念术语介绍》和官方文档

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

目标

学习如何:

  • 跟踪不同的函数
  • 创建一个包含多个span的链路
  • 在进程(其实是线程)内部上下文之间传递链路信息

开发步骤

说明:源代码的exercise包下面的类是空的,是留给我们按教程一步步补充完善的;solution包是已经编写好的代码。

本文基于教程1,所以先把把教程1里面solution下面的Hello.java拷贝到lesson02/exercise/Hello.java下面。

跟踪不同的函数

上篇文章的sayHello(String helloTo)方法做了两件事情:格式化输出字符串打印输出。因为在一个方法里面,所以只产生了一个span,为了演示如何产生多个san,这里我们将这个方法拆分成两个单独的函数:

String helloStr = formatString(span, helloTo);
printHello(span, helloStr);

函数体如下:

// 格式化字符串
private String formatString(Span span, String helloTo) {
    String helloStr = String.format("Hello, %s!", helloTo);
    span.log(ImmutableMap.of("event", "string-format", "value", helloStr));
    return helloStr;
}
// 打印输出
private void printHello(Span span, String helloStr) {
    System.out.println(helloStr);
    span.log(ImmutableMap.of("event", "println"));
}

然后我们再给每个函数里面加上span信息:

private  String formatString(Span rootSpan, String helloTo) {
    Span span = tracer.buildSpan("formatString").start();
    try {
        String helloStr = String.format("Hello, %s!", helloTo);
        span.log(ImmutableMap.of("event", "string-format", "value", helloStr));
        return helloStr;
    } finally {
        span.finish();
    }
}

private void printHello(Span rootSpan, String helloStr) {
    Span span = tracer.buildSpan("printHello").start();
    try {
        System.out.println(helloStr);
        span.log(ImmutableMap.of("event", "println"));
    } finally {
        span.finish();
    }
}

运行(这截取了span相关的日志信息):

-> % ./run.sh lesson02.solution.HelloManual Bryan
08:07:28.078 [main] INFO io.jaegertracing.internal.reporters.LoggingReporter - Span reported: 7e2c775eb9a9ac55:7e2c775eb9a9ac55:0:1 - formatString
Hello, Bryan!
08:07:28.087 [main] INFO io.jaegertracing.internal.reporters.LoggingReporter - Span reported: ab294a35b8deb4c3:ab294a35b8deb4c3:0:1 - printHello
08:07:28.087 [main] INFO io.jaegertracing.internal.reporters.LoggingReporter - Span reported: 45dcde709e09015d:45dcde709e09015d:0:1 - say-hello

这里简单介绍一下Span reported:后面的输出,这里有几个使用冒号分隔的ID:

  • 第一个ID代表traceID,唯一标识一个链路;
  • 第二个是spanID,唯一标识一个span;
  • 第三个是当前span的父spanID,如果没有(即自己就是根节点),则为0。

这里的确产生了三个span,但这三个span的TraceID各不一样,也就是说这三个span分别代表一个链路,即我们创建了三个trace,每个trace包含1个span,这显然不是我们想要的,因为这里三个span是有调用关系的,它们最终应该形成一个trace,这个trace包含3个span。

lesson-02-1.png

产生这个结果的原因是tracer.buildSpan("操作名").start();会创建一个新的span,而且默认这个span就是根节点,如果需要将其作为子节点的话,需要使用ChildOf明确指出其父节点:

// formatString
Span span = tracer.buildSpan("formatString").asChildOf(rootSpan).start();
// printHello
Span span = tracer.buildSpan("printHello").asChildOf(rootSpan).start();

修改代码后,重新运行函数:

./run.sh lesson02.solution.HelloManual Bryan
08:20:56.204 [main] INFO io.jaegertracing.internal.reporters.LoggingReporter - Span reported: 836db6b9e1cb6e87:159e8c7d440d171a:836db6b9e1cb6e87:1 - formatString
Hello, Bryan!
08:20:56.209 [main] INFO io.jaegertracing.internal.reporters.LoggingReporter - Span reported: 836db6b9e1cb6e87:149c8f7aeadb5319:836db6b9e1cb6e87:1 - printHello
08:20:56.209 [main] INFO io.jaegertracing.internal.reporters.LoggingReporter - Span reported: 836db6b9e1cb6e87:836db6b9e1cb6e87:0:1 - say-hello

这次可以看到,三个span的traceID是一样的,说明它们三个都属于一个trace,且前两个spanID的父spanID都是最后一个,这个结果是比较符合预期的。我们再看下链路图:

lesson-02-2.png

可以看到只新产生了一个trace,且里面包含了3个span。点开这个trace:

lesson-02-3.png

从这个链路图就可以看出来函数间的调用关系,以及各个调用的耗时。如果点击每个span,还可以查看附加的一些基本信息。

至此,功能基本就完成了。美中不足的是每个函数调用都要传递父span,非常不方便,幸运的是java有thread local变量,可以使用这个机制省略掉同一个线程里面函数间的这个参数传递。代码如下:

import io.opentracing.Scope;

private void sayHello(String helloTo) {
    Span span = tracer.buildSpan("say-hello").start();
    try (Scope scope = tracer.scopeManager().activate(span)) {
        span.setTag("hello-to", helloTo);
        
        String helloStr = formatString(helloTo);
        printHello(helloStr);
    } finally {
        span.finish();
    }
}

private  String formatString(String helloTo) {
    Span span = tracer.buildSpan("formatString").start();
    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();
    }
}

private void printHello(String helloStr) {
    Span span = tracer.buildSpan("printHello").start();
    try (Scope scope = tracer.scopeManager().activate(span)) {
        System.out.println(helloStr);
        span.log(ImmutableMap.of("event", "println"));
    } finally {
        span.finish();
    }
}

代码说明:

  • trace.scopeManager().active(span) 使当前span变为active span(每个线程内只能有一个active span)。当这个span关闭后,上一个span会自动又变为active span。
  • Scope 实现了AutoCloseable接口, 所以我们可以使用try-with-resource语法.
  • 如果当前(当前线程内)已经有一个active span的话,使用buildSpan()创建新span的时候,会自动将当前active span设置为新span的父span。

修改完后运行程序,效果和前面我们自己在函数间传递span信息是一样的。

总结

本文展示了如何在线程内部传递链路信息,下篇文章介绍如何跟踪RPC请求,也就是如何在网络间(跨进程/线程)传递链路信息。

]]>
0 http://niyanchun.com/opentracing-java-library-tutorial-2.html#comments http://niyanchun.com/feed/tag/opentracing/
OpenTracing Java Library教程(1)——trace和span入门 http://niyanchun.com/opentracing-java-library-tutorial-1.html http://niyanchun.com/opentracing-java-library-tutorial-1.html Sun, 12 Jul 2020 21:51:00 +0800 NYC 本文内容主要翻译(意译)自Yurishkuro大神的opentracing-tutorial java,加了一些补充说明,方便理解,习惯看英文的也可以看原文。总共4篇,本文是第1篇。如果你还没接触过OpenTracing,建议先读这篇文章《OpenTracing概念术语介绍》和官方文档

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

目标

学习如何:

  • 实例化一个Tracer
  • 创建一个简单的链路
  • 给链路增加一些注解(annotation):即增加Tag和Log。

开发步骤

说明:源代码的exercise包下面的类是空的,是留给我们按教程一步步补充完善的;solution包是已经编写好的代码。我翻译的时候,都直接运行的是solution里面的代码,但教程里面是逐步完善代码的,也就是会有一个中间状态。所以我会根据内容作了一些必要的注释和修改。但如果你是第一次看的话,建议按照教程自己手动在exercise里面完善。跟着教程一步步学习。

创建一个简单的Hello-World程序

创建一个简单的打印程序:接受一个参数,输出"Hello, {arg}!"。代码如下:

package lesson01.exercise;

public class Hello {

    private void sayHello(String helloTo) {
        String helloStr = String.format("Hello, %s!", helloTo);
        System.out.println(helloStr);
    }

    public static void main(String[] args) {
        if (args.length != 1) {
            throw new IllegalArgumentException("Expecting one argument");
        }
        String helloTo = args[0];
        new Hello().sayHello(helloTo);
    }
}

运行:

$ ./run.sh lesson01.exercise.Hello Bryan
Hello, Bryan!

创建一个trace

一个trace是由若干span组成的有向无环图。一个span代表应用中的一个逻辑操作,每个span至少包含三个属性:操作名(an operation time)、开始时间(start time)、结束时间(finish time)。

下面我们使用一个io.opentracing.Tracer实例创建由一个span组成的trace,可以使用io.opentracing.util.GlobalTracer.get()创建一个全局的Tracer实例。代码如下:

import io.opentracing.Span;
import io.opentracing.Tracer;
import io.opentracing.util.GlobalTracer;

public class Hello {

    private final Tracer tracer;

    private Hello(Tracer tracer) {
        this.tracer = tracer;
    }

    private void sayHello(String helloTo) {
        Span span = tracer.buildSpan("say-hello").start();

        String helloStr = String.format("Hello, %s!", helloTo);
        System.out.println(helloStr);

        span.finish();
    }

    public static void main(String[] args) {
        if (args.length != 1) {
            throw new IllegalArgumentException("Expecting one argument");
        }
        String helloTo = args[0];
        new Hello(GlobalTracer.get()).sayHello(helloTo);
    }
}

这里我们使用了OpenTracing API的一些基本特性:

  • 调用tracer实例的buildSpan()方法创建一个span
  • buildSpan()方法的参数就是span的操作名
  • 调用start()方法真正创建出一个span
  • 通过调用finish()方法结束一个span
  • span的开始时间和结束时间由具体的tracer实现自动生成(获取创建和结束span时的系统时间戳)

此时,我们运行程序并不会和原来的程序有什么区别,也不会产生链路数据。因为OpenTracing只提供了SDK,并没有提供具体的链路实现,所以要产生真正的链路数据,需要借助具体的链路实现。

部署Jaeger(补充段落,原文没有)

这里我们选择Uber开源的Jaeger(发音为\ˈyā-gər\ ),因为它对OpenTracing支持的比较好,而且部署使用也非常简单。另外Jaeger的作者就是Yurishkuro。这里就不介绍Jaeger的细节了,有兴趣的可以去官网了解:Jaeger官网

Jaeger部署非常简单,从这里下载安装包或者下载docker镜像。这里我下载的macOS的安装包,解压后可以看到如下文件:

example-hotrod
jaeger-agent      
jaeger-all-in-one 
jaeger-collector  
jaeger-ingester   
jaeger-query

直接运行./jaeger-all-in-one便可以启动一个完整的Jaeger。此时访问http://localhost:16686/即可查看Jaeger的UI:

Jaeger UI

这样,一个OpenTracing的实现(Jaeger)就有了。接下来我们看如何在代码中集成。

集成Jaeger

在pom.xml中引入Jaeger的依赖:

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

然后写一个创建tracer的函数:

import io.jaegertracing.Configuration;
import io.jaegertracing.Configuration.ReporterConfiguration;
import io.jaegertracing.Configuration.SamplerConfiguration;
import io.jaegertracing.internal.JaegerTracer;

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

最后,修改原来代码中的main函数:

Tracer tracer = initTracer("hello-world");
new Hello(tracer).sayHello(helloTo);

注意我们给initTracer()方法传入了一个参数hello-world,这个是服务名。该服务里面产生的所有span公用这个服务名,一般服务名会用来做过滤和聚合。

现在运行代码,可以看到日志中有输出产生的span信息,而且也能看到Tracer实例的一些信息:

19:07:10.645 [main] DEBUG io.jaegertracing.thrift.internal.senders.ThriftSenderFactory - Using the UDP Sender to send spans to the agent.
19:07:10.729 [main] DEBUG io.jaegertracing.internal.senders.SenderResolver - Using sender UdpSender()
# tracer实例信息
19:07:10.776 [main] INFO io.jaegertracing.Configuration - Initialized tracer=JaegerTracer(version=Java-1.1.0, serviceName=hello-world, reporter=CompositeReporter(reporters=[RemoteReporter(sender=UdpSender(), closeEnqueueTimeout=1000), LoggingReporter(logger=Logger[io.jaegertracing.internal.reporters.LoggingReporter])]), sampler=ConstSampler(decision=true, tags={sampler.type=const, sampler.param=true}), tags={hostname=NYC-MacBook, jaeger.version=Java-1.1.0, ip=192.168.0.109}, zipkinSharedRpcSpan=false, expandExceptionLogs=false, useTraceId128Bit=false)
Hello, Bryan!
# span信息
19:07:10.805 [main] INFO io.jaegertracing.internal.reporters.LoggingReporter - Span reported: a86d76defe28d413:a86d76defe28d413:0:1 - say-hello

当然也可以以调试模式启动,观察更多细节。

这个时候,我们打开Jaeger的UI,左侧的Service选择“hello-world”,然后点击最下面的“Find Traces”,就可以查到刚才这次程序运行产生的Trace信息了:

lesson-01-1.png

点击链路详情进去后,再次点击操作名,可以查看一些基本信息,Jaeger默认已经加了一些基本的信息。

lesson-01-2.png

下面我们来看如何加一些自定义的信息。

增加Tags和Logs

OpenTracing规定了可以给Span增加三种类型的注解信息:

  • Tags:key-value格式的数据,key和value完全由用户自定义。需要注意的是Tags增加的信息应该是属于描述整个span的,也就是它是span的一个静态属性,记录的信息适用于span从创建到完成的任何时刻。再说直白点就是记录和时间点无关的信息,这个主要是和下面的Logs作区分。
  • Logs:和Tags类似,也是key-value格式的数据,区别在于Logs的信息都会带一个时间戳属性,记录这条属性产生的时间戳,所以比较适合记录日志、异常栈等一些和时间相关的信息。
  • Baggage Items:这个主要是用于跨进程全局传输数据,后面的lesson04专门演示这个特性,这里先不展开介绍了。

Tags和Logs的记录非常的简单和方便:

private void sayHello(String helloTo) {
    Span span = tracer.buildSpan("say-hello").start();
    // 增加Tags信息
    span.setTag("hello-to", helloTo);

    String helloStr = String.format("Hello, %s!", helloTo);
    // 增加Logs信息
    span.log(ImmutableMap.of("event", "string-format", "value", helloStr));

    System.out.println(helloStr);
    // 增加Logs信息
    span.log(ImmutableMap.of("event", "println"));

    span.finish();
}

注意这里使用了Guava's ImmutableMap.of()来构造一个Map。

再次运行程序,同样会产生一个span,但这次span会多了一个Tag和Log信息(Jaeger默认已经加了一些内部的tag数据):

lesson-01-3.png

从图中可以看到代码中加的Tags信息和Logs信息,而且Logs信息是带了时间了(这里展示的是从span开始时间经过的毫秒数)。关于Tags和Logs的规范,OpenTracing做了一些引导规范,可以参考:semantic_conventions.

总结

本文主要展示了如何创建一个span,下篇文章演示如何如果创建一个包含多个span的trace,以及如何在进程内部(不同方法间)传递span信息。

]]>
0 http://niyanchun.com/opentracing-java-library-tutorial-1.html#comments http://niyanchun.com/feed/tag/opentracing/