写点什么

pfinder 实现原理揭秘

  • 2024-06-03
    北京
  • 本文字数:8614 字

    阅读完需:约 28 分钟

1.引言


在现代软件开发过程中,性能优化和故障排查是保证应用稳定运行的关键任务之一。Java 作为一种广泛使用的编程语言,其生态中涌现出了许多优秀的监控和诊断工具,诸如:SkyWalking、Zipkin 等,它们帮助开发者和运维人员深入了解应用的运行状态,快速定位和解决问题。在京东内部,则使用的是自研的 pfinder。


本文旨在深入探讨 pfinder 的核心原理和架构设计,揭示它是如何实现应用全链路监控的。我们将从 pfinder 的基本概念和功能开始讲起,逐步深入到其具体实现机制。


1.pfinder 概述

2.1.pfinder 简介

PFinder (problem finder) 是 UMP 团队打造的新一代 APM(应用性能追踪)系统,集调用链追踪、应用拓扑、多维监控于一身,无需修改代码,只需要在启动文件增加 2 行脚本,便可实现接入。接入后便会对应用提供可观测能力,目前支持京东主流的中间件,包括:jimdb,jmq,jsf,以及一些常用的开源组件:tomcat、http client,mysql,es 等。

2.2.pfinder 功能

PFinder 除了具备 ump 现有功能的基础上,增加了以下重磅功能:


多维监控: 支持按多个维度统计监控指标,按机房、按分组、按 JSF 别名、按调用方,各种维度随心组合查看


自动埋点: 自动对 SpringMVC,JSF,MySQL,JMQ 等常用中间件进行性能埋点,无需改动代码,接入即可观测


应用拓扑: 自动梳理服务的上下游和中间件的依赖拓扑


调用链追踪: 基于请求的跨服务调用追踪,助你快速分析性能瓶颈


自动故障分析: 通过 AI 算法自动分析调用拓扑上所有服务的监控数据,自动判断故障根因


流量录制回放: 通过录制线上流量,回放至待特定环境(测试、预发),对比回放与录制时产生的差异,帮助用户补全业务场景、完善测试用例


跨单元逃逸流量监控: 支持 JSF 跨单元流量、逃逸流量监控,单元化应用运行状态一目了然

2.3.APM 类组件对比


更重要的一点是:pfinder 对京东内部自研组件提供了支持,比如:jsf、jmq、jimdb


1.pfinder 背后的秘密


既然 pfinder 是基于字节码增强实现的,那么讲到 pfinder,字节码增强技术自然也是无法避开的话题。这里我将字节码增强技术分两点来说,也是我认为实现字节码增强需要解决的两个关键点:


1.字节码是为了机器设计的,而非人类,字节码可读性极差、修改门槛极高,那么我们如何修改字节码呢?


2.修改后的字节码如何注入运行时 JVM 中呢?


欲攻善其事,必先利其器,所以下面我们围绕着这两个问题进行展开,当然,对这方面知识已经有所掌握的同学可忽略。

3.1.字节码修改

字节码修改成熟的框架已经很多了,诸如:ASM、javassist、bytebuddy、bytekit,下面我们用这几个字节码修改框架实现一个相同的功能,来对比下这几个框架使用上的区别。现在我们通过字节码修改来实现下面的功能:



1.ASM 实现


   @Override        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);        }        @Override        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);        }
复制代码


1.javassist 实现


        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();
复制代码


1.bytebuddy 实现


    // 使用ByteBuddy动态生成一个新的HelloWord类        Class<?> dynamicType = new ByteBuddy()                .subclass(HelloWord.class) // 指定要修改的类                .method(ElementMatchers.named("printHelloWord")) // 指定要拦截的方法名                .intercept(MethodDelegation.to(LoggingInterceptor.class)) // 指定拦截器                .make()                .load(HelloWord.class.getClassLoader()) // 加载生成的类                .getLoaded();
