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

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

0x00 摘要

SOFA是蚂蚁金服自主研发的金融级分布式中间件,包含了构建金融级云原生架构所需的各个组件,SOFATracer 是其中用于分布式系统调用跟踪的组件。html

笔者以前有过zipkin的经验,但愿扩展到Opentracing,因而在学习SOFATracer官方博客结合源码的基础上总结出此文,与你们分享。java

0x01 原因 & 问题

1.1 选择

为何选择了从SOFATracer入手来学习?理由很简单:有大公司背书(是在金融场景里锤炼出来的最佳实践),有开发者和社区整理的官方博客,有直播,示例简便易调试,为何不研究使用呢?git

1.2 问题

让咱们用问题来引导阅读。github

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

1.3 本文讨论范围

全链路跟踪分红三个跟踪级别:web

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

本文只讨论 跨进程跟踪 (cross-process),由于跨进程跟踪是最简单的 ,容易上手^_^。对于跨进程跟踪,你能够编写拦截器或过滤器来跟踪每一个请求,它只须要编写极少的代码。spring

0x02 背景知识

2.1 趋势和挑战

容器、Serverless 编程方式的诞生极大提高了软件交付与部署的效率。在架构的演化过程当中,能够看到两个变化:数据库

  • 应用架构开始从单体系统逐步转变为微服务,其中的业务逻辑随之而来就会变成微服务之间的调用与请求。
  • 资源角度来看,传统服务器这个物理单位也逐渐淡化,变成了看不见摸不到的虚拟资源模式。

从以上两个变化能够看到这种弹性、标准化的架构背后,原先运维与诊断的需求也变得愈来愈复杂。如何理清服务依赖调用关系、如何在这样的环境下快速 debug、追踪服务处理耗时、查找服务性能瓶颈、合理对服务的容量评估都变成一个棘手的事情。编程

2.2 可观察性(Observability)

为了应对这些问题,可观察性(Observability) 这个概念被引入软件领域。传统的监控和报警主要关注系统的异常状况和失败因素,可观察性更关注的是从系统自身出发,去展示系统的运行情况,更像是一种对系统的自我审视。一个可观察的系统中更关注应用自己的状态,而不是所处的机器或者网络这样的间接证据。咱们但愿直接获得应用当前的吞吐和延迟信息,为了达到这个目的,咱们就须要合理主动暴露更多应用运行信息。在当前的应用开发环境下,面对复杂系统咱们的关注将逐渐由点 到 点线面体的结合,这能让咱们更好的理解系统,不只知道What,更能回答Why。后端

可观察性目前主要包含如下三大支柱:api

  • 日志(Logging) : Logging 主要记录一些离散的事件,应用每每经过将定义好格式的日志信息输出到文件,而后用日志收集程序收集起来用于分析和聚合。虽然能够用时间将全部日志点事件串联起来,可是却很难展现完整的调用关系路径;
  • 度量(Metrics) :Metric 每每是一些聚合的信息,相比 Logging 丧失了一些具体信息,可是占用的空间要比完整日志小的多,能够用于监控和报警,在这方面 Prometheus 已经基本上成为了事实上的标准;
  • 分布式追踪(Tracing) : Tracing 介于 LoggingMetric 之间, 以请求的维度来串联服务间的调用关系并记录调用耗时,即保留了必要的信息,又将分散的日志事件经过 Span 串联,帮助咱们更好的理解系统的行为、辅助调试和排查性能问题。

三大支柱有以下特色:

  • Metric的特色是,它是可累加的。具备原子性,每一个都是一个逻辑计量单元,或者一个时间段内的柱状图。 例如:队列的当前深度能够被定义为一个计量单元,在写入或读取时被更新统计; 输入HTTP请求的数量能够被定义为一个计数器,用于简单累加;请求的执行时间能够被定义为一个柱状图,在指定时间片上更新和统计汇总。
  • Logging的特色是,它描述一些离散的(不连续的)事件。 例如:应用经过一个滚动的文件输出debug或error信息,并经过日志收集系统,存储到Elasticsearch中;审批明细信息经过Kafka,存储到数据库(BigTable)中; 又或者,特定请求的元数据信息,从服务请求中剥离出来,发送给一个异常收集服务,如NewRelic。
  • Tracing的最大特色就是,它在单次请求的范围内处理信息。 任何的数据、元数据信息都被绑定到系统中的单个事务上。 例如:一次调用远程服务的RPC执行过程;一次实际的SQL查询语句;一次HTTP请求的业务性ID。

2.3 Tracing

分布式追踪,也称为分布式请求追踪,是一种用于分析和监视应用程序的方法,特别是那些使用微服务体系结构构建的应用程序;分布式追踪有助于查明故障发生的位置以及致使性能低下的缘由,开发人员可使用分布式跟踪来帮助调试和优化他们的代码,IT和DevOps团队可使用分布式追踪来监视应用程序。

2.3.1 Tracing 的诞生

Tracing 是在90年代就已出现的技术。但真正让该领域流行起来的仍是源于 Google 的一篇论文”Dapper, a Large-Scale Distributed Systems Tracing Infrastructure”,而另外一篇论文”Uncertainty in Aggregate Estimates from Sampled Distributed Traces”中则包含关于采样的更详细分析。论文发表后一批优秀的 Tracing 软件孕育而生。

2.3.2 Tracing的功能

  • 故障定位——能够看到请求的完整路径,相比离散的日志,更方便定位问题(因为真实线上环境会设置采样率,能够利用debug开关实现对特定请求的全采样);
  • 依赖梳理——基于调用关系生成服务依赖图;
  • 性能分析和优化——能够方便的记录统计系统链路上不一样处理单元的耗时占用和占比;
  • 容量规划与评估;
  • 配合LoggingMetric强化监控和报警。

2.4 OpenTracing

为了解决不一样的分布式追踪系统 API 不兼容的问题,出现了OpenTracing。OpenTracing旨在标准化Trace数据结构和格式,其目的是:

  • 不一样语言开发的Trace客户端的互操做性。Java/.Net/PHP/Python/NodeJs等语言开发的客户端,只要遵循OpenTracing标准,就均可以对接OpenTracing兼容的监控后端。
  • Tracing监控后端的互操做性。只要遵循OpenTracing标准,企业能够根据须要替换具体的Tracing监控后端产品,好比从Zipkin替换成Jaeger/CAT/Skywalking等后端。

OpenTracing不是一个标准,OpenTracing API提供了一个标准的、与供应商无关的框架,是对分布式链路中涉及到的一些列操做的高度抽象集合。这意味着若是开发者想要尝试一种不一样的分布式追踪系统,开发者只须要简单地修改Tracer配置便可,而不须要替换整个分布式追踪系统。

0x03 OpenTracing 数据模型

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

  • Trace(追踪) :在广义上,一个trace表明了一个事务或者流程在(分布式)系统中的执行过程。在OpenTracing标准中,trace是多个span组成的一个有向无环图(DAG),每个span表明trace中被命名并计时的连续性的执行片断。

  • Span(跨度) :一个span表明系统中具备开始时间和执行时长的逻辑运行单元,即应用中的一个逻辑操做。span之间经过嵌套或者顺序排列创建逻辑因果关系。一个span能够被理解为一次方法调用,一个程序块的调用,或者一次RPC/数据库访问,只要是一个具备完整时间周期的程序访问,均可以被认为是一个span。

  • Logs :每一个span能够进行屡次Logs操做,每一次Logs操做,都须要一个带时间戳的时间名称,以及可选的任意大小的存储结构。

  • Tags :每一个span能够有多个键值对(key :value)形式的Tags,Tags是没有时间戳的,支持简单的对span进行注解和补充。

  • SpanContext :SpanContext更像是一个“概念”,而不是通用 OpenTracing 层的有用功能。在建立Span、向传输协议Inject(注入)和从传输协议中Extract(提取)调用链信息时,SpanContext发挥着重要做用。

3.1 Span

表示分布式调用链条中的一个调用单元,他的边界包含一个请求进到服务内部再由某种途径(http/dubbo等)从当前服务出去。

一个span通常会记录这个调用单元内部的一些信息,例如每一个Span包含的操做名称、开始和结束时间、附加额外信息的Span Tag、可用于记录Span内特殊事件Span Log、用于传递Span上下文的SpanContext和定义Span之间关系的References

  • Operation 的 名字(An operation name)
  • 开始时间 (A start timestamp)
  • 结束时间 (A finish timestamp)
  • 标签信息 :0个或多个以 keys:values 为形式组成的 Span Tags。 key 必须是 string, values 则能够是 strings, bool,numeric types
  • 日志信息 :0个或多个 Span logs
  • 一个 SpanContext
  • 经过 SpanContext 能够指向 0个 或者多个 因果相关的 Span

3.2 Tracer

Trace 描述在分布式系统中的一次"事务"。一个trace是由若干span组成的有向无环图

Tracer 用于建立Span,并理解如何跨进程边界注入(序列化)和提取(反序列化)Span。它有如下的职责:

  1. 创建和开启一个span
  2. 从某种媒介中提取/注入一个spanContext

用图论的观点来看的话,traces 能够被认为是 spans 的 DAG。也就是说,多个 spans 造成的 DAG 是一个 Traces。

