0x00 摘要
SOFA是蚂蚁金服自主研发的金融级分布式中间件,包含了构建金融级云原生架构所需的各个组件,SOFATracer 是其中用于分布式系统调用跟踪的组件。
笔者之前有过zipkin的经验,希望扩展到Opentracing,于是在学习SOFATracer官方博客结合源码的基础上总结出此文,与大家分享。
因为字数限制,本文分为上下两篇。
0x01 缘由 & 问题
1.1 选择
为什么选择了从SOFATracer入手来学习?理由很简单:有大公司背书(是在金融场景里锤炼出来的最佳实践),有开发者和社区整理的官方博客,有直播,示例简便易调试,为什么不研究使用呢?
1.2 问题
让我们用问题来引导阅读。
- spanId是怎么生成的,有什么规则?
- traceId是怎么生成的,有什么规则?
- 客户端哪里生成的Span?
- ParentSpan 从哪儿来?
- ChildSpan由ParentSpan创建,什么时候创建?
- Trace信息怎么传递?
- 服务器接收到请求之后做什么?
- SpanContext在服务器端怎么处理?
- 链路信息如何搜集?
1.3 本文讨论范围
全链路跟踪分成三个跟踪级别:
- 跨进程跟踪 (cross-process)(调用另一个微服务)
- 数据库跟踪
- 进程内部的跟踪 (in-process)(在一个函数内部的跟踪)
本文只讨论 跨进程跟踪 (cross-process),因为跨进程跟踪是最简单的 ,容易上手^_^。对于跨进程跟踪,你可以编写拦截器或过滤器来跟踪每个请求,它只需要编写极少的代码。
0x02 背景知识
2.1 趋势和挑战
容器、Serverless 编程方式的诞生极大提升了软件交付与部署的效率。在架构的演化过程中,可以看到两个变化:
- 应用架构开始从单体系统逐步转变为微服务,其中的业务逻辑随之而来就会变成微服务之间的调用与请求。
- 资源角度来看,传统服务器这个物理单位也逐渐淡化,变成了看不见摸不到的虚拟资源模式。
从以上两个变化可以看到这种弹性、标准化的架构背后,原先运维与诊断的需求也变得越来越复杂。如何理清服务依赖调用关系、如何在这样的环境下快速 debug
、追踪服务处理耗时、查找服务性能瓶颈、合理对服务的容量评估都变成一个棘手的事情。
2.2 可观察性(Observability)
为了应对这些问题,可观察性(Observability
) 这个概念被引入软件领域。传统的监控和报警主要关注系统的异常情况和失败因素,可观察性更关注的是从系统自身出发,去展现系统的运行状况,更像是一种对系统的自我审视。一个可观察的系统中更关注应用本身的状态,而不是所处的机器或者网络这样的间接证据。我们希望直接得到应用当前的吞吐和延迟信息,为了达到这个目的,我们就需要合理主动暴露更多应用运行信息。在当前的应用开发环境下,面对复杂系统我们的关注将逐渐由点 到 点线面体的结合,这能让我们更好的理解系统,不仅知道What,更能回答Why。
可观察性目前主要包含以下三大支柱:
- 日志(
Logging
) :Logging
主要记录一些离散的事件,应用往往通过将定义好格式的日志信息输出到文件,然后用日志收集程序收集起来用于分析和聚合。虽然可以用时间将所有日志点事件串联起来,但是却很难展示完整的调用关系路径; - 度量(
Metrics
) :Metric
往往是一些聚合的信息,相比Logging
丧失了一些具体信息,但是占用的空间要比完整日志小的多,可以用于监控和报警,在这方面 Prometheus 已经基本上成为了事实上的标准; - 分布式追踪(
Tracing
) :Tracing
介于Logging
和Metric
之间, 以请求的维度来串联服务间的调用关系并记录调用耗时,即保留了必要的信息,又将分散的日志事件通过 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开关实现对特定请求的全采样);
- 依赖梳理——基于调用关系生成服务依赖图;
- 性能分析和优化——可以方便的记录统计系统链路上不同处理单元的耗时占用和占比;
- 容量规划与评估;
- 配合
Logging
和Metric
强化监控和报警。
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。它有以下的职责:
- 建立和开启一个span
- 从某种媒介中提取/注入一个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 span 和 parent span 建立了直接因果关系。
3.4 SpanContext
表示一个span对应的上下文,span和spanContext基本上是一一对应的关系,这个SpanContext可以通过某些媒介和方式传递给调用链的下游来做一些处理(例如子Span的id生成、信息的继承打印日志等等)。
上下文存储的是一些需要跨越边界的(传播跟踪所需的)一些信息,例如:
- 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
数据结构简化版如下:
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可能处在下列的状态中:
- 等待子span完成
- 等待某种阻塞方法
- 创建但是并未开始
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、OkHttp3、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
来分析。
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
接口,并扩展了对 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是服务端收到客户端时间 到 发送响应结果给客户端的这段过程。
4.3 SpanContext & SofaTracerSpanContext
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();
}
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();
}
}
}
下文介绍埋点机制与请求总体过程等等,敬请期待。
0xEE 个人信息
★★★★★★关于生活和技术的思考★★★★★★
微信公众账号:罗西的思考
如果您想及时得到个人撰写文章的消息推送,或者想看看个人推荐的技术资料,敬请关注。
0xFF 参考
开放分布式追踪(OpenTracing)入门与 Jaeger 实现
OpenTracing Java Library教程(3)——跨服务传递SpanContext
OpenTracing Java Library教程(1)——trace和span入门
蚂蚁金服分布式链路跟踪组件 SOFATracer 总览|剖析
蚂蚁金服开源分布式链路跟踪组件 SOFATracer 链路透传原理与SLF4J MDC 的扩展能力剖析
蚂蚁金服开源分布式链路跟踪组件 SOFATracer 采样策略和源码剖析
The OpenTracing Semantic Specification
蚂蚁金服分布式链路跟踪组件 SOFATracer 数据上报机制和源码剖析