// 创建动态生成类的实例,并调用方法 HelloWord dynamicService = (HelloWord) dynamicType.newInstance(); dynamicService.printHelloWord();
复制代码


public class LoggingInterceptor {    @RuntimeType    public static Object intercept(@AllArguments Object[] allArguments, @Origin Method method, @SuperCall Callable<?> callable) throws Exception {        // 打印start        System.out.println("start");        try {            // 调用原方法            Object result = callable.call();            // 打印end            System.out.println("end");            return result;        } catch (Exception e) {            System.out.println("exception end");            throw e;        }    }}
复制代码


1.bytekit 实现


 // Parse the defined Interceptor class and related annotations        DefaultInterceptorClassParser interceptorClassParser = new DefaultInterceptorClassParser();        List<InterceptorProcessor> processors = interceptorClassParser.parse(HelloWorldInterceptor.class);        // load bytecode        ClassNode classNode = AsmUtils.loadClass(HelloWord.class);        // Enhanced process of loaded bytecodes        for (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"); }}
复制代码


3.2.字节码注入

相信大家经常使用 idea 去 debug 我们写的代码,我们是否想过 debug 是如何实现的呢?暂时先卖个关子。


1.JVMTIAgent


JVM 在设计之初就考虑到了对 JVM 运行时内存、线程等指标的监控和分析和代码 debug 功能的实现,基于这两点,早在 JDK5 之前,JVM 规范就定义了 JVMPI(JVM 分析接口)和 JVMDI(JVM 调试接口),JDK5 之后,这两个规范就合并成为了 JVMTI(JVM 工具接口)。JVMTI 其实是一种 JVM 规范,每个 JVM 厂商都有不同的实现,另外,JVMTI 接口需使用 C 语言开发,以动态链接的形式加载并运行。



其实 idea 的 debug 功能便是借助 JVMTI 实现的,具体说是利用了 jre 内置的 jdwp agent 来实现的。我们在 idea 中 debug 程序时,控制台命令如下:



这里 agentlib 参数就是用来跟要加载的 agent 的名字,比如这里的 jdwp(不过这不是动态库的名字,而 JVM 是会做一些名称上的扩展,比如在 MACOS 下会去找 libjdwp.dylib 的动态库进行加载,也就是在名字的基础上加前缀 lib,再加后缀.dylib)。


1.instrument


上面说到 JVMTIAgent 基于 C 语言开发,以动态链接的形式加载并运行,这对 java 开发者不太友好。在 JDK5 之后,JDK 开始提供 java.lang.instrument.Instrumentation 接口,让开发者可以使用 Java 语言编写 Agent。其实,instrument 也是基于 JVMTI 实现的,在 MACOS 下 instrument 动态库名为 libinstrument.dylib。



1.instrument 和 ByteBuddy 实现 javaagent 打印方法耗时


1.agent 包 MANIFEST.MF 配置(maven 插件)


<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) { //创建一个代理增强对象 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);    @RuntimeType    public static Object intercept(@SuperCall Callable<?> callable) throws Exception {        long start = System.currentTimeMillis();        try {            // 原方法调用            return callable.call();        } finally {            long end = System.currentTimeMillis();            log.info("Method call took {} ms",(end - start));        }    }}
复制代码


4.效果



1.pfinder 实现原理

4.1.pfinder 应用架构


1.pfinder agent 启动时首先加载 META-INF/pfinder/service.addon 和 META-INF/pfinder/plugin.addon 配置文件中的服务和插件。2.根据加载的插件做字节码增强。3.使用 JMTP 将服务和插件产生的数据(trace、指标等)进行上报。

4.2.pfinder 插件增强代码解析

1.service 加载



创建 SimplePFinderServiceLoader 实例,在 profilerBootstrap.boot(serviceLoaders)方法中加载配置文件中的 service。



使用创建的 SimplePFinderServiceLoader 实例加载 service,并返回一个 service 工厂的迭代器。



真正的加载走的是 AddonLoader 中的 load 方法。service 加载完成后,继续看 bootService 方法:



bootService 中完成创建 service 实例、注册 service、初始化 service,service 的加载至此就完成了。


1.plugin 加载 &字节码增强


在介绍插件加载前,我们先了解下插件的包含了哪些信息。



增强拦截器:这个类里面放了具体的增强逻辑


增强点类型:增强时根据不同类型走不同逻辑


增强类/方法匹配器:用于匹配需要增强的类/方法


InterceptPoint 是个数组,增强点可以配置多个。


plugin 的加载和字节码增强发生在初始化 service 过程中,具体地说发生在 com.jd.pfinder.profiler.service.impl.PluginRegistrar 这个 service 初始化的过程中了。


 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; }
复制代码


第 8 行,先从上下文中取出注册的 PluginLoader(插件加载器),第 12 行遍历插件加载器加载插件,插件加载逻辑其实和 service 一样,使用的都是 AddonLoader 中的 load 方法。插件加载完成之后被插件加载器持有,第 14-19 行则收集插件中增强类的匹配器,用于 AgentBuilder 的创建。AgentBuilder 的创建标志着字节码增强的开始,具体的逻辑在 transform 的实例方法中。



transform 方法中遍历插件,enhance 方法中对各个插件做增强。



enhance 方法中遍历各个插件的增强点数组走 enhanceInterceptPoint 方法做增强。



enhanceInterceptPoint 方法中根据增强点类型做增强。



上图是以 Advice 方式增强实例方法,传递了 interceptorFieldAppender 和 methodCacheFieldAppender 两个参数,并使用 AdviceMethodEnhanceInvoker 访问并修改待增强的类和方法。AdviceMethodEnhanceInvoker 中有 onMethodEnter、onMethodExit 两个方法,分别表示进入方法后和退出方法前。




AdviceMethodEnhanceInvoker 中 onMethodEnter、onMethodExit 两个方法还会调用插件中配置 interceptor 对应的 onMethodEnter、onMethodExit、onException 方法,至此插件字节码增强就结束了。


1.我的思考

5.1.多线程 traceId 丢失问题

pfinder 目前已经将 traceId 放到了 MDC 中,我们通过在日志配置文件中添加[%X{PFTID}]便能在日志中打印 traceId。但是我们知道 MDC 使用的是 ThreadLocal 去保存的 traceId,在跨线程时会出现线程丢失的情况。pfinder 在这方面做了字节码增强,无论使用线程池还是 @Async,都不会存在 traceId 丢失的问题。


