Sermant 类隔离架构解析——解决 JavaAgent 场景类冲突的实践
一、JavaAgent 场景为什么要注意类冲突问题?
类冲突问题并非仅存在于 JavaAgent 场景中,在 Java 场景中一直都存在,该问题通常会导致运行时触发 NoClassDefFoundError、ClassNotFoundException、NoSuchMethodError 等异常。
从使用场景来看,基于 JavaAgent 技术所实现的工具,往往用于监控、治理等场景,并非企业核心业务程序。如果在使用时引入类冲突问题,可能会造成核心业务程序故障,得不偿失,所以避免向核心业务程序引入类冲突是一个 JavaAgent 工具的基本要求。
还有一个重要原因是在 Java 应用中可以于开发态采用依赖的升降级、统一依赖架构治理等手段解决该问题。但基于 JavaAgent 技术实现的工具作用于运行态,无法在开发态就和需要被增强的 Java 应用进行统一的依赖管理,所以引入类冲突问题的可能性更大。
二、JavaAgent 场景如何解决该问题?
无论是在 Java 应用中,还是在 JavaAgent 场景,修复类冲突的逻辑都是一致的,就是避免引入会冲突的类。不同点在于基于 JavaAgent 技术实现的各式各样的工具,往往都具有业务无关性,在设计和实现之初,并不会为特定的 Java 应用类型而定制。对于 JavaAgent 程序而言,需要被字节码增强的应用即是黑盒,所以无法像 Java 应用那样去梳理依赖结构,排除、升级依赖项,统一进行依赖架构治理。并且 JavaAgent 往往在运行时使用,所以只能通过保障依赖绝对隔离的方式来避免引入冲突。
为何会产生类冲突,本文重点不在此,简单讲是因为我们在 Java 中因为重复引入传递依赖、类的加载顺序无法控制等问题,导致引入了相同【类加载器和全限定类名(Fully Qualified Class Name)都相同】但又表现不同【因为类版本不同而导致的类逻辑不一致】的类。所以为了避免冲突,我们就需要避免在运行时引入相同的类,如何让 JavaAgent 引入的类和宿主完全不相同,从全限定类名和类加载器下手才是根本:
基于 maven shade plugin 进行类隔离
该插件是 Maven 提供用于构建打包的插件,通过 maven-shade-plugin 的‘Relocating Classes’能力,来修改某些类的全限定类名。
此方法的原理便是通过改变全限定类名来让 JavaAgent 引入的类和 Java 程序的类完全不可能出现相同的情况,从根本上避免类冲突。但是我们在使用一种框架,或者使用一种产品时,往往约定要优于配置,基于 maven-shade-plugin 通过配置去改变全限定类名并不是一个简单的办法,在使用时就需要针对 JavaAgent 所涉及依赖进行梳理,在 maven-shade-plugin 中进行配置,并且需要在每次依赖变更后重新筛查。对持续迭代极不友好。
采用上述方法也对 Debug 造成阻碍,在 Debug 过程中被重定向的类的断点将不可达,严重降低调试效率。
基于类加载机制进行类隔离
基于 maven-shade-plugin 修改全限定类名往往用来解决单点的类冲突问题,虽然也能做到将 JavaAgent 所引入类完全隔离,但并不是一个好的解决方案。
基于类冲突原理,我们还可以通过限制两个相同全限定类名的类的加载器来让其不同,如 Tomcat 那样,通过自定义类加载器破坏 Java 的双亲委派原则,来隔离 JavaAgent 引入的类。这样既避免了繁重的配置,也避免了依赖变更而带来的影响。但也有其弊端,在 JavaAgent 场景中往往会利用到 Java 应用程序的类,所以基于类加载器的隔离机制,往往就让开发者只能通过反射等操作完成此类逻辑,这会对性能和开发效率产生不良影响。
三、Sermant 如何做?
Sermant 是基于 Java 字节码增强技术的无代理服务网格,不仅是一个开箱即用的服务治理工具,也同样是一个易用的服务治理能力开发框架。
“把简单留给别人,把麻烦留给自己!”
Sermant 从设计之初就遵循上述重要原则,并规划了全方面的类隔离架构,利用 Java 的类加载机制对自身各模块做了充分类隔离,让使用者和开发者无需考虑因使用 JavaAgent 而导致类冲突问题,并且也针对开发者的使用场景做了优化,可以在开发中无缝使用被增强 Java 程序的类,避免因反射等行为带来的不利影响。Sermant 是如何实现的呢,下文将对 Sermant 类隔离架构进行详细解析。
1) Sermant 的类隔离架构解析
如上文所说,Sermant 不仅是个开箱即用的服务网格,也同样是一个易用的服务治理能力开发框架,服务治理能力是多样的包括但不限于流量治理、可用性治理、安全治理等,所以 Sermant 采用插件化的架构来让用户能更灵活的接入和开发服务治理能力。
在 Sermant 的整体架构下,我们不仅需要保证不向宿主服务引入类冲突问题,避免在开箱即用时对宿主服务造成负面影响,同时也需要保障框架与插件、插件与插件之间不会引入类冲突问题,避免插件开发者因为和其他服务治理插件产生类冲突问题而苦恼,所以 Sermant 设计了如下类隔离结构:
SermantClassLoader,破坏双亲委派,用于加载 Sermant 框架核心逻辑,并在 AppClassLoader 下隔离出 Sermant 的类加载模型。避免受到宿主服务自身复杂类加载结构的影响,减少应对不同类加载结构服务的适配需求。
FrameworkClassLoader,破坏双亲委派,主要作用是隔离 Sermant 核心能力所引入的三方依赖,避免向宿主服务及服务治理插件引入类冲突问题。目前的主要场景 ①用于隔离 Sermant 的日志系统,避免对宿主服务的日志系统产生影响 ②隔离 Sermant 框架的核心服务(心跳、动态配置、统一消息网关)所需三方依赖。
PluginClassLoader,遵循双亲委派,主要用于隔离 Sermant 各服务治理插件,避免不同服务治理插件之间产生类冲突问题。
ServiceClassLoader,破坏双亲委派,主要用于隔离插件中的依赖,通过该类加载器加载插件服务的相关 lib(插件服务会在插件加载时被 Sermant 初始化),开发者可任意引入三方依赖,无需关心对插件主逻辑的影响。
其中的 PluginClassloader 和 ServiceClassloader 不仅在类隔离中起到至关重要的作用,更是一种长远的考虑,为每个插件设计独立的类加载器,使得 Sermant 可以平滑的进行插件动态安装 &卸载以及插件热更新。
2) 插件隔离的特殊之处
在上文中所述类隔离架构中,可以看到一处特别的逻辑(红框处),这也是 Sermant 中 PluginClassLoader(插件类加载器)的特殊之处,在实际使用过程中,每个插件类加载器会在其中为每个线程维护一个局部的类加载器(localLoader)。
PluginClassLoader 遵循双亲委派,在类加载过程中先委派 SermantClassLoader 加载 Sermant 的核心类,再通过自身加载插件类,当需要使用宿主服务的类时,则会委托局部类加载器(其 Parent 可以是任何类加载器,不局限于图中所指示)进行加载。用于让字节码增强的切面逻辑(Sermant 拦截器)可以获取到宿主服务所使用的类,这有利于服务治理场景,其逻辑如下图所示:
通过重写类加载器 loadClass 逻辑,在执行 Sermant 拦截器时,配置一个局部的类加载环境,让 Sermant 拦截器中的逻辑可以顺利的使用宿主服务加载的类,这样开发服务治理插件时无需通过反射获取宿主服务的类,从而提升服务治理能力的开发效率和最终运行时的性能,同时还避免了宿主服务和服务治理插件的类冲突。
(代码实现可以在开源仓库进行查看:)
3) 实战效果如何
因接入 JavaAgent 而导致的依赖冲突、类冲突问题乃是业界通病,但如果有 Sermant 的类加载机制加持,该问题则可从根源避免,不再让广大 JavaAgent 的使用者和开发者深受其害!
《拜托,别在 agent 中依赖 fastjson 了》所述案例,是一个因 JavaAgent 而产生的依赖冲突问题的典型场景,其应用通过 AppClassLoader 加载到了 Agent 中 fastjson 的类 FastJsonHttpMessageConverter, 该类依赖 spring-web.jar 的类 GenericHttpMessageConverter,但由于 AppClassLoader 的搜索路径中并没有 spring-web.jar(fastjson 通过 provide 方式引入),最终加载类失败。
但如基于 Sermant 开发则不会产生该问题,基于 Sermant 开发 JavaAgent 和 Spring 应用一起运行时的类隔离架构如下:
在此类加载器的结构下,有两个关键的不同:
由于 Sermant 改变了类加载的结构,通过 Agent 引入的 fastjson 已不在 AppClassLoader 的搜索路径中,因此 Agent 中的 FastJsonHttpMessageConverter 类不再会被 Spring 应用通过 AppClassLoader 加载到,从根源上避免了文中所触发的类冲突问题。
当运行时若 Agent 需要使用 spring-web 的类 GenericHttpMessageConverter 时,则可通过 Sermant 提供的局部类加载环境成功通过 LaunchedUrlClassloader 成功从 Spring 应用中获取。
正是因为此两点差异,让基于 Sermant 开发的能力可以在和应用之间进行类隔离,避免通过 JavaAgent 引入类冲突问题,同时可以在运行时使用应用所引入的类。
四、总结
Sermant 是基于 Java 字节码增强技术的无代理服务网格,其利用 Java 字节码增强技术为宿主应用程序提供服务治理功能。因深知 JavaAgent 场景中类冲突问题会造成的影响,Sermant 在设计之初便为此规划了全面的类隔离架构。经历多次迭代,如今 Sermant 的类隔离架构已可以轻松的应对各种复杂的类加载环境。
除了保证类隔离,Sermant 作为服务网格需要重点关注自身的服务治理能力对宿主服务带来的性能影响,所以也通过独有设计避免因为过度隔离带来的性能损耗。同时 Sermant 还在构建开放的服务治理插件开发生态,并提供高效的服务治理能力开发框架。在类隔离设计时也考虑到了易用性、开发效率提升等方面的问题。并未因为类隔离机制的存在,而降低开发的效率,增大学习曲线的陡峭程度。
Sermant 作为专注于服务治理领域的字节码增强框架,致力于提供高性能、可扩展、易接入、功能丰富的服务治理体验,并会在每个版本中做好性能、功能、体验的看护,广泛欢迎大家的加入。
Sermant 官网:https://sermant.io
GitHub 仓库地址:https://github.com/huaweicloud/Sermant
扫码加入 Sermant 社区交流群
评论