zoukankan      html  css  js  c++  java
  • [业界方案]用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在优秀的三方库协助下,提供了不错的抽象层,使之更有效率,更简单的编写生产用途的微服务。

    • Servlet容器使用Jetty
    • REST/JAX-RS实现使用Jersey
    • JSON序列化使用Jackson
    • 集成Hibernate Validator
    • Guava
    • Metrics
    • SLF4J + Logback
    • 数据访问层上使用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 来存储;

    • 构造函数就是把目前Span暂存,然后把传入的参数Span设置为当前Span。即 将之前活动的scope作为当前scope的属性toRestore来存储,并将当前scope设置到scopeManager中作为当前线程最新的scope。
    • 在操作当span操作完成(span.finish)时,需要调用scope.close方法做恢复,触发关联新的激活span,否则调用链条会出错。

    具体定义如下:

    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
      • asChildOf(scopeManager.activeSpan()); 这里构建了Span之间的关系,即本 span在初始化时就先构建了与之前span的关系。
      • createNewContext() 或者 createChildContext()。如果是root span就随机生成id作为traceId与spanId,如果不是root span则使用reference属性中找到该span的parent span(根据是否为child_of的关系来判断)获取其traceId作为自己的traceId,获取其spanId作为自己的parentId。
    • 调用 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代码阅读思路整理

  • 相关阅读:
    Qt应用如何发布
    关于在windows下部署发布QT程序的总结
    干净地发布QT程序
    解析 Qt 程序在Windows 下发布
    Qt 5.2.0 和 VS 2012集成
    Unable to find a qt build, to solve this problem specify a qt build
    运行python程序不显示cmd的方法
    py2exe使用方法
    python 类
    在Pygtk和Glade使用Gtkbuilder
  • 原文地址:https://www.cnblogs.com/rossiXYZ/p/13654065.html
Copyright © 2011-2022 走看看