插件
緊接skywalking-agent初始化, skywalking使用微內(nèi)核架構,對每一種框架的支持都是通過插件形式實現(xiàn)的
使用bytebuddy可以非常友好的進行切面編程,但skywalking畢竟是帶有特定主題的切面:APM
于是skywalking把APM相關的API(例如與OAP通信)進行進一步封裝,并抽象出監(jiān)控的字節(jié)碼增強插件定義:AbstractClassEnhancePluginDefine,它的實現(xiàn)即為各個插件的定義類

這些插件實現(xiàn)類可以基于skywalking-agent-core中提供的API輕松完成上報(一般指創(chuàng)建span)
分布式鏈路
結合整體示意圖

span
skywalking-agent字節(jié)碼增強大部分都是上報一個span給OAP,一個span就是分布式鏈路中的一個節(jié)點,包含主要屬性:
-
spanidID -
endpointName名稱,一般是url路徑或方法名稱 -
serviceCode節(jié)點運行的服務名稱 -
component描述監(jiān)控的框架,如SpringMVC/Fegin/Dubbo等 -
isError該節(jié)點是否異常 -
startTime&endTime開始時間和結束時間,可計算出節(jié)點運行的時長 -
peerip+端口 -
typespan類型,下面細說 -
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)用鏈路,如下

上下文
當發(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 -
continued以ContextSnapshot為參數(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)接口匹配
ConstructorInterceptPoint和InstanceMethodsInterceptPoint下面介紹
ClassStaticMethodsEnhancePluginDefine
針對靜態(tài)方法攔截定義,繼承者需實現(xiàn)
/**
* 構造器切點,可以是多個
* @return
*/
@Override
public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
return new ConstructorInterceptPoint[0];
}
/**
* 方法切點,可以是多個
* @return InstanceMethodsInterceptPoint
*/
@Override
public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
return new InstanceMethodsInterceptPoint[0];
}
ConstructorInterceptPoint和InstanceMethodsInterceptPoint下面介紹
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上報和采集