举例来讲,下图是一个由八个 Spans 造成的一个 Trace。

单个 Trace 中 Span 之间的因果关系


        [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)

某些时候, 用时间顺序来具象化更让人理解。下面就是一个例子。

单个 Trace 中 Spans 之间的时间关系

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

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

3.3 References between Spans

一个span能够和一个或者多个span间存在因果关系。OpenTracing定义了两种关系:ChildOf 和 FollowsFrom。这两种引用类型表明了子节点和父节点间的直接因果关系。

ChildOf 将成为当前 Span 的 child,而 FollowsFrom则会成为 parent。 这两种关系为 child spanparent span 创建了直接因果关系。

3.4 SpanContext

表示一个span对应的上下文,span和spanContext基本上是一一对应的关系,这个SpanContext能够经过某些媒介和方式传递给调用链的下游来作一些处理(例如子Span的id生成、信息的继承打印日志等等)。

上下文存储的是一些须要跨越边界的(传播跟踪所需的)一些信息,例如:

  • spanId :当前这个span的id
  • traceId :这个span所属的traceId(也就是此次调用链的惟一id)。
    • trace_idspan_id 用以区分Trace中的Span;任何 OpenTraceing 实现相关的状态(好比 trace 和 span id)都须要被一个跨进程的 Span 所联系。
  • baggage :其余的能过跨越多个调用单元的信息,即跨进程的 key value 对。Baggage ItemsSpan Tag 结构相同,惟一的区别是:Span Tag只在当前Span中存在,并不在整个trace中传递,而Baggage Items 会随调用链传递。

SpanContext数据结构简化版以下:

SpanContext:
- trace_id: "abc123"
- span_id: "xyz789
- Baggage Items:
	- special_id: "vsid1738"

在跨界(跨服务或者协议)传输过程当中实现调用关系的传递和关联,须要可以将 SpanContext 向下游介质注入,并在下游传输介质中提取 SpanContext

每每可使用协议自己提供的相似HTTP Headers的机制实现这样的信息传递,像Kafka这样的消息中间件也有提供实现这样功能的Headers机制。

OpenTracing 实现,可使用 api 中提供的 Tracer.Inject(...) 和 Tracer.Extract(...) 方便的实现 SpanContext的注入和提取。

  • “extarct()”从媒介(一般是HTTP头)获取跟踪上下文。
  • “inject()”将跟踪上下文放入媒介,来保证跟踪链的连续性。

3.5 Carrier

Carrier 表示的是一个承载spanContext的媒介,比方说在http调用场景中会有HttpCarrier,在dubbo调用场景中也会有对应的DubboCarrier。

3.6 Formatter

这个接口负责了具体场景中序列化反序列化上下文的具体逻辑,例如在HttpCarrier使用中一般就会有一个对应的HttpFormatter。Tracer的注入和提取就是委托给了Formatter。

3.7 ScopeManager

这个类是0.30版本以后新加入的组件,这个组件的做用是可以经过它获取当前线程中启用的Span信息,而且能够启用一些处于未启用状态的span。在一些场景中,咱们在一个线程中可能同时创建多个span,可是同一时间同一线程只会有一个span在启用,其余的span可能处在下列的状态中:

  1. 等待子span完成
  2. 等待某种阻塞方法
  3. 建立可是并未开始

3.8 Reporter

除了上述组件以外,在实现一个分布式全链路监控框架的时候,还须要有一个reporter组件,经过它来打印或者上报一些关键链路信息(例如span建立和结束),只有把这些信息进行处理以后咱们才能对全链路信息进行可视化和真正的监控。

0x04 SOFATracer

SOFATracer 是一个用于分布式系统调用跟踪的组件,经过统一的 traceId 将调用链路中的各类网络调用状况以日志的方式记录下来,以达到透视化网络调用的目的。这些日志可用于故障的快速发现,服务治理等。

SOFATracer 团队已经为咱们搭建了一个完整的 Tracer 框架内核,包括数据模型、编码器、跨进程透传 traceId、采样、日志落盘与上报等核心机制,并提供了扩展 API 及基于开源组件实现的部分插件,为咱们基于该框架打造本身的 Tracer 平台提供了极大便利。

SOFATracer 目前并无提供数据采集器和 UI 展现的功能;主要有两个方面的考虑:

  • SOFATracer 做为 SOFA 体系中一个很是轻量的组件,意在将 span 数据以日志的方式落到磁盘,以便于用户可以更加灵活的来处理这些数据
  • UI 展现方面,SOFATracer 自己基于 OpenTracing 规范实现,在模型上与开源的一些产品能够实现无缝对接,在必定程度上能够弥补自己在链路可视化方面的不足。

所以在上报模型上,SOFATracer 提供了日志输出和外部上报的扩展,方便接入方可以足够灵活的方式来处理上报的数据。经过SOFARPC + SOFATracer + zipKin 能够快速搭建一套完整的链路追踪系统,包括埋点、收集、分析展现等。 收集和分析主要是借用zipKin的能力。

目前 SOFATracer 已经支持了对如下开源组件的埋点支持:Spring MVC、RestTemplate、HttpClient、OkHttp三、JDBC、Dubbo(2.6⁄2.7)、SOFARPC、Redis、MongoDB、Spring Message、Spring Cloud Stream (基于 Spring Message 的埋点)、RocketMQ、Spring Cloud FeignClient、Hystrix。

Opentracing 中将全部核心的组件都声明为接口,例如 TracerSpanSpanContextFormat(高版本中还包括 ScopeScopeManager)等。SOFATracer 使用的版本是 0.22.0 ,主要是对 TracerSpanSpanContext 三个概念模型的实现。下面就针对几个组件结合 SOFATracer 来分析。

4.1 Tracer & SofaTracer

Tracer 是一个简单、广义的接口,它的做用就是构建 span 和传输 span

SofaTracer 实现了 io.opentracing.Tracer 接口,并扩展了采样、数据上报等能力。

public class SofaTracer implements Tracer {
    public static final String ROOT_SPAN_ID = "0";
    private final String tracerType;
    private final Reporter clientReporter;
    private final Reporter serverReporter;
    private final Map<String, Object> tracerTags = new ConcurrentHashMap();
    private final Sampler sampler;
}

4.2 Span & SofaTracerSpan

Span 是一个跨度单元,在实际的应用过程当中,Span 就是一个完整的数据包,其包含的就是当前节点所须要上报的数据。

SofaTracerSpan 实现了 io.opentracing.Span 接口,并扩展了对 Referencetags、线程异步处理以及插件扩展中所必须的 logType和产生当前 spanTracer类型等处理的能力。

每一个span 包含两个重要的信息 span id(当前模块的span id)和 span parent ID(上一个调用模块的span id),经过这两个信息能够定位一个span 在调用链的位置。 这些属于核心信息,存储在SpanContext

public class SofaTracerSpan implements Span {
    public static final char                                ARRAY_SEPARATOR      = '|';
    private final SofaTracer                                sofaTracer;
    private final List<SofaTracerSpanReferenceRelationship> spanReferences;
    /** tags for String  */
    private final Map<String, String>                       tagsWithStr          = new LinkedHashMap<>();
    /** tags for Boolean */
    private final Map<String, Boolean>                      tagsWithBool         = new LinkedHashMap<>();
    /** tags for Number  */
    private final Map<String, Number>                       tagsWithNumber       = new LinkedHashMap<>();
    private final List<LogData>                             logs                 = new LinkedList<>();
    private String                                          operationName        = StringUtils.EMPTY_STRING;
    private final SofaTracerSpanContext                     sofaTracerSpanContext;
    private long                                            startTime;
    private long                                            endTime              = -1;
}

在SOFARPC中分为 ClientSpan 和ServerSpan。 ClientSpan记录从客户端发送请求给服务端,到接受到服务端响应结果的过程。ServerSpan是服务端收到客户端时间 到 发送响应结果给客户端的这段过程。

4.3 SpanContext & SofaTracerSpanContext

SpanContext 对于 OpenTracing 实现是相当重要的,经过 SpanContext 能够实现跨进程的链路透传,而且能够经过 SpanContext 中携带的信息将整个链路串联起来。

官方文档中有这样一句话:“在 OpenTracing 中,咱们强迫 SpanContext 实例成为不可变的,以免 Spanfinishreference 操做时会有复杂的生命周期问题。” 这里是能够理解的,若是 SpanContext 在透传过程当中发生了变化,好比改了 tracerId,那么就可能致使链路出现断缺。

SofaTracerSpanContext 实现了 SpanContext 接口,扩展了构建 SpanContext、序列化 baggageItems 以及SpanContext等新的能力。

public interface SofaTraceContext {
    void push(SofaTracerSpan var1);
    SofaTracerSpan getCurrentSpan();
    SofaTracerSpan pop();
    int getThreadLocalSpanSize();
    void clear();
    boolean isEmpty();
}

4.3.1 传递Trace信息

本小节回答了 Trace信息怎么传递?

OpenTracing之中是经过SpanContext来传递Trace信息。

