10

[业界方案]用Jaeger来学习分布式追踪系统Opentracing

 3 years ago
source link: http://www.cnblogs.com/rossiXYZ/p/13654065.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.

[业界方案]用Jaeger来学习分布式追踪系统Opentracing

目录

  • [业界方案]用Jaeger来学习分布式追踪系统Opentracing

0x00 摘要

笔者之前有过zipkin的经验,希望扩展到Opentracing,于是在学习Jaeger基础上总结出此文,与大家分享。

0x01 缘由 & 问题

1.1 选择Jaeger

JaegerUber 开发的一款调用链服务端产品,开发语言为 golang ,能够兼容接收 OpenTracing 格式的数据。根据其发展历史,可以说是 Zipkin 的升级版。另外,其基于 udp (也可以 http )的传输协议,更加定位了其高效、迅速的特点。

在前文 [业界方案] 用SOFATracer学习分布式追踪系统Opentracing ,我们使用SOFATracer来进行学习,本次我们选择了Jaeger,这又是什么原因?具体如下:

  • Jaeger是Opentracing官方推荐的。
  • Jaeger支持Opentracing高版本。

而且我们正好可以和SOFATracer进行对比印证。

1.2 问题

让我们用问题来引导阅读。

  • Jaeger 和 SOFATracer 对比如何?
  • spanId是怎么生成的,有什么规则?
  • traceId是怎么生成的,有什么规则?
  • 客户端哪里生成的Span?
  • ParentSpan 从哪儿来?
  • ChildSpan由ParentSpan创建,那么什么时候创建?
  • Trace信息怎么传递?
  • 服务器接收到请求之后做什么?
  • SpanContext在服务器端怎么处理?
  • 链路信息如何搜集?

1.3 本文讨论范围

1.3.1 Jaeger构成

Jaeger主要由以下几部分组成:

  1. Jaeger Client: 为了不同语言实现了符合OpenTracing标准的SDK。应用程序通过API写入数据, client library把trace信息按照应用程序制定的采样策略传递给jaeger-agent。
  2. Agent: 他是一个监听在UDP端口上接收span数据的网络守护进程,它会将数据批量发送给collector。他被设计成一个基础组件,部署到所有的宿主机上。Agent将client library和collector解耦,为client library屏蔽了路由和发现collector的细节。
  3. Collector:接收jaeger-agent发送来的数据,然后将数据写入后端存储。Collector被设计成无状态的组件,因此用户可以运行任意数量的Collector。
  4. Data Store:后端存储被设计成一个可插拔的组件,支持数据写入cassandra, elastic search。
  5. Query:接收查询请求,然后从后端存储系统中检索tarce并通过UI进行展示。Query是无状态的,可以启动多个实例。把他们部署在nginx这样的负载均衡器后面。

本文只讨论 Jaeger Client 功能

1.3.2 全链路跟踪

全链路跟踪分成三个跟踪级别:

  • 跨进程跟踪 (cross-process)(调用另一个微服务)
  • 数据库跟踪
  • 进程内部的跟踪 (in-process)(在一个函数内部的跟踪)

本文只讨论 跨进程跟踪 (cross-process) ,因为跨进程跟踪是最简单的 ^_^。对于跨进程跟踪,你可以编写拦截器或过滤器来跟踪每个请求,它只需要编写极少的代码。

0x02 背景知识

因为前文已经对背景知识做了较详细的介绍,本文只是提一下几个必要概念。

分布式追踪系统发展很快,种类繁多,但核心步骤一般有三个: 代码埋点,数据存储、查询展示

在数据采集过程,需要侵入用户代码做埋点,不同系统的API不兼容会导致切换追踪系统需要做很大的改动。为了解决这个问题,诞生了opentracing 规范。

+-------------+  +---------+  +----------+  +------------+
   | Application |  | Library |  |   OSS    |  |  RPC/IPC   |
   |    Code     |  |  Code   |  | Services |  | Frameworks |
   +-------------+  +---------+  +----------+  +------------+
          |              |             |             |
          |              |             |             |
          v              v             v             v
     +-----------------------------------------------------+
     | · · · · · · · · · · OpenTracing · · · · · · · · · · |
     +-----------------------------------------------------+
       |               |                |               |
       |               |                |               |
       v               v                v               v
 +-----------+  +-------------+  +-------------+  +-----------+
 |  Tracing  |  |   Logging   |  |   Metrics   |  |  Tracing  |
 | System A  |  | Framework B |  | Framework C |  | System D  |
 +-----------+  +-------------+  +-------------+  +-----------+

大多数分布式追踪系统的思想模型都来自Google's Dapper论文, OpenTracing 也使用相似的 术语 。有几个基本概念我们需要提前了解清楚:

  • Trace(追踪) :Dapper 将一个调用过程构建成一棵调用树(称为Tracer),Tracer树中的每个节点表示链路调用中的一个模块或系统。 通过一个全局唯一的 traceId 来标识一个请求调用链。 在广义上,一个trace代表了一个事务或者流程在(分布式)系统中的执行过程 。在OpenTracing标准中,trace是多个span组成的一个有向无环图(DAG),每一个span代表trace中被命名并计时的连续性的执行片段。
  • Span(跨度) : 一个span代表系统中具有开始时间和执行时长的逻辑运行单元,即应用中的一个逻辑操作 。span之间通过嵌套或者顺序排列建立逻辑因果关系。一个span可以被理解为一次方法调用,一个程序块的调用,或者一次RPC/数据库访问,只要是一个具有完整时间周期的程序访问,都可以被认为是一个span。 Dapper中 ,一个span 包含以下阶段(不同软件可能有不同的实现 ,比如有的会细分为 Client Span 和 Server Span):
    • Start: 发起调用
    • cleint send(cs): 客户端发送请求
    • Server Recv(sr):服务端收到请求
    • Server Send(ss): 服务端发送响应
    • Client Recv(cr) : 客户端收到服务端响应
    • End: 整个链路完成。
