SOFA是蚂蚁金服自主研发的金融级分布式中间件,包含了构建金融级云原生架构所需的各个组件,SOFATracer 是其中用于分布式系统调用跟踪的组件。html
笔者以前有过zipkin的经验,但愿扩展到Opentracing,因而在学习SOFATracer官方博客结合源码的基础上总结出此文,与你们分享。java
为何选择了从SOFATracer入手来学习?理由很简单:有大公司背书(是在金融场景里锤炼出来的最佳实践),有开发者和社区整理的官方博客,有直播,示例简便易调试,为何不研究使用呢?git
让咱们用问题来引导阅读。github
全链路跟踪分红三个跟踪级别:web
本文只讨论 跨进程跟踪 (cross-process),由于跨进程跟踪是最简单的 ,容易上手^_^。对于跨进程跟踪,你能够编写拦截器或过滤器来跟踪每一个请求,它只须要编写极少的代码。spring
容器、Serverless 编程方式的诞生极大提高了软件交付与部署的效率。在架构的演化过程当中,能够看到两个变化:数据库
从以上两个变化能够看到这种弹性、标准化的架构背后,原先运维与诊断的需求也变得愈来愈复杂。如何理清服务依赖调用关系、如何在这样的环境下快速 debug
、追踪服务处理耗时、查找服务性能瓶颈、合理对服务的容量评估都变成一个棘手的事情。编程
为了应对这些问题,可观察性(Observability
) 这个概念被引入软件领域。传统的监控和报警主要关注系统的异常状况和失败因素,可观察性更关注的是从系统自身出发,去展示系统的运行情况,更像是一种对系统的自我审视。一个可观察的系统中更关注应用自己的状态,而不是所处的机器或者网络这样的间接证据。咱们但愿直接获得应用当前的吞吐和延迟信息,为了达到这个目的,咱们就须要合理主动暴露更多应用运行信息。在当前的应用开发环境下,面对复杂系统咱们的关注将逐渐由点 到 点线面体的结合,这能让咱们更好的理解系统,不只知道What,更能回答Why。后端
可观察性目前主要包含如下三大支柱:api
Logging
) : Logging
主要记录一些离散的事件,应用每每经过将定义好格式的日志信息输出到文件,而后用日志收集程序收集起来用于分析和聚合。虽然能够用时间将全部日志点事件串联起来,可是却很难展现完整的调用关系路径;Metrics
) :Metric
每每是一些聚合的信息,相比 Logging
丧失了一些具体信息,可是占用的空间要比完整日志小的多,能够用于监控和报警,在这方面 Prometheus 已经基本上成为了事实上的标准;Tracing
) : Tracing
介于 Logging
和 Metric
之间, 以请求的维度来串联服务间的调用关系并记录调用耗时,即保留了必要的信息,又将分散的日志事件经过 Span 串联,帮助咱们更好的理解系统的行为、辅助调试和排查性能问题。三大支柱有以下特色:
分布式追踪,也称为分布式请求追踪,是一种用于分析和监视应用程序的方法,特别是那些使用微服务体系结构构建的应用程序;分布式追踪有助于查明故障发生的位置以及致使性能低下的缘由,开发人员可使用分布式跟踪来帮助调试和优化他们的代码,IT和DevOps团队可使用分布式追踪来监视应用程序。
Tracing 是在90年代就已出现的技术。但真正让该领域流行起来的仍是源于 Google 的一篇论文”Dapper, a Large-Scale Distributed Systems Tracing Infrastructure”,而另外一篇论文”Uncertainty in Aggregate Estimates from Sampled Distributed Traces”中则包含关于采样的更详细分析。论文发表后一批优秀的 Tracing 软件孕育而生。
Logging
和Metric
强化监控和报警。为了解决不一样的分布式追踪系统 API 不兼容的问题,出现了OpenTracing。OpenTracing旨在标准化Trace数据结构和格式,其目的是:
OpenTracing不是一个标准,OpenTracing API提供了一个标准的、与供应商无关的框架,是对分布式链路中涉及到的一些列操做的高度抽象集合。这意味着若是开发者想要尝试一种不一样的分布式追踪系统,开发者只须要简单地修改Tracer配置便可,而不须要替换整个分布式追踪系统。
大多数分布式追踪系统的思想模型都来自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
发挥着重要做用。
表示分布式调用链条中的一个调用单元,他的边界包含一个请求进到服务内部再由某种途径(http/dubbo等)从当前服务出去。
一个span通常会记录这个调用单元内部的一些信息,例如每一个Span
包含的操做名称、开始和结束时间、附加额外信息的Span Tag
、可用于记录Span
内特殊事件Span Log
、用于传递Span
上下文的SpanContext
和定义Span
之间关系的References
。
Trace 描述在分布式系统中的一次"事务"。一个trace是由若干span组成的有向无环图。
Tracer 用于建立Span,并理解如何跨进程边界注入(序列化)和提取(反序列化)Span。它有如下的职责:
用图论的观点来看的话,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··]
一个span能够和一个或者多个span间存在因果关系。OpenTracing定义了两种关系:ChildOf 和 FollowsFrom。这两种引用类型表明了子节点和父节点间的直接因果关系。
ChildOf
将成为当前 Span 的 child,而 FollowsFrom
则会成为 parent。 这两种关系为 child span 和 parent span 创建了直接因果关系。
表示一个span对应的上下文,span和spanContext基本上是一一对应的关系,这个SpanContext能够经过某些媒介和方式传递给调用链的下游来作一些处理(例如子Span的id生成、信息的继承打印日志等等)。
上下文存储的是一些须要跨越边界的(传播跟踪所需的)一些信息,例如:
trace_id
和 span_id
用以区分Trace
中的Span
;任何 OpenTraceing 实现相关的状态(好比 trace 和 span id)都须要被一个跨进程的 Span 所联系。Baggage Items
和 Span 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
的注入和提取。
Carrier 表示的是一个承载spanContext的媒介,比方说在http调用场景中会有HttpCarrier,在dubbo调用场景中也会有对应的DubboCarrier。
这个接口负责了具体场景中序列化反序列化上下文的具体逻辑,例如在HttpCarrier使用中一般就会有一个对应的HttpFormatter。Tracer的注入和提取就是委托给了Formatter。
这个类是0.30版本以后新加入的组件,这个组件的做用是可以经过它获取当前线程中启用的Span信息,而且能够启用一些处于未启用状态的span。在一些场景中,咱们在一个线程中可能同时创建多个span,可是同一时间同一线程只会有一个span在启用,其余的span可能处在下列的状态中:
除了上述组件以外,在实现一个分布式全链路监控框架的时候,还须要有一个reporter组件,经过它来打印或者上报一些关键链路信息(例如span建立和结束),只有把这些信息进行处理以后咱们才能对全链路信息进行可视化和真正的监控。
SOFATracer 是一个用于分布式系统调用跟踪的组件,经过统一的 traceId 将调用链路中的各类网络调用状况以日志的方式记录下来,以达到透视化网络调用的目的。这些日志可用于故障的快速发现,服务治理等。
SOFATracer 团队已经为咱们搭建了一个完整的 Tracer 框架内核,包括数据模型、编码器、跨进程透传 traceId、采样、日志落盘与上报等核心机制,并提供了扩展 API 及基于开源组件实现的部分插件,为咱们基于该框架打造本身的 Tracer 平台提供了极大便利。
SOFATracer 目前并无提供数据采集器和 UI 展现的功能;主要有两个方面的考虑:
所以在上报模型上,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
中将全部核心的组件都声明为接口,例如 Tracer
、Span
、SpanContext
、Format
(高版本中还包括 Scope
和 ScopeManager
)等。SOFATracer
使用的版本是 0.22.0 ,主要是对 Tracer
、Span
、SpanContext
三个概念模型的实现。下面就针对几个组件结合 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; }
Span
是一个跨度单元,在实际的应用过程当中,Span
就是一个完整的数据包,其包含的就是当前节点所须要上报的数据。
SofaTracerSpan
实现了 io.opentracing.Span
接口,并扩展了对 Reference
、tags
、线程异步处理以及插件扩展中所必须的 logType
和产生当前 span
的 Tracer
类型等处理的能力。
每一个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是服务端收到客户端时间 到 发送响应结果给客户端的这段过程。
SpanContext
对于 OpenTracing
实现是相当重要的,经过 SpanContext
能够实现跨进程的链路透传,而且能够经过 SpanContext
中携带的信息将整个链路串联起来。
官方文档中有这样一句话:“在
OpenTracing
中,咱们强迫SpanContext
实例成为不可变的,以免Span
在finish
和reference
操做时会有复杂的生命周期问题。” 这里是能够理解的,若是SpanContext
在透传过程当中发生了变化,好比改了tracerId
,那么就可能致使链路出现断缺。
SofaTracerSpanContext
实现了 SpanContext
接口,扩展了构建 SpanContext
、序列化 baggageItems
以及SpanContext
等新的能力。
public interface SofaTraceContext { void push(SofaTracerSpan var1); SofaTracerSpan getCurrentSpan(); SofaTracerSpan pop(); int getThreadLocalSpanSize(); void clear(); boolean isEmpty(); }
本小节回答了 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); }
在链路环节每一个节点中,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(); } }
日志落盘又分为摘要日志落盘 和 统计日志落盘;
数据上报是 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 :
咱们使用的是 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 ......."); } }
这里首先要提一下SOFATracer 的埋点机制,不一样组件有不一样的应用场景和扩展点,所以对插件的实现也要因地制宜,SOFATracer 埋点方式通常是经过 Filter、Interceptor 机制实现的。因此下面咱们提到的Client启动 / Server 启动就主要是建立了 Filter、Interceptor 机制。
咱们就以 RestTemplate 为例看看SofaTracer的启动。
代码中只用到 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 类管理。
这部分代码是 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; } }
这部分代码是 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(); } } }
对一个应用的跟踪要关注的无非就是 客户端--->web 层--->rpc 服务--->dao 后端存储、cache 缓存、消息队列 mq 等这些基础组件
。SOFATracer 插件的做用实际上也就是对不一样组件进行埋点,以便基于这些组件采集应用的链路数据。
不一样组件有不一样的应用场景和扩展点,所以对插件的实现也要因地制宜,SOFATracer 埋点方式通常是经过 Filter、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 类。
对于一个组件来讲,一次处理过程通常是产生一个 Span;这个 Span 的生命周期是从接收到请求到返回响应这段过程。
可是这里须要考虑的问题是如何与上下游链路关联起来呢?在 Opentracing 规范中,能够在 Tracer 中 extract 出一个跨进程传递的 SpanContext 。而后经过这个 SpanContext 所携带的信息将当前节点关联到整个 Tracer 链路中去,固然有提取(extract)就会有对应的注入(inject)。
链路的构建通常是 client------server------client------server 这种模式的,那这里就很清楚了,就是会在 client 端进行注入(inject),而后再 server 端进行提取(extract),反复进行,而后一直传递下去。
在拿到 SpanContext 以后,此时当前的 Span 就能够关联到这条链路中了,那么剩余的事情就是收集当前组件的一些数据;整个过程大概分为如下几个阶段:
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 的过程大体以下:
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; }
在 SOFATracer 中将请求大体分为如下几个过程:
不管是哪一个插件,在请求处理周期内均可以从上述几个阶段中找到对应的处理方法。所以,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
产生trace ID 是在 客户端发送请求 clientSend cs 这个阶段,即,此 ID 通常由集群中第一个处理请求的系统产生,并在分布式调用下经过网络传递到下一个被请求系统。就是 AbstractTracer # clientSend 函数。
调用 buildSpan 构建一个 SofaTracerSpan clientSpan,而后调用 start 函数创建一个 Span。
若是不存在Parent context,则调用 createRootSpanContext 创建了 new root span context。
sofaTracerSpanContext = this.createRootSpanContext();
若是存在 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) { } } }
有两个地方会生成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。
接上面小节,在客户端发送请求 clientSend cs 这个阶段,就会构建Span,从而生成 SpanID。
调用 buildSpan 构建一个 SofaTracerSpan clientSpan,而后调用 start 函数创建一个 Span。
若是不存在Parent context,则调用 createRootSpanContext 创建了 new root span context。
sofaTracerSpanContext = this.createRootSpanContext();
若是存在 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());
咱们再以 Server Receive这个动做为例,能够看到在Server端 的 Span构建过程。
SpringMvcSofaTracerFilter # doFilter 会从 Header 中提取 SofaTracerSpanContext。
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(); } }
本节咱们看看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 的拦截器中,这里会作处理。
具体实现代码是在 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中。
,injectCarrier(request, sofaTracerSpan);
发送请求。
收到服务器返回以后进一步处理。
从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); } } }
上文提到了发送时候会调用 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)”的函数:
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_id
和span_id
用以区分Trace
中的Span
;任何 OpenTraceing 实现相关的状态(好比 trace 和 span id)都须要被一个跨进程的 Span 所联系。- baggage :其余的能过跨越多个调用单元的信息,即跨进程的 key value 对。
Baggage Items
和Span 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}
发送的最后一步是 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。
采样是对于整条链路来讲的,也就是说从 RootSpan 被建立开始,就已经决定了当前链路数据是否会被记录了。在 SofaTracer 类中,Sapmler 实例做为成员变量存在,而且被设置为 final,也就是当构建好 SofaTracer 实例以后,采样策略就不会被改变。当 Sampler 采样器绑定到 SofaTracer 实例以后,SofaTracer 对于产生的 Span 数据的落盘行为都会依赖采样器的计算结果(针对某一条链路而言)。
类 SpringMvcSofaTracerFilter 完成了服务端接收相关工做。主要就是设置 SpanContext 和 Span。
public class SpringMvcSofaTracerFilter implements Filter { private SpringMvcTracer springMvcTracer; @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) { ...... } }
回忆下:在 client 端就是
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);
恢复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)); } } } }
咱们在最初提出的问题,如今都有了解答。
clientSpan = (SofaTracerSpan)this.sofaTracer.buildSpan(operationName).asChildOf(serverSpan).start();
获得自己的 client Span。开放分布式追踪(OpenTracing)入门与 Jaeger 实现
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 数据上报机制和源码剖析