SpanContext存储的是一些须要跨越边界的一些信息,好比trace Id,span id,Baggage。这些信息会不一样组件根据本身的特色序列化进行传递,好比序列化到 http header 之中再进行传递。而后经过这个 SpanContext 所携带的信息将当前节点关联到整个 Tracer 链路中去。

简单来讲就是使用HTTP头做为媒介(Carrier)来传递跟踪信息(traceID)。不管微服务是gRPC仍是RESTFul,它们都使用HTTP协议。若是是消息队列(Message Queue),则将跟踪信息(traceID)放入消息报头中。

SofaTracerSpanContext 类就包括而且实现了 “一些须要跨越边界的一些信息” 。

public class SofaTracerSpanContext implements SpanContext {

    //spanId separator
    public static final String        RPC_ID_SEPARATOR       = ".";

    //======= The following is the key for serializing data ========================

    private static final String       TRACE_ID_KET           = "tcid";

    private static final String       SPAN_ID_KET            = "spid";

    private static final String       PARENT_SPAN_ID_KET     = "pspid";

    private static final String       SAMPLE_KET             = "sample";

    /**
     * The serialization system transparently passes the prefix of the attribute key
     */
    private static final String       SYS_BAGGAGE_PREFIX_KEY = "_sys_";

    private String                    traceId                = StringUtils.EMPTY_STRING;

    private String                    spanId                 = StringUtils.EMPTY_STRING;

    private String                    parentId               = StringUtils.EMPTY_STRING;

    /**
     * Default will not be sampled
     */
    private boolean                   isSampled              = false;

    /**
     * The system transparently transmits data,
     * mainly refers to the transparent transmission data of the system dimension.
     * Note that this field cannot be used for transparent transmission of business.
     */
    private final Map<String, String> sysBaggage             = new ConcurrentHashMap<String, String>();

    /**
     * Transparent transmission of data, mainly refers to the transparent transmission data of the business
     */
    private final Map<String, String> bizBaggage             = new ConcurrentHashMap<String, String>();

    /**
     * sub-context counter
     */
    private AtomicInteger             childContextIndex      = new AtomicInteger(0);
}

4.3.2 线程存储

在链路环节每一个节点中,SpanContext 都是线程相关,具体都存储在线程ThreadLocal之中。

实现是 SofaTracerThreadLocalTraceContext 函数。咱们能够看到使用了 ThreadLocal,这是由于Context是和线程上下文相关的。

public class SofaTracerThreadLocalTraceContext implements SofaTraceContext {
    private final ThreadLocal<SofaTracerSpan> threadLocal = new ThreadLocal();

    public void push(SofaTracerSpan span) {
        if (span != null) {
            this.threadLocal.set(span);
        }
    }

    public SofaTracerSpan getCurrentSpan() throws EmptyStackException {
        return this.isEmpty() ? null : (SofaTracerSpan)this.threadLocal.get();
    }

    public SofaTracerSpan pop() throws EmptyStackException {
        if (this.isEmpty()) {
            return null;
        } else {
            SofaTracerSpan sofaTracerSpan = (SofaTracerSpan)this.threadLocal.get();
            this.clear();
            return sofaTracerSpan;
        }
    }

    public int getThreadLocalSpanSize() {
        SofaTracerSpan sofaTracerSpan = (SofaTracerSpan)this.threadLocal.get();
        return sofaTracerSpan == null ? 0 : 1;
    }

    public boolean isEmpty() {
        SofaTracerSpan sofaTracerSpan = (SofaTracerSpan)this.threadLocal.get();
        return sofaTracerSpan == null;
    }

    public void clear() {
        this.threadLocal.remove();
    }
}

4.4 Reporter

日志落盘又分为摘要日志落盘 和 统计日志落盘;

  • 摘要日志是每一次调用均会落地磁盘的日志;
  • 统计日志是每隔必定时间间隔进行统计输出的日志。

数据上报是 SofaTracer 基于 OpenTracing Tracer 接口扩展实现出来的功能;Reporter 实例做为 SofaTracer 的属性存在,在构造 SofaTracer 实例时,会初始化 Reporter 实例。

Reporter 接口的设计中除了核心的上报功能外,还提供了获取 Reporter 类型的能力,这个是由于 SOFATracer 目前提供的埋点机制方案须要依赖这个实现。

public interface Reporter {
    String REMOTE_REPORTER = "REMOTE_REPORTER";
    String COMPOSITE_REPORTER = "COMPOSITE_REPORTER";

    //获取 Reporter 实例类型
    String getReporterType();
    //输出 span
    void report(SofaTracerSpan span);
    //关闭输出 span 的能力
    void close();
}

Reporter 的实现类有两个,SofaTracerCompositeDigestReporterImpl 和 DiskReporterImpl :

  • SofaTracerCompositeDigestReporterImpl:组合摘要日志上报实现,上报时会遍历当前 SofaTracerCompositeDigestReporterImpl 中全部的 Reporter ,逐一执行 report 操做;可供外部用户扩展使用。
  • DiskReporterImpl:数据落磁盘的核心实现类,也是目前 SOFATracer 中默认使用的上报器。

0x05 示例代码

5.1 RestTemplate

咱们使用的是 RestTemplate 示例

import com.sofa.alipay.tracer.plugins.rest.SofaTracerRestTemplateBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.ResponseEntity;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.web.client.AsyncRestTemplate;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
public class RestTemplateDemoApplication {
    private static Logger logger = LoggerFactory.getLogger(RestTemplateDemoApplication.class);

    public static void main(String[] args) throws Exception {
        SpringApplication.run(RestTemplateDemoApplication.class, args);
        RestTemplate restTemplate = SofaTracerRestTemplateBuilder.buildRestTemplate();
        ResponseEntity<String> responseEntity = restTemplate.getForEntity(
            "http://localhost:8801/rest", String.class);
        logger.info("Response is {}", responseEntity.getBody());

        AsyncRestTemplate asyncRestTemplate = SofaTracerRestTemplateBuilder
            .buildAsyncRestTemplate();
        ListenableFuture<ResponseEntity<String>> forEntity = asyncRestTemplate.getForEntity(
            "http://localhost:8801/asyncrest", String.class);
        //async
        logger.info("Async Response is {}", forEntity.get().getBody());

        logger.info("test finish .......");
    }
}

0x06 启动

这里首先要提一下SOFATracer 的埋点机制,不一样组件有不一样的应用场景和扩展点,所以对插件的实现也要因地制宜,SOFATracer 埋点方式通常是经过 Filter、Interceptor 机制实现的。因此下面咱们提到的Client启动 / Server 启动就主要是建立了 Filter、Interceptor 机制。

咱们就以 RestTemplate 为例看看SofaTracer的启动。

6.1 Spring SPI

代码中只用到 SofaTracerRestTemplateBuilder,怎么就可以作到一个完整的链路跟踪?原来机密在pom.xml文件之中。

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alipay.sofa</groupId>
            <artifactId>tracer-sofa-boot-starter</artifactId>
        </dependency>
</dependencies>

在tracer-sofa-boot-starter 的 spring.factories 文件中,定义了不少类。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alipay.sofa.tracer.boot.configuration.SofaTracerAutoConfiguration,\
com.alipay.sofa.tracer.boot.springmvc.configuration.OpenTracingSpringMvcAutoConfiguration,\
com.alipay.sofa.tracer.boot.zipkin.configuration.ZipkinSofaTracerAutoConfiguration,\
com.alipay.sofa.tracer.boot.datasource.configuration.SofaTracerDataSourceAutoConfiguration,\
com.alipay.sofa.tracer.boot.springcloud.configuration.SofaTracerFeignClientAutoConfiguration,\
com.alipay.sofa.tracer.boot.flexible.configuration.TracerAnnotationConfiguration,\
com.alipay.sofa.tracer.boot.resttemplate.SofaTracerRestTemplateConfiguration
org.springframework.context.ApplicationListener=com.alipay.sofa.tracer.boot.listener.SofaTracerConfigurationListener

Spring Boot中有一种很是解耦的扩展机制:Spring Factories。这种扩展机制其实是仿照Java中的SPI扩展机制来实现的。

SPI的全名为Service Provider Interface,这是一种服务发现机制,为某个接口寻找服务实现。可让模块装配时候能够动态指明服务。有点相似IOC的思想,就是将装配的控制权移到程序以外。

Spring Factories是在META-INF/spring.factories文件中配置接口的实现类名称,而后在程序中读取这些配置文件并实例化。这种自定义的SPI机制是Spring Boot Starter实现的基础。

对于 SpringBoot 工程来讲,引入 tracer-sofa-boot-starter 以后,Spring程序直接读取了 tracer-sofa-boot-starter 的 spring.factories 文件中的类而且实例化。用户就能够在程序中直接使用不少SOFA的功能

以Reporter为例。自动配置类 SofaTracerAutoConfiguration 会将当前全部 SpanReportListener 类型的 bean 实例保存到 SpanReportListenerHolder 的 List 对象中。而SpanReportListener 类型的 Bean 会在 ZipkinSofaTracerAutoConfiguration 自动配置类中注入到当前 Ioc 容器中。这样 invokeReportListeners 被调用时,就能够拿到 zipkin 的上报类,从而就能够实现上报。

