Skywalking源碼研究之a(chǎn)gent插件與鏈路監(jiān)控

插件

緊接skywalking-agent初始化, skywalking使用微內(nèi)核架構,對每一種框架的支持都是通過插件形式實現(xiàn)的

使用bytebuddy可以非常友好的進行切面編程,但skywalking畢竟是帶有特定主題的切面:APM

于是skywalking把APM相關的API(例如與OAP通信)進行進一步封裝,并抽象出監(jiān)控的字節(jié)碼增強插件定義:AbstractClassEnhancePluginDefine,它的實現(xiàn)即為各個插件的定義類

img_1.png

這些插件實現(xiàn)類可以基于skywalking-agent-core中提供的API輕松完成上報(一般指創(chuàng)建span)

分布式鏈路

結合整體示意圖


img_2.png
span

skywalking-agent字節(jié)碼增強大部分都是上報一個span給OAP,一個span就是分布式鏈路中的一個節(jié)點,包含主要屬性:

  • spanid ID
  • endpointName 名稱,一般是url路徑或方法名稱
  • serviceCode 節(jié)點運行的服務名稱
  • component 描述監(jiān)控的框架,如SpringMVC/Fegin/Dubbo等
  • isError 該節(jié)點是否異常
  • startTime&endTime 開始時間和結束時間,可計算出節(jié)點運行的時長
  • peer ip+端口
  • type span類型,下面細說
  • traceId 事務ID
  • segmentId 片段ID
  • parentSpanId 父ID
trace

每個span有一個traceId屬性標識所屬事務,多個相同traceId的span共同組成一個事務(trace),它們通過parentSpanId形成了一個鏈路,鏈路不絕對是鏈條的結構,也有可能是樹形結構(一個父節(jié)點可能有多個子節(jié)點)

segment

在分布式事務中,一個trace中的span隸屬不同的線程,為了區(qū)分,引入了segment做為區(qū)分

segment是一個trace中隸屬相同線程的span集合,因此也可以說多個相同線程的span組成segment,多個segment組成trace

同時,segment也是探針進行數(shù)據(jù)上報的基本單位

span類型

span的type屬性表示span的類型,包含三種

  • Entry 代表某個segment的入口span,就是第一個span,比如使用@RequestMapping定義的接口、dubbo的服務提供者
  • Local 代表普通的 Java 方法, 它與遠程服務無關,所有本地方法調(diào)用都是local類型,包括異步線程調(diào)用
  • Exit 代表某個segment的出口span,例如訪問數(shù)據(jù)庫、使用Fegin調(diào)用其他服務、Dubbo的服務調(diào)用者
ui

skywalking-ui直觀的展示了整個調(diào)用鏈路,如下


img_3.png

上下文

當發(fā)生A->B調(diào)用時,已知通過相關技術插件可實現(xiàn):

  • A發(fā)起調(diào)用時上報
  • B被調(diào)用時上報

但問題是OAP如何得知兩個span隸屬一個trace,或者如何得知兩個span是否屬于一個segment?

實際上,A、B在上報span時已提交相同的traceId,OAP在分析數(shù)據(jù)時才能展示出調(diào)用鏈路關系,所以問題的關鍵是A,B兩個span如何共享上下文信息,涉及到主要三種情況

  • 單線程調(diào)用 即普通的A方法調(diào)用B方法
  • 跨線程調(diào)用 A方法中異步調(diào)用B方法
  • 跨進程調(diào)用 即分布式調(diào)用,A、B方法屬于兩個進程

單線程調(diào)用

普通方法調(diào)用比較簡單,skywalking-agent-core中提供的ContextManager使用ThreadLocal即可在上報span時注入上下文信息,實現(xiàn)A、B方法的上下文信息共享

public class ContextManager implements BootService {
    // 使用ThreadLocal實現(xiàn)線程內(nèi)的上下文
    private static ThreadLocal<AbstractTracerContext> CONTEXT = new ThreadLocal<AbstractTracerContext>();
}