Client                             Server

+--------------+     Request        +--------------+
| Client Send  | +----------------> |Server Receive|
+------+-------+                    +------+-------+
       |                                   |
       |                                   v
       |                            +------+--------+
       |                            |Server Business|
       |                            +------+--------+
       |                                   |
       |                                   |
       v                                   v
+------+--------+    Response       +------+-------+
|Client Receive | <---------------+ |Server Send   |
+------+--------+                   +------+-------+
       |                                   |
       |                                   |
       v                                   v
  • Logs :每个span可以进行多次Logs操作,每一次Logs操作,都需要一个带时间戳的时间名称,以及可选的任意大小的存储结构。比较适合记录日志、异常栈等一些和时间相关的信息。
  • Tags :每个span可以有多个键值对(key :value)形式的Tags,Tags是没有时间戳的,支持简单的对span进行注解和补充。记录的信息适用于span从创建到完成的任何时刻。再说直白点就是记录和时间点无关的信息,这个主要是和下面的Logs作区分。
  • Baggage Items:这个主要是用于跨进程全局传输数据
  • SpanContext : SpanContext 更像是一个“概念”,而不是通用 OpenTracing 层的有用功能。在创建 Span 、向传输协议 Inject (注入)和从传输协议中 Extract (提取)调用链信息时, SpanContext 发挥着重要作用。

0x03 示例代码

3.1 代码

代码全部来自 https://github.com/yurishkuro/opentracing-tutorial ,大家可以自己去下载。

这里的tracer使用的是 JaegerTracer。

public class Hello {

    private final Tracer tracer;
    private final OkHttpClient client;

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

    private String getHttp(int port, String path, String param, String value) {
        try {
            HttpUrl url = new HttpUrl.Builder().scheme("http").host("localhost").port(port).addPathSegment(path)
                    .addQueryParameter(param, value).build();
            Request.Builder requestBuilder = new Request.Builder().url(url);
            
            Span activeSpan = tracer.activeSpan();
            Tags.SPAN_KIND.set(activeSpan, Tags.SPAN_KIND_CLIENT);
            Tags.HTTP_METHOD.set(activeSpan, "GET");
            Tags.HTTP_URL.set(activeSpan, url.toString());
            tracer.inject(activeSpan.context(), Format.Builtin.HTTP_HEADERS, Tracing.requestBuilderCarrier(requestBuilder));

            Request request = requestBuilder.build();
            Response response = client.newCall(request).execute();

            Tags.HTTP_STATUS.set(activeSpan, response.code());
            if (response.code() != 200) {
                throw new RuntimeException("Bad HTTP result: " + response);
            }
            return response.body().string();
        } catch (Exception e) {
            Tags.ERROR.set(tracer.activeSpan(), true);
            tracer.activeSpan().log(ImmutableMap.of(Fields.EVENT, "error", Fields.ERROR_OBJECT, e));
            throw new RuntimeException(e);
        }
    }

    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);
            span.setBaggageItem("greeting", greeting);

            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 = getHttp(8081, "format", "helloTo", 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)) {
            getHttp(8082, "publish", "helloStr", helloStr);
            span.log(ImmutableMap.of("event", "println"));
        } finally {
            span.finish();
        }
    }

    public static void main(String[] args) {
        try (JaegerTracer tracer = Tracing.init("hello-world")) {
            new Hello(tracer).sayHello("helloTo", "greeting");
        }
    }
}

3.2 dropwizard

此处虽然不是SOFATracer和Jaeger的本质区别,但是也挺有趣,即 SOFATracer是使用SprintBoot来做示例代码,而此处是使用dropwizard来做示例

可能有人对dropwizard不熟悉,现在大致讲解如下:

  • Dropwizard是 Coda HaleYammer 公司时创立的,它旨在提升公司分布式系统的架构(现在叫:微服务)。虽然它最早被用来构建REST Web 服务,而现在它具备了越来越多的功能,但是它的目标始终是作为轻量化、为生产环境准备且容易使用的web框架。
  • Dropwizard与Spring Boot类似,也是构建微服务可选的工具,但是它显得比Spring Boot更加规范一些。它使用的组件一般不会做可选替换,而好处是可以不需要那么多的修饰,比如写基于REST的web服务。
  • Dropwizard默认也不具备依赖注入的容器(像Spring或者CDI),你当然可以自行添加,但是Dropwizard推荐你把微服务弄的简单一些,不需要这些额外的组件。
  • 就像Spring Boot一样,Dropwizard推荐将整个工程打包成一个可执行的jar,通过这种方式开发人员不用在担心程序运行的应用服务器是什么,需要什么额外的配置,应用再也不需要被构建成war包了,而且也不会有那么多复杂层级的类加载器了。