对于非 SpringBoot 应用的上报支持,本质上是须要实例化 ZipkinSofaTracerSpanRemoteReporter 对象,并将此对象放在 SpanReportListenerHolder 的 List 对象中。因此 SOFATracer 在 zipkin 插件中提供了一个ZipkinReportRegisterBean,并经过实现 Spring 提供的 bean 生命周期接口 InitializingBean,在ZipkinReportRegisterBean 初始化以后构建一个 ZipkinSofaTracerSpanRemoteReporter 实例,并交给SpanReportListenerHolder 类管理。

6.2 Client启动

这部分代码是 SofaTracerRestTemplateConfiguration。主要做用是生成一个 RestTemplateInterceptor。

RestTemplateInterceptor 的做用是在请求以前能够先一步作处理

首先 SofaTracerRestTemplateConfiguration 的做用是生成一个 SofaTracerRestTemplateEnhance。

@Configuration
@ConditionalOnWebApplication
@ConditionalOnProperty(prefix = "com.alipay.sofa.tracer.resttemplate", value = "enable", matchIfMissing = true)
public class SofaTracerRestTemplateConfiguration {

    @Bean
    public SofaTracerRestTemplateBeanPostProcessor sofaTracerRestTemplateBeanPostProcessor() {
        return new SofaTracerRestTemplateBeanPostProcessor(sofaTracerRestTemplateEnhance());
    }

    @Bean
    public SofaTracerRestTemplateEnhance sofaTracerRestTemplateEnhance() {
        return new SofaTracerRestTemplateEnhance();
    }
}

其次,SofaTracerRestTemplateEnhance 会生成一个 RestTemplateInterceptor,这样就能够在请求以前作处理

public class SofaTracerRestTemplateEnhance {

    private final RestTemplateInterceptor restTemplateInterceptor;

    public SofaTracerRestTemplateEnhance() {
        AbstractTracer restTemplateTracer = SofaTracerRestTemplateBuilder.getRestTemplateTracer();
        this.restTemplateInterceptor = new RestTemplateInterceptor(restTemplateTracer);
    }

    public void enhanceRestTemplateWithSofaTracer(RestTemplate restTemplate) {
        // check interceptor
        if (checkRestTemplateInterceptor(restTemplate)) {
            return;
        }
        List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>(
            restTemplate.getInterceptors());
        interceptors.add(0, this.restTemplateInterceptor);
        restTemplate.setInterceptors(interceptors);
    }

    private boolean checkRestTemplateInterceptor(RestTemplate restTemplate) {
        for (ClientHttpRequestInterceptor interceptor : restTemplate.getInterceptors()) {
            if (interceptor instanceof RestTemplateInterceptor) {
                return true;
            }
        }
        return false;
    }
}

6.3 服务端启动

这部分代码是 OpenTracingSpringMvcAutoConfiguration。主要做用是注册了 SpringMvcSofaTracerFilter。Spring Filter 用来对某个 Servlet 程序进行拦截处理时,它能够决定是否将请求继续传递给 Servlet 程序,以及对请求和响应消息是否进行修改

@Configuration
@EnableConfigurationProperties({ OpenTracingSpringMvcProperties.class, SofaTracerProperties.class })
@ConditionalOnWebApplication
@ConditionalOnProperty(prefix = "com.alipay.sofa.tracer.springmvc", value = "enable", matchIfMissing = true)
@AutoConfigureAfter(SofaTracerAutoConfiguration.class)
public class OpenTracingSpringMvcAutoConfiguration {

    @Autowired
    private OpenTracingSpringMvcProperties openTracingSpringProperties;

    @Configuration
    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
    public class SpringMvcDelegatingFilterProxyConfiguration {
        @Bean
        public FilterRegistrationBean springMvcDelegatingFilterProxy() {
            FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
            SpringMvcSofaTracerFilter filter = new SpringMvcSofaTracerFilter();
            filterRegistrationBean.setFilter(filter);
            List<String> urlPatterns = openTracingSpringProperties.getUrlPatterns();
            if (urlPatterns == null || urlPatterns.size() <= 0) {
                filterRegistrationBean.addUrlPatterns("/*");
            } else {
                filterRegistrationBean.setUrlPatterns(urlPatterns);
            }
            filterRegistrationBean.setName(filter.getFilterName());
            filterRegistrationBean.setAsyncSupported(true);
            filterRegistrationBean.setOrder(openTracingSpringProperties.getFilterOrder());
            return filterRegistrationBean;
        }
    }

    @Configuration
    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
    public class WebfluxSofaTracerFilterConfiguration {
        @Bean
        @Order(Ordered.HIGHEST_PRECEDENCE + 10)
        public WebFilter webfluxSofaTracerFilter() {
            return new WebfluxSofaTracerFilter();
        }
    }
}

0x07 SOFATracer 的插件埋点机制

对一个应用的跟踪要关注的无非就是 客户端--->web 层--->rpc 服务--->dao 后端存储、cache 缓存、消息队列 mq 等这些基础组件。SOFATracer 插件的做用实际上也就是对不一样组件进行埋点,以便基于这些组件采集应用的链路数据。

不一样组件有不一样的应用场景和扩展点,所以对插件的实现也要因地制宜,SOFATracer 埋点方式通常是经过 Filter、Interceptor 机制实现的。

7.1 组件扩展入口之 Filter or Interceptor

SOFATracer 目前已实现的插件中,像 SpringMVC 插件是基于 Filter 进行埋点的,httpclient、resttemplate 等是基于 Interceptor 机制进行埋点的。在实现插件时,要根据不一样插件的特性和扩展点来选择具体的埋点方式。正所谓条条大路通罗马,无论怎么实现埋点,都是依赖 SOFATracer 自身 API 的扩展机制来实现。

SOFATracer 中全部的插件均须要实现本身的 Tracer 实例,如 SpringMVC 的 SpringMvcTracer 、HttpClient 的 HttpClientTracer 等。

AbstractTracer 是 SOFATracer 用于插件扩展使用的一个抽象类,根据插件类型不一样,又能够分为 clientTracer 和 serverTracer,分别对应于 AbstractClientTracer 和 AbstractServerTracer;再经过 AbstractClientTracer 和 AbstractServerTracer 衍生出具体的组件 Tracer 实现,好比上图中提到的 HttpClientTracer 、RestTemplateTracer 、SpringMvcTracer 等插件 Tracer 实现。

如何肯定一个组件是 client 端仍是 server 端呢?就是看当前组件是请求的发起方仍是请求的接受方,若是是请求发起方则通常是 client 端,若是是请求接收方则是 server 端。那么对于 RPC 来讲,便是请求的发起方也是请求的接受方,所以这里实现了 AbstractTracer 类。

7.2 插件扩展基本思路总结

对于一个组件来讲,一次处理过程通常是产生一个 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 结束&上报

7.3 标准 Servlet 规范埋点原理

SOFATracer 支持对标准 Servlet 规范的 Web MVC 埋点,包括普通的 Servlet 和 Spring MVC 等,基本原理就是基于 Servelt 规范所提供的 javax.servlet.Filter 过滤器接口扩展实现。

过滤器位于 Client 和 Web 应用程序之间,用于检查和修改二者之间流过的请求和响应信息。在请求到达 Servlet 以前,过滤器截获请求。在响应送给客户端以前,过滤器截获响应。多个过滤器造成一个 FilterChain,FilterChain 中不一样过滤器的前后顺序由部署文件 web.xml 中过滤器映射的顺序决定。最早截获客户端请求的过滤器将最后截获 Servlet 的响应信息。

Web 应用程序通常做为请求的接收方,在 SOFATracer 中应用是做为 Server 存在的,其在解析 SpanContext 时所对应的事件为 sr (server receive)。

SOFATracer 在 sofa-tracer-springmvc-plugin 插件中解析及产生 Span 的过程大体以下:

  • Servlet Filter 拦截到 request 请求;
  • 从请求中解析 SpanContext;
  • 经过 SpanContext 构建当前 MVC 的 Span;
  • 给当前 Span 设置 tag、log;
  • 在 Filter 处理的最后,结束 Span;

7.4 HTTP 客户端埋点原理

HTTP 客户端埋点包括 HttpClient、OkHttp、RestTemplate 等,此类埋点通常都是基于拦截器机制来实现的,如 HttpClient 使用的 HttpRequestInterceptor、HttpResponseInterceptor;OkHttp 使用的 okhttp3.Interceptor;RestTemplate 使用的 ClientHttpRequestInterceptor。

以 OkHttp 为例,简单分析下 HTTP 客户端埋点的实现原理:

@Override
public Response intercept(Chain chain) throws IOException {
    // 获取请求
    Request request = chain.request();
    // 解析出 SpanContext ,而后构建 Span
    SofaTracerSpan sofaTracerSpan = okHttpTracer.clientSend(request.method());
    // 发起具体的调用
    Response response = chain.proceed(appendOkHttpRequestSpanTags(request, sofaTracerSpan));
    // 结束 span
    okHttpTracer.clientReceive(String.valueOf(response.code()));
    return response;
}

0x08 请求整体过程