這一部分由于skywalking-agent-core已經(jīng)封裝好,所以插件不需再做額外處理

跨線程調(diào)用

當出現(xiàn)跨線程異步調(diào)用時,ThreadLocal就失效了,此時上下文信息就需要在線程之間傳輸

ContextManager提供了兩個方法來支持跨線程的上下文傳遞

  • capture 生成上下文信息的快照ContextSnapshot,信息一般來源為當前線程的ThreadLocal
  • continuedContextSnapshot為參數(shù)重現(xiàn)上下文信息(存入當前線程的ThreadLocal)

ContextSnapshot的具體傳遞就需要插件自己來實現(xiàn),步驟如下

  • 父線程調(diào)用ContextManager#capture方法生成上下文快照
  • 父線程調(diào)用子線程,并通過修改參數(shù)等方式傳遞快照至子線程
  • 子線程使用ContextManager#continued方法,傳入快照信息,重現(xiàn)父線程的上下文

跨進程實現(xiàn)原理

當發(fā)生A->B分布式調(diào)用時,由于跨進程,ThreadLocal肯定行不通,A與B之間的上下文傳遞必然是序列化后通過網(wǎng)絡傳輸?shù)?/strong>

core中提供了可序列化的網(wǎng)絡傳輸載體對象:ContextCarrier,同時ContextManager提供了兩個方法來支持ContextCarrier的注入和解壓

  • inject 將當前上下文注入到ContextCarrier對象
  • extract 將ContextCarrier對象解壓到當前上下文

ContextCarrier的傳遞方式是不同插件根據(jù)實際組件自己實現(xiàn)的,比如:

  • Fegin調(diào)用(Http)時是通過把ContextCarrier放到請求頭實現(xiàn)的
  • Dubbo調(diào)用時是通過Dubbo框架提供的附件傳遞的
  • Kafka是通過消息的形式傳遞的(todo 這里待細看)

以Fegin調(diào)用為例,F(xiàn)egin發(fā)起調(diào)用的客戶端攔截器是:DefaultHttpClientInterceptor

@Override
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
                         MethodInterceptResult result) {
    // 創(chuàng)建載體
    ContextCarrier contextCarrier = new ContextCarrier(); 
    // 創(chuàng)建出口span,內(nèi)部執(zhí)行了inject方法注入載體
    AbstractSpan span = ContextManager.createExitSpan(operationName, contextCarrier, remotePeer);
    ...
    // 獲取上下文信息的每一項
    CarrierItem next = contextCarrier.items();
    while (next.hasNext()) {
        next = next.next();
        List<String> contextCollection = new ArrayList<String>(1);
        contextCollection.add(next.getHeadValue());
        // 加入請求的header中
        headers.put(next.getHeadKey(), contextCollection);
    }
    ...
}

對應的服務端一般是spring的@RequestMapping接口,對應的攔截器是RequestMappingMethodInterceptor

@Override
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
                         MethodInterceptResult result) throws Throwable {

    ...
    // 創(chuàng)建載體
    final ContextCarrier contextCarrier = new ContextCarrier();
    // http請求
    final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
    CarrierItem next = contextCarrier.items();
    // 循環(huán)上下文信息的每一項
    while (next.hasNext()) {
        next = next.next();
        // 從header中獲取對應項,裝載到載體上
        next.setHeadValue(httpServletRequest.getHeader(next.getHeadKey()));
    }
    // 內(nèi)部調(diào)用extract,將載體解壓到上下文
    AbstractSpan span = ContextManager.createEntrySpan(operationName, contextCarrier);
}

插件定義

AbstractClassEnhancePluginDefine

skywalking-agent的插件首先要有一個類增強插件定義,skywalking-agent抽象出插件定義的規(guī)范:AbstractClassEnhancePluginDefine,各插件要給出具體實現(xiàn),同時skywalking-agent-core情況有進一步抽象了兩種實現(xiàn)

  • ClassInstanceMethodsEnhancePluginDefine 針對類實例攔截定義
  • ClassStaticMethodsEnhancePluginDefine 針對靜態(tài)方法攔截定義