 public class TracingRunnable   implements 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 + ''' + '}'; } }
复制代码


拿线程池执行 Runnable 任务来说,pfinder 通过 TracingRunnable 包装我们的 Runnable 的实现,利用构造函数将主线程的 traceId 通过 snapshot 参数传给 TracingRunnable,在 run 方法中将参数 snapshot 放到上下文中,最后从上下文中取出放到子线程的 MDC 中,从而实现 traceId 跨线程传递。

5.2.热部署

既然 javaagent 能做字节码增强,也能实现热部署,此外, pfinder 客户端和服务端通过 jmtp 有命令的交互,可以通过服务端向 agent 发送命令来实现类搜索、反编译、热更新等功能,笔者基于这一想法粗略实现了一个在线热部署的功能,具体如下:


类搜索:



反编译:



热更新:



上述只是笔者做的一个简单的实现,还有很多不足的地方:


1.对于 Spring XML、MyBatis XML 的支持。


2.Instrumentation 的局限性:由于 jvm 基于安全考虑,不允许改类结构,比如新增字段,新增方法和修改类的父类等。想要突破这种局限,就需要使用 Dcevm(Java Hostspot 的补丁)了。


欢迎有兴趣的同学一起学习交流。

发布于: 刚刚阅读数: 3
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
pfinder实现原理揭秘_京东科技开发者_InfoQ写作社区