在 SOFATracer 中将请求大体分为如下几个过程:

  • 客户端发送请求 clientSend cs
  • 服务端接受请求 serverReceive sr
  • 服务端返回结果 serverSend ss
  • 客户端接受结果 clientReceive cr

不管是哪一个插件,在请求处理周期内均可以从上述几个阶段中找到对应的处理方法。所以,SOFATracer 对这几个阶段处理进行了封装。

在SOFA这里,四个阶段实际上会产生两个 Span,第一个 Span 的起点是 cs,到 cr 结束;第二个 Span 是从 sr 开始,到 ss 结束。

clientSend // 客户端发送请求,也就是 cs 阶段,会产生一个 Span。
    serverReceive // 服务端接收请求 sr 阶段,产生了一个 Span 。
    ...
    serverSend
clientReceive

从时间序列上看,以下图所示。

Client                             Server

+--------------+     Request        +--------------+
| Client Send  | +----------------> |Server Receive|
+------+-------+                    +------+-------+
       |                                   |
       |                                   v
       |                            +------+--------+
       |                            |Server Business|
       |                            +------+--------+
       |                                   |
       |                                   |
       v                                   v
+------+--------+    Response       +------+-------+
|Client Receive | <---------------+ |Server Send   |
+------+--------+                   +------+-------+
       |                                   |
       |                                   |
       v                                   v

8.1 TraceID

产生trace ID 是在 客户端发送请求 clientSend cs 这个阶段,即,此 ID 通常由集群中第一个处理请求的系统产生,并在分布式调用下经过网络传递到下一个被请求系统。就是 AbstractTracer # clientSend 函数。

  • 调用 buildSpan 构建一个 SofaTracerSpan clientSpan,而后调用 start 函数创建一个 Span。

    • 若是不存在Parent context,则调用 createRootSpanContext 创建了 new root span context。

      • sofaTracerSpanContext = this.createRootSpanContext();
        • 调用 String traceId = TraceIdGenerator.generate(); 来构建 trace ID。
    • 若是存在 Parent context,则调用 createChildContext 创建 span context。

  • 对 clientSpan 设置各类 Tag。

    • clientSpan.setTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT);
  • 对 clientSpan 设置 log。

    • clientSpan.log(LogData.CLIENT_SEND_EVENT_VALUE);
  • 把 clientSpan 设置进入SpanContext.

    • sofaTraceContext.push(clientSpan);

具体产生traceId 的代码是在类 TraceIdGenerator 中。能够看到,TraceId 是由 ip,时间戳,递增序列,进程ID等构成,即traceId为服务器 IP + 产生 ID 时候的时间 + 自增序列 + 当前进程号,以此保证全局惟一性。这就回答了咱们以前提过的问题:traceId是怎么生成的,有什么规则?

public class TraceIdGenerator {
    private static String IP_16 = "ffffffff";
    private static AtomicInteger count = new AtomicInteger(1000);

    private static String getTraceId(String ip, long timestamp, int nextId) {
        StringBuilder appender = new StringBuilder(30);
        appender.append(ip).append(timestamp).append(nextId).append(TracerUtils.getPID());
        return appender.toString();
    }

    public static String generate() {
        return getTraceId(IP_16, System.currentTimeMillis(), getNextId());
    }

    private static String getIP_16(String ip) {
        String[] ips = ip.split("\\.");
        StringBuilder sb = new StringBuilder();
        String[] var3 = ips;
        int var4 = ips.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            String column = var3[var5];
            String hex = Integer.toHexString(Integer.parseInt(column));
            if (hex.length() == 1) {
                sb.append('0').append(hex);
            } else {
                sb.append(hex);
            }
        }

        return sb.toString();
    }

    private static int getNextId() {
        int current;
        int next;
        do {
            current = count.get();
            next = current > 9000 ? 1000 : current + 1;
        } while(!count.compareAndSet(current, next));

        return next;
    }

    static {
        try {
            String ipAddress = TracerUtils.getInetAddress();
            if (ipAddress != null) {
                IP_16 = getIP_16(ipAddress);
            }
        } catch (Throwable var1) {
        }
    }
}

8.2 SpanID

有两个地方会生成SpanId : CS, SR。SOFARPC 和 Dapper不一样,spanId中已经包含了调用链上下文关系,包含parent spanId 的信息。好比 系统在处理一个请求的过程当中依次调用了 B,C,D 三个系统,那么这三次调用的的 SpanId 分别是:0.1,0.2,0.3。若是 C 系统继续调用了 E,F 两个系统,那么这两次调用的 SpanId 分别是:0.2.1,0.2.2。

8.2.1 Client Send

接上面小节,在客户端发送请求 clientSend cs 这个阶段,就会构建Span,从而生成 SpanID。

  • 调用 buildSpan 构建一个 SofaTracerSpan clientSpan,而后调用 start 函数创建一个 Span。

    • 若是不存在Parent context,则调用 createRootSpanContext 创建了 new root span context。

      • sofaTracerSpanContext = this.createRootSpanContext();
        • 调用 SofaTracerSpanContext 生成新的SpanContext,里面就生成了新的Span ID。
    • 若是存在 Parent context,则调用 createChildContext 创建 span context,这里的 preferredReference.getSpanId() 就生成了Span ID。由于此时已经有了Parent Context,因此新的Span Id是在 Parent Span id基础上构建。

      • SofaTracerSpanContext sofaTracerSpanContext = new SofaTracerSpanContext(
            preferredReference.getTraceId(), preferredReference.nextChildContextId(),
            preferredReference.getSpanId(), preferredReference.isSampled());

8.2.2 Server Receive

咱们再以 Server Receive这个动做为例,能够看到在Server端 的 Span构建过程。

  • SpringMvcSofaTracerFilter # doFilter 会从 Header 中提取 SofaTracerSpanContext。

    • 利用 SofaTracer # extract 提取SofaTracerSpanContext,这里用到了 SpringMvcHeadersCarrier。
      • 利用 RegistryExtractorInjector # extract 从 SpringMvcHeadersCarrier 中提取 SpanContext。
        • 利用 AbstractTextB3Formatter # extract 从 SpringMvcHeadersCarrier 中提取 SpanContext。
  • AbstractTracer # serverReceive 会根据 SofaTracerSpanContext 进行后续操做,此时 SofaTracerSpanContext 以下:

    • sofaTracerSpanContext = {SofaTracerSpanContext@6056} "SofaTracerSpanContext{traceId='c0a80103159927161709310013925', spanId='0', parentId='', isSampled=true, bizBaggage={}, sysBaggage={}, childContextIndex=0}"
       traceId = "c0a80103159927161709310013925"
       spanId = "0"
       parentId = ""
       isSampled = true
       sysBaggage = {ConcurrentHashMap@6060}  size = 0
       bizBaggage = {ConcurrentHashMap@6061}  size = 0
       childContextIndex = {AtomicInteger@6062} "0"
    • 从当前线程取出当前的SpanContext,而后提取serverSpan,此 serverSpan 可能为null,也可能有值。

      • SofaTraceContext sofaTraceContext = SofaTraceContextHolder.getSofaTraceContext();
        SofaTracerSpan serverSpan = sofaTraceContext.pop();
      • 若是serverSpan为null,则生成一个新的 newSpan,而后调用 setSpanId 对传入的 SofaTracerSpanContext 参数进行设置新的 SpanId

        • sofaTracerSpanContext.setSpanId(sofaTracerSpanContext.nextChildContextId());
          
          此时 sofaTracerSpanContext 内容有变化了,具体就是spanId。
          sofaTracerSpanContext = {SofaTracerSpanContext@6056} 
           traceId = "c0a80103159927161709310013925"
           spanId = "0.1"
           parentId = ""  
           .....
      • 若是serverSpan 不为 null,则 newSpan = serverSpan

    • 设置log

    • 设置Tag

    • 把 newSpan 设置进入本地上下文。sofaTraceContext.push(newSpan);

须要注意,在链路的后续环节中,traceId 和 spanId 都是存储在本地线程的 sofaTracerSpanContext 之中,不是在 Span 之中

具体代码以下:

首先,SpringMvcSofaTracerFilter # doFilter 会从 Header 中提取 SofaTracerSpanContext

public class SpringMvcSofaTracerFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                         FilterChain filterChain) {
            // 从header中提取Context
            SofaTracerSpanContext spanContext = getSpanContextFromRequest(request);
            // sr
            springMvcSpan = springMvcTracer.serverReceive(spanContext);      
    }
}

其次,AbstractTracer # serverReceive 会根据 SofaTracerSpanContext 进行后续操做

public abstract class AbstractTracer {
    public SofaTracerSpan serverReceive(SofaTracerSpanContext sofaTracerSpanContext) {
        SofaTracerSpan newSpan = null;
        SofaTraceContext sofaTraceContext = SofaTraceContextHolder.getSofaTraceContext();
        SofaTracerSpan serverSpan = sofaTraceContext.pop();
        try {
            if (serverSpan == null) {
                if (sofaTracerSpanContext == null) {
                    sofaTracerSpanContext = SofaTracerSpanContext.rootStart();
                    isCalculateSampled = true;
                } else {                    			
                sofaTracerSpanContext.setSpanId(sofaTracerSpanContext.nextChildContextId());
                }
                newSpan = this.genSeverSpanInstance(System.currentTimeMillis(),
                    StringUtils.EMPTY_STRING, sofaTracerSpanContext, null);
            } else {
                newSpan = serverSpan;
            }
        } 
    }    
}

