<SOFA:Channel/>,有趣實用的分佈式架構頻道。
回顧視頻以及 PPT 查看地址見文末。歡迎加入直播互動釘釘羣 : 21992058,不錯過每場直播。
本文根據 SOFAChannel#15 直播分享整理,主題:分佈式鏈路組件 SOFATracer 埋點機制解析。html
你們好,我是宋國磊,花名衛恆,是 SOFATracer 的開源負責人。今天要和你們分享的是分佈式鏈路組件 SOFATracer 埋點機制解析,將經過具體 Demo 演示快速上手 SOFATracer,同時介紹 SOFATracer 功能點,並詳細介紹其核心關鍵「埋點機制」的原理。java
SOFATracer 是螞蟻金服開源的基於 OpenTracing 規範 的分佈式鏈路跟蹤系統,其核心理念就是經過一個全局的 TraceId 將分佈在各個服務節點上的同一次請求串聯起來。經過統一的 TraceId 將調用鏈路中的各類網絡調用狀況以日誌的方式記錄下來同時也提供遠程彙報到 Zipkin 進行展現的能力,以此達到透視化網絡調用的目的。git
SOFATracer:https://github.com/sofastack/sofa-tracergithub
SOFATracer 做爲 SOFAStack 中的分佈式鏈路組件,也伴隨着 SOFAStack 走過了兩年的時間,在此首先對兩年來對 SOFATracer 保持關注而且參與社區建設的同窗表示感謝,也但願你們可以繼續關注 SOFAStack 的發展,也歡迎更多的同窗加入到 SOFAStack 的社區參與中來。web
今天的分享內容主要將會圍繞如下三個部分展開:redis
關於 SOFATracer 更多的問題也歡迎在 Github 上跟咱們交流。spring
首先簡單介紹一下 SOFATracer。上圖展現的是 SOFATracer 目前所包括的基本能力和所支持的插件。下面虛線框中綠色背景部分,是 SOFATracer 提供的基本功能,具體能夠參考官方文檔描述。上面虛線框中是 SOFATracer 目前所支持的組件,大概分了如下幾種類型:客戶端、Web、數據存儲、消息、RPC、Spring Cloud。sql
以前社區也發起過 剖析 | SOFATracer 框架 的源碼解析系列文章,在此係列中對 SOFATracer 所提供的能力及實現原理都作了比較全面的分析,有興趣的同窗能夠看下。apache
今天主要聊一下埋點機制。不一樣組件的埋點機制也是有很大的差別,SOFATracer 是如何實現對上述組件進行埋點的,下面就詳細分析下不一樣組件的埋點機制。api
目前 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。
大多數能力提供在 3.x 版本,2.x 版本從官方 issue 中能夠看到後續將再也不繼續提供新的功能更新;這也是和 SpringBoot 宣佈再也不繼續維護 1.x 版本有關係。
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 的過程大體以下:
固然這裏面還會設計到其餘不少細節,好比給 Span 設置哪些 tag 屬性、若是處理異步線程透傳等等。本次分享就不展開細節探討,有興趣的同窗能夠自行閱讀代碼或者和咱們交流。
Dubbo 埋點在 SOFATracer 中實際上提供了兩個插件,分別用於支持 Dubbo 2.6.x 和 Dubbo 2.7.x;Dubbo 埋點也是基於 Filter ,此 Filter 是 Dubbo 提供的 SPI 擴展-調用攔截擴展 機制實現。
像 Dubbo 或者 SOFARPC 等 RPC 框架的埋點,一般須要考慮的點比較多。首先, RPC 框架分客戶端和服務端,因此在埋點時 RPC 的客戶端和服務端必需要有所區分;再者就是 RPC 的調用方式包括不少種,如常見的同步調用、異步調用、oneway 等等,調用方式不一樣,所對應的 Span 的結束時機也不一樣,重要的是基本全部的 RPC 框架都會使用線程池用來發起和處理請求,那麼如何保證 SOFATracer 在多線程環境下不串也很重要。
另外 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 模塊中提供了各類不一樣的 Message Channel 實現和 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 來從消息中解析出透傳的 SOFATracer 信息而後再將 SOFATracer 信息透傳到下游鏈路中去。
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 不一樣。
以上就是本期分享的所有內容,若是你們對 SOFATracer 感興趣,也能夠在羣內或者 Github 上與咱們交流。
SOFATracer:https://github.com/sofastack/sofa-tracer
本期 Demo 工程地址:https://github.com/glmapper/tracers-guides