Dropwizard在优秀的三方库协助下,提供了不错的抽象层,使之更有效率,更简单的编写生产用途的微服务。

Jetty
Jersey
Jackson
Hibernate Validator
JDBI

Dropwizard偏执的认为框架就是用来写代码的,因此对于框架的底层技术栈的调整,原则上Dropwizard是拒绝的。正因为它这么做,使得Dropwizard开发起代码来更快,而且配置更加容易。

对于我们的示例代码,对Dropwizard使用举例如下,即使用 Dropwizard 建立了两个服务和一个测试client。

io.dropwizard.Application

public class Formatter extends Application<Configuration> {

    private final Tracer tracer;

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

    @Path("/format")
    @Produces(MediaType.TEXT_PLAIN)
    public class FormatterResource {

        @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)) {
                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();
            }
        }
    }

    @Override
    public void run(Configuration configuration, Environment environment) throws Exception {
        environment.jersey().register(new FormatterResource());
    }

    public static void main(String[] args) throws Exception {
        System.setProperty("dw.server.applicationConnectors[0].port", "8081");
        System.setProperty("dw.server.adminConnectors[0].port", "9081");
        try (JaegerTracer tracer = Tracing.init("formatter")) {
            new Formatter(tracer).run("server");
        }
    }
}

0x04 链路逻辑

对于一个组件来说,一次处理过程一般是产生一个 Span;这个 Span 的生命周期是从接收到请求到返回响应这段过程。

这里需要考虑的问题是如何与上下游链路关联起来呢?在 Opentracing 规范中,可以在 Tracer 中 extract 出一个跨进程传递的 SpanContext 。然后通过这个 SpanContext 所携带的信息将当前节点关联到整个 Tracer 链路中去,当然有提取(extract)就会有对应的注入(inject)。

链路的构建一般是 client-server-client-server 这种模式的,那这里就很清楚了,就是会在 client 端进行注入(inject),然后再 server 端进行提取(extract),反复进行,然后一直传递下去。

在拿到 SpanContext 之后,此时当前的 Span 就可以关联到这条链路中了,那么剩余的事情就是收集当前组件的一些数据;整个过程大概分为以下几个阶段:

  • 从请求中提取 spanContext
  • 构建 Span,并将当前 Span 存入当前 tracer上下文中(SofaTraceContext.push(Span)) 。
  • 设置一些信息到 Span 中
  • 返回响应
  • Span 结束&上报

0x05 数据模型

5.1 Tracer & JaegerTracer

Jaeger中的Tracer控制了一个完整的服务的追踪,包括注册服务名(serviceName),发送span(reporter),采样(sampler),对span的序列化与反序列化以及传输(registry的injector,extractor),统计追踪系统的信息(metrics,如发送span成功数量等)。

因此opentracing建议每个服务使用一个Tracer,除此之外Tracer还担负构造span,获取当前span以及获取scopeManager的功能。

通过opentracing的规范亦可以看出,opentracing对Tracer的功能描述为:Tracer is a simple, thin interface for Span creation and propagation across arbitrary transports。而jaeger只是在其基础上增加了其他功能。

Tracer是opentracing给出的接口。

package io.opentracing;
public interface Tracer extends Closeable {
    ScopeManager scopeManager();
    Span activeSpan();
    Scope activateSpan(Span var1);
    Tracer.SpanBuilder buildSpan(String var1);
    <C> void inject(SpanContext var1, Format<C> var2, C var3);
    <C> SpanContext extract(Format<C> var1, C var2);
    void close();
}

JaegerTracer 实现了 io.opentracing.Tracer。

public class JaegerTracer implements Tracer, Closeable {
    private final String version;
    private final String serviceName;
    private final Reporter reporter;
    private final Sampler sampler;
    private final Map<String, ?> tags;
    private final boolean zipkinSharedRpcSpan;
    private final boolean expandExceptionLogs;
    private final boolean useTraceId128Bit;
    private final PropagationRegistry registry;
    private final Clock clock;
    private final Metrics metrics;
    private final ScopeManager scopeManager;
    private final BaggageSetter baggageSetter;
    private final JaegerObjectFactory objectFactory;
    private final int ipv4;
}

5.2 Span & JaegerSpan

io.opentracing.Span 是 Opentracing 给出的概念。

public interface Span {
    SpanContext context();
    Span setTag(String var1, String var2);
    Span setTag(String var1, boolean var2);
    Span setTag(String var1, Number var2);
    <T> Span setTag(Tag<T> var1, T var2);
    Span setBaggageItem(String var1, String var2);
    String getBaggageItem(String var1);
    Span setOperationName(String var1);
    void finish();
    void finish(long var1);
}

JaegerSpan 实现了 io.opentracing.SPan。

public class JaegerSpan implements Span {
  private final JaegerTracer tracer;
  private final long startTimeMicroseconds;
  private final long startTimeNanoTicks;
  private final boolean computeDurationViaNanoTicks;
  private final Map<String, Object> tags;
  private long durationMicroseconds; // span durationMicroseconds
  private String operationName;
  private final List<Reference> references;
  private JaegerSpanContext context;
  private List<LogData> logs;
  private boolean finished = false; // to prevent the same span from getting reported multiple times
}