咱们能够看到,SpanID的构建规则相对简单,这就回答了咱们以前提过的问题:spanId是怎么生成的,有什么规则? 以及 ParentSpan 从哪儿来?

public class SofaTracerSpanContext implements SpanContext {
  private AtomicInteger childContextIndex = new AtomicInteger(0);

  public String nextChildContextId() {
    return this.spanId + RPC_ID_SEPARATOR + childContextIndex.incrementAndGet();
  }
}

0x09 Client 发送

本节咱们看看RestTemplate是如何发送请求的。

首先,打印出程序运行时候的Stack以下,这样你们能够先有一个大体的印象:

intercept:56, RestTemplateInterceptor (com.sofa.alipay.tracer.plugins.rest.interceptor)
execute:92, InterceptingClientHttpRequest$InterceptingRequestExecution (org.springframework.http.client)
executeInternal:76, InterceptingClientHttpRequest (org.springframework.http.client)
executeInternal:48, AbstractBufferingClientHttpRequest (org.springframework.http.client)
execute:53, AbstractClientHttpRequest (org.springframework.http.client)
doExecute:734, RestTemplate (org.springframework.web.client)
execute:669, RestTemplate (org.springframework.web.client)
getForEntity:337, RestTemplate (org.springframework.web.client)
main:40, RestTemplateDemoApplication (com.alipay.sofa.tracer.examples.rest)

在 InterceptingClientHttpRequest # execute 此处代码中

class InterceptingClientHttpRequest extends AbstractBufferingClientHttpRequest {
    @Override
		public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
			if (this.iterator.hasNext()) {
				ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
				return nextInterceptor.intercept(request, body, this); // 这里进行拦截处理
			}
    }
}

最后是来到了 SOFA 的拦截器中,这里会作处理。

9.1 生成Span

具体实现代码是在 RestTemplateInterceptor # intercept函数。

咱们能够看到,RestTemplateInterceptor这里有一个成员变量 restTemplateTracer,具体处理就是在 restTemplateTracer 这里实现。能够看到这里包含了 clientSend 和 clientReceive 两个过程。

  • 首先生成一个Span。SofaTracerSpan sofaTracerSpan = restTemplateTracer.clientSend(request.getMethod().name());

    • 先从 SofaTraceContext 取出 serverSpan。若是本 client 就是 一个服务中间点(即 serverSpan 不为空),那么须要给新span设置父亲Span。

    • 调用 clientSpan = (SofaTracerSpan)this.sofaTracer.buildSpan(operationName).asChildOf(serverSpan).start(); 获得自己的 client Span。若是有 server Span,则本 Client Span 就是 Sever Span的 child。

      • public Tracer.SpanBuilder asChildOf(Span parentSpan) {
            if (parentSpan == null) {
                return this;
            }
            return addReference(References.CHILD_OF, parentSpan.context());
        }
    • 设置父亲 clientSpan.setParentSofaTracerSpan(serverSpan);

  • 而后调用 appendRestTemplateRequestSpanTags 来把Span放入Request的Header中。

    • 给Span加入各类Tag,好比 app, url, method...
    • 进行Carrier处理,injectCarrier(request, sofaTracerSpan);
      • 调用 AbstractTextB3Formatter.inject 设置 traceId, spanId, parentId ....
  • 发送请求。

  • 收到服务器返回以后进一步处理。

    • 从ThreadLocal中获取 sofaTraceContext

    • 从 SofaTracerSpan 中获取 currentSpan

    • 调用 appendRestTemplateResponseSpanTags 设置各类 Tag

    • 调用 restTemplateTracer.clientReceive(resultCode); 处理

      • clientSpan = sofaTraceContext.pop(); 把以前的Span移除
      • 调用 clientReceiveTagFinish ,进而调用 clientSpan.finish();

        • 调用 SpanTracer.reportSpan 进行 Span 上报,其中Reporter 数据上报 reportSpan 或者链路跨度 SofaTracerSpan 启动调用采样器 sample 方法检查链路是否须要采样,获取采样状态 SamplingStatus 是否采样标识 isSampled。
      • 若是还有父亲Span,则须要再push 父亲 Span进入Context。sofaTraceContext.push(clientSpan.getParentSofaTracerSpan()); 以备后续处理。

具体代码以下:

public class RestTemplateInterceptor implements ClientHttpRequestInterceptor {

    protected AbstractTracer restTemplateTracer; // Sofa内部逻辑实现

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body,
                                        ClientHttpRequestExecution execution) throws IOException {
        SofaTracerSpan sofaTracerSpan = restTemplateTracer.clientSend(request.getMethod().name()); // 生成Span
        appendRestTemplateRequestSpanTags(request, sofaTracerSpan); //放入Header
        ClientHttpResponse response = null;
        Throwable t = null;
        try {
            return response = execution.execute(request, body); //发送请求
        } catch (IOException e) {
            t = e;
            throw e;
        } finally {
            SofaTraceContext sofaTraceContext = SofaTraceContextHolder.getSofaTraceContext();
            SofaTracerSpan currentSpan = sofaTraceContext.getCurrentSpan();
            String resultCode = SofaTracerConstant.RESULT_CODE_ERROR;
            // is get error
            if (t != null) {
                currentSpan.setTag(Tags.ERROR.getKey(), t.getMessage());
                // current thread name
                sofaTracerSpan.setTag(CommonSpanTags.CURRENT_THREAD_NAME, Thread.currentThread()
                    .getName());
            }
            if (response != null) {
                //tag append
                appendRestTemplateResponseSpanTags(response, currentSpan);
                //finish
                resultCode = String.valueOf(response.getStatusCode().value());
            }
            restTemplateTracer.clientReceive(resultCode);
        }
    }
}

9.2 Fomatter

上文提到了发送时候会调用 AbstractTextB3Formatter.inject 设置 traceId, spanId, parentId。

Fomatter 这个接口负责了具体场景中序列化/反序列化上下文的具体逻辑,例如在HttpCarrier使用中一般就会有一个对应的HttpFormatter。Tracer的注入和提取就是委托给了Formatter。

执行时候堆栈以下:

inject:141, AbstractTextB3Formatter (com.alipay.common.tracer.core.registry)
inject:26, AbstractTextB3Formatter (com.alipay.common.tracer.core.registry)
inject:115, SofaTracer (com.alipay.common.tracer.core)
injectCarrier:146, RestTemplateInterceptor (com.sofa.alipay.tracer.plugins.rest.interceptor)
appendRestTemplateRequestSpanTags:141, RestTemplateInterceptor (com.sofa.alipay.tracer.plugins.rest.interceptor)
intercept:57, RestTemplateInterceptor (com.sofa.alipay.tracer.plugins.rest.interceptor)
execute:92, InterceptingClientHttpRequest$InterceptingRequestExecution (org.springframework.http.client)
executeInternal:76, InterceptingClientHttpRequest (org.springframework.http.client)
executeInternal:48, AbstractBufferingClientHttpRequest (org.springframework.http.client)
execute:53, AbstractClientHttpRequest (org.springframework.http.client)
doExecute:734, RestTemplate (org.springframework.web.client)
execute:669, RestTemplate (org.springframework.web.client)
getForEntity:337, RestTemplate (org.springframework.web.client)
main:40, RestTemplateDemoApplication (com.alipay.sofa.tracer.examples.rest)

OpenTracing提供了两个处理“跟踪上下文(trace context)”的函数:

  • “extract(format,carrier)”从媒介(一般是HTTP头)获取跟踪上下文。
  • “inject(SpanContext,format,carrier)” 将跟踪上下文放入媒介,来保证跟踪链的连续性。

Inject 和 extract 分别对应了序列化 和 反序列化

public abstract class AbstractTextB3Formatter implements RegistryExtractorInjector<TextMap> {
    public static final String TRACE_ID_KEY_HEAD = "X-B3-TraceId";
    public static final String SPAN_ID_KEY_HEAD = "X-B3-SpanId";
    public static final String PARENT_SPAN_ID_KEY_HEAD = "X-B3-ParentSpanId";
    public static final String SAMPLED_KEY_HEAD = "X-B3-Sampled";
    static final String FLAGS_KEY_HEAD = "X-B3-Flags";
    static final String BAGGAGE_KEY_PREFIX = "baggage-";
    static final String BAGGAGE_SYS_KEY_PREFIX = "baggage-sys-";

