字节码增强常见问题系列(二)| 兼容性难题:如何让不同字节码增强框架和谐共存?
往期回顾:
字节码增强常见问题系列(一)| 记一次多个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 社区交流群
评论