在jaeger的实现中, Span 的信息分为如下几方面:

  • span核心信息,如:traceId,spanId,parentId,baggage等
  • log信息 与tag的区别是带有时间戳
  • tag信息
  • span的其他信息,如:startTime,duration

其中span的核心信息存储在 SpanContext

5.3 SpanContext & JaegerSpanContext

JaegerSpanContext 实现了 io.opentracing.SpanContext

public interface SpanContext {
    String toTraceId();
    String toSpanId();
    Iterable<Entry<String, String>> baggageItems();
}
public class JaegerSpanContext implements SpanContext {
  protected static final byte flagSampled = 1;
  protected static final byte flagDebug = 2;
  private final long traceIdLow;
  private final long traceIdHigh;
  private final long spanId;
  private final long parentId;
  private final byte flags;
  private final Map<String, String> baggage;
  private final String debugId;
  private final JaegerObjectFactory objectFactory;
  private final String traceIdAsString;
  private final String spanIdAsString;
}

span的核心信息存储在 SpanContext 中,在构建span时候就会创建,为了防止用户擅自修改核心信息,spanContext中的所有成员都是final修饰的。

根据opentracing的规范, SpanContext

represents Span state that must propagate to descendant Spans and across process boundaries. SpanContext is logically divided into two pieces:

(1) the user-level "Baggage" that propagates across Span boundaries and

(2) any Tracer-implementation-specific fields that are needed to identify or otherwise contextualize the associated Span instance (e.g., a tuple).

上面是说SpanContext代表的是span中必须传递的信息,在逻辑上分为两部分,一分部分是普通的traceId,spanId等信息,另一部分是baggage这种用户自定义需要传递的信息。

JaegerSpanContext这里只是保存了上下文环境应有的信息,与 SofaTraceContext 不同,SofaTraceContext 里面还存有Span,但是在 Jaeger,这个功能是在 ScopeManager中完成的

5.4 Reporter

默认的 RemoteReporter 实现了 Reporter,功能就是我们在前文中所说的发送报告。

public class RemoteReporter implements Reporter {
  private static final int DEFAULT_CLOSE_ENQUEUE_TIMEOUT_MILLIS = 1000;
  public static final int DEFAULT_FLUSH_INTERVAL_MS = 1000;
  public static final int DEFAULT_MAX_QUEUE_SIZE = 100;

  private final Sender sender;
  private final int closeEnqueueTimeout;

  @ToString.Exclude private final BlockingQueue<Command> commandQueue;
  @ToString.Exclude private final Timer flushTimer;
  @ToString.Exclude private final Thread queueProcessorThread;
  @ToString.Exclude private final QueueProcessor queueProcessor;
  @ToString.Exclude private final Metrics metrics;
}

5.5 Scope

OpenTracing 抽象了 Scope (active span) 和 ScopeManager (设置Scope与获取当前Scope)概念。简单来说,OpenTracing-Java的实现中, 用 ScopeScopeManager 来处理了OpenTracing中的上下文 (即:get_current_span 过程);

为什么要抽象出Scope的概念?直接使用ThreadLocal 存储Span不就可以了吗?

答: 首先理解Scope是什么?Scope 是Active Span的一个容器, Scope 代表着当前活跃的Span; 是对当前活跃Span的一个抽象, 代表了当前上下文所处于的一个过程;

另外, ThreadLocalScope 还记录了 toRestore Span , 这样结束时,可以恢复到上一个Span的状态;

我理解如果只是 get_current_span() 逻辑的话,直接把 span 塞到 ThreadLocal里就可以在线程内传递了;但是ScopeManager看代码是这样实现的,ScopeManager 包含一个 Scope, Scope 又包含了 当前Span, recover Scope;我理解它的好处是: 这样就保证了,如果开启一个子Span(子span 会产生孙子span), 这样 子span 结束后,还可以回到 父span (这样可以继续产生以 父span 为基础的兄弟span), 如果只是ThreadLocal 里塞一个当前span的话,是解决不了这种情况的。

或者说

在多线程环境下 ScopeManager 管理着各个线程的 Scope ,而每个线程中的 Scope 管理着该线程中的 Span 。这样当某个线程需要获取其线程中当前 活动的 span时,可以通过 ScopeManager 找到对应该线程的 Scope ,并从 Scope 中取出该线程 活动的 span。

Scope 对象是 Active Span的容器;通过Scope能拿到当前上下文内的Active Span;

io.opentracing.util.ThreadLocalScope是Scope的一个实现,通过ThreadLocal 来存储;

toRestore

具体定义如下:

public class ThreadLocalScope implements Scope {
    private final ThreadLocalScopeManager scopeManager;
    private final Span wrapped; // 当前 Active Span
    private final ThreadLocalScope toRestore; // 上一Active Span,wrapped 结束时,会恢复到此Span

    ThreadLocalScope(ThreadLocalScopeManager scopeManager, Span wrapped) {
        this.scopeManager = scopeManager;
        this.wrapped = wrapped;
        // 这两句设置了当前活动Scope
        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;
    }
}

5.6 ScopeManager

Scope是站在CPU角度激活或者失效Span。ScopeManager管理Scope。一个Scope里可以有多个span,但是只有一个激活的span。

