原文连接 一文详解蚂蚁金服分布式链路组件 SOFATracer 的埋点机制html
SOFATracer 是一个用于分布式系统调用跟踪的组件,经过统一的 TraceId 将调用链路中的各类网络调用状况以日志的方式记录下来,以达到透视化网络调用的目的,这些链路数据可用于故障的快速发现,服务治理等。java
GITHUB 地址:https://github.com/sofastack/sofa-tracer/pulls (欢迎 star)
官方文件地址:https://www.sofastack.tech/projects/sofa-tracer/overview/git
2018 年底时至 2019 年初,SOFA 团队发起过 剖析-sofatracer-框架 的源码解析系列文章。这个系列中,基本对 SOFATracer 所提供的能力及实现原理都作了比较全面的分析,有兴趣的同窗能够看下。github
从官方文档及 PR 来看,目前 SOFATracer 已经支持了对如下开源组件的埋点支持:web
大多数能力提供在 3.x 版本,2.x 版本从官方 issue 中能够看到后续将不在继续提供新的功能更新;这也是和 SpringBoot 宣布不在继续维护 1.x 版本有关系。redis
本文将从插件的角度来分析,SOFATracer 是如何实现对上述组件进行埋点的;经过本文,除了了解 SOFATracer 的埋点机制以外,也能够对上述组件的基本扩展机制以及基本原理有一点学习。spring
SOFATracer 支持对标准 Servlet 规范的 web mvc 埋点,包括普通的 servlet 和 Springmvc 等;基本原理就是基于 Servelt 规范所提供的 javax.servlet.Filter
过滤器接口扩展实现。sql
过滤器位于 client 和 web 应用程序之间,用于检查和修改二者之间流过的请求和响应信息。在请求到达 Servlet 以前,过滤器截获请求。在响应送给客户端以前,过滤器截获响应。多个过滤器造成一个 FilterChain,FilterChain 中不一样过滤器的前后顺序由部署文件 web.xml 中过滤器映射的顺序决定。最早截获客户端请求的过滤器将最后截获 Servlet 的响应信息。mongodb
web 应用程序通常做为请求的接收方,在 Tracer 中应用是做为 server 存在的,其在解析 SpanContext 时所对应的事件为 sr (server receive)。apache
SOFATracer 在 sofa-tracer-springmvc-plugin 插件中解析及产生 span 的过程大体以下:
固然这里面还会设计到其余不少细节,好比给 span 设置哪些 tag 属性、若是处理异步线程透传等等。本篇不展开细节探讨,有兴趣的同窗能够自行阅读代码或者和我交流。
Dubbo 埋点在 SOFATracer 中实际上提供了两个插件,分别用于支持 Dubbo 2.6.x 和 Dubbo 2.7.x;Duddo 埋点也是基于 Filter ,此Filter 是 Dubbo 提供的 SPI 扩展-调用拦截扩展 机制实现。
像 Dubbo 或者 SOFARpc 等 rpc 框架的埋点,一般须要考虑的点比较多,首先是 rpc 框架分客户端和服务端,因此在埋点时 rpc 的客户端和服务端必需要有所区分;再者就是 rpc 的调用方式包括不少种,如常见的同步调用、异步调用、oneway 等等,调用方式不一样,所对应的 span 的结束时机也不一样,重要是的基本全部的 rpc 框架都会使用线程池用来发起和处理请求,那么如何保证 tracer 在多线程环境下不串也很重要。
另外 Dubbo 2.6.x 和 Dubbo 2.7.x 在异步回调处理上差别比较大,Dubbo 2.7.x 中提供了 onResponse 方法(后面又升级为 Listener,包括 onResponse 和 onError 两个方法);而 Dubbo 2.6.x 中则并未提供相应的机制,只能经过对 future 的硬编码处理来完成埋点和上报。
这个问题 zipkin brave 对 Dubbo 2.6.x 的埋点时其实也没有考虑到,在作 SOFATracer 支持 Dubbo 2.6.x 时发现了这个 bug,并作了修复。
SOFATracer 中提供的 DubboSofaTracerFilter 类:
@Activate(group = { CommonConstants.PROVIDER, CommonConstants.CONSUMER }, value = "dubboSofaTracerFilter", order = 1)
public class DubboSofaTracerFilter implements Filter {
// todo trace
}
复制代码
SOFATracer 中用于处理 Dubbo 2.6.x 版本中异步回调处理的核心代码:
Dubbo 异步处理依赖 ResponseFuture 接口,可是 ResponseFuture 在核心链路上并不是是以数据或者 list 的形式存在,因此在链路上只会存在一个 ResponseFuture,所以若是我自定义一个类来实现 ResponseFuture 接口是无法达到预期目的的,由于运行期会存在覆盖 ResponseFuture 的问题。因此在设计上,SOFATracer 会经过 ResponseFuture 构建一个新的 FutureAdapter出来用于传递。
boolean ensureSpanFinishes(Future<Object> future, Invocation invocation, Invoker<?> invoker) {
boolean deferFinish = false;
if (future instanceof FutureAdapter) {
deferFinish = true;
ResponseFuture original = ((FutureAdapter<Object>) future).getFuture();
ResponseFuture wrapped = new AsyncResponseFutureDelegate(invocation, invoker, original);
// Ensures even if no callback added later, for example when a consumer, we finish the span
wrapped.setCallback(null);
RpcContext.getContext().setFuture(new FutureAdapter<>(wrapped));
}
return deferFinish;
}
复制代码
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;
}
复制代码
和标准 servlet 规范实现同样,全部基于 javax.sql.DataSource
实现的 DataSource 都可以使用 SOFATracer 进行埋点。由于 DataSource 并无提供像 Servlet 那样的过滤器或者拦截器,因此 SOFATracer 中无法直接经过常规的方式(Filter/SPI扩展拦截/拦截器等)进行埋点,而是使用了代理模式的方式来实现的。
上图为 SOFATracer 中 DataSource 代理类实现的类继承结构体系;能够看出,SOFATracer 中自定义了一个 BaseDataSource 抽象类,该抽象类继承 javax.sql.DataSource
接口,SmartDataSource 做为 BaseDataSource 的惟一子类,也就是 SOFATracer 中所使用的 代理类。因此若是你使用了 sofa-tracer-datasource-plugin
插件的话,能够看到最终运行时的 Datasource 类型是 com.alipay.sofa.tracer.plugins.datasource.SmartDataSource
。
public abstract class BaseDataSource implements DataSource {
// 实际被代理的 datasource
protected DataSource delegate;
// sofatracer 中自定义的拦截器,用于对链接操做、db操做等进行拦截埋点
protected List<Interceptor> interceptors;
protected List<Interceptor> dataSourceInterceptors;
}
复制代码
Interceptor 主要包括如下三种类型:
以 StatementTracerInterceptor 为例 StatementTracerInterceptor 将将会拦截到全部 PreparedStatement 接口的方法,代码以下:
public class StatementTracerInterceptor implements Interceptor {
// tracer 类型为 client
private DataSourceClientTracer clientTracer;
public void setClientTracer(DataSourceClientTracer clientTracer) {
// tracer 对象实例
this.clientTracer = clientTracer;
}
@Override
public Object intercept(Chain chain) throws Exception {
// 记录当前系统时间
long start = System.currentTimeMillis();
String resultCode = SofaTracerConstant.RESULT_SUCCESS;
try {
// 开始一个 span
clientTracer.startTrace(chain.getOriginalSql());
// 执行
return chain.proceed();
} catch (Exception e) {
resultCode = SofaTracerConstant.RESULT_FAILED;
throw e;
} finally {
// 这里计算执行时间 System.currentTimeMillis() - start
// 结束一个 span
clientTracer.endTrace(System.currentTimeMillis() - start, resultCode);
}
}
}
复制代码
整体思路是,Datasource 经过组合的方式自定义一个代理类(实际上也能够理解为适配器模式中的对象适配模型方式),对全部目标对象的方式进行代理拦截,在执行具体的 sql 或者链接操做以前建立 datasource 的 span,在操做结束以后结束 span,并进行上报。
消息框架组件包括不少,像常见的 RocketMQ、Kafka 等;处理各个组件本身提供的客户端以外,像 Spring 就提供了不少消息组件的封装,包括Spring Cloud Stream、Spring Integration、Spring Message 等等。SOFATracer 基于 Spring Message 标准实现了对常见消息组件和 Spring Cloud Stream 的埋点支持,同时也提供了基于 RocketMQ 客户端模式的埋点实现。
spring-messaging 模块为集成 messaging api 和消息协议提供支持。这里咱们先看一个 pipes-and-filters 架构模型:
spring-messaging 的 support 模块中提供了各类不一样的 MessageChannel 实现和 channel interceptor 支持,所以在对 spring-messaging 进行埋点时咱们天然就会想到去使用 channel interceptor。
// SOFATracer 实现的基于 spring-messaging 消息拦截器
public class SofaTracerChannelInterceptor implements ChannelInterceptor, ExecutorChannelInterceptor {
// todo trace
}
// THIS IS ChannelInterceptor
public interface ChannelInterceptor {
// 发送以前
@Nullable
default Message<?> preSend(Message<?> message, MessageChannel channel) {
return message;
}
// 发送后
default void postSend(Message<?> message, MessageChannel channel, boolean sent) {
}
// 完成发送以后
default void afterSendCompletion(Message<?> message, MessageChannel channel, boolean sent, @Nullable Exception ex) {
}
// 接收消息以前
default boolean preReceive(MessageChannel channel) {
return true;
}
// 接收后
@Nullable
default Message<?> postReceive(Message<?> message, MessageChannel channel) {
return message;
}
// 完成接收消息以后
default void afterReceiveCompletion(@Nullable Message<?> message, MessageChannel channel, @Nullable Exception ex) {
}
}
复制代码
能够看到 ChannelInterceptor 实现了消息传递全生命周期的管控,经过暴露出来的方法,能够轻松的实现各个阶段的扩展埋点。
RocketMQ 自己是提供了对 Opentracing 规范支持的,因为其支持的版本较高,与 SOFATracer 所实现的 Opentracing 版本不一致,因此在必定程度上不兼容;所以 SOFATracer(opentracing 0.22.0 版本)自身又单独提供了 RocketMQ 的插件。
RocketMQ 埋点实际上是经过两个 hook 接口来完成,实际上在 RocketMQ 的官方文档中貌似并无提到这两个点。
// RocketMQ 消息消费端 hook 接口埋点实现类
public class SofaTracerConsumeMessageHook implements ConsumeMessageHook {
}
// RocketMQ 消息发送端 hook 接口埋点实现类
public class SofaTracerSendMessageHook implements SendMessageHook {}
复制代码
首先是 SendMessageHook 接口,SendMessageHook 接口提供了两个方法,sendMessageBefore 和 sendMessageAfter,SOFATracer 在实现埋点时,sendMessageBefore 中用来解析和构建 span,sendMessageAfter 中用于拿到结果真后结束 span。
一样的,ConsumeMessageHook 中也提供了两个方法(consumeMessageBefore和consumeMessageAfter),能够提供给 SOFATracer 来从消息中解析出透传的 tracer 信息而后再将 tracer 信息透传到下游链路中去。
SOFATracer 中的 redis 埋点是基于 spring data redis 实现的,没有针对具体的 redis 客户端来埋点。另外 redis 埋点部分参考的是开源社区opentracing-spring-cloud-redis-starter中的实现逻辑。
redis 的埋点实现与 Datasource 的锚点实现基本思路是一致的,都是经过一层代理来是实现的拦截。sofa-tracer-redis-plugin
中对全部的 redis 操做都经过 RedisActionWrapperHelper 进行了一层包装,在执行具体的命令先后经过 SOFATracer 本身提供的 API 进行埋点操做。代码以下:
public <T> T doInScope(String command, Supplier<T> supplier) {
// 构建 span
Span span = buildSpan(command);
return activateAndCloseSpan(span, supplier);
}
// 在 span 的生命周期内执行具体命令
private <T> T activateAndCloseSpan(Span span, Supplier<T> supplier) {
Throwable candidateThrowable = null;
try {
// 执行命令
return supplier.get();
} catch (Throwable t) {
candidateThrowable = t;
throw t;
} finally {
if (candidateThrowable != null) {
// ...
} else {
// ...
}
// 经过 tracer api 结束一个span
redisSofaTracer.clientReceiveTagFinish((SofaTracerSpan) span, "00");
}
}
复制代码
除此以后 mongodb 的埋点也是基于 spring data 实现,埋点的实现思路和 redis 基本相同,这里就不在单独分析。
本文对蚂蚁金服分布式链路组件 SOFATracer 的埋点机制作了简要的介绍;从各个组件的埋点机制来看,总体思路就是对组件操做进行包装,在请求或者命令执行的先后进行 span 构建和上报。目前一些主流的链路跟踪组件像 brave 也是基于此思路,区别在于 brave 并不是是直接基于 opentracing 规范进行编码,而是其本身封装了一整套 api ,而后经过面向 opentracing api 进行一层适配;另一个很是流行的 skywalking 则是基于 java agent 实现,埋点实现的机制上与 SOFATracer 和 brave 不一样。