glmapper

一文详解蚂蚁金服分布式链路组件 SOFATracer 的埋点机制

字数统计: 3.1k阅读时长: 12 min
2020/01/20 Share

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

GITHUB 地址:https://github.com/sofastack/sofa-tracer/pulls (欢迎 star)
官方文件地址:https://www.sofastack.tech/projects/sofa-tracer/overview/

2018 年末时至 2019 年初,SOFA 团队发起过 剖析-sofatracer-框架 的源码解析系列文章。这个系列中,基本对 SOFATracer 所提供的能力及实现原理都做了比较全面的分析,有兴趣的同学可以看下。

从官方文档及 PR 来看,目前 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

大多数能力提供在 3.x 版本,2.x 版本从官方 issue 中可以看到后续将不在继续提供新的功能更新;这也是和 SpringBoot 宣布不在继续维护 1.x 版本有关系。

本文将从插件的角度来分析,SOFATracer 是如何实现对上述组件进行埋点的;通过本文,除了了解 SOFATracer 的埋点机制之外,也可以对上述组件的基本扩展机制以及基本原理有一点学习。

标准 Servlet 规范埋点原理

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

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

web 应用程序一般作为请求的接收方,在 Tracer 中应用是作为 server 存在的,其在解析 SpanContext 时所对应的事件为 sr (server receive)。

SOFATracer 在 sofa-tracer-springmvc-plugin 插件中解析及产生 span 的过程大致如下:

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

当然这里面还会设计到其他很多细节,比如给 span 设置哪些 tag 属性、如果处理异步线程透传等等。本篇不展开细节探讨,有兴趣的同学可以自行阅读代码或者和我交流。

Dubbo 埋点原理

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 类:

1
2
3
4
@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出来用于传递。

1
2
3
4
5
6
7
8
9
10
11
12
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 客户端埋点原理

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

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

1
2
3
4
5
6
7
8
9
10
11
12
@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;
}

Datasource 埋点原理

和标准 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

1
2
3
4
5
6
7
public abstract class BaseDataSource implements DataSource {
// 实际被代理的 datasource
protected DataSource delegate;
// sofatracer 中自定义的拦截器,用于对连接操作、db操作等进行拦截埋点
protected List<Interceptor> interceptors;
protected List<Interceptor> dataSourceInterceptors;
}

Interceptor 主要包括以下三种类型:

以 StatementTracerInterceptor 为例 StatementTracerInterceptor 将将会拦截到所有 PreparedStatement 接口的方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 埋点实现原理

spring-messaging 模块为集成 messaging api 和消息协议提供支持。这里我们先看一个 pipes-and-filters 架构模型:

spring-messaging 的 support 模块中提供了各种不同的 MessageChannel 实现和 channel interceptor 支持,因此在对 spring-messaging 进行埋点时我们自然就会想到去使用 channel interceptor。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 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 埋点实现原理

RocketMQ 本身是提供了对 Opentracing 规范支持的,由于其支持的版本较高,与 SOFATracer 所实现的 Opentracing 版本不一致,所以在一定程度上不兼容;因此 SOFATracer(opentracing 0.22.0 版本)自身又单独提供了 RocketMQ 的插件。

RocketMQ 埋点其实是通过两个 hook 接口来完成,实际上在 RocketMQ 的官方文档中貌似并没有提到这两个点。

1
2
3
4
5
// 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 信息透传到下游链路中去。

redis 埋点原理

SOFATracer 中的 redis 埋点是基于 spring data redis 实现的,没有针对具体的 redis 客户端来埋点。另外 redis 埋点部分参考的是开源社区opentracing-spring-cloud-redis-starter中的实现逻辑。

redis 的埋点实现与 Datasource 的锚点实现基本思路是一致的,都是通过一层代理来是实现的拦截。sofa-tracer-redis-plugin 中对所有的 redis 操作都通过 RedisActionWrapperHelper 进行了一层包装,在执行具体的命令前后通过 SOFATracer 自己提供的 API 进行埋点操作。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 不同。

参考

原文作者:GuoLei Song

原文链接:http://www.glmapper.com/2020/01/20/sofa-tracer-integration-analysis/

发表日期:January 20th 2020, 11:43:56 am

更新日期:February 2nd 2020, 2:56:42 pm

版权声明:转载请注明出处

CATALOG
  1. 1. 标准 Servlet 规范埋点原理
  2. 2. Dubbo 埋点原理
  3. 3. http 客户端埋点原理
  4. 4. Datasource 埋点原理
  5. 5. 消息埋点
    1. 5.1. Spring Messaging 埋点实现原理
    2. 5.2. RocketMQ 埋点实现原理
  6. 6. redis 埋点原理
  7. 7. 总结
  8. 8. 参考