在多线程环境下 ScopeManager 管理着各个线程的 Scope ,而每个线程中的 Scope 管理着该线程中的 Span 。这样当某个线程需要获取其线程中当前 活动的 span时,可以通过 ScopeManager 找到对应该线程的 Scope ,并从 Scope 中取出该线程 活动的 span。

有了ScopeManager, 我们就可以通过 scopeManager.activeSpan() 方法获取到当前Span, 并且通过 scopeManager().activate(span) 方法设置当前上下文 active span ;

io.opentracing.util.ThreadLocalScopeManager是opentracing提供的ScopeManager的实现,Jaeger并没有自己重写一个新类,而是直接使用ThreadLocalScopeManager。

  • activate 函数的作用是 激活当前 Span。返回Scope(可以理解为 代表当前 Span 活跃的一个阶段)。即调用 ThreadLocalScope 的构造方法,将传入的span激活为 当前活动的 span。我们看一下ThreadLocalScope构造函数就能发现,与其说是激活传入的span倒不如说是激活 包裹(wrapped)该span的scope当前活动的 scope。

    Span 活跃期结束后,需要关闭 Scope, 推荐使用 try-with-resources 关闭。

  • activeSpan函数则是返回当前 激活(active)状态Span, 无则返回null。

public class ThreadLocalScopeManager implements ScopeManager {
    // 使用原始的ThreadLocal 来存储 Active Span; ScopeManager中仅包含一个 Scope( Active Span), 即当前上下文中的 active span
    final ThreadLocal<ThreadLocalScope> tlsScope = new ThreadLocal<ThreadLocalScope>();

    // 可以看到,activate 函数就是把span放进一个新生成的 ThreadLocalScope 中,其实就是tlsScope 成员变量中。
    @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();
    }
}

Jaeger使用scopeManager来处理管理了上下文,可以从 scopeManager中拿到当前上下文Span;那具体是在哪里设置的父子关系呢?

在OpenTracing-Java实现中, 是在 tracer.start() 方法中处理的; start() 方法中通过 scopeManager 判断是存在active span,若存在则生成CHILD_OF关系的上下文, 如果不存在则createNewContext;

这点和SOFATtacer不同,SOFATtacer把这个上下文管理功能放在了SofaTraceContext之中,确实在分析代码时候感到有些许混乱。

5.7 SpanID & TraceID

SpanId 和 TraceID 都是在构建SpanContext 时候生成的。

private JaegerSpanContext createNewContext() {
  String debugId = getDebugId();
  long spanId = Utils.uniqueId();  // span
  long traceIdLow = spanId;  // trace
  long traceIdHigh = isUseTraceId128Bit() ? Utils.uniqueId() : 0;
	......
}

具体规则如下:

public static long uniqueId() {
  long val = 0;
  while (val == 0) {
    val = Java6CompatibleThreadLocalRandom.current().nextLong();
  }
  return val;
}

然后是调用到了ThreadLocalRandom # current。

public static Random current() {
  if (threadLocalRandomPresent) {
    return ThreadLocalRandomAccessor.getCurrentThreadLocalRandom();
  } else {
    return threadLocal.get();
  }
}

static class ThreadLocalRandomAccessor {
    @IgnoreJRERequirement
    private static Random getCurrentThreadLocalRandom() {
      return ThreadLocalRandom.current();
    }
}

最后格式如下:

context = {JaegerSpanContext@1701} "c29c9e0f4a0a681c:36217443515fc248:c29c9e0f4a0a681c:1"
 traceIdLow = -4423486945480775652
 traceIdHigh = 0
 spanId = 3900526584756421192
 parentId = -4423486945480775652
 flags = 1
 baggage = {HashMap@1693}  size = 1
 debugId = null
 objectFactory = {JaegerObjectFactory@1673} 
 traceIdAsString = "c29c9e0f4a0a681c"
 spanIdAsString = "36217443515fc248"

0x06 启动

6.1 手动埋点

要通过Jaeger将Java应用数据上报至链路追踪控制台,首先需要完成埋点工作。 本示例为手动埋点

6.2 pom配置

pom.xml中添加了对Jaeger客户端的依赖。

<dependency>
    <groupId>io.jaegertracing</groupId>
    <artifactId>jaeger-client</artifactId>
    <version>${jaeger.version}</version>
</dependency>

6.3 启动

示例代码并没有使用注入的组件,而是手动启动,具体启动/初始化代码如下:

public final class Tracing {
    private Tracing() { }
    
    public static JaegerTracer init(String service) {
        SamplerConfiguration samplerConfig = SamplerConfiguration.fromEnv()
                .withType(ConstSampler.TYPE)
                .withParam(1);

        ReporterConfiguration reporterConfig = ReporterConfiguration.fromEnv()
                .withLogSpans(true);

        // 这里启动
        Configuration config = new Configuration(service)
                .withSampler(samplerConfig)
                .withReporter(reporterConfig);

        return config.getTracer();
    }
}

示例中启动的 io.dropwizard.Application 都会调用init进行初始化。

try (JaegerTracer tracer = Tracing.init("publisher")) {
    new Publisher(tracer).run("server");
}

具体启动逻辑都是在 io.jaegertracing.Configuration 中完成的。我们可以看到 其中实现了众多配置和一个tracer

