字节码增强常见问题系列(二)| 兼容性难题:如何让不同字节码增强框架和谐共存?
往期回顾:
字节码增强 常见 问题系列(一) | 记一次多个 JavaAgent同时使用的类增强冲突问题及分析
一、前言
当前市面上JavaAgent广泛被用于解决各种场景的问题,包括服务治理、链路追踪等。各大厂商和开源社区也都推出了自己的JavaAgent产品,例如Skywalking、阿里的JVM-Sandbox、Sermant等。用户在真实生产环境中可能会采用多个JavaAgent产品,不同的JavaAgent产品可能采用不同的字节码增强框架(ASM、Javassist、ByteBuddy、CGLIB),而在使用不同的字节码增强框架时,可能会出现各种冲突问题,这些冲突可能导致字节码增强失效、应用程序无法启动等问题。即使是使用相同的字节码增强框架也可能会出现冲突问题。对用户而言,在生产环境引入多个不同或者相同的字节码增强框架而不出现兼容性问题尤为重要。在上一期中,我们从字节码增强的底层逻辑角度分析了多个Java Agent加载冲突的问题。本期我们将重点介绍在使用多个字节码增强框架时可能遇到的兼容性问题,并结合我们的经验给出相应的解决方式以及合理的规避手段。
二、常见的字节码增强框架
ASM是基于访问者模式封装的字节码增强框架,可以生成二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。由于AMS是在虚拟机指令层面进行字节码操作,需要开发者熟悉JVM的各种指令集。
Javassist是基于ASM进行二次开发的框架,在AMS的基础上屏蔽了JVM指令集的概念,提供了易于使用的高级 API,使开发者能够动态地创建、修改、分析Java类。但Javassist的增强逻辑采用的是硬编码形式,在开发过程中无法进行debug。
ByteBuddy 是一个简单而强大的字节码操作库,可以与 Java Agent 结合使用来实现动态修改和增强类的字节码。它提供了易于使用的 API,并支持在运行时生成代理对象、修改方法的行为、实现 AOP 和动态类创建等功能。
CGLIB(Code Generation Library)是一个基于 ASM 的高性能字节码生成库,它用于在运行时动态生成子类来扩展现有类。CGLIB 可以对类进行拦截和代理,实现方法增强、AOP 功能等。
三、兼容性问题案例
在社区的场景案例中,遇到了以下由于使用不同字节码增强框架而导致的兼容性问题:
案例一:Javassist和ByteBuddy增强冲突
• 背景描述:
用户在使用Sermant进行字节码增强的基础上再使用基于Javassist的JavaAgent产品进行增强时发现Sermant的功能都失效了。而切换两者的加载顺序之后发现功能都正常生效。
• 原因分析:
通过对字节码增强框架的源码进行分析发现:ByteBuddy进行字节码增强时是基于JVM的Java Class元数据来进行的,如果在ByteBuddy之前已经加载了Javassist增强框架,则获取到的Class元数据是Javassist增强之后的,ByteBuddy会在原有增强的基础上再进行增强,进而保证两个增强效果都可以保留。
Javassist进行字节码增强时,是从ClassPool 里面去拿Class元信息,如果在Javassist之前已经加载了其他字节码增强框架,通过这种方式无法获取到增强之后的Class信息,Javassist在此基础上增强的话就会把之前增强的Class信息覆盖掉,导致前面的增强失效。
• 解决方案:
由于Javassist无法在ByteBuddy增强的基础上进行增强,因此在同时使用Javassist和ByteBuddy时,一定要注意两者的加载顺序,需要将基于Javassist的JavaAgent产品在基于ByteBuddy的JavaAgent产品之前进行加载。
案例二:基于ByteBuddy的不同组件之间增强冲突
• 背景描述:
用户在使用SkyWalking进行链路采集的基础上想通过Sermant实现微服务的治理功能(SkyWalking和Sermant都是基于ByteBuddy的),但在挂载SkyWalking的基础上再挂载Sermant之后发现,服务启动耗时大幅增加,而单独挂载Sermant服务启动耗时无明显增加。
• 原因分析:
通过分析服务启动的CPU火焰图(见下图,紫色是Skywalking中ByteBuddy对Sermant进行增强前的类型扫描和字节码增强的CPU占用情况),发现SkyWalking使用的ByteBuddy对Sermant的类进行了字节码增强前的类型扫描,并进行了字节码增强,由于引入额外的JavaAgent组件,导致需要JVM加载的类增多,ByteBuddy在进行增强前的类型扫描时需要扫描的类也增多,最终导致服务启动耗时增加。并且在分析过程中我们也发现,ByteBuddy在进行扫描并且查找插桩点的过程中,基于模糊匹配(比如,通过类的父类,或者类所实现的接口来匹配)时,启动时的ByteBuddy所占用CPU时间片占用明显增多。
• 解决方案:
方案一:ByteBuddy可以通过ignore方法取消对于某些类进行增强前的类型扫描。Ignore方法可以取消对于指定前缀的类的扫描。因此在使用多字节码框架时,可以通过配置需要取消扫描的类的前缀信息,减少进行增强前需要扫描的类,进而减少服务启动时间。
方案二:ByteBuddy也可以通过定义匹配器Ignored的方式取消对于某些类进行增强前的类型扫描。Ignored支持对于指定类加载器加载的类进行匹配。因此在使用多字节码框架时,可以通过配置需要取消扫描的类的类加载器信息,减少进行增强前需要扫描的类,进而减少服务启动时间。
方案三:
我们可以在使用ByteBuddy过程中,尽量减少使用模糊匹配来匹配我们想要增强的类,在程序规模较大时,通过精确匹配可以有效的降低字节码增强产品对启动耗时的影响。
下图为采用上述方案优化后的火焰图,可以明显看出,在启动过程中Skywalking中ByteBuddy所占用的CPU时间片减少:
另外,针对方案一和方案二,当前Sermant已支持通过配置的方式,来实施针对特定类的忽略,我们也在Skywalking社区提交了issue - 11160 ,一起讨论该方案是否也在Skywalking中可行,来帮助社区用户更好的使用字节码增强的工具。
案例三:ByteBuddy和与CGLIB增强冲突
• 背景描述:
用户在业务服务使用Spring的Cglib实现服务负载均衡的基础上,又想使用Sermant(基于ByteBuddy框架)进行流量管理,但是挂载Sermant之后出现了服务启动失败的问题,问题详细情况见下图:
• 原因分析:
由于Spring使用CGLIB对LoadBalancerFeignClient进行增强时会创建代理类并修改方法参数和局部变量等字节码信息,因此当Sermant使用ByteBuddy对LoadBalancerFeignClient的代理类进行增强时,会出现字节码校验错误(由于该问题成因复杂,我们将在后续文章中对其进行深入分析) 。
• 解决方案:
在CGLIB增强逻辑中,往往都是继承被增强类的方式创建代理类,所以我们只需要将插桩位置精确选择到需要增强的类即可,因为是继承关系,即使程序使用的是代理类的方法,最终也会触发我们在其父类同方法中织入的逻辑。
四、总结
当前市面上JavaAgent广泛被用于解决各种场景的问题,各大厂商和开源社区也针对字节码增强技术推出了自己的JavaAgent产品,用户在真实生产环境中可能会采用多个JavaAgent产品,结合上文中提到的案例,这里针对用户使用不同字节码增强框架时可能出现的兼容问题给出几条建议。
• 合理安排字节码增强框架的加载顺序
在案例一中我们提到Javassist在ByteBuddy之后加载时,通过ByteBuddy增强的功能都失效了,而Javassist在ByteBuddy之前加载时两者功能都正常,因此在使用不同的字节码增强框架时,应该合理安排加载顺序,把兼容性更高的框架放在后面进行加载。
• 减少字节码增强框架之间的相互影响
在进行类匹配时,可以通过忽略对于指定类(一些无需被增强的类或者其他字节码增强框架的类)的扫描来减少字节码增强框架的启动耗时,尤其在多字节码增强框架时,效果更加明显。与此同时还可以避免不同字节码增强框架之间相互增强可能导致的逻辑异常。
• 遵守字节码增强的使用要求和限制
在进行字节码增强时应该按照官方接口的设计理念,增强类时不要新增、删除或者重命名字段和方法,也不要更改方法的签名,更不要更改类的继承关系。相关案例和原理分析可以参考:记一次多个 JavaAgent同时使用的类增强冲突问题及分析
Sermant 作为专注于服务治理领域的字节码增强框架,致力于提供高性能、可扩展、易接入、功能丰富的服务治理体验,并会在每个版本中做好性能、功能、体验的看护,广泛欢迎大家的加入。
• Sermant 官网: https://sermant.io
• GitHub 仓库地址: https://github.com/huaweicloud/Sermant
• 扫码加入 Sermant 社区交流群
- 点赞
- 收藏
- 关注作者
评论(0)