作者:栾文飞 华为云高级软件工程师
前言:
在《Sermant框架下的服务治理插件快速开发及使用指南》中带大家一起体验了Sermant插件的开发,快速的了解了Sermant插件开发的全过程,本着从入门到精通的思路,本文对在开发中所常用的能力,从机制上进行更深入的解析。
插件加载&插件调度
解析插件的加载和调度前,可以再回顾一下,Sermant作为一个基于Java字节码增强技术的插件化服务网格,在设计之初就为插件设计了完整的类隔离机制,在《Sermant类隔离架构解析——解决JavaAgent场景类冲突的实践》中进行的详尽的介绍和分析,避免让开发者陷入到复杂的类冲突问题中,从开发者视角来看,可以无需关注类冲突问题,也对Sermant的类隔离机制无感知,同时借助Sermant的局部类加载机制,可以更建议的开发出高性能的服务治理插件。
图- Sermant类隔离机制
插件加载
既然是开发Sermant插件,最先应该了解的是插件是如何加载和调度的,Sermant的插件化机制中得益于Java 的SPI机制,在很多高可扩展的项目中,都会利用SPI去加载自己的扩展,常见的使用SPI机制的场景包括日志框架、数据库驱动、序列化工具、缓存框架等。
Java SPI(Service Provider Interface)是Java提供的一种服务提供者接口,用于在运行时动态加载实现某个接口或者抽象类的类。通过SPI机制,提供实现的一方可以将自己的实现以插件的形式注入到系统中,而无需修改原有的代码。SPI机制是Java中一种基于接口编程的思想,它提高了代码的可扩展性和灵活性,使得应用程序更加易于扩展和维护。
图- Sermant SPI加载机制
在SPI中有三个关键的要素——接口定义、实现创建、配置文件。在Sermant中,框架中定义插件声明接口用于让插件开发者来定义插件的核心要素,插件开发者只需要按照接口契约,创建自身所需的插件声明实现即可,插件声明接口定义如下:
public interface PluginDeclarer {
/**
* 获取插件的类匹配器
*
* @return 类匹配器
*/
ClassMatcher getClassMatcher();
/**
* 获取插件的拦截声明
*
* @param classLoader 被增强类的类加载器
* @return 拦截声明集
*/
InterceptDeclarer[] getInterceptDeclarers(ClassLoader classLoader);
/**
* 由插件声明器决定是否需要增强被拦截的方法,默认为true
*
* @return 加载与否
*/
default boolean isEnabled() {
return true;
}
}
至于SPI机制中的另一个核心要素——配置文件,则需要开发者在插件声明实现创建完成后资源目录resources中添加META-INF/services目录,并在其中创建名为com.huaweicloud.sermant.core.plugin.agent.declarer.PluginDeclarer的SPI文件,并向其中添加插件声明实现的类名,这样再接入Sermant时,Sermant就可以按照配置的指定来将对应的插件声明加载起来。
插件调度
仅依赖SPI机制是无法支持Sermant强大的框架能力的,在Sermant体系中,每一种不同的治理能力都是一个独立的插件,并且每个服务治理能力的实现,都依赖于多个插件声明的组合和拦截器的组合,通俗来讲,每个插件中都依赖了多个字节码切面来完成完整的服务治理,多插件难免会出现使用相同的切点来执行字节码增强逻辑。在如此情况下,Sermant该如何保证各插件的执行顺序,并且保证不会重复的进行字节码的织入呢。
Sermant在最底层维护了一个切面的调度器,首先在插件加载的过程中,调度器会将插件的拦截器(Sermant中定义服务治理逻辑的组件)通过有序列表进行缓存,当字节码织入点被触发时,会进行拦截器的调度,此时Sermant将模仿方法堆栈的执行方式,先进入的方法后结束:
图- Sermant插件调度逻辑
当在进入目标方法时,调度器将对拦截器按照插件加载的方式执行,这样保证在进入方法时的运行顺序符合方法运行的规律;当执行出目标方法时,调度器将对拦截器按照插件加载顺序的逆序执行,这服务方法堆栈中,方法结束的规律。
基于上述逻辑,通过调度器这一层,可以保证不会对相同目标类目标方法进行重复的无意义的字节码增强,同时保证插件在相同目标的执行逻辑符合方法调用堆栈的逻辑,更符合切面程序的执行风格。并且可以通过控制插件来达到控制拦截器执行顺序的目的,也就是达到了控制插件顺序来控制服务治理生效时机的作用,这对一些特殊场景大有裨益。
开发者相关能力解析
除了插件化和类加载等框架的核心机制,插件开发者更多的需要了解Sermant所提供的一些开发这能力,只有更深入的了解这些能力,才能在服务治理插件开发的时候信手拈来。 插件简单来讲就是一系列切面的集合,最终完成了复杂的治理能力。在面向切面编程时,有两个核心的概念,即Join point(切点)——指定切面的横切位置;Advice(通知)——切面执行的具体行为。对应Sermant的插件开发中也有逻辑与之对应,在Sermant中声明切面位置的称之为插件声明,执行切面逻辑的称之为拦截器。
Sermant的插件声明可以基于类名、超类、注解等进行类定位,并通过方法名、类型、参数、返回值等进行方法定位,通过丰富的类匹配能力和方法匹配能力,可以更容易的指定自己期望的织入点。
Sermant的拦截器提供了Before、After、Throw三个关键的生命周期,并在其上提供了形如跳过方法执行,修改方法参数,修改方法返回,修改异常抛出等通用能力。
图- Sermant拦截器提供的能力
拦截器的Before逻辑将会被Sermant的切面调度器在方法执行前按照插件的加载顺序进行调度,这里我们可以通过Sermant提供的API来终止方法的执行,并且可以获取到当前拦截的对象的相关信息,并且还可以获取和修改方法的入参,这里就要注意了,修改入参可能会被其他插件所感知,这里就体现了切面调度器的重要性,如果修改参数产生了预期外的影响,可以通过调整插件顺序的方式来避免这种影响。
插件的After和Throw逻辑将会在目标方法执行结束时,被Sermant的切面调度器统一按照插件加载顺序的逆序进行调度,在此时我们还可以再次来修改方法的返回值和异常。在After中如果需要修改方法的返回值,则也同Before逻辑一样,需要注意拦截器的执行顺序,如果产生了预期外的影响,可以尝试通过调整插件顺序来进行避免。
在Throw逻辑中,只有当方法抛出异常时,Sermant才能触发拦截器处理Throw逻辑,如果异常在方法中被捕获,则无法触发Throw的拦截器处理逻辑,如果在Throw逻辑中将异常修改为null,此时方法将不再会抛出异常。
统一动态配置
在《如何利用动态配置中心在JavaAgent中实现微服务的多样化治理》中,已经对动态配置进行了详细的介绍,本文就不再进行详细的叙述,Sermant动态配置模型是一种基于分层模型设计的配置管理方案,它的核心组件包括Group和Key。Sermant通过不同的Group(分组信息)来对配置项进行隔离,使配置管理更具灵活性和可扩展性;同时,通过Key对配置项进行具体属性的标识,实现了对配置项的精准控制和高效维护,其在主流的配置中心中的概念对应关系如下:
图- Sermant统一动态配置相关概念
Sermant为开发者和使用者屏蔽了配置中心的差异,可以无需修改任何代码,就可以让Sermant对接多种配置中心,开发者只需要通过Group和Key进行配置的划分,无需了解各配置中心的实际字段,就可以开发出不依赖配置中心的动态服务治理能力。
统一日志解析
日志是在程序开发中不可或缺的能力,通过日志可以快速找出程序运行时的状态及遇到的问题。Sermant的日志有两个很重要诉求,第一个就是需要隔离,避免Sermant日志系统对微服务带来不良的影响,例如破坏了微服务的日志配置,Sermant日志和微服务日志交叉输出,影响微服务日志检索定位问题。第二个就是需要有监控能力,可以高性能的将执行过程中的异常信息通过Sermant Backend可观测,及时发现边车运行的异常问题。
Sermant的统一日志,首先Sermant框架通过自定义的类加载器将日志引擎和微服务的日志引擎进行隔离,这样避免共用日志引擎,并且限制日志引擎的资源加载只在Sermant自定义的类加载器中进行:
@Override
public Enumeration<URL> getResources(String name) throws IOException {
// 由于类隔离的原因针对StaticLoggerBinder不再通过父类加载器获取重复资源,只返回加载器内的资源
if ("org/slf4j/impl/StaticLoggerBinder.class".equals(name)) {
return findResources(name);
}
return super.getResources(name);
}
第二步Sermant框架通过自定义类加载器来限制日志引擎所能加载到的配置,通过限制"logback.xml"文件资源的加载,来限制日志的配置:
@Override
public URL getResource(String name) {
URL url = null;
// 针对日志配置文件,定制化getResource方法,获取FrameworkClassloader下资源文件中的logback.xml
if (CommonConstant.LOG_SETTING_FILE_NAME.equals(name)) {
File logSettingFile = BootArgsIndexer.getLogSettingFile();
if (logSettingFile.exists() && logSettingFile.isFile()) {
try {
url = logSettingFile.toURI().toURL();
} catch (MalformedURLException e) {
url = findResource(name);
}
} else {
url = findResource(name);
}
}
if (url == null) {
url = super.getResource(name);
}
return url;
}
最后通过JUL桥接日志,借助于jul-to-slf4j (opens new window)的桥接能力将JUL日志桥接到日志引擎。最终,Sermant开发者在使用统一日志时,通过JUL接口来构造日志即可,无需再依赖其他第三方日志门面依赖,仅需使用Java 原生日志接口,日志和微服务完全隔离的,避免了边车日志系统对微服务日志系统带来不良的影响。
图- Sermant日志系统
除此之外,Sermant中改造了日志处理器,通过包装日志的桥接处理器,在高级别日志构造时通过Sermant的事件系统进行监控:
public class SermantBridgeHandler extends SLF4JBridgeHandler {
@Override
protected void callLocationAwareLogger(LocationAwareLogger lal, LogRecord record) {
// 覆写SLF4JBridgeHandler的日志转换方法,上报日志事件
int julLevelValue = record.getLevel().intValue();
if (julLevelValue > Level.INFO.intValue() && julLevelValue <= Level.WARNING.intValue()) {
// 记录警告级别日志
LogEventCollector.getInstance().offerWarning(record);
} else if (julLevelValue > Level.WARNING.intValue()) {
// 记录错误级别日志
LogEventCollector.getInstance().offerError(record);
}
super.callLocationAwareLogger(lal, record);
}
}
针对高级别的日志进行监控,可以通过配置事件系统将高级别日志进行异常的上报,通过Sermant Backend可以第一时间发现边车运行的异常状态。
结语
本文针对Sermant插件开发中的总会接触到的一些能力进行了更深层次的解析,基于更深入的了解,在插件开发时,才能更灵活的使用Sermant提供的丰富开发者能力,希望本篇文章可以对广大插件开发者带来一定的启发,除了上述能力,在插件开发中还可能需要用到利用Archetype能力快速构建项目并使用如心跳、链路标记等加速服务治理的开发,如何构建局部类加载环境等更多的开发指导可见Sermant开发者指南。
开发完成后,如想在k8s环境下快速部署Sermant、动态的执行Sermant的安装和卸载、重复安装插件以及完成边车的自监控等,可通过Sermant用户使用手册学习更多技巧。
-----------------------------------------------------------------------------------------
Sermant作为专注于服务治理领域的字节码增强框架,致力于提供高性能、可扩展、易接入、功能丰富的服务治理体验,并会在每个版本中做好性能、功能、体验的看护,广泛欢迎大家的加入。
- Sermant 官网:Sermant
- GitHub 仓库地址:GitHub - huaweicloud/Sermant: Sermant, a proxyless service mesh solution based on Javaagent.
- 扫码加入 Sermant 社区交流群