6.4 构建Tracer

上节代码中有 config.getTracer(); ,这就是 jaeger采用builder模式来构建 Tracer

public class Configuration {
    private String serviceName;
    private Configuration.SamplerConfiguration samplerConfig;
    private Configuration.ReporterConfiguration reporterConfig;
    private Configuration.CodecConfiguration codecConfig;
    private MetricsFactory metricsFactory;
    private Map<String, String> tracerTags;
    private boolean useTraceId128Bit;
    private JaegerTracer tracer;
  
    public synchronized JaegerTracer getTracer() {
      if (tracer != null) {
        return tracer;
      }

      tracer = getTracerBuilder().build(); // 构建
      return tracer;
    }
  
    ......
}

build() 方法最终完成了 Tracer 对象的构造。

  • 默认使用 RemoteReporter 来report Span 到agent,
  • 采样默认使用 RemoteControlledSampler
  • 共同使用的 metrics 是在Builder内部类中的有默认值的成员变量 metrics
public JaegerTracer build() {
  if (reporter == null) {
    reporter = new RemoteReporter.Builder()
        .withMetrics(metrics)
        .build();
  }
  if (sampler == null) {
    sampler = new RemoteControlledSampler.Builder(serviceName)
        .withMetrics(metrics)
        .build();
  }
  return createTracer();
}

protected JaegerTracer createTracer() {
      return new JaegerTracer(this);
}

Tracer对象可以用来创建Span对象以便记录分布式操作时间、通过Extract/Inject方法跨机器透传数据、或设置当前Span。Tracer对象还配置了上报数据的网关地址、本机IP地址、采样率、服务名等数据。用户可以通过调整采样率来减少因上报数据产生的开销。

在启动之后,用户得到 Tracer 来进行后续手动埋点。

JaegerTracer tracer = Tracing.init("hello-world")

0x07 客户端发送

下面都是手动埋点。

7.1 构建Span

构造 Span 对象是一件很简单的事情,通过opentracing对 Tracer 接口的规定可知 Span 是由 Tracer 负责构造的,如下我们“启动”了一个 Span (实际上只是构造了该对象而已):

Span span = tracer.buildSpan("printHello").start();

Tracer中的start方法( 开启一个Span ) 使用了scopeManager 来获取上下文,从而来处理父子关系;

public JaegerSpan start() {
      // 此处从ScopeManager获取上下文(线程)中,获取到激活的Span, 而后创建父子关系
      if (this.references.isEmpty() && !this.ignoreActiveSpan && null != JaegerTracer.this.scopeManager.activeSpan()) {
                this.asChildOf(JaegerTracer.this.scopeManager.activeSpan());
      }

      JaegerSpanContext context;
      if (!this.references.isEmpty() && ((Reference)this.references.get(0)).getSpanContext().hasTrace()) {
                context = this.createChildContext();
      } else {
                context = this.createNewContext();
      }
      ...
      return jaegerSpan;
}

7.2 Parent Span

本示例中会涉及到两个Span:Parent Span 和 Child Span。我们首先介绍 Parent Span。

其大致策略是:

  • 调用 tracer.buildSpan("say-hello").start() 生成Span
    reference
    
  • 调用 tracer.scopeManager().activate 函数就是把span放进一个新生成的 ThreadLocalScope 中,其实就是 tlsScope 成员变量中。 结果是后续可以通过tracer.scopeManager.activeSpan();获取span信息。
  • setTag
  • setBaggageItem
  • 最后finish

具体代码如下:

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);
            span.setBaggageItem("greeting", greeting);
            String helloStr = formatString(helloTo);
            printHello(helloStr);
        } finally {
            span.finish();
        }
}

得到的运行时Span如下:

span = {JaegerSpan@1685} 
 startTimeMicroseconds = 1598707136698000
 startTimeNanoTicks = 1018098763618500
 computeDurationViaNanoTicks = true
 tags = {HashMap@1700}  size = 2
 durationMicroseconds = 0
 operationName = "say-hello"
 references = {ArrayList@1701}  size = 0
 context = {JaegerSpanContext@1666} "c8b87cc5fb01ef31:c8b87cc5fb01ef31:0:1"
  traceIdLow = -3983296680647594191
  traceIdHigh = 0
  spanId = -3983296680647594191
  parentId = 0
  flags = 1
  baggage = {Collections$EmptyMap@1704}  size = 0
  debugId = null
  objectFactory = {JaegerObjectFactory@994} 
  traceIdAsString = "c8b87cc5fb01ef31"
  spanIdAsString = "c8b87cc5fb01ef31"
 logs = null
 finished = false

7.3 Child Span

示例代码然后在 formatString 中会:

  • 生成一个子 Span
  • 加入了Tag
  • 调用Inject方法传入Context信息。
  • 并且会调用http请求。

具体代码如下:

private String getHttp(int port, String path, String param, String value) {
		HttpUrl url = new HttpUrl.Builder().scheme("http").host("localhost").port(port).addPathSegment(path)
                    .addQueryParameter(param, value).build();
		Request.Builder requestBuilder = new Request.Builder().url(url);
  
  	Span activeSpan = tracer.activeSpan();

    Tags.SPAN_KIND.set(activeSpan, Tags.SPAN_KIND_CLIENT);
    Tags.HTTP_METHOD.set(activeSpan, "GET");
    Tags.HTTP_URL.set(activeSpan, url.toString());

    tracer.inject(activeSpan.context(), Format.Builtin.HTTP_HEADERS, 
                  Tracing.requestBuilderCarrier(requestBuilder));

    Request request = requestBuilder.build();
    Response response = client.newCall(request).execute();
}

