原文連接 一文詳解螞蟻金服分佈式鏈路組件 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 不一樣。