性能优化那些事(3)

2022-04-22  本文已影响0人  ThoughtWorks

在讨论完性能优化的方面策略之后,这次我们的文章更偏向技术层面,来分享下如何开发一个自己的性能分析工具(基于JVM)。

『新』知识

考虑到咱们大多数还是开发业务为主,所以Java里面一些『鲜为人知』的API可能很多人都不知道,这里就简单介绍一番,如果想深究的,就自己谷歌一下吧。

[图片上传失败...(image-cb712e-1650620049744)]

有了上面两个知识,其实我们就可以开发一个简单的Agent了,Instrumentation可以理解为JVM层面的AOP(Aspect Oriented Programming),通过应用启动时挂载Agent,我们可以对每一个class字节码进行查看和修改。

好了,介绍完Instrumentation和ASM,我们是不是就可以满足制作性能分析工具的前提条件了呢?你看我们通过Instrumentation进行JVM层面的AOP,再通过ASM对JAVA的字节码进行修改,就可以着手完成性能分析最重要的埋点环节了。

看起来没有错,但是谁也不希望我们增强修改过的代码一直存在内存中,分析一次就对环境造成不可逆的破坏吧。Instrumentation可以通过addTransformer添加字节码转换器,也可以将字节码恢复原样(只需要removeTransformer再retransformClasses就可以恢复了),但javaAgent毕竟是个单独的jar包,它也会有一些依赖,将其加载进来必然会引发新的Class加载甚至是Class的冲突。那么新的问题就出来了,javaAgent如何不对现有的类有影响呢?

额外的类加载器实现了业务代码和Agent代码类的隔离,使它们可以安全引用包,并且可以对Agent的类进行卸载,但这样同时引入了一个新的问题。类是隔离的,我在对业务代码进行增强时,如何向agent代码传递信息?增强的代码一定是被加载在AppClassLoader里,如何与AgentClassLoader进行通讯呢?

通过上面的介绍,我们现在可以动手做一个自己的APM工具了,通过Instrumentation+ASM,我们可以实现Class文件的修改增强,甚至可以修改JDK自带的类比如String,通过自定义的ClassLoader我们可以隔离Agent的类和业务的类,通过打入BootStrap的Spy,我们可以实现跨ClassLoader之间的通讯。

万事俱备,我们现在可以开始动手实现一个自己的APM工具了吧!

打住,其实上面这些功能不需要自己一一实现,我们不需要重复制造轮子,来自阿里开源项目JVM-SANDBOX此时华丽登场。这个项目屏蔽了ASM难以使用的缺点,也简化了Instrumentation打桩过程,并且实现了ClassLoader的隔离,也有了BootStrapClassLoader中的Spy类,我们在此框架的基础上进行开发更为简单。

[图片上传失败...(image-7a4a0f-1650620049744)]
原图链接:https://github.com/alibaba/jvm-sandbox/wiki/img/jvm-sandbox-classloader.png

集『大』成

我们拥有了JVM-SANDBOX这一利器,似乎节约了我们很多的时间,我们现在终于可以着手性能分析了。

那么怎么进行性能分析呢?

我们可以引入Zipkin或者Jaeger作为收集者和UI展现,根据自己的喜好选择一个好用的开源工具。通过sandbox提供的功能,我们可以很方便编写埋点代码,将我们的链路追踪工具集成到Agent里面,最终实现无侵入的定制化链路追踪。

通过集成ZipkinClient或者JaegerClient我们可以进行埋点收集,我们似乎把一些功能以搭积木的方式组装起来,解决了一个颇为复杂的实现,这就是开源的魅力所在。其实在实际的过程中我们还遇到了一些困难,比如如何追踪异步调用,如何追踪跨线程的调用,如何处理线程池,如何处理ForkJoin?

其中最为复杂的是如何处理那些跨线程的派发,我们如何将链路的上下文在多个线程中传递。JDK的InheritableThreadLocal类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时的ThreadLocal值传递到任务执行时

说起来可能不好理解,总得来说无论是ThreadLocal还是InheritableThreadLocal都无法处理线程池或者ForkJoin带来的线程复用的副作用,即无法有效准确安全的传递链路的上下文,不信大家可以试一试。

那么怎么解决这个问题呢?没错,就是修改JDK源码,让线程池在进行调度的时候具有安全准确传递上下文信息的能力,比如对Runnable和Callable接口进行增强处理,让其可以携带线程的上下文。如果要对JDK的代码进行增强,我们需要非常熟悉线程调度、线程池、Forkjoin的源码,还需要小心处理值的传递确保安全,听起来就很危险,也很困难。不用担心我们不是第一次遇到这种问题的人,我们再次搬来了阿里的开源产品TTL,这个库解决的就是上面描述的问题。

但是找到开源产品也并不一定能解决所有的问题,transmittable-thread-local虽然能够解决线程复用时传值的问题,但是它的实现对JDK代码进行了『过分』的修改,以至于Instrumentation不能进行动态增强,它需要在启动时未加载到ClassLoader的时候对JDK的源码进行增强,并不能对已加载的JDK源码进行动态增强,也就是说这种增强只能发生在一开始,不能发生在中间时间,且不可卸载。

这是因为Instrumentation的redefineClasses这个方法存在限制:重定义不得添加、移除、重命名字段或方法;不得更改方法签名、继承关系(不然那些商业的热重载技术怎么赚钱。。)。而TTL的增强违反了这个原则,我们需要对其修改,并集成到Agent中。这个改造比较无趣也不好解说,可以直接看改造后的JVM-SANDBOX,我们为了后续使用方便,将TTL库直接用BootStrapClassLoader加载了进去。

开源

最终开源的性能分析工具可以在这里找到:https://github.com/tmtbe/PVisualization,配合改造后的JVM-SANDBOX,可以实现360度无死角的性能链路追踪分析,开发埋点也非常便捷,也无需考虑任何线程池的问题。有兴趣的小伙伴可以试着使用一下,如有项目需要可以直接找我支持。

[图片上传失败...(image-7b5fbd-1650620049744)]
原图链接:https://github.com/tmtbe/PVisualization/raw/master/source/img.png



文/Thoughtworks张锦程
原文链接:https://insights.thoughtworks.cn/performance-turning-practice-3/

上一篇下一篇

猜你喜欢

热点阅读