7.4 Inject

上文中的 tracer.inject 函数,是用来把 SpanContext 的信息序列化到 Request.Builder 之中。这样后续操作就可以把序列化之后的信息转换到 Header之中。

tracer.inject(activeSpan.context(), Format.Builtin.HTTP_HEADERS, 
              Tracing.requestBuilderCarrier(requestBuilder));

具体序列化代码如下:

public void inject(JaegerSpanContext spanContext, TextMap carrier) {
  carrier.put(contextKey, encodedValue(contextAsString(spanContext)));
  for (Map.Entry<String, String> entry : spanContext.baggageItems()) {
    carrier.put(keys.prefixedKey(entry.getKey(), baggagePrefix), encodedValue(entry.getValue()));
  }
}

7.5 Finish

当服务端返回之后,在Client端,jaeger会进行后续操作:finish,report

调用 span.finish() 方法标志着span的结束。finish方法应该是对应span实例的最后一个调用的方法。在span中finish方法还只是校验和记录的作用,真正发送span的就是开头提到的tracer,tracer包含了sampler、report等全局的功能,因此在finish中调用了 tracer.report(span) 方法。而tracer中的report方法是使用其成员 report 的report方法,上面讲过默认实现是 RemoteReporter ,它默认使用的是 UdpSender

span.finish会触发span上报。调用了 JaegerSpan.finishWithDuration。其中会判断本次Trace是否采样。如果是采样了,就会上报。

@Override
  public void finish(long finishMicros) {
    finishWithDuration(finishMicros - startTimeMicroseconds);
  }

  private void finishWithDuration(long durationMicros) {
    synchronized (this) {
      if (finished) {
        log.warn("Span has already been finished; will not be reported again.");
        return;
      }
      finished = true;

      this.durationMicroseconds = durationMicros;
    }

    if (context.isSampled()) {
      tracer.reportSpan(this);
    }
  }

7.6 Reporter

上报是在 RemoteReporter 中。

RemoteReporter 中有一个 BlockingQueue 队列其作用是接收Command接口的实现类,其长度可在构造方法中传入。在 RemoteReporter 的构造函数中开启了两个守护线程。一个线程定时往 BlockingQueue 队列中添加flush命令,另外一个线程不停的从 BlockingQueue 队列中take数据,然后执行Command.excute()方法。而report(span)方法就是往 BlockingQueue 队列中添加 AppendCommand 类。

@Override
  public void report(JaegerSpan span) {
    // Its better to drop spans, than to block here
    boolean added = commandQueue.offer(new AppendCommand(span));

    if (!added) {
      metrics.reporterDropped.inc(1);
    }
  }

可以看到如果返回的added变量为false,也就是队列满了无法再加入数据,就会抛弃该span的,最终该span的信息不会发送到agent中。因此队列的长度也是有一定的影响。

AppendCommand 类的excute()方法为:

class AppendCommand implements Command {
    private final Span span;

    public AppendCommand(Span span) {
      this.span = span;
    }

    @Override
    public void execute() throws SenderException {
      sender.append(span);
    }
  }

所以,我们看到,execute()方法并不是真正的发送span了,而只是把span添加到sender中去,由sender实现span的发送,reporter类只负责发送刷新与发送的命令。

如果我们继续深入下去,会发现 UdpSender 是抽象类 ThriftSender 的实现类, sender.append(span) 方法调用的是 ThriftSenderappend(Span) 方法,而该方法又会调用 ThriftSenderflush() 方法,最后这个 flush() 方法会调用抽象类 ThriftSender 的抽象方法 send(Process process, List spans)

Jaeger中其他Reporter如下 :

  • CompositeReporter 顾名思义就是将各个reporter组合起来,内部有一个list,它所实现的接口的 report(Span span) 方法也只是把list中的所有reporter依次调用 report(Span span) 方法而已。
  • InMemoryReporter 类是将 Span 存到内存中,该类含有一个list用于存储span,该类中的report方法即为将span通过add方法添加到list中,通过 getSpans() 方法获取到list,同时有 clear() 方法清除list数据。
  • LoggingReporter 类作用是将span作为日志内容打印出来,其report方法即为 log.info() 打印span的内容。
  • NoopReporter 是一个实现了 Reporter 接口但是实现方法为空的一个类,表示使用该类report span将毫无影响。

0x08 服务端接受

8.1 手动埋点

服务端也是手动埋点。

public class FormatterResource {
    @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)) {
            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();
        }
    }
}

8.2 业务逻辑

业务逻辑在 startServerSpan 之中:

  • 调用Extract方法解析Context信息。
  • 根据是否有Parent Context 来进行Span构建,其中会用到SpanContext。

具体代码如下:

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);
        }
        // TODO could add more tags like http.url
        return spanBuilder.withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER).start();
}

8.3 解析Context

解析代码如下:

