一
問題描述
當(dāng)我們的業(yè)務(wù)發(fā)展到一定階段的時候,系統(tǒng)的復(fù)雜度往往會非常高,不再是一個簡單的單體應(yīng)用所能夠承載的,隨之而來的是系統(tǒng)架構(gòu)的不斷升級與演變。一般對于大型的To C的互聯(lián)網(wǎng)企業(yè)來說,整個系統(tǒng)都是構(gòu)建于微服務(wù)的架構(gòu)之上,原因是To C的業(yè)務(wù)有著天生的微服務(wù)化的訴求:需求迭代快、業(yè)務(wù)系統(tǒng)多、領(lǐng)域劃分多、鏈路調(diào)用關(guān)系復(fù)雜、容忍延遲低、故障傳播快。微服務(wù)化之后帶來的問題也很明顯:服務(wù)的管理復(fù)雜、鏈路的梳理復(fù)雜、系統(tǒng)故障會在整個鏈路中迅速傳播。
這里我們不討論鏈路的依賴或服務(wù)的管理等問題,本次要解決的問題是怎么防止單個系統(tǒng)故障影響整個系統(tǒng)。這是一個復(fù)雜的問題,因?yàn)榉?wù)的傳播特性,一個服務(wù)出現(xiàn)故障,其他依賴或被依賴的服務(wù)都會受到影響。為了找到解決問題的辦法,我們試著通過5why提問法來找答案。
PS:這里說的系統(tǒng)故障,是特指由于慢調(diào)用、慢查詢等影響系統(tǒng)性能而導(dǎo)致的系統(tǒng)故障。
問
怎么防止單個系統(tǒng)故障影響整個系統(tǒng)?
避免單個系統(tǒng)的故障的傳播。
答
問
怎么避免故障的傳播?
找到系統(tǒng)故障的原因,解決故障。
答
問
怎么找到系統(tǒng)故障的原因?
找到并優(yōu)化系統(tǒng)中耗時長的方法。
答
問
怎么找到系統(tǒng)中耗時長的方法?
通過對特定方法做AOP攔截。
答
問
怎么對特定方法做AOP攔截?
通過字節(jié)碼增強(qiáng)的方式對目標(biāo)方法做攔截并植入內(nèi)聯(lián)代碼。
答
通過5why提問法,我們得到了解決問題的方法,我們需要對目標(biāo)方法做AOP攔截,統(tǒng)計業(yè)務(wù)方法及各個子方法的耗時,得到所有方法的耗時分布,快速定位到比較慢的方法,最后找出業(yè)務(wù)系統(tǒng)的性能瓶頸在哪里。
二
方案選型
我們知道AOP是一種編碼思想,跟OOP不同,AOP是將特定的方法邏輯,以切面的形式編織到目標(biāo)方法中,這里不再贅述AOP的思想。
如果在網(wǎng)上搜一下“AOP的實(shí)現(xiàn)方式”,你會得到大致相同的結(jié)果:AOP的實(shí)現(xiàn)方式是通過動態(tài)代理或CGLIB代理。其實(shí)這不太準(zhǔn)確,準(zhǔn)確的來說,AOP可以通過代理或Advice兩種方式來實(shí)現(xiàn)。請注意這里說的Advice并不是Spring所依賴的aspectj中的Advice,而是一種代碼織入的技術(shù),它與代理的區(qū)別在于,代碼織入技術(shù)不需要創(chuàng)建代理類。
如果用圖形表示的話,可以更簡單更直觀的感受到兩者的區(qū)別。代碼織入的方式,不會創(chuàng)建代理類,而是直接在目標(biāo)方法的方法體的前后織入一段內(nèi)聯(lián)的代碼,以達(dá)到增強(qiáng)的效果,如下圖所示:
我選擇代碼織入技術(shù)而不是AOP,原因是可以避免創(chuàng)建大量的代理類增加元空間的內(nèi)存占用,另外代碼織入技術(shù)更底層一些,能實(shí)現(xiàn)的能力更強(qiáng),此外內(nèi)聯(lián)代碼會隨著原方法一起執(zhí)行,性能也更好。
有了具體的技術(shù)選型的方案之后,我們還需要確定該方案的建設(shè)目標(biāo),以下整理了一些基本的目標(biāo):
三
技術(shù)方案
代碼織入的時機(jī)也有多種方式,比如Lombok是通過在編譯器對代碼進(jìn)行織入,主要依賴的是在 Javac 編譯階段利用“annotation Processor”,對自定義的注解進(jìn)行預(yù)處理后生成代碼然后織入;其他的像CGLIB、ByteBuddy等框架是在運(yùn)行時對代碼進(jìn)行織入的,主要依賴的是Java Agent技術(shù),通過JVMTI的接口實(shí)現(xiàn)在運(yùn)行時對字節(jié)碼進(jìn)行增強(qiáng)。
本次的技術(shù)方案,用一句話可以概括為:通過字節(jié)碼增強(qiáng),對指定的目標(biāo)方法進(jìn)行攔截,并在方法前后織入一段內(nèi)聯(lián)代碼,在內(nèi)聯(lián)代碼中計算目標(biāo)方法的耗時,最后將統(tǒng)計到的方法信息進(jìn)行分析。
項目結(jié)構(gòu)
整個方案的代碼實(shí)現(xiàn)非常簡單,用一個圖描述如下:
項目的代碼結(jié)構(gòu)如下所示,核心代碼非常少:
核心組件
其中Enhancer是增強(qiáng)器的入口類,在增強(qiáng)器啟動時會掃描所有的插件:EnhancedPlugin。
EnhancedPlugin表示的是一個執(zhí)行代碼增強(qiáng)的插件,其中定義了幾個抽象方法,需要由用戶自己實(shí)現(xiàn):
/** * 執(zhí)行代碼增強(qiáng)的插件 * * @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(); /** * 負(fù)責(zé)執(zhí)行增強(qiáng)邏輯的攔截器 * * @return 攔截器 * @since 0.0.1 */ public abstract Class<? extends Interceptor> interceptorClass();}
此外EnhancedPlugin中還需要指定一個Interceptor,一個Interceptor是對目標(biāo)方法執(zhí)行代碼增強(qiáng)的攔截器,主要的攔截邏輯定義在Interceptor中。
增強(qiáng)原理
掃描到EnhancedPlugin之后,會構(gòu)建ByteBuddy的AgentBuilder,主要的構(gòu)建過程為:
1、找到所有匹配的類型
2、找到所有匹配的方法
3、傳入執(zhí)行代碼增強(qiáng)的Transformer
最后通過AgentBuilder.install方法將增強(qiáng)的代碼Transformer,傳遞給Instrumentation實(shí)例,實(shí)現(xiàn)運(yùn)行時的字節(jié)碼retransformation。
這里的Transformer是由Advice負(fù)責(zé)實(shí)現(xiàn)的,而在Advice中實(shí)現(xiàn)了增強(qiáng)邏輯的dispatch,即根據(jù)不同的EnhancedPlugin可以將增強(qiáng)邏輯交給指定的Interceptor攔截器去實(shí)現(xiàn),主要在攔截器中抽象了兩個方法。一個是beforeMethod,負(fù)責(zé)在目標(biāo)方法調(diào)用之前進(jìn)行攔截:
/** * 在方法執(zhí)行前進(jìn)行切面 * * @param pluginName 綁定在該目標(biāo)方法上的插件名稱 * @param target 目標(biāo)方法所屬的對象,需要注意的是@Advice.This不能標(biāo)識構(gòu)造方法 * @param method 目標(biāo)方法 * @param arguments 方法參數(shù) * @return 方法執(zhí)行返回的臨時數(shù)據(jù) * @since 0.0.1 */@Advice.OnMethodEnterpublic static <T> T beforeMethod( // 接收動態(tài)傳遞過來的參數(shù) @PluginName String pluginName, // optional=true,表示this注解可以接收:構(gòu)造方法或靜態(tài)方法(會將this賦值為null),而不報錯 @Advice.This(optional = true) Object target, // 目標(biāo)方法 @Advice.Origin Method method, // nullIfEmpty=true,表示可以接收空參數(shù) @Advice.AllArguments(nullIfEmpty = true) Object[] arguments) { String[] parameterNames = new String[]{}; T transmitResult = null; try { InstanceMethodInterceptor<T> interceptor = getInterceptor(pluginName); // 執(zhí)行beforeMethod的攔截邏輯 transmitResult = interceptor.beforeMethod(target, method, parameterNames, arguments); } catch (Throwable e) { InternalLogger.AutoDetect.INSTANCE.error("InstanceMethodAdvice beforeMethod occurred error", e); } return transmitResult;}
一個是afterMethod,負(fù)責(zé)在目標(biāo)方法被調(diào)用之后進(jìn)行攔截:
/** * 在方法執(zhí)行后進(jìn)行切面 * * @param pluginName 綁定在該目標(biāo)方法上的插件名稱 * @param transmitResult beforeMethod所傳遞過來的臨時數(shù)據(jù) * @param originResult 目標(biāo)方法原始返回結(jié)果,如果目標(biāo)方法是void型,則originResult為null * @param throwable 目標(biāo)方法拋出的異常 */@Advice.OnMethodExit(onThrowable = Throwable.class)public static <T> void afterMethod( // 接收動態(tài)傳遞過來的參數(shù) @PluginName String pluginName, // beforeMethod傳遞過來的臨時數(shù)據(jù) @Advice.Enter T transmitResult, // typing=DYNAMIC,表示可以接收void類型的方法 @Advice.Return(typing = Assigner.Typing.DYNAMIC) Object originResult, // 目標(biāo)方法自己拋出的運(yùn)行時異常,可以在方法中進(jìn)行捕獲,看具體的需求 @Advice.Thrown Throwable throwable) { try { InstanceMethodInterceptor<T> interceptor = getInterceptor(pluginName); // 執(zhí)行afterMethod的攔截邏輯 interceptor.afterMethod(transmitResult, originResult); } catch (Throwable e) { InternalLogger.AutoDetect.INSTANCE.error("InstanceMethodAdvice afterMethod occurred error", e); }}
Advice的特點(diǎn)是:不會更改目標(biāo)類的字節(jié)碼結(jié)構(gòu),比如:不會增加字段、方法,不會修改方法的參數(shù)等等。
四
方案實(shí)現(xiàn)
該增強(qiáng)組件是一個輕量化的通用的增強(qiáng)包,幾乎可以實(shí)現(xiàn)你能想到的任意功能,本次我們的需求是要采集特定目標(biāo)方法的方法耗時,以便分析出方法的性能瓶頸。
定義插件
基于該組件我們需要實(shí)現(xiàn)兩個類:一個是插件,一個是攔截器。
插件中主要實(shí)現(xiàn)的是兩個方法:匹配特定的類型,匹配特定的方法。
這里的類型匹配或方法匹配,是采用的ByteBuddy的ElementMatcher,它是一個非常靈活的匹配器,在ElementMatchers中有很多內(nèi)置的匹配實(shí)現(xiàn),只要你能想到的匹配方式,通過它幾乎都能實(shí)現(xiàn)匹配。
匹配特定的類型目前我定義了兩種匹配方式,一種是根據(jù)類名(或者包名),一種是根據(jù)方法上的注解,具體的代碼實(shí)現(xiàn)如下:
public class MethodCallPlugin extends EnhancedPlugin { private final List<String> anyClassNameStartWith; private final List<String> anyAnnotationNameOnMethod; /** * 方法調(diào)用攔截插件 * * @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) { // 根據(jù)類的前綴或者全限定類名進(jìn)行匹配 anyTypes = anyTypes.or(nameStartsWith(classNameStartWith)); } } if (anyAnnotationNameOnMethod != null && !anyAnnotationNameOnMethod.isEmpty()) { ElementMatcher.Junction<MethodDescription> methodsWithAnnotation = none(); for (String annotationNameOnMethod : anyAnnotationNameOnMethod) { // 根據(jù)方法上是否有特定注解進(jìn)行匹配 methodsWithAnnotation = methodsWithAnnotation.or(isAnnotatedWith(named(annotationNameOnMethod))); } anyTypes = anyTypes.or(declaresMethod(methodsWithAnnotation)); } return anyTypes; }}
匹配特定方法的邏輯就比較簡單了,可以匹配除了構(gòu)造方法之外的任意方法:
public class MethodCallPlugin extends EnhancedPlugin { @Override public ElementMatcher.Junction<MethodDescription> methodMatcher() { return any().and(not(isConstructor())); }}
實(shí)現(xiàn)攔截器
類型匹配和方法都匹配到之后,就需要實(shí)現(xiàn)方法增強(qiáng)的攔截器了:
我們需要獲取方法調(diào)用的信息,包括方法名、調(diào)用堆棧及深度、調(diào)用的耗時,所以我們需要定義三個ThreadLocal用來保存方法調(diào)用的堆棧:
/** * 方法調(diào)用信息的攔截器 * 在方法調(diào)用之前進(jìn)行攔截,將方法調(diào)用信息封裝后,放入堆棧中, * 在方法調(diào)用之后,從堆棧中將所有方法取出來,按照進(jìn)入堆棧的順序進(jìn)行排序, * 得到方法調(diào)用信息的列表,最后將該列表交給{@link MethodCallHandler}進(jìn)行處理 * 如果用戶指定了自己的{@link MethodCallHandler}則優(yōu)先使用用戶自定義的Handler進(jìn)行處理 * 否則使用SDK內(nèi)置的{@link MethodCallHandler.PrintLogHandler}進(jìn)行處理,即將方法調(diào)用信息打印到日志中 * * @auther houyi.wh * @date 2023-08-16 10:16:48 * @since 0.0.1 */public class MethodCallInterceptor implements InstanceMethodInterceptor<MethodCall> { /** * 當(dāng)前方法進(jìn)入方法棧的順序 * 用以最后一個方法出棧后,進(jìn)行方法調(diào)用棧的排序 * * @since 0.0.1 */ private static final ThreadLocal<AtomicInteger> methodEnterStackOrderThreadLocal = new TransmittableThreadLocal<AtomicInteger>() { @Override protected AtomicInteger initialValue() { return new AtomicInteger(0); } }; /** * 當(dāng)前方法調(diào)用棧 * * @since 0.0.1 */ private static final ThreadLocal<Deque<MethodCall>> methodStackThreadLocal = new ThreadLocal<Deque<MethodCall>>() { @Override protected Deque<MethodCall> initialValue() { return new ArrayDeque<>(); } }; /** * 當(dāng)前方法棧中所有方法調(diào)用的信息 * * @since 0.0.1 */ private static final ThreadLocal<List<MethodCall>> methodCallThreadLocal = new ThreadLocal<List<MethodCall>>() { @Override protected ArrayList<MethodCall> initialValue() { return new ArrayList<>(); } }; }
這里主要使用了三個ThreadLocal來保存方法調(diào)用過程中的數(shù)據(jù):方法的完整堆棧、方法進(jìn)入堆棧的順序、方法的調(diào)用信息列表,為什么使用ThreadLocal而不是TransmittableThreadLocal,這里先按下不表,后面我們通過具體的例子來分析下原因。
緊接著,我們需要定義方法進(jìn)入前的攔截邏輯,將方法調(diào)用信息壓入堆棧中:
@Overridepublic 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(); // 當(dāng)前方法進(jìn)入整個方法調(diào)用棧的順序 int methodEnterOrder = methodEnterStackOrderThreadLocal.get().addAndGet(1); // 當(dāng)前方法在整個方法棧中的深度 int methodInStackDepth = methodCallStack.size() 1; MethodCall methodCall = MethodCall.Default.of() .setMethodName(methodName) .setCallTime(System.nanoTime()) .setThreadName(Thread.currentThread().getName()) .setCurrentMethodEnterStackOrder(methodEnterOrder) .setCurrentMethodInStackDepth(methodInStackDepth); // 將當(dāng)前方法的調(diào)用信息壓入調(diào)用棧 methodCallStack.push(methodCall); return methodCall;}
最后在方法退出時,我們需要從ThreadLocal中取出方法調(diào)用信息,并做相關(guān)的處理:
@Overridepublic 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); // 如果堆??樟?,則說明最頂層的方法已經(jīng)退出了 if (methodCallStack.isEmpty()) { // 對方法調(diào)用列表進(jìn)行排序 sortMethodCallList(methodCallList); // 獲取MethodCallHandler對MethodCall的信息進(jìn)行處理 MethodCallHandler methodCallHandler = Configuration.Global.getGlobal().getMethodCallHandler(); methodCallHandler.handle(methodCallList); // 方法退出時,將ThreadLocal中保存的內(nèi)容清空掉,而不是將ThreadLocal remove, // 因?yàn)槿绻看畏椒ㄍ顺鰰r,都將ThreadLocal都清空,當(dāng)下一個方法再進(jìn)入時又需要初始化新的ThreadLocal,性能會有損耗 methodCallStack.clear(); methodCallList.clear(); // 將臨時保存的方法調(diào)用順序清空 methodEnterStackOrderThreadLocal.get().set(0); }}private void sortMethodCallList(List<MethodCall> methodCallList) { methodCallList.sort(new Comparator<MethodCall>() { @Override public int compare(MethodCall o1, MethodCall o2) { // 根據(jù)每個方法進(jìn)入方法棧的順序進(jìn)行排序 return Integer.compare(o1.getCurrentMethodEnterStackOrder(), o2.getCurrentMethodEnterStackOrder()); } });}
需要注意的是,這里我定義了一個MethodCallHandler接口,該接口可以實(shí)現(xiàn)對采集到的方法調(diào)用信息的處理,用戶可以自定義自己的MethodCallHandler。組件中也提供了默認(rèn)的實(shí)現(xiàn),即將采集到的方法調(diào)用信息打印到日志中:
五
方案測試
普通方法
我們定義一個方法調(diào)用的測試樣例類,其中定義了很多普通的方法,如下所示:
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,并調(diào)用測試樣例中的方法:
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 { // 這里主要是防止主線程提前結(jié)束 Thread.sleep(5000); } catch (InterruptedException e) { throw new RuntimeException(e); }}
執(zhí)行后,可以得到如下的結(jié)果:
從結(jié)果上看已經(jīng)可以滿足絕大多數(shù)的情況了,我們拿到了每個方法的調(diào)用耗時,以及整個方法的調(diào)用堆棧信息。
但是這里的方法都是同步方法,如果有異步方法,會怎么樣呢?
異步方法
我們將其中一個方法改成異步線程執(zhí)行:
private void randomSleep() { new Thread(() -> { Random random = new Random(); try { Thread.sleep(random.nextInt(100)); } catch (InterruptedException e) { throw new RuntimeException(e); } }).start();}
再次執(zhí)行后,得到如下的結(jié)果:
從結(jié)果中可以看到,因?yàn)閞andomSleep方法中通過Thread變成了異步執(zhí)行,而增強(qiáng)器攔截到的randomSleep實(shí)際是Thread.start()的方法耗時,Thread內(nèi)部的Runnable的方法耗時沒有采集到。
Lambda表達(dá)式
為什么Runnable的方法耗時沒有采集到呢?原因是Runnable內(nèi)部是一個lambda表達(dá)式,生成的是一個匿名方法,而匿名方法的默認(rèn)是無法被攔截到的。
具體的原因可以參考這篇文章:
https://stackoverflow.com/questions/33912026/intercepting-calls-to-java-8-lambda-expressions-using-byte-buddy
ByteBuddy的作者解釋了lambda的特殊性,包括為什么無法對lambda做instrument,以及ByteBuddy為了實(shí)現(xiàn)對lambda表達(dá)式的攔截做了一些支持。
不過只在OpenJDK8u40版本以上才能生效,因?yàn)橹鞍姹镜腏DK在invokedynamic指令上有bug。
我們打開這個Lambda的策略開關(guān):
可以攔截到lambda表達(dá)式生成的匿名方法了:
如果我們不打開Lambda的策略開關(guān),也可以將匿名方法實(shí)現(xiàn)為具名方法:
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方法中的具名方法:
TransmittableThreadLocal
上面我提了一個問題,為什么攔截器中保存方法調(diào)用信息的ThreadLocal不用TransmittableThreadLocal,而是用普通的ThreadLocal,這里我們把攔截器中的代碼改一下:
執(zhí)行后發(fā)現(xiàn)效果如下:
可以看到異步方法和主方法合并到一起了,原因是我們保存方法調(diào)用堆棧信息使用了TransmittableThreadLocal,而TTL是會在主子線程中共享變量的,當(dāng)主線程中的costTime1方法還未退出堆棧時,子線程中的doSleep方法已經(jīng)進(jìn)入堆棧了,所以導(dǎo)致堆棧信息一直未清空,而我們是在每個方法退出時判斷當(dāng)前線程中的堆棧是否為空,如果為空則說明方法調(diào)用的最頂層方法已經(jīng)退出了,但是TTL導(dǎo)致堆棧不為空,只有當(dāng)所有方法執(zhí)行完畢后堆棧才為空,所以出現(xiàn)了這樣的情況。所以這里保存方法調(diào)用堆棧的ThreadLocal需要用原生的ThreadLocal。
串聯(lián)主子線程
那么怎么實(shí)現(xiàn)一個方法的主方法在不同的主子線程中串起來呢?
通過常規(guī)的共享堆棧的方案無法實(shí)現(xiàn)主子線程中的方法的串聯(lián),那么可以通過TraceId來實(shí)現(xiàn)方法的串聯(lián),鏈路追蹤的技術(shù)方案中提供了TraceId和rpcId兩字字段,分別用來表示一個請求的唯一鏈路以及每個方法在該鏈路中的順序(通過rpcId來表示)。這里我們只需要利用鏈路追蹤里面的TraceId來串聯(lián)同一個方法即可。具體的原理可以描述如下:
由于不同的鏈路追蹤的實(shí)現(xiàn)方式不同,我這里定義了一個Tracer接口,由用戶指定具體的Tracer實(shí)現(xiàn):
/** * 鏈路追蹤器 * * @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(); /** * 一個空的實(shí)現(xiàn)類 * @since 0.0.1 */ enum Empty implements Tracer { INSTANCE; @Override public String getTraceId() { return ""; } }}
然后在Configuration中設(shè)置該Tracer:
// 啟動代碼增強(qiáng)Enhancer enhancer = Enhancer.Default.INSTANCE;Configuration config = Configuration.of() // 指定自定義的Tracer .setTracer(yourTracer) .xxx() // 其他配置項 ;enhancer.enhance(config);
需要注意的是,如果不指定Tracer,則會默認(rèn)使用內(nèi)置的空實(shí)現(xiàn):
六
性能測試
該組件的主要是通過攔截器進(jìn)行代碼增強(qiáng),因?yàn)槲覀冃枰獙r截器的beforeMethod和afterMethod進(jìn)行性能測試,通常常規(guī)的性能測試,是通過JMH基準(zhǔn)測試工具來做的。
我們定義一個基準(zhǔn)測試的類:
/* * 因?yàn)?JVM 的 JIT 機(jī)制的存在,如果某個函數(shù)被調(diào)用多次之后,JVM 會嘗試將其編譯成為機(jī)器碼從而提高執(zhí)行速度。 * 所以為了讓 benchmark 的結(jié)果更加接近真實(shí)情況就需要進(jìn)行預(yù)熱 * 其中的參數(shù) iterations 是預(yù)熱輪數(shù) */@Warmup(iterations = 1)/* * 基準(zhǔn)測試的類型: * Throughput:吞吐量,指1s內(nèi)可以執(zhí)行多少次操作 * AverageTime:調(diào)用時間,指1次調(diào)用所耗費(fèi)的時間 */@BenchmarkMode({Mode.AverageTime, Mode.Throughput})/* * 測試的一些度量 * iterations:進(jìn)行測試的輪次 * time:每輪進(jìn)行的時長 * timeUnit:時長單位 */@Measurement(iterations = 2, time = 1)/* * 基準(zhǔn)測試結(jié)果的時間類型。一般選擇秒、毫秒、微秒。 */@OutputTimeUnit(TimeUnit.MILLISECONDS)/* * fork出幾個進(jìn)場進(jìn)行測試。 * 如果 fork 數(shù)是 2 的話,則 JMH 會 fork 出兩個進(jìn)程來進(jìn)行測試。 */@Fork(value = 2)/* * 每個進(jìn)程中測試線程的個數(shù)。 */@Threads(8)/* * State 用于聲明某個類是一個“狀態(tài)”,然后接受一個 Scope 參數(shù)用來表示該狀態(tài)的共享范圍。 * 因?yàn)楹芏?benchmark 會需要一些表示狀態(tài)的類,JMH 允許你把這些類以依賴注入的方式注入到 benchmark 函數(shù)里。 * Scope 主要分為三種: * Thread - 該狀態(tài)為每個線程獨(dú)享。 * Group - 該狀態(tài)為同一個組里面所有線程共享。 * Benchmark - 該狀態(tài)在所有線程間共享。 */@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(); }}
基準(zhǔn)測試的結(jié)果如下:
針對beforeMethod方法做了吞吐量和平均耗時的測試,每次調(diào)用的平均耗時為0.592ms,而吞吐量則為1ms內(nèi)可以執(zhí)行82.99次調(diào)用。
七
使用方式
引入該Enhancer組件的依賴:
<dependency> <groupId>com.shizhuang.duapp</groupId> <artifactId>commodity-common-enhancer</artifactId> <version>${commodity-common-enhancer-version}</version></dependency>
使用很簡單,只需要在項目啟動之后,調(diào)用代碼增強(qiáng)的方法即可,對現(xiàn)有的業(yè)務(wù)代碼幾乎無侵入。
不指定配置信息,直接啟動:
public class CommodityAdminApplication { public static void main(String[] args) { SpringApplication.run(CommodityAdminApplication.class, args); // 啟動代碼增強(qiáng) Enhancer enhancer = Enhancer.Default.INSTANCE; enhancer.enhance(null); }}
指定配置信息啟動:
public class CommodityAdminApplication { public static void main(String[] args) { SpringApplication.run(CommodityAdminApplication.class, args); // 啟動代碼增強(qiáng) Enhancer enhancer = Enhancer.Default.INSTANCE; Configuration config = Configuration.of() .setPlugins(Collections.singletonList(plugin)) .xxx() // 其他配置項 ; enhancer.enhance(config); }}
實(shí)現(xiàn)方法耗時過濾
比如你只想對方法耗時大于xx毫秒的方法進(jìn)行分析,你可以在定義的MethodCallHandler中引入ark配置,然后過濾出耗時大于xx毫秒的方法,如:
enum MyCustomHandler implements MethodCallHandler { INSTANCE; private double maxCostTime() { // 這里可以通過動態(tài)配置想要分析的方法耗時的最小值 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; } // 然后在這里實(shí)現(xiàn)方法耗時的打印 logger.info(getMethodCallInfo(methodCallList)); }}
實(shí)現(xiàn)整體開關(guān)控制
比如你想通過動態(tài)開關(guān)來控制對方法耗時的統(tǒng)計分析,可以實(shí)現(xiàn)MethodCallSwither接口,然后在Configuration中傳入自定義的MethodCallSwitcher,如下所示:
請注意,如果用戶不指定MethodCallSwitcher,SDK會使用內(nèi)置的MethodCallSwitcher.NeverStop 實(shí)現(xiàn),表示永遠(yuǎn)不會停止采集。
/** * 是否停止采集MethodCall的開關(guān) * * @auther houyi.wh * @date 2023-08-27 18:56:47 * @since 0.0.1 */public interface MethodCallSwitcher { /** * 是否停止對方法的MethodCall的采集 * 如果返回true,則會停止對方法MethodCall的采集 * * @return true:停止采集 false:繼續(xù)采集 */ boolean stopScratch(); /** * 永遠(yuǎn)不停止采集 */ enum NeverStop implements MethodCallSwitcher { INSTANCE; @Override public boolean stopScratch() { // 一直進(jìn)行采集 return false; } }}
八
擴(kuò)展能力
用戶如果想要實(shí)現(xiàn)自己的擴(kuò)展能力,只需要實(shí)現(xiàn)EnhancedPlugin,以及Interceptor即可。
實(shí)現(xiàn)自定義插件
通過如下方式實(shí)現(xiàn)自定義插件:
public MyCustomePlugin extends EnhancedPlugin { @Override public ElementMatcher.Junction<TypeDescription> typeMatcher() { // 實(shí)現(xiàn)類型匹配 } @Override public ElementMatcher.Junction<MethodDescription> methodMatcher() { // 實(shí)現(xiàn)方法匹配 } @Override public Class<? extends Interceptor> interceptorClass() { // 指定攔截器 return MyInterceptor.class; }}
實(shí)現(xiàn)攔截器
通過如下方式實(shí)現(xiàn)自定義攔截器:
// 臨時傳遞數(shù)據(jù)的對象public class Carrier {}public class MyInterceptor implements InstanceMethodInterceptor<Carrier> { @Override public Carrier beforeMethod(Object target, Method method, String[] parameters, Object[] arguments) { // 實(shí)現(xiàn)方法調(diào)用前攔截 } @Override public void afterMethod(Carrier transmitResult, Object originResult) { // 實(shí)現(xiàn)方法調(diào)用后攔截 }}
啟用插件
最后在項目啟動時,啟用自定義的插件,如下所示:
public class CommodityAdminApplication { public static void main(String[] args) { SpringApplication.run(CommodityAdminApplication.class, args); // 啟動代碼增強(qiáng) Enhancer enhancer = Enhancer.Default.INSTANCE; Configuration config = Configuration.of() // 指定自定義的插件 .setPlugins(Collections.singletonList(new MyCustomePlugin())) .xxx() // 其他配置項 ; enhancer.enhance(config); }}
九
總結(jié)與規(guī)劃
本篇文章我們介紹了在項目中遇到的性能診斷的需求和場景,并提供了一種通過插樁的方式對具體方法進(jìn)行分析的技術(shù)方案,介紹了方案中遇到的難點(diǎn)以及解決方法,以及實(shí)際使用過程中可能存在的擴(kuò)展場景。
未來我們將使用Enhancer在運(yùn)行時動態(tài)的獲取應(yīng)用系統(tǒng)的性能分析數(shù)據(jù),比如通過對某些性能有問題的嫌疑代碼進(jìn)行增強(qiáng),提取到性能分析的數(shù)據(jù)后,最后結(jié)合Grafana大盤,展示出系統(tǒng)的性能大盤。
作者:逅弈
來源:微信公眾號:得物技術(shù)
出處:https://mp.weixin.qq.com/s/HueDJQZ7Vmqf7KyQHfRMRQ
版權(quán)聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶自發(fā)貢獻(xiàn),該文觀點(diǎn)僅代表作者本人。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如發(fā)現(xiàn)本站有涉嫌抄襲侵權(quán)/違法違規(guī)的內(nèi)容, 請發(fā)送郵件至 舉報,一經(jīng)查實(shí),本站將立刻刪除。