    public SofaTracerSpanContext extract(TextMap carrier) {
        if (carrier == null) {
            return SofaTracerSpanContext.rootStart();
        } else {
            String traceId = null;
            String spanId = null;
            String parentId = null;
            boolean sampled = false;
            boolean isGetSampled = false;
            Map<String, String> sysBaggage = new ConcurrentHashMap();
            Map<String, String> bizBaggage = new ConcurrentHashMap();
            Iterator var9 = carrier.iterator();

            while(var9.hasNext()) {
                Entry<String, String> entry = (Entry)var9.next();
                String key = (String)entry.getKey();
                if (!StringUtils.isBlank(key)) {
                    if (traceId == null && "X-B3-TraceId".equalsIgnoreCase(key)) {
                        traceId = this.decodedValue((String)entry.getValue());
                    }

                    if (spanId == null && "X-B3-SpanId".equalsIgnoreCase(key)) {
                        spanId = this.decodedValue((String)entry.getValue());
                    }

                    if (parentId == null && "X-B3-ParentSpanId".equalsIgnoreCase(key)) {
                        parentId = this.decodedValue((String)entry.getValue());
                    }

                    String keyTmp;
                    if (!isGetSampled && "X-B3-Sampled".equalsIgnoreCase(key)) {
                        keyTmp = this.decodedValue((String)entry.getValue());
                        if ("1".equals(keyTmp)) {
                            sampled = true;
                        } else if ("0".equals(keyTmp)) {
                            sampled = false;
                        } else {
                            sampled = Boolean.parseBoolean(keyTmp);
                        }

                        isGetSampled = true;
                    }

                    String valueTmp;
                    if (key.indexOf("baggage-sys-") == 0) {
                        keyTmp = StringUtils.unescapeEqualAndPercent(key).substring("baggage-sys-".length());
                        valueTmp = StringUtils.unescapeEqualAndPercent(this.decodedValue((String)entry.getValue()));
                        sysBaggage.put(keyTmp, valueTmp);
                    }

                    if (key.indexOf("baggage-") == 0) {
                        keyTmp = StringUtils.unescapeEqualAndPercent(key).substring("baggage-".length());
                        valueTmp = StringUtils.unescapeEqualAndPercent(this.decodedValue((String)entry.getValue()));
                        bizBaggage.put(keyTmp, valueTmp);
                    }
                }
            }

            if (traceId == null) {
                return SofaTracerSpanContext.rootStart();
            } else {
                if (spanId == null) {
                    spanId = "0";
                }

                if (parentId == null) {
                    parentId = "";
                }

                SofaTracerSpanContext sofaTracerSpanContext = new SofaTracerSpanContext(traceId, spanId, parentId, sampled);
                if (sysBaggage.size() > 0) {
                    sofaTracerSpanContext.addSysBaggage(sysBaggage);
                }

                if (bizBaggage.size() > 0) {
                    sofaTracerSpanContext.addBizBaggage(bizBaggage);
                }

                return sofaTracerSpanContext;
            }
        }
    }

    public void inject(SofaTracerSpanContext spanContext, TextMap carrier) {
        if (carrier != null && spanContext != null) {
            carrier.put("X-B3-TraceId", this.encodedValue(spanContext.getTraceId()));
            carrier.put("X-B3-SpanId", this.encodedValue(spanContext.getSpanId()));
            carrier.put("X-B3-ParentSpanId", this.encodedValue(spanContext.getParentId()));
            carrier.put("X-B3-SpanId", this.encodedValue(spanContext.getSpanId()));
            carrier.put("X-B3-Sampled", this.encodedValue(String.valueOf(spanContext.isSampled())));
            Iterator var3 = spanContext.getSysBaggage().entrySet().iterator();

            Entry entry;
            String key;
            String value;
            while(var3.hasNext()) {
                entry = (Entry)var3.next();
                key = "baggage-sys-" + StringUtils.escapePercentEqualAnd((String)entry.getKey());
                value = this.encodedValue(StringUtils.escapePercentEqualAnd((String)entry.getValue()));
                carrier.put(key, value);
            }

            var3 = spanContext.getBizBaggage().entrySet().iterator();

            while(var3.hasNext()) {
                entry = (Entry)var3.next();
                key = "baggage-" + StringUtils.escapePercentEqualAnd((String)entry.getKey());
                value = this.encodedValue(StringUtils.escapePercentEqualAnd((String)entry.getValue()));
                carrier.put(key, value);
            }

        }
    }
}

通过序列化以后,最后发送的Header以下,咱们须要回忆下 spanContext 的概念。

上下文存储的是一些须要跨越边界的一些信息,例如:

  • spanId :当前这个span的id
  • traceId :这个span所属的traceId(也就是此次调用链的惟一id)。
    • trace_idspan_id 用以区分Trace中的Span;任何 OpenTraceing 实现相关的状态(好比 trace 和 span id)都须要被一个跨进程的 Span 所联系。
  • baggage :其余的能过跨越多个调用单元的信息,即跨进程的 key value 对。Baggage ItemsSpan Tag 结构相同,惟一的区别是:Span Tag只在当前Span中存在,并不在整个trace中传递,而Baggage Items 会随调用链传递。

能够看到,spanContext 已经被分解而且序列化到 Header 之中

request = {InterceptingClientHttpRequest@5808} 
 requestFactory = {SimpleClientHttpRequestFactory@5922} 
 interceptors = {ArrayList@5923}  size = 1
 method = {HttpMethod@5924} "GET"
 uri = {URI@5925} "http://localhost:8801/rest"
 bufferedOutput = {ByteArrayOutputStream@5926} ""
 headers = {HttpHeaders@5918}  size = 6
  "Accept" -> {LinkedList@5938}  size = 1
  "Content-Length" -> {LinkedList@5940}  size = 1
  "X-B3-TraceId" -> {LinkedList@5942}  size = 1
   key = "X-B3-TraceId"
   value = {LinkedList@5942}  size = 1
    0 = "c0a800031598690915258100115720"
  "X-B3-SpanId" -> {LinkedList@5944}  size = 2
   key = "X-B3-SpanId"
   value = {LinkedList@5944}  size = 2
    0 = "0"
    1 = "0"
  "X-B3-ParentSpanId" -> {LinkedList@5946}  size = 1
  "X-B3-Sampled" -> {LinkedList@5948}  size = 1
 executed = false
body = {byte[0]@5810}

9.3 Report

发送的最后一步是 clientSpan.finish()。

在 Opentracing 规范中提到,Span#finish 方法是 span 生命周期的最后一个执行方法,也就意味着一个 span 跨度即将结束。那么当一个 span 即将结束时,也是当前 span 具备最完整状态的时候。因此在 SOFATracer 中,数据上报的入口就是 Span#finish 方法,其调用堆栈以下:

doReportStat:43, RestTemplateStatJsonReporter (com.sofa.alipay.tracer.plugins.rest)
reportStat:179, AbstractSofaTracerStatisticReporter (com.alipay.common.tracer.core.reporter.stat)
statisticReport:143, DiskReporterImpl (com.alipay.common.tracer.core.reporter.digest)
doReport:60, AbstractDiskReporter (com.alipay.common.tracer.core.reporter.digest)
report:51, AbstractReporter (com.alipay.common.tracer.core.reporter.facade)
reportSpan:141, SofaTracer (com.alipay.common.tracer.core)
finish:165, SofaTracerSpan (com.alipay.common.tracer.core.span)
finish:158, SofaTracerSpan (com.alipay.common.tracer.core.span)
clientReceiveTagFinish:176, AbstractTracer (com.alipay.common.tracer.core.tracer)
clientReceive:157, AbstractTracer (com.alipay.common.tracer.core.tracer)
intercept:82, RestTemplateInterceptor (com.sofa.alipay.tracer.plugins.rest.interceptor)
execute:92, InterceptingClientHttpRequest$InterceptingRequestExecution (org.springframework.http.client)
executeInternal:76, InterceptingClientHttpRequest (org.springframework.http.client)
executeInternal:48, AbstractBufferingClientHttpRequest (org.springframework.http.client)
execute:53, AbstractClientHttpRequest (org.springframework.http.client)
doExecute:734, RestTemplate (org.springframework.web.client)
execute:669, RestTemplate (org.springframework.web.client)
getForEntity:337, RestTemplate (org.springframework.web.client)
main:40, RestTemplateDemoApplication (com.alipay.sofa.tracer.examples.rest)

SOFATracer 自己提供了两种上报模式,一种是落到磁盘,另一种是上报到zipkin。在实现细节上,SOFATracer 没有将这两种策略分开以提供独立的功能支持,而是将两种上报方式组合在了一块儿,而且在执行具体上报的流程中经过参数来调控是否执行具体的上报。

此过程当中涉及到了三个上报点,首先是上报到 zipkin,后面是落盘;在日志记录方面,SOFATracer 中为不一样的组件均提供了独立的日志空间,除此以外,SOFATracer 在链路数据采集时提供了两种不一样的日志记录模式:摘要日志和统计日志,这对于后续构建一些如故障的快速发现、服务治理等管控端提供了强大的数据支撑。。

好比 zipkin 对应上报是:

public class ZipkinSofaTracerSpanRemoteReporter implements SpanReportListener, Flushable, Closeable {
    public void onSpanReport(SofaTracerSpan span) {
        //convert
        Span zipkinSpan = zipkinV2SpanAdapter.convertToZipkinSpan(span);
        this.delegate.report(zipkinSpan);
    }  
}

其会调用到 zipkin2.reporter.AsyncReporter 进行具体 report。

9.4 采样计算

