一、問題描述
當我們的業務發展到一定階段的時候,系統的複雜度往往會非常高,不再是一個簡單的單體應用所能夠承載的,隨之而來的是系統架構的不斷升級與演變。一般對於大型的To C的互聯網企業來説,整個系統都是構建於微服務的架構之上,原因是To C的業務有着天生的微服務化的訴求:需求迭代快、業務系統多、領域劃分多、鏈路調用關係複雜、容忍延遲低、故障傳播快。微服務化之後帶來的問題也很明顯:服務的管理複雜、鏈路的梳理複雜、系統故障會在整個鏈路中迅速傳播。
這裏我們不討論鏈路的依賴或服務的管理等問題,本次要解決的問題是怎麼防止單個系統故障影響整個系統。這是一個複雜的問題,因為服務的傳播特性,一個服務出現故障,其他依賴或被依賴的服務都會受到影響。為了找到解決問題的辦法,我們試着通過5why提問法來找答案。
PS:這裏説的系統故障,是特指由於慢調用、慢查詢等影響系統性能而導致的系統故障。
Q1
怎麼防止單個系統故障影響整個系統?
A:避免耽擱系統的故障的傳播。
Q2
怎麼避免故障的傳播?
A:找到系統故障的原因,解決故障。
Q3
怎麼找到故障的原因?
A:找到並優化系統中耗時長的方法。
Q4
怎麼找到系統中耗時長的方法?
A:通過對特定方法進行AOP攔截。
Q5
怎麼對特定方法做AOP攔截?
A:通過字節碼增強的方式對目標方法做攔截並植入內聯代碼。
通過5why提問法,我們得到了解決問題的方法,我們需要對目標方法做AOP攔截,統計業務方法及各個子方法的耗時,得到所有方法的耗時分佈,快速定位到比較慢的方法,最後找出業務系統的性能瓶頸在哪裏。
二、方案選型
我們知道AOP是一種編碼思想,跟OOP不同,AOP是將特定的方法邏輯,以切面的形式編織到目標方法中,這裏不再贅述AOP的思想。
如果在網上搜一下“AOP的實現方式”,你會得到大致相同的結果:AOP的實現方式是通過動態代理或Cglib代理。其實這不太準確,準確的來説,AOP可以通過代理或Advice兩種方式來實現。請注意這裏説的Advice並不是Spring所依賴的aspectj中的Advice,而是一種代碼織入的技術,它與代理的區別在於,代碼織入技術不需要創建代理類。
如果用圖形表示的話,可以更簡單更直觀的感受到兩者的區別。代碼織入的方式,不會創建代理類,而是直接在目標方法的方法體的前後織入一段內聯的代碼,以達到增強的效果,如下圖所示:
我選擇代碼織入技術而不是AOP,原因是可以避免創建大量的代理類增加元空間的內存佔用,另外代碼織入技術更底層一些,能實現的能力更強,此外內聯代碼會隨着原方法一起執行,性能也更好。
有了具體的技術選型的方案之後,我們還需要確定該方案的建設目標,以下整理了一些基本的目標:
三、技術方案
代碼織入的時機也有多種方式,比如Lombok是通過在編譯器對代碼進行織入,主要依賴的是在 Javac 編譯階段利用“Annotation Processor”,對自定義的註解進行預處理後生成代碼然後織入;其他的像CGLIB、ByteBuddy等框架是在運行時對代碼進行織入的,主要依賴的是Java Agent技術,通過JVMTI的接口實現在運行時對字節碼進行增強。
本次的技術方案,用一句話可以概括為:通過字節碼增強,對指定的目標方法進行攔截,並在方法前後織入一段內聯代碼,在內聯代碼中計算目標方法的耗時,最後將統計到的方法信息進行分析。
1 項目結構
整個方案的代碼實現非常簡單,用一個圖描述如下:
項目的代碼結構如下所示,核心代碼非常少:
2 核心組件
其中Enhancer是增強器的入口類,在增強器啓動時會掃描所有的插件:EnhancedPlugin。
EnhancedPlugin表示的是一個執行代碼增強的插件,其中定義了幾個抽象方法,需要由用户自己實現:
/**
* 執行代碼增強的插件
*
* @auther houyi.wh
* @date 2023-08-15 20:12:01
* @since 0.0.1
*/
public abstract class EnhancedPlugin {
/**
* 匹配特定的類型
*
* @return 類型匹配器
* @since 0.0.1
*/
public abstract ElementMatcher.Junction<TypeDescription> typeMatcher();
/**
* 匹配特定的方法
*
* @return 方法匹配器
* @since 0.0.1
*/
public abstract ElementMatcher.Junction<MethodDescription> methodMatcher();
/**
* 負責執行增強邏輯的攔截器
*
* @return 攔截器
* @since 0.0.1
*/
public abstract Class<? extends Interceptor> interceptorClass();
}
此外EnhancedPlugin中還需要指定一個Interceptor,一個Interceptor是對目標方法執行代碼增強的攔截器,主要的攔截邏輯定義在Interceptor中。
3 增強原理
掃描到EnhancedPlugin之後,會構建ByteBuddy的AgentBuilder,主要的構建過程為:
(1)找到所有匹配的類型
(2)找到所有匹配的方法
(3)傳入執行代碼增強的Transformer
最後通過AgentBuilder.install方法將增強的代碼Transformer,傳遞給Instrumentation實例,實現運行時的字節碼retransformation。
這裏的Transformer是由Advice負責實現的,而在Advice中實現了增強邏輯的dispatch,即根據不同的EnhancedPlugin可以將增強邏輯交給指定的Interceptor攔截器去實現,主要在攔截器中抽象了兩個方法。一個是beforeMethod,負責在目標方法調用之前進行攔截:
/**
* 在方法執行前進行切面
*
* @param pluginName 綁定在該目標方法上的插件名稱
* @param target 目標方法所屬的對象,需要注意的是@Advice.This不能標識構造方法
* @param method 目標方法
* @param arguments 方法參數
* @return 方法執行返回的臨時數據
* @since 0.0.1
*/
@Advice.OnMethodEnter
public static <T> T beforeMethod(
// 接收動態傳遞過來的參數
@PluginName String pluginName,
// optional=true,表示this註解可以接收:構造方法或靜態方法(會將this賦值為null),而不報錯
@Advice.This(optional = true) Object target,
// 目標方法
@Advice.Origin Method method,
// nullIfEmpty=true,表示可以接收空參數
@Advice.AllArguments(nullIfEmpty = true) Object[] arguments
) {
String[] parameterNames = new String[]{};
T transmitResult = null;
try {
InstanceMethodInterceptor<T> interceptor = getInterceptor(pluginName);
// 執行beforeMethod的攔截邏輯
transmitResult = interceptor.beforeMethod(target, method, parameterNames, arguments);
} catch (Throwable e) {
InternalLogger.AutoDetect.INSTANCE.error("InstanceMethodAdvice beforeMethod occurred error", e);
}
return transmitResult;
}
一個是afterMethod,負責在目標方法被調用之後進行攔截:
/**
* 在方法執行後進行切面
*
* @param pluginName 綁定在該目標方法上的插件名稱
* @param transmitResult beforeMethod所傳遞過來的臨時數據
* @param originResult 目標方法原始返回結果,如果目標方法是void型,則originResult為null
* @param throwable 目標方法拋出的異常
*/
@Advice.OnMethodExit(onThrowable = Throwable.class)
public static <T> void afterMethod(
// 接收動態傳遞過來的參數
@PluginName String pluginName,
// beforeMethod傳遞過來的臨時數據
@Advice.Enter T transmitResult,
// typing=DYNAMIC,表示可以接收void類型的方法
@Advice.Return(typing = Assigner.Typing.DYNAMIC) Object originResult,
// 目標方法自己拋出的運行時異常,可以在方法中進行捕獲,看具體的需求
@Advice.Thrown Throwable throwable
) {
try {
InstanceMethodInterceptor<T> interceptor = getInterceptor(pluginName);
// 執行afterMethod的攔截邏輯
interceptor.afterMethod(transmitResult, originResult);
} catch (Throwable e) {
InternalLogger.AutoDetect.INSTANCE.error("InstanceMethodAdvice afterMethod occurred error", e);
}
}
Advice的特點是:不會更改目標類的字節碼結構,比如:不會增加字段、方法,不會修改方法的參數等等。
四、方案實現
該增強組件是一個輕量化的通用的增強包,幾乎可以實現你能想到的任意功能,本次我們的需求是要採集特定目標方法的方法耗時,以便分析出方法的性能瓶頸。
1 定義插件
基於該組件我們需要實現兩個類:一個是插件,一個是攔截器。
插件中主要實現的是兩個方法:匹配特定的類型,匹配特定的方法。
這裏的類型匹配或方法匹配,是採用的ByteBuddy的ElementMatcher,它是一個非常靈活的匹配器,在ElementMatchers中有很多內置的匹配實現,只要你能想到的匹配方式,通過它幾乎都能實現匹配。
匹配特定的類型目前我定義了兩種匹配方式,一種是根據類名(或者包名),一種是根據方法上的註解,具體的代碼實現如下:
public class MethodCallPlugin extends EnhancedPlugin {
private final List<String> anyClassNameStartWith;
private final List<String> anyAnnotationNameOnMethod;
/**
* 方法調用攔截插件
*
* @param anyClassNameStartWith 任何包路徑,或者全限定類名
* @param anyAnnotationNameOnMethod 任何方法上的註解的全限定名稱
*/
public MethodCallPlugin(List<String> anyClassNameStartWith, List<String> anyAnnotationNameOnMethod) {
boolean nameStartWithInvalid = anyClassNameStartWith == null || anyClassNameStartWith.isEmpty();
boolean annotationNameOnMethodInvalid = anyAnnotationNameOnMethod == null || anyAnnotationNameOnMethod.isEmpty();
if (nameStartWithInvalid && annotationNameOnMethodInvalid) {
throw new IllegalArgumentException("anyClassNameStartWith and anyAnnotationNameOnMethod can't be both empty");
}
this.anyClassNameStartWith = anyClassNameStartWith;
this.anyAnnotationNameOnMethod = anyAnnotationNameOnMethod;
}
@Override
public ElementMatcher.Junction<TypeDescription> typeMatcher() {
ElementMatcher.Junction<TypeDescription> anyTypes = none();
if (anyClassNameStartWith != null && !anyClassNameStartWith.isEmpty()) {
for (String classNameStartWith : anyClassNameStartWith) {
// 根據類的前綴或者全限定類名進行匹配
anyTypes = anyTypes.or(nameStartsWith(classNameStartWith));
}
}
if (anyAnnotationNameOnMethod != null && !anyAnnotationNameOnMethod.isEmpty()) {
ElementMatcher.Junction<MethodDescription> methodsWithAnnotation = none();
for (String annotationNameOnMethod : anyAnnotationNameOnMethod) {
// 根據方法上是否有特定註解進行匹配
methodsWithAnnotation = methodsWithAnnotation.or(isAnnotatedWith(named(annotationNameOnMethod)));
}
anyTypes = anyTypes.or(declaresMethod(methodsWithAnnotation));
}
return anyTypes;
}
}
匹配特定方法的邏輯就比較簡單了,可以匹配除了構造方法之外的任意方法:
public class MethodCallPlugin extends EnhancedPlugin {
@Override
public ElementMatcher.Junction<MethodDescription> methodMatcher() {
return any().and(not(isConstructor()));
}
}
2 實現攔截器
類型匹配和方法都匹配到之後,就需要實現方法增強的攔截器了:
我們需要獲取方法調用的信息,包括方法名、調用堆棧及深度、調用的耗時,所以我們需要定義三個ThreadLocal用來保存方法調用的堆棧:
/**
* 方法調用信息的攔截器
* 在方法調用之前進行攔截,將方法調用信息封裝後,放入堆棧中,
* 在方法調用之後,從堆棧中將所有方法取出來,按照進入堆棧的順序進行排序,
* 得到方法調用信息的列表,最後將該列表交給{@link MethodCallHandler}進行處理
* 如果用户指定了自己的{@link MethodCallHandler}則優先使用用户自定義的Handler進行處理
* 否則使用SDK內置的{@link MethodCallHandler.PrintLogHandler}進行處理,即將方法調用信息打印到日誌中
*
* @auther houyi.wh
* @date 2023-08-16 10:16:48
* @since 0.0.1
*/
public class MethodCallInterceptor implements InstanceMethodInterceptor<MethodCall> {
/**
* 當前方法進入方法棧的順序
* 用以最後一個方法出棧後,進行方法調用棧的排序
*
* @since 0.0.1
*/
private static final ThreadLocal<AtomicInteger> methodEnterStackOrderThreadLocal = new TransmittableThreadLocal<AtomicInteger>() {
@Override
protected AtomicInteger initialValue() {
return new AtomicInteger(0);
}
};
/**
* 當前方法調用棧
*
* @since 0.0.1
*/
private static final ThreadLocal<Deque<MethodCall>> methodStackThreadLocal = new ThreadLocal<Deque<MethodCall>>() {
@Override
protected Deque<MethodCall> initialValue() {
return new ArrayDeque<>();
}
};
/**
* 當前方法棧中所有方法調用的信息
*
* @since 0.0.1
*/
private static final ThreadLocal<List<MethodCall>> methodCallThreadLocal = new ThreadLocal<List<MethodCall>>() {
@Override
protected ArrayList<MethodCall> initialValue() {
return new ArrayList<>();
}
};
}
這裏主要使用了三個ThreadLocal來保存方法調用過程中的數據:方法的完整堆棧、方法進入堆棧的順序、方法的調用信息列表,為什麼使用ThreadLocal而不是TransmittableThreadLocal,這裏先按下不表,後面我們通過具體的例子來分析下原因。
緊接着,我們需要定義方法進入前的攔截邏輯,將方法調用信息壓入堆棧中:
@Override
public MethodCall beforeMethod(Object target, Method method, String[] parameters, Object[] arguments) {
// 排除掉各種非法攔截到的方法
if (target == null) {
return null;
}
String methodName = target.getClass().getName() + ":" + method.getName() + "()";
Deque<MethodCall> methodCallStack = methodStackThreadLocal.get();
// 當前方法進入整個方法調用棧的順序
int methodEnterOrder = methodEnterStackOrderThreadLocal.get().addAndGet(1);
// 當前方法在整個方法棧中的深度
int methodInStackDepth = methodCallStack.size() + 1;
MethodCall methodCall = MethodCall.Default.of()
.setMethodName(methodName)
.setCallTime(System.nanoTime())
.setThreadName(Thread.currentThread().getName())
.setCurrentMethodEnterStackOrder(methodEnterOrder)
.setCurrentMethodInStackDepth(methodInStackDepth);
// 將當前方法的調用信息壓入調用棧
methodCallStack.push(methodCall);
return methodCall;
}
最後在方法退出時,我們需要從ThreadLocal中取出方法調用信息,並做相關的處理:
@Override
public void afterMethod(MethodCall transmitResult, Object originResult) {
if (target == null) {
return null;
}
Deque<MethodCall> methodCallStack = methodStackThreadLocal.get();
MethodCall lastMethodCall = methodCallStack.pop();
// 毫秒單位的耗時
double costTimeInMills = (double) (System.nanoTime() - lastMethodCall.getCallTime()) / 1000000.0;
lastMethodCall.setCostInMills(costTimeInMills);
List<MethodCall> methodCallList = methodCallThreadLocal.get();
methodCallList.add(lastMethodCall);
// 如果堆棧空了,則説明最頂層的方法已經退出了
if (methodCallStack.isEmpty()) {
// 對方法調用列表進行排序
sortMethodCallList(methodCallList);
// 獲取MethodCallHandler對MethodCall的信息進行處理
MethodCallHandler methodCallHandler = Configuration.Global.getGlobal().getMethodCallHandler();
methodCallHandler.handle(methodCallList);
// 方法退出時,將ThreadLocal中保存的內容清空掉,而不是將ThreadLocal remove,
// 因為如果每次方法退出時,都將ThreadLocal都清空,當下一個方法再進入時又需要初始化新的ThreadLocal,性能會有損耗
methodCallStack.clear();
methodCallList.clear();
// 將臨時保存的方法調用順序清空
methodEnterStackOrderThreadLocal.get().set(0);
}
}
private void sortMethodCallList(List<MethodCall> methodCallList) {
methodCallList.sort(new Comparator<MethodCall>() {
@Override
public int compare(MethodCall o1, MethodCall o2) {
// 根據每個方法進入方法棧的順序進行排序
return Integer.compare(o1.getCurrentMethodEnterStackOrder(), o2.getCurrentMethodEnterStackOrder());
}
});
}
需要注意的是,這裏我定義了一個MethodCallHandler接口,該接口可以實現對採集到的方法調用信息的處理,用户可以自定義自己的MethodCallHandler。組件中也提供了默認的實現,即將採集到的方法調用信息打印到日誌中:
五、方案測試
1 普通方法
我們定義一個方法調用的測試樣例類,其中定義了很多普通的方法,如下所示:
public class MethodCallExample {
public void costTime1() {
System.out.println("costTime1");
randomSleep();
innerCostTime1();
}
public void costTime2() {
System.out.println("costTime2");
randomSleep();
innerCostTime2();
}
public void costTime3() {
System.out.println("costTime3");
randomSleep();
}
public void innerCostTime1() {
System.out.println("innerCostTime1");
randomSleep();
}
public void innerCostTime2() {
System.out.println("innerCostTime2");
randomSleep();
}
private void randomSleep() {
Random random = new Random();
try {
Thread.sleep(random.nextInt(100));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
啓動Enhancer,並調用測試樣例中的方法:
public static void main(String[] args) {
MethodCallPlugin plugin = new MethodCallPlugin(Collections.singletonList("com.shizhuang.duapp.enhancer.example"), null);
Enhancer enhancer = Enhancer.Default.INSTANCE;
enhancer.enhance(Configuration.of().setPlugins(Collections.singletonList(plugin)));
MethodCallExample example = new MethodCallExample();
example.costTime1();
example.costTime2();
example.costTime3();
try {
// 這裏主要是防止主線程提前結束
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
執行後,可以得到如下的結果:
從結果上看已經可以滿足絕大多數的情況了,我們拿到了每個方法的調用耗時,以及整個方法的調用堆棧信息。
但是這裏的方法都是同步方法,如果有異步方法,會怎麼樣呢?
2 異步方法
我們將其中一個方法改成異步線程執行:
private void randomSleep() {
new Thread(() -> {
Random random = new Random();
try {
Thread.sleep(random.nextInt(100));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
}
再次執行後,得到如下的結果:
從結果中可以看到,因為randomSleep方法中通過Thread變成了異步執行,而增強器攔截到的randomSleep實際是Thread.start()的方法耗時,Thread內部的Runnable的方法耗時沒有采集到。
3 表達式
為什麼Runnable的方法耗時沒有采集到呢?原因是Runnable內部是一個lambda表達式,生成的是一個匿名方法,而匿名方法的默認是無法被攔截到的。
具體的原因可以參考這篇文章:
ByteBuddy的作者解釋了lambda的特殊性,包括為什麼無法對lambda做instrument,以及ByteBuddy為了實現對lambda表達式的攔截做了一些支持。
不過只在OpenJDK8u40版本以上才能生效,因為之前版本的JDK在invokedynamic指令上有bug。
我們打開這個Lambda的策略開關:
可以攔截到lambda表達式生成的匿名方法了:
如果我們不打開Lambda的策略開關,也可以將匿名方法實現為具名方法:
private void randomSleep() {
new Thread(() -> {
doSleep();
}).start();
}
private void doSleep() {
Random random = new Random();
try {
Thread.sleep(random.nextInt(100));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
甚至可以攔截到lambda方法中的具名方法:
4 TransmittableThreadLocal
上面我提了一個問題,為什麼攔截器中保存方法調用信息的ThreadLocal不用TransmittableThreadLocal,而是用普通的ThreadLocal,這裏我們把攔截器中的代碼改一下:
執行後發現效果如下:
可以看到異步方法和主方法合併到一起了,原因是我們保存方法調用堆棧信息使用了TransmittableThreadLocal,而TTL是會在主子線程中共享變量的,當主線程中的costTime1方法還未退出堆棧時,子線程中的doSleep方法已經進入堆棧了,所以導致堆棧信息一直未清空,而我們是在每個方法退出時判斷當前線程中的堆棧是否為空,如果為空則説明方法調用的最頂層方法已經退出了,但是TTL導致堆棧不為空,只有當所有方法執行完畢後堆棧才為空,所以出現了這樣的情況。所以這裏保存方法調用堆棧的ThreadLocal需要用原生的ThreadLocal。
5 串聯主子線程
那麼怎麼實現一個方法的主方法在不同的主子線程中串起來呢?
通過常規的共享堆棧的方案無法實現主子線程中的方法的串聯,那麼可以通過TraceId來實現方法的串聯,鏈路追蹤的技術方案中提供了TraceId和rpcId兩字字段,分別用來表示一個請求的唯一鏈路以及每個方法在該鏈路中的順序(通過rpcId來表示)。這裏我們只需要利用鏈路追蹤裏面的TraceId來串聯同一個方法即可。具體的原理可以描述如下:
由於不同的鏈路追蹤的實現方式不同,我這裏定義了一個Tracer接口,由用户指定具體的Tracer實現:
/**
* 鏈路追蹤器
*
* @auther houyi.wh
* @date 2023-08-22 14:59:50
* @since 0.0.1
*/
public interface Tracer {
/**
* 獲取鏈路id
*
* @return 鏈路id
* @since 0.0.1
*/
String getTraceId();
/**
* 一個空的實現類
* @since 0.0.1
*/
enum Empty implements Tracer {
INSTANCE;
@Override
public String getTraceId() {
return "";
}
}
}
然後在Configuration中設置該Tracer:
// 啓動代碼增強
Enhancer enhancer = Enhancer.Default.INSTANCE;
Configuration config = Configuration.of()
// 指定自定義的Tracer
.setTracer(yourTracer)
.xxx() // 其他配置項
;
enhancer.enhance(config);
需要注意的是,如果不指定Tracer,則會默認使用內置的空實現:
六、性能測試
該組件的主要是通過攔截器進行代碼增強,因為我們需要對攔截器的beforeMethod和afterMethod進行性能測試,通常常規的性能測試,是通過JMH基準測試工具來做的。
我們定義一個基準測試的類:
/*
* 因為 JVM 的 JIT 機制的存在,如果某個函數被調用多次之後,JVM 會嘗試將其編譯成為機器碼從而提高執行速度。
* 所以為了讓 benchmark 的結果更加接近真實情況就需要進行預熱
* 其中的參數 iterations 是預熱輪數
*/
@Warmup(iterations = 1)
/*
* 基準測試的類型:
* Throughput:吞吐量,指1s內可以執行多少次操作
* AverageTime:調用時間,指1次調用所耗費的時間
*/
@BenchmarkMode({Mode.AverageTime, Mode.Throughput})
/*
* 測試的一些度量
* iterations:進行測試的輪次
* time:每輪進行的時長
* timeUnit:時長單位
*/
@Measurement(iterations = 2, time = 1)
/*
* 基準測試結果的時間類型。一般選擇秒、毫秒、微秒。
*/
@OutputTimeUnit(TimeUnit.MILLISECONDS)
/*
* fork出幾個進場進行測試。
* 如果 fork 數是 2 的話,則 JMH 會 fork 出兩個進程來進行測試。
*/
@Fork(value = 2)
/*
* 每個進程中測試線程的個數。
*/
@Threads(8)
/*
* State 用於聲明某個類是一個“狀態”,然後接受一個 Scope 參數用來表示該狀態的共享範圍。
* 因為很多 benchmark 會需要一些表示狀態的類,JMH 允許你把這些類以依賴注入的方式注入到 benchmark 函數裏。
* Scope 主要分為三種:
* Thread - 該狀態為每個線程獨享。
* Group - 該狀態為同一個組裏面所有線程共享。
* Benchmark - 該狀態在所有線程間共享。
*/
@State(Scope.Benchmark)
public class MethodCallInterceptorBench {
private MethodCallInterceptor methodCallInterceptor;
private Object target;
private Method method;
private String[] parameters;
private Object[] arguments;
@Setup
public void prepare() {
methodCallInterceptor = new MethodCallInterceptor();
target = new MethodCallExample();
try {
method = target.getClass().getMethod("costTime1");
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
parameters = null;
arguments = null;
}
@Benchmark
public void testMethodCallInterceptor_beforeMethod() {
methodCallInterceptor.beforeMethod(target, method, parameters, arguments);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(MethodCallInterceptorBench.class.getSimpleName())
.build();
new Runner(opt).run();
}
}
基準測試的結果如下:
針對beforeMethod方法做了吞吐量和平均耗時的測試,每次調用的平均耗時為0.592ms,而吞吐量則為1ms內可以執行82.99次調用。
七、使用方式
引入該Enhancer組件的依賴:
<dependency>
<groupId>com.shizhuang.duapp</groupId>
<artifactId>commodity-common-enhancer</artifactId>
<version>${commodity-common-enhancer-version}</version>
</dependency>
使用很簡單,只需要在項目啓動之後,調用代碼增強的方法即可,對現有的業務代碼幾乎無侵入。
不指定配置信息,直接啓動:
public class CommodityAdminApplication {
public static void main(String[] args) {
SpringApplication.run(CommodityAdminApplication.class, args);
// 啓動代碼增強
Enhancer enhancer = Enhancer.Default.INSTANCE;
enhancer.enhance(null);
}
}
指定配置信息啓動:
public class CommodityAdminApplication {
public static void main(String[] args) {
SpringApplication.run(CommodityAdminApplication.class, args);
// 啓動代碼增強
Enhancer enhancer = Enhancer.Default.INSTANCE;
Configuration config = Configuration.of()
.setPlugins(Collections.singletonList(plugin))
.xxx() // 其他配置項
;
enhancer.enhance(config);
}
}
1 實現方法耗時過濾
比如你只想對方法耗時大於xx毫秒的方法進行分析,你可以在定義的MethodCallHandler中引入ark配置,然後過濾出耗時大於xx毫秒的方法,如:
enum MyCustomHandler implements MethodCallHandler {
INSTANCE;
private double maxCostTime() {
// 這裏可以通過動態配置想要分析的方法耗時的最小值
return 500;
}
@Override
public void handle(List<MethodCall> methodCallList) {
logger.info("=========================================================================");
// 檢查方法耗時超過xx時,才打印
MethodCall firstMethodCall = methodCallList.stream().findFirst().orElse(null);
if (firstMethodCall == null) {
return;
}
// 方法耗時
double costInMills = firstMethodCall.getCostInMills();
int currentMethodEnterStackOrder = firstMethodCall.getCurrentMethodEnterStackOrder();
// 如果整體的方法小於500毫秒,則直接放棄
if (currentMethodEnterStackOrder == 1 && costInMills < maxCostTime()) {
return;
}
// 然後在這裏實現方法耗時的打印
logger.info(getMethodCallInfo(methodCallList));
}
}
2 實現整體開關控制
比如你想通過動態開關來控制對方法耗時的統計分析,可以實現MethodCallSwither接口,然後在Configuration中傳入自定義的MethodCallSwitcher,如下所示:
請注意,如果用户不指定MethodCallSwitcher,SDK會使用內置的MethodCallSwitcher.NeverStop 實現,表示永遠不會停止採集。
/**
* 是否停止採集MethodCall的開關
*
* @auther houyi.wh
* @date 2023-08-27 18:56:47
* @since 0.0.1
*/
public interface MethodCallSwitcher {
/**
* 是否停止對方法的MethodCall的採集
* 如果返回true,則會停止對方法MethodCall的採集
*
* @return true:停止採集 false:繼續採集
*/
boolean stopScratch();
/**
* 永遠不停止採集
*/
enum NeverStop implements MethodCallSwitcher {
INSTANCE;
@Override
public boolean stopScratch() {
// 一直進行採集
return false;
}
}
}
八、擴展能力
用户如果想要實現自己的擴展能力,只需要實現EnhancedPlugin,以及Interceptor即可。
1 實現自定義插件
通過如下方式實現自定義插件:
public MyCustomePlugin extends EnhancedPlugin {
@Override
public ElementMatcher.Junction<TypeDescription> typeMatcher() {
// 實現類型匹配
}
@Override
public ElementMatcher.Junction<MethodDescription> methodMatcher() {
// 實現方法匹配
}
@Override
public Class<? extends Interceptor> interceptorClass() {
// 指定攔截器
return MyInterceptor.class;
}
}
2 實現攔截器
// 臨時傳遞數據的對象
public class Carrier {
}
public class MyInterceptor implements InstanceMethodInterceptor<Carrier> {
@Override
public Carrier beforeMethod(Object target, Method method, String[] parameters, Object[] arguments) {
// 實現方法調用前攔截
}
@Override
public void afterMethod(Carrier transmitResult, Object originResult) {
// 實現方法調用後攔截
}
}
3 啓動插件
最後在項目啓動時,啓用自定義的插件,如下所示:
public class CommodityAdminApplication {
public static void main(String[] args) {
SpringApplication.run(CommodityAdminApplication.class, args);
// 啓動代碼增強
Enhancer enhancer = Enhancer.Default.INSTANCE;
Configuration config = Configuration.of()
// 指定自定義的插件
.setPlugins(Collections.singletonList(new MyCustomePlugin()))
.xxx() // 其他配置項
;
enhancer.enhance(config);
}
}
九、總結與規劃
本篇文章我們介紹了在項目中遇到的性能診斷的需求和場景,並提供了一種通過插樁的方式對具體方法進行分析的技術方案,介紹了方案中遇到的難點以及解決方法,以及實際使用過程中可能存在的擴展場景。
未來我們將使用Enhancer在運行時動態的獲取應用系統的性能分析數據,比如通過對某些性能有問題的嫌疑代碼進行增強,提取到性能分析的數據後,最後結合Grafana大盤,展示出系統的性能大盤。
*文 / 逅弈
本文屬得物技術原創,更多精彩文章請看:得物技術官網
未經得物技術許可嚴禁轉載,否則依法追究法律責任!