一文詳解螞蟻金服分佈式鏈路組件 SOFATracer 的埋點機制

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

  • 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 版本有關係。redis

本文將從插件的角度來分析,SOFATracer 是如何實現對上述組件進行埋點的;經過本文,除了瞭解 SOFATracer 的埋點機制以外,也能夠對上述組件的基本擴展機制以及基本原理有一點學習。spring

標準 Servlet 規範埋點原理

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 的過程大體以下:

  • 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 類:

@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 客戶端埋點原理

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;
}
複製代碼

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

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 埋點實現原理

spring-messaging 模塊爲集成 messaging api 和消息協議提供支持。這裏咱們先看一個 pipes-and-filters 架構模型:

spring-messaging 的 support 模塊中提供了各類不一樣的 MessageChannel 實現和 channel interceptor 支持,所以在對 spring-messaging 進行埋點時咱們天然就會想到去使用 channel interceptor。

// SOFATracer 實現的基於 spring-messaging 消息攔截器
public class SofaTracerChannelInterceptor implements ChannelInterceptorExecutorChannelInterceptor {
    // 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 的官方文檔中貌似並無提到這兩個點。

// 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 進行埋點操做。代碼以下:

public <T> doInScope(String command, Supplier<T> supplier) {
    // 構建 span
    Span span = buildSpan(command);
    return activateAndCloseSpan(span, supplier);
}

// 在 span 的生命週期內執行具體命令
private <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 不一樣。

參考

相關文章
相關標籤/搜索