作者:华为云高级软件工程师 栾文飞
一、概述
Sermant是基于Java字节码增强技术的无代理服务网格,其利用Java字节码增强技术,为宿主应用程序提供服务治理功能,以解决大规模微服务场景中的服务治理问题,通过Java字节码增强技术,可以非侵入的提供服务治理能力。在以往版本中,Sermant通过配置-javaagent指令在微服务启动时接入服务治理能力,当需要接入及卸载Sermant时都需要通过重新启动微服务来完成。但从1.2.0版本开始,Sermant实现了在服务不停机状态下进行安装和卸载的能力,为服务治理能力带来全新接入体验。本文将会对这种动态接入的机制,从技术基础到Sermant设计进行一次深入分析。
二、JavaAgent加载方式
首先介绍一下JavaAgent的不同接入方式,这是Sermant实现动态接入能力的技术基础。Java 中Instrumentation API 提供了一种修改字节码的机制,利用该API,可以通过修改字节码的方式来改变程序的行为,而不用触及程序的源码。JavaAgent为Instrumentation API的客户端,通过JavaAgent可以调用API进行字节码的操作,其提供了两种加载方式给开发者重载:
静态加载:利用premain,在应用程序启动时加载 JavaAgent称为静态加载,静态加载会在启动时在执行任何代码之前修改字节码。
静态加载时,字节码增强是在类加载时发生的,当Java程序启动时,类加载过程中所有被加载的类都会经过JavaAgent所定义的类文件转换器的处理。
动态加载:利用agentmain通过Java Attach API将JavaAgent加载到已运行的JVM中,动态加载可以通过字节码重转换的方式在运行时修改字节码。
动态加载时,和静态加载不同的是,此时JVM已在运行,目标类已被加载,就不能像静态加载时一样触发字节码增强过程,在使用动态加载的过程中,往往会通过Instrumentation API来触发目标类(当然也可以指定所有已被加载的类)的重转换过程,在重转换过程中就会触发到Agent构建的类文件转换器,从而完成字节码增强过程。
动态加载方式为JavaAgent提供了在JVM运行时接入的能力,但通过类重转换来触发字节码增强相对于在类加载时增强有一定的局限性,例如不能在增强时修改类的继承关系,不能为类添加静态代码块,不能增强内存中和资源文件中字节码不一致的类等,这些也是在使用动态加载和多JavaAgent场景中常见的问题,综上,两种加载方式各有利弊,可以在使用时按照业务场景选择。
三、Sermant热插拔能力关键问题剖析
在了解技术基础后,我们能轻易的想到,理论上基于JavaAgent的动态加载方式,只需要在使用Sermant时,将通过premain方式启动改为通过agentmain方式启动,就可以将微服务治理能力动态的接入到微服务中,做到微服务零侵入、微服务不停机的状态下接入服务治理能力,但通往前方的路上总是充满了障碍:
3.1 如何保证动态安装过程中重转换可顺利执行?
这个问题的出现,根源在于JavaAgent通过agentmain方式加载到已运行的JVM中时,不同于静态加载,会在类初次被加载时完成字节码的转换,动态加载时一些需要被字节码增强类已经完成了类加载过程,这时候需要使用Instrumentation提供的类重转换(retransform classes)能力来修改字节码,在Instrumentation的Javadoc中关于这个能力有这样一段描述:
“The retransformation must not add, remove or rename fields or methods, change the signatures of methods, or change inheritance.(重转换过程中,我们不能新增、删除或者重命名字段和方法,不能更改方法的签名,不能更改类的继承。)”
从中可以看出,在引入动态加载能力前,优先要保证字节码增强时,不可以有上述内容中所描述的限制操作。
不过Sermant不太需要担心这个问题,因为这种限制不仅仅在动态加载时会触发,在多个JavaAgent同时使用时也可能会触发,可以参考Sermant团队的另一篇文章:《记一次多个JavaAgent同时使用的类增强冲突问题及分析》。为了保证在多Agent场景下的兼容性,Sermant的字节码增强模板严格遵循Instrumentation API的限制,因此Sermant在兼容性上的不断改进过程中无心插柳,帮助动态加载能力铺平了路。
3.2 如何保证在服务治理插件安装和卸载时不互相影响?
Sermant的设计中,通过字节码增强引入的服务治理能力,是通过在目标方法上添加服务治理功能切面来完成的,每一个服务治理插件,通过一系列切面的配合来达成最终的服务治理效果。不同的服务治理功能,可能会对同一个目标方法进行处理。但并不会对同一个方法进行多次字节码增强,而是通过一次字节码增强织入调度切面(onMethodEnter、onMethodExit等),通过该切面对相关的服务治理能力(通过拦截器实现,每一个切面会对应一个拦截器的列表)进行调度:
对于服务治理能力的调度逻辑我们在另一篇文章《开发者能力机制解析,玩转Sermant开发》有讲过,本篇不再赘述。
基于框架的基本设计,就需要考虑两个问题,当插件在动态安装时,如何保证不重复字节码增强?当插件卸载时,如何保证不会导致有相同目标方法的插件失效。
- 安装时如何保证不重复执行字节码增强?
在字节码增强开发过程中,类文件转换器(ClassFileTransformer)是一定会接触到的概念,开发者需要基于该转换器来进行字节码的处理。在大多数的字节码增强框架中,都会对其进行封装,用于降低字节码处理的难度。Sermant基于ByteBuddy提供的类文件转换器实现了一种可重入的类转换器,在插件动态安装时,虽然目标方法已经被已安装的插件增强过了,但此时还是会触发类文件转换(因为动态安装插件的过程是独立的),当触发类文件转换时,所有相关的类文件转换器都会被唤醒,再次触发类文件转换过程。每次可重入类转换器被唤醒时,将发生以下行为:
在Sermant中维护了一个针对目标方法的字节码增强锁(AdviceKey锁),即针对每一个目标方法,维护了1个信号量当做锁,用于让各类文件转换器来检查目标方法的字节码增强状态,当目标方法对应的类被类转换时,就会触发Sermant所提供的类文件转换器,此时类文件转换器将尝试获取针对目标方法的信号量,如果能获取信号量,则执行对目标方法的字节码增强,如果不能获取,则不执行字节码增强。
基于字节码增强锁,在转换器触发时,主要有两条路径可以走,类文件转换器会通过目标方法的AdviceKey(类名+方法hash+类加载器组成的一个唯一表示,用于表示字节码增强的目标)来检查其所关联的锁,判断当前目标方法是否已被Sermant进行过字节码增强(织入拦截器调度的切面):
- 能获取锁,说明未被增强:则当前文件转换器获取当前AdviceKey所关联的锁,将其获取的锁通过其对应的插件来维护,并且执行字节码增强,将服务治理所需的拦截器放入该AdviceKey所对应的拦截器列表;
- 不能获取锁,说明已被增强:则只将拦截器放入该AdviceKey对应的拦截器列表中,不执行字节码增强。
通过上述机制,就可以保证Sermant在安装不同服务治理插件时,不会进行重复的字节码增强,避免无端的性能和资源损耗。
- 卸载时如何保证不会导致其他插件失效?
当插件需要卸载时,会再次触发相关目标类的重转换,与安装时不同的是,这次需要被卸载的插件释放自身已经持有的AdviceKey锁。释放锁后,触发目标类重转换时,目标类所对应的各个插件的类文件转换器将会再次触发和安装时相同的流程:
在这个过程中,未被卸载的插件所提供的对目标类的类文件转换器,会在目标类重转换时,再次触发,并且只会经历获取锁和字节码增强的过程。这样就保证,如果还有插件需要对该目标方法进行字节码增强时,可以获得目标方法所对应的锁,不会因为目标方法的交集而导致其他插件能力失效。
四、总结
本篇文章对Sermant的热插拔能力的核心机制进行了解析,希望可以为开发者及使用者在开发或使用相关能力时带来更多的灵感和便利。更多的热插拔能力介绍可以参考官网相关文档,Sermant Agent使用手册,后续我们也会针对热插拔适用的场景进行进一步分享,敬请期待。
Sermant作为专注于服务治理领域的字节码增强框架,致力于提供高性能、可扩展、易接入、功能丰富的服务治理体验,并会在每个版本中做好性能、功能、体验的看护,广泛欢迎大家的加入。
Sermant 官网:https://sermant.io
GitHub 仓库地址:https://github.com/huaweicloud/Sermant
扫码加入 Sermant 社区交流群