采样是对于整条链路来讲的,也就是说从 RootSpan 被建立开始,就已经决定了当前链路数据是否会被记录了。在 SofaTracer 类中,Sapmler 实例做为成员变量存在,而且被设置为 final,也就是当构建好 SofaTracer 实例以后,采样策略就不会被改变。当 Sampler 采样器绑定到 SofaTracer 实例以后,SofaTracer 对于产生的 Span 数据的落盘行为都会依赖采样器的计算结果(针对某一条链路而言)。

0x10 服务端接收

类 SpringMvcSofaTracerFilter 完成了服务端接收相关工做。主要就是设置 SpanContext 和 Span。

public class SpringMvcSofaTracerFilter implements Filter {
    private SpringMvcTracer springMvcTracer;
   
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                         FilterChain filterChain) {
       ......
    }
}

回忆下:在 client 端就是

  • 将当前请求线程的产生的 traceId 相关信息 Inject 到 SpanContext。
  • 而后经过 Fomatter 将 SpanContext序列化到Header之中。

server 端则是 从请求的 Header 中 extract 出 spanContext,来还本来次请求线程的上下文。由于上下文是和所处理的线程相关,放入 ThreadLocal中。

大体能够用以下图演示整体流程以下:

Client Span                                                Server Span
┌──────────────────┐                                       ┌──────────────────┐
│                  │                                       │                  │
│   TraceContext   │           Http Request Headers        │   TraceContext   │
│ ┌──────────────┐ │          ┌───────────────────┐        │ ┌──────────────┐ │
│ │ TraceId      │ │          │ X-B3-TraceId      │        │ │ TraceId      │ │
│ │              │ │          │                   │        │ │              │ │
│ │ ParentSpanId │ │ Inject   │ X-B3-ParentSpanId │Extract │ │ ParentSpanId │ │
│ │              ├─┼─────────>│                   ├────────┼>│              │ │
│ │ SpanId       │ │          │ X-B3-SpanId       │        │ │ SpanId       │ │
│ │              │ │          │                   │        │ │              │ │
│ │ Sampled      │ │          │ X-B3-Sampled      │        │ │ Sampled      │ │
│ └──────────────┘ │          └───────────────────┘        │ └──────────────┘ │
│                  │                                       │                  │
└──────────────────┘                                       └──────────────────┘

这就回答了以前的问题:服务器接收到请求以后作什么?SpanContext在服务器端怎么处理?

SpringMvcSofaTracerFilter 这里有一个成员变量 SpringMvcTracer, 其是 Server Tracer,这里是逻辑所在。

public class SpringMvcTracer extends AbstractServerTracer {
    private static volatile SpringMvcTracer springMvcTracer = null;
}

具体 SpringMvcSofaTracerFilter 的 doFilter 的大体逻辑以下:

  • 调用 getSpanContextFromRequest 从 request 中获取 SpanContext,其中使用了 tracer.extract函数。

    • SofaTracerSpanContext spanContext = (SofaTracerSpanContext)tracer.extract(Builtin.B3_HTTP_HEADERS, new SpringMvcHeadersCarrier(headers));
  • 调用 serverReceive 获取 Span

    • springMvcSpan = this.springMvcTracer.serverReceive(spanContext);
      • SofaTracerSpan serverSpan = sofaTraceContext.pop(); // 取出父亲Span,若是不存在,则
        sofaTracerSpanContext.setSpanId(sofaTracerSpanContext.nextChildContextId()); // 设定为下一个child id
      • sofaTraceContext.push(newSpan); // 把Span放入 SpanContext
  • Span 设置各类 setTag

  • 调用 this.springMvcTracer.serverSend(String.valueOf(httpStatus)); 来 结束Span。

    • 结束 & report

      • this.clientReceiveTagFinish(clientSpan, resultCode);
        • 设置log,resultCode,结束Client Span :clientSpan.finish();
          • 调用 SofaTracer # reportSpan 来 report。这部分和 Client 代码功能相似。
    • 恢复restore parent span

      • sofaTraceContext.push(clientSpan.getParentSofaTracerSpan());

函数代码具体以下

public class SpringMvcSofaTracerFilter implements Filter {
    private SpringMvcTracer springMvcTracer;

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) {
        if (this.springMvcTracer == null) {
            this.springMvcTracer = SpringMvcTracer.getSpringMvcTracerSingleton();
        }

        SofaTracerSpan springMvcSpan = null;
        long responseSize = -1L;
        int httpStatus = -1;

        try {
            HttpServletRequest request = (HttpServletRequest)servletRequest;
            HttpServletResponse response = (HttpServletResponse)servletResponse;
            SofaTracerSpanContext spanContext = this.getSpanContextFromRequest(request);
            springMvcSpan = this.springMvcTracer.serverReceive(spanContext);
            if (StringUtils.isBlank(this.appName)) {
                this.appName = SofaTracerConfiguration.getProperty("spring.application.name");
            }

            springMvcSpan.setOperationName(request.getRequestURL().toString());
            springMvcSpan.setTag("local.app", this.appName);
            springMvcSpan.setTag("request.url", request.getRequestURL().toString());
            springMvcSpan.setTag("method", request.getMethod());
            springMvcSpan.setTag("req.size.bytes", request.getContentLength());
            SpringMvcSofaTracerFilter.ResponseWrapper responseWrapper = new SpringMvcSofaTracerFilter.ResponseWrapper(response);
            filterChain.doFilter(servletRequest, responseWrapper);
            httpStatus = responseWrapper.getStatus();
            responseSize = (long)responseWrapper.getContentLength();
        } catch (Throwable var15) {
            httpStatus = 500;
            throw new RuntimeException(var15);
        } finally {
            if (springMvcSpan != null) {
                springMvcSpan.setTag("resp.size.bytes", responseSize);
                this.springMvcTracer.serverSend(String.valueOf(httpStatus));
            }
        }
    }
}

0x11 问题解答

咱们在最初提出的问题,如今都有了解答。

  • traceId是怎么生成的,有什么规则?答案以下:
    • 在clientSend cs 这个阶段,创建Span时候,若是不存在 Parent context,则调用 createRootSpanContext 创建了 new root span context。此时会生成一个 traceId
    • TraceId 是由 ip,时间戳,递增序列,进程ID等构成,具体能够参见 TraceIdGenerator 类。
  • spanId是怎么生成的,有什么规则?答案以下:
    • 在 Server Receive 这个阶段,若是当前线程SpanContext中没有Span,则生成一个新的 newSpan,而后调用 setSpanId 对传入的 SofaTracerSpanContext 参数进行设置新的 SpanId。
    • 规则很简单,就是在以前Span ID基础上单调递增,参见 SofaTracerSpanContext #nextChildContextId。
  • 客户端哪里生成的Span?答案以下:
    • 在 客户端发送请求 clientSend cs 这个阶段,就是 AbstractTracer # clientSend 函数,调用 buildSpan 构建一个 SofaTracerSpan clientSpan,而后调用 start 函数创建一个 Span。
  • ParentSpan 从哪儿来?答案以下:
    • 在 clientSend 阶段,先从 SofaTraceContext 取出 serverSpan。若是本 client 就是 一个服务中间点(即 serverSpan 不为空),则 serverSpan 就是 parentSpan,那么须要给新span设置父亲Span。
  • ChildSpan由ParentSpan建立,那么何时建立?答案以下:
    • 接上面回答,若是存在 ParentSpan,则调用 clientSpan = (SofaTracerSpan)this.sofaTracer.buildSpan(operationName).asChildOf(serverSpan).start(); 获得自己的 client Span。
    • 即若是存在active span ,若存在则生成CHILD_OF关系的上下文, 若是不存在则createNewContext;
  • Trace信息怎么传递?答案以下:
    • OpenTracing之中是经过SpanContext来传递Trace信息。
    • SpanContext存储的是一些须要跨越边界的一些信息,好比trace Id,span id,Baggage。这些信息会不一样组件根据本身的特色序列化进行传递,好比序列化到 http header 之中再进行传递。
    • 而后经过这个 SpanContext 所携带的信息将当前节点关联到整个 Tracer 链路中去
  • 服务器接收到请求以后作什么?答案以下:
    • server 端则是 从请求的 Header 中 extract 出 spanContext,来还本来次请求线程的上下文。由于上下文是和所处理的线程相关,放入 ThreadLocal中。
  • SpanContext在服务器端怎么处理?答案见上面回答。
  • 链路信息如何搜集?答案以下:
    • 采样是对于整条链路来讲的,也就是说从 RootSpan 被建立开始,就已经决定了当前链路数据是否会被记录了。
    • 在 SofaTracer 类中,Sapmler 实例做为成员变量存在,而且被设置为 final,也就是当构建好 SofaTracer 实例以后,采样策略就不会被改变。当 Sampler 采样器绑定到 SofaTracer 实例以后,SofaTracer 对于产生的 Span 数据的落盘行为都会依赖采样器的计算结果(针对某一条链路而言)。

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

蚂蚁金服分布式链路跟踪组件 SOFATracer 数据上报机制和源码剖析

蚂蚁金服开源分布式链路跟踪组件 SOFATracer 埋点机制剖析

分布式链路组件 SOFATracer 埋点机制解析

【剖析 | SOFARPC 框架】之 SOFARPC 链路追踪剖析

相关文章
相关标签/搜索