ClassInstanceMethodsEnhancePluginDefine

ClassInstanceMethodsEnhancePluginDefine,是針對對類實例的一種增強插件定義的抽象,插件通過繼承它可以實現(xiàn)對實例的攔截,只需實現(xiàn)如下方法:

/**
 * 需要被攔截Class
 * @return
 */
@Override
protected ClassMatch enhanceClass() {
    return null;
}

/**
 * 構造器切點,可以是多個
 * @return 
 */
@Override
public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
    return new ConstructorInterceptPoint[0];
}

/**
 * 方法切點,可以是多個
 * @return InstanceMethodsInterceptPoint
 */
@Override
public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
    return new InstanceMethodsInterceptPoint[0];
}

ClassMatch用來匹配類,agent-core提供如下常用API來實現(xiàn)類匹配

  • NameMatch.byName 根據(jù)名稱匹配
  • ClassAnnotationMatch.byClassAnnotationMatch 根據(jù)類注解匹配
  • MethodAnnotationMatchbyMethodAnnotationMatch 根據(jù)類中方法注解匹配
  • HierarchyMatch.byHierarchyMatch 根據(jù)父類或?qū)崿F(xiàn)接口匹配

ConstructorInterceptPointInstanceMethodsInterceptPoint下面介紹

ClassStaticMethodsEnhancePluginDefine

針對靜態(tài)方法攔截定義,繼承者需實現(xiàn)

/**
 * 構造器切點,可以是多個
 * @return 
 */
@Override
public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
    return new ConstructorInterceptPoint[0];
}

/**
 * 方法切點,可以是多個
 * @return InstanceMethodsInterceptPoint
 */
@Override
public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
    return new InstanceMethodsInterceptPoint[0];
}

ConstructorInterceptPointInstanceMethodsInterceptPoint下面介紹

InstanceMethodsInterceptPoint

無論是實例還是靜態(tài)方法,都需要InstanceMethodsInterceptPoint數(shù)組來進行方法切點和攔截器,主要包含如下屬性

public interface InstanceMethodsInterceptPoint {
    /**
     * 方法的匹配
     */
    ElementMatcher<MethodDescription> getMethodsMatcher();

    /**.
     *  返回一個攔截器全類名,所有攔截器必須實現(xiàn)InstanceMethodsAroundInterceptor 接口
     */
    String getMethodsInterceptor();

    /**
     * 是否要覆蓋原方法入?yún)?     */
    boolean isOverrideArgs();
}

其中指定的攔截器都需要實現(xiàn)InstanceMethodsAroundInterceptor接口

InstanceMethodsAroundInterceptor

具體的攔截代碼,主要實現(xiàn)如下方法

public interface InstanceMethodsAroundInterceptor {
    /**
     * 前置處理
     */
    void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
        MethodInterceptResult result) throws Throwable;

    /**
     * 后置處理
     */
    Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
        Object ret) throws Throwable;

    /**
     * 異常處理
     */
    void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments,
        Class<?>[] argumentsTypes, Throwable t);
}
ConstructorInterceptPoint

與InstanceMethodsInterceptPoint基本差不多,只不過針對的是構造方法

總結

插件的開發(fā)基本就是對skywalking-agent-core定義的一些抽象的具體實現(xiàn),最總打成jar包,放入plugins目錄,插件即可生效

注:插件的resources目錄中一定要添加skywalking-plugin.def文件,內(nèi)容是

{name}={增強插件定義全路徑名}

可以是多個,以springmvc舉例如下

spring-mvc-annotation-5.x=org.apache.skywalking.apm.plugin.spring.mvc.v5.define.ControllerInstrumentation
spring-mvc-annotation-5.x=org.apache.skywalking.apm.plugin.spring.mvc.v5.define.RestControllerInstrumentation

同時skywalking-agent-core提供豐富的api用于插件攔截后的上報,詳見skywalking上報和采集

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容