public JaegerSpanContext extract(TextMap carrier) {
  JaegerSpanContext context = null;
  Map<String, String> baggage = null;
  String debugId = null;
  for (Map.Entry<String, String> entry : carrier) {
    // TODO there should be no lower-case here
    String key = entry.getKey().toLowerCase(Locale.ROOT);
    if (key.equals(contextKey)) {
      context = contextFromString(decodedValue(entry.getValue()));
    } else if (key.equals(Constants.DEBUG_ID_HEADER_KEY)) {
      debugId = decodedValue(entry.getValue());
    } else if (key.startsWith(baggagePrefix)) {
      if (baggage == null) {
        baggage = new HashMap<String, String>();
      }
      baggage.put(keys.unprefixedKey(key, baggagePrefix), decodedValue(entry.getValue()));
    } else if (key.equals(Constants.BAGGAGE_HEADER_KEY)) {
      baggage = parseBaggageHeader(decodedValue(entry.getValue()), baggage);
    }
  }
  if (debugId == null && baggage == null) {
    return context;
  }
  return objectFactory.createSpanContext(
    context == null ? 0L : context.getTraceIdHigh(),
    context == null ? 0L : context.getTraceIdLow(),
    context == null ? 0L : context.getSpanId(),
    context == null ? 0L : context.getParentId(),
    context == null ? (byte)0 : context.getFlags(),
    baggage,
    debugId);
}

0x09 问题解答

  • Jaeger 和 SOFATracer 对比如何?

    • Jaeger对OpenTracing支持的更完备,版本更高。
  • spanId是怎么生成的,有什么规则?

  • traceId是怎么生成的,有什么规则?

    • 最终都是调用到 ThreadLocalRandom # current # nextLong 完成,举例如下:

    • traceIdLow = -4423486945480775652
       traceIdHigh = 0
       spanId = 3900526584756421192
       parentId = -4423486945480775652
  • 客户端哪里生成的Span?

    • 本示例代码是手动调用 tracer.buildSpan("say-hello").start() 生成Span。
  • ParentSpan 从哪儿来?

    • 在 客户端发送阶段,先从 scopeManager.activeSpan 获取当前活动span。如果不为空,则需要给新span设置父亲Span。

      • if (references.isEmpty() && !ignoreActiveSpan && null != scopeManager.activeSpan()) {
          asChildOf(scopeManager.activeSpan());
        }
  • ChildSpan由ParentSpan创建,那么什么时候创建?

    • 在OpenTracing-Java实现中, 是在 tracer.start() 方法中处理的; start() 方法中通过 scopeManager 判断是存在active span ,若存在则生成CHILD_OF关系的上下文, 如果不存在则createNewContext;
  • Trace信息怎么传递?

    • 把 SpanContext 的信息序列化到 Request.Builder 之中。后续操作把序列化之后的信息转换到 Header之中,然后就可以传递。
  • 服务器接收到请求之后做什么?

    • 调用Extract方法解析Context信息。
    • 根据是否有Parent Context 来进行Span构建,其中会用到SpanContext。
    • 进行具体其他业务。
  • SpanContext在服务器端怎么处理?见上问题回答。

  • 链路信息如何搜集?

    • 采样是对于整条链路来说的,也就是说从 RootSpan 被创建开始,就已经决定了当前链路数据是否会被记录了。
    • 如果已经确定本次Trace被采样,就会发送报告。

0xFF 参考

分布式追踪系统 -- Opentracing

开放分布式追踪(OpenTracing)入门与 Jaeger 实现

OpenTracing 语义说明

分布式追踪系统概述及主流开源系统对比

Skywalking分布式追踪与监控:起始篇

分布式全链路监控 -- opentracing小试

opentracing实战

Go微服务全链路跟踪详解

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

OpenTracing Java Library教程(1)——trace和span入门

蚂蚁金服分布式链路跟踪组件 SOFATracer 总览|剖析

蚂蚁金服开源分布式链路跟踪组件 SOFATracer 链路透传原理与SLF4J MDC 的扩展能力剖析

蚂蚁金服开源分布式链路跟踪组件 SOFATracer 采样策略和源码剖析

https://github.com/sofastack-guides/sofa-tracer-guides

The OpenTracing Semantic Specification

OpenTracing Java Library教程(2)——进程间传递SpanContext

OpenTracing Java Library教程(4)——Baggage介绍

https://github.com/yurishkuro/opentracing-tutorial

微服务系统架构之分布式traceId追踪参考实现

监控之traceid

jaeger代码阅读思路整理

分布式系统中如何优雅地追踪日志(原理篇)traceid

sky-walking的traceId生成

分布式链路追踪系列番外篇一(jaeger异步批量发送span)

分布式链路追踪系列番外篇二(Spark Job优化记)

Jaeger服务端埋点分析

通过Jaeger上报Java应用数据

OpenTracing(Jaeger) 遭遇多线程

OpenTracing-Java Scope与ScopeManager

OpenTracing-Java实现的灵魂十问

OpenTracing实现思路(附OpenTracing-Jaeger-Java实例)

OpenTracing API 自动埋点调研

Jaeger服务端埋点分析

OpenTracing(Jaeger) 遭遇多线程

jaegeropentracing的Java-client完整分布式追踪链

基于opentracing + jaeger 实现全链路追踪

jaeger代码阅读思路整理


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK