pfinder實(shí)現(xiàn)原理揭秘
共 18757字,需瀏覽 38分鐘
·
2024-06-28 11:10
引言
在現(xiàn)代軟件開發(fā)過(guò)程中,性能優(yōu)化和故障排查是保證應(yīng)用穩(wěn)定運(yùn)行的關(guān)鍵任務(wù)之一。Java作為一種廣泛使用的編程語(yǔ)言,其生態(tài)中涌現(xiàn)出了許多優(yōu)秀的監(jiān)控和診斷工具,諸如:SkyWalking、Zipkin等,它們幫助開發(fā)者和運(yùn)維人員深入了解應(yīng)用的運(yùn)行狀態(tài),快速定位和解決問(wèn)題。在京東內(nèi)部,則使用的是自研的pfinder。
pfinder簡(jiǎn)介
pfinder功能
APM類組件對(duì)比
字節(jié)碼修改
ASM實(shí)現(xiàn)
public void visitCode() {super.visitCode();mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");mv.visitLdcInsn("start");mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);}public void visitInsn(int opcode) {if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {//方法在返回之前,打印"end"mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");mv.visitLdcInsn("end");mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);}mv.visitInsn(opcode);}
javassist實(shí)現(xiàn)
ClassPool cp = ClassPool.getDefault();CtClass cc = cp.get("com.ggc.javassist.HelloWord");CtMethod m = cc.getDeclaredMethod("printHelloWord");m.insertBefore("{ System.out.println(\"start\"); }");m.insertAfter("{ System.out.println(\"end\"); }");Class c = cc.toClass();cc.writeFile("/Users/gonghanglin/workspace/workspace_me/bytecode_enhance/bytecode_enhance_javassist/target/classes/com/ggc/javassist");HelloWord h = (HelloWord)c.newInstance();h.printHelloWord();
bytebuddy實(shí)現(xiàn)
// 使用ByteBuddy動(dòng)態(tài)生成一個(gè)新的HelloWord類Class<?> dynamicType = new ByteBuddy().subclass(HelloWord.class) // 指定要修改的類.method(ElementMatchers.named("printHelloWord")) // 指定要攔截的方法名.intercept(MethodDelegation.to(LoggingInterceptor.class)) // 指定攔截器.make().load(HelloWord.class.getClassLoader()) // 加載生成的類.getLoaded();// 創(chuàng)建動(dòng)態(tài)生成類的實(shí)例,并調(diào)用方法HelloWord dynamicService = (HelloWord) dynamicType.newInstance();dynamicService.printHelloWord();
public class LoggingInterceptor {public static Object intercept( Object[] allArguments, Method method, Callable<?> callable) throws Exception {// 打印startSystem.out.println("start");try {// 調(diào)用原方法Object result = callable.call();// 打印endSystem.out.println("end");return result;} catch (Exception e) {System.out.println("exception end");throw e;}}}
bytekit實(shí)現(xiàn)
// Parse the defined Interceptor class and related annotationsDefaultInterceptorClassParser interceptorClassParser = new DefaultInterceptorClassParser();List<InterceptorProcessor> processors = interceptorClassParser.parse(HelloWorldInterceptor.class);// load bytecodeClassNode classNode = AsmUtils.loadClass(HelloWord.class);// Enhanced process of loaded bytecodesfor (MethodNode methodNode : classNode.methods) {MethodProcessor methodProcessor = new MethodProcessor(classNode, methodNode);for (InterceptorProcessor interceptor : processors) {interceptor.process(methodProcessor);}}
public class HelloWorldInterceptor {@AtEnter(inline = true)public static void atEnter() {System.out.println("start");}@AtExit(inline = true)public static void atEit() {System.out.println("end");}}
| 特性 | ASM | Javassist | ByteBuddy | ByteKit |
| 性能 | ASM的性能最高,因為它直接操作字節(jié)碼,沒有中間環(huán)節(jié) | 劣于ASM | 介于javassist和ASM之間 | 介于javassist和ASM之間 |
| 易用性 | 需精通字節(jié)碼,學(xué)習(xí)成本高,不支持debug | Java語(yǔ)法進(jìn)行開發(fā),但是采用的硬編碼形式開發(fā),不支持debug | 比Javassist更高級(jí),更符文Java開發(fā)習(xí)慣,可以對(duì)增強(qiáng)代碼進(jìn)行斷點(diǎn)調(diào)試 | 比Javassist更高級(jí),更符文Java開發(fā)習(xí)慣,可以對(duì)增強(qiáng)代碼進(jìn)行斷點(diǎn)調(diào)試 |
| 功能 | 直接操作字節(jié)碼,功能最為強(qiáng)大。 | 功能相對(duì)完備 | 功能相對(duì)完備 | 功能相對(duì)完備,對(duì)比ByteBuddy,ByteKit能防止重復(fù)增強(qiáng) |
字節(jié)碼注入
相信大家經(jīng)常使用idea去debug我們寫的代碼,我們是否想過(guò)debug是如何實(shí)現(xiàn)的呢?暫時(shí)先賣個(gè)關(guān)子。
JVMTIAgent
JVM在設(shè)計(jì)之初就考慮到了對(duì)JVM運(yùn)行時(shí)內(nèi)存、線程等指標(biāo)的監(jiān)控和分析和代碼debug功能的實(shí)現(xiàn),基于這兩點(diǎn),早在JDK5之前,JVM規(guī)范就定義了JVMPI(JVM分析接口)和JVMDI(JVM調(diào)試接口),JDK5之后,這兩個(gè)規(guī)范就合并成為了JVMTI(JVM工具接口)。JVMTI其實(shí)是一種JVM規(guī)范,每個(gè)JVM廠商都有不同的實(shí)現(xiàn),另外,JVMTI接口需使用C語(yǔ)言開發(fā),以動(dòng)態(tài)鏈接的形式加載并運(yùn)行。
| JVMTI接口 | |
| 接口 | 功能 |
| Agent_OnLoad(JavaVM *vm, char *options, void *reserved); | agent在啟動(dòng)時(shí)加載的情況下,也就是在vm參數(shù)里通過(guò)-agentlib來(lái)指定,那在啟動(dòng)過(guò)程中就會(huì)去執(zhí)行這個(gè)agent里的Agent_OnLoad函數(shù)。 |
| Agent_OnAttach(JavaVM* vm, char* options, void* reserved); | agent是attach到目標(biāo)進(jìn)程上,然后給對(duì)應(yīng)的目標(biāo)進(jìn)程發(fā)送load命令來(lái)加載agent,在加載過(guò)程中就會(huì)調(diào)用Agent_OnAttach函數(shù)。 |
| Agent_OnUnload(JavaVM *vm); | 在agent卸載的時(shí)候調(diào)用 |
| instrument主要方法 | |
| 方法 | 功能 |
| void addTransformer(ClassFileTransformer transformer) | 添加一個(gè)字節(jié)碼轉(zhuǎn)換器,用來(lái)修改加載類的字節(jié)碼 |
| Class[] getAllLoadedClasses() | 返回當(dāng)前JVM中加載的所有的類的數(shù)組 |
| Class[] getInitiatedClasses(ClassLoader loader) | 返回指定的類加載器中的所有的類的數(shù)據(jù) |
| void redefineClasses(ClassDefinition... definitions) | 用給定的類的字節(jié)碼數(shù)組替換指定的類的字節(jié)碼文件,也就是重新定義指定的類 |
| void retransformClasses(Class<?>... classes) | 指定一系列的Class對(duì)象,被指定的類都會(huì)重新變回去(去掉附加的字節(jié)碼) |
<archive><manifestEntries>// 指定premain()的所在方法<Agent-CLass>com.ggc.agent.GhlAgent</Agent-CLass><Premain-Class>com.ggc.agent.GhlAgent</Premain-Class><Can-Redefine-Classes>true</Can-Redefine-Classes><Can-Retransform-Classes>true</Can-Retransform-Classes></manifestEntries></archive>
2.agen主類
public class GhlAgent {public static Logger log = LoggerFactory.getLogger(GhlAgent.class);public static void agentmain(String agentArgs, Instrumentation instrumentation) {log.info("agentmain方法");boot(instrumentation);}public static void premain(String agentArgs, Instrumentation instrumentation) {log.info("premain方法");boot(instrumentation);}private static void boot(Instrumentation instrumentation) {//創(chuàng)建一個(gè)代理增強(qiáng)對(duì)象new AgentBuilder.Default().type(ElementMatchers.nameStartsWith("com.jd.aviation.performance.service.impl"))//攔截指定的類.transform((builder, typeDescription, classLoader, javaModule) ->builder.method(ElementMatchers.isMethod().and(ElementMatchers.isPublic())).intercept(MethodDelegation.to(TimingInterceptor.class))).installOn(instrumentation);}}
3.攔截器
public class TimingInterceptor {public static Logger log = LoggerFactory.getLogger(TimingInterceptor.class);public static Object intercept(@SuperCall Callable<?> callable) throws Exception {long start = System.currentTimeMillis();try {// 原方法調(diào)用return callable.call();} finally {long end = System.currentTimeMillis();log.info("Method call took {} ms",(end - start));}}}
4.效果
4.1.pfinder應(yīng)用架構(gòu)
4.2.pfinder插件增強(qiáng)代碼解析
protected boolean doInitialize(ProfilerContext profilerContext) {AgentEnvService agentEnvService = (AgentEnvService)profilerContext.getService(AgentEnvService.class);Instrumentation instrumentation = agentEnvService.instrumentation();if (instrumentation == null) {LOGGER.info("Instrumentation missing, PFinder PluginRegistrar enhance ignored!");return false;}this.pluginLoaders = profilerContext.getAllService(PluginLoader.class);this.enhanceHandler = new EnhancePluginHandler(profilerContext);ElementMatcher.Junction<TypeDescription> typeMatcherChain = null;for (PluginLoader pluginLoader : this.pluginLoaders) {pluginLoader.loadPlugins(profilerContext);for (ElementMatcher.Junction<TypeDescription> typeMatcher : (Iterable<ElementMatcher.Junction<TypeDescription>>)pluginLoader.typeMatchers()) {if (typeMatcherChain == null) {typeMatcherChain = typeMatcher; continue;}typeMatcherChain = typeMatcherChain.or((ElementMatcher)typeMatcher);}}if (typeMatcherChain == null) {LOGGER.warn("no any enhance-point. pfinder enhance will be ignore.");return false;}ConfigurationService configurationService = (ConfigurationService)profilerContext.getService(ConfigurationService.class);String enhanceExcludePolicy = (String)configurationService.get(ConfigKey.PLUGIN_ENHANCE_EXCLUDE);LoadedClassSummaryHandler loadedClassSummaryHandler = null;if (((Boolean)configurationService.get(ConfigKey.LOADED_CLASSES_SUMMARY_ENABLED, Boolean.valueOf(false))).booleanValue()) {loadedClassSummaryHandler = new LoadedClassSummaryHandler.DefaultImpl(configurationService, ((ScheduledService)profilerContext.getService(ScheduledService.class)).getDefault());}(new AgentBuilder.Default()).with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION).with(AgentBuilder.RedefinitionStrategy.REDEFINITION).with(new AgentBuilder.RedefinitionStrategy.Listener(){public void onBatch(int index, List<Class<?>> batch, List<Class<?>> types) {}public Iterable<? extends List<Class<?>>> onError(int index, List<Class<?>> batch, Throwable throwable, List<Class<?>> types) {return Collections.emptyList();}public void onComplete(int amount, List<Class<?>> types, Map<List<Class<?>>, Throwable> failures) {for (Map.Entry<List<Class<?>>, Throwable> entry : failures.entrySet()) {for (Class<?> aClass : entry.getKey()) {PluginRegistrar.LOGGER.warn("Redefine class: {} failure! ignored!", new Object[] { aClass.getName(), entry.getValue() });}}}}).ignore((ElementMatcher)ElementMatchers.nameStartsWith("org.groovy.").or((ElementMatcher)ElementMatchers.nameStartsWith("jdk.nashorn.")).or((ElementMatcher)ElementMatchers.nameStartsWith("javax.script.")).or((ElementMatcher)ElementMatchers.nameContains("javassist")).or((ElementMatcher)ElementMatchers.nameContains(".asm.")).or((ElementMatcher)ElementMatchers.nameContains("$EnhancerBySpringCGLIB$")).or((ElementMatcher)ElementMatchers.nameStartsWith("sun.reflect")).or((ElementMatcher)ElementMatchers.nameStartsWith("org.apache.jasper")).or((ElementMatcher)pfinderIgnoreMather()).or((ElementMatcher)Matchers.forPatternLine(enhanceExcludePolicy)).or((ElementMatcher)ElementMatchers.isSynthetic())).type((ElementMatcher)typeMatcherChain).transform(this).with(new Listener(loadedClassSummaryHandler)).installOn(instrumentation);return true;}
5.1.多線程traceId丟失問(wèn)題
public class TracingRunnableimplements PfinderWrappedRunnable{private final Runnable origin;private final TracingSnapshot<?> snapshot;private final Component component;private final String operationName;private final String interceptorName;private final InterceptorClassLoader interceptorClassLoader;public TracingRunnable(Runnable origin, TracingSnapshot<?> snapshot, Component component, String operationName, String interceptorName, InterceptorClassLoader interceptorClassLoader) {this.origin = origin;this.snapshot = snapshot;this.component = component;this.operationName = operationName;this.interceptorClassLoader = interceptorClassLoader;this.interceptorName = interceptorName;}public void run() {TracingContext tracingContext = ContextManager.tracingContext();if (tracingContext.isTracing() && tracingContext.traceId().equals(this.snapshot.getTraceId())) {this.origin.run();return;}LowLevelAroundTracingContext context = SpringAsyncTracingContext.create(this.operationName, this.interceptorName, this.snapshot, this.interceptorClassLoader, this.component);context.onMethodEnter();try {this.origin.run();} catch (RuntimeException ex) {context.onException(ex);throw ex;} finally {context.onMethodExit();}}public Runnable getOrigin() {return this.origin;}public String toString() {return "TracingRunnable{origin=" + this.origin + ", snapshot=" + this.snapshot + ", component=" + this.component + ", operationName='" + this.operationName + '\'' + '}';}}
拿線程池執(zhí)行Runnable任務(wù)來(lái)說(shuō),pfinder通過(guò)TracingRunnable包裝我們的Runnable的實(shí)現(xiàn),利用構(gòu)造函數(shù)將主線程的traceId通過(guò)snapshot參數(shù)傳給TracingRunnable,在run方法中將參數(shù)snapshot放到上下文中,最后從上下文中取出放到子線程的MDC中,從而實(shí)現(xiàn)traceId跨線程傳遞。
