Sermant热插拔能力在故障注入场景的实践

作者:张豪鹏 华为云高级软件工程师

一、 前言

Sermant是基于Java字节码增强技术的无代理服务网格,采用Java字节码增强技术为宿主应用程序提供服务治理功能。从1.2.0版本开始,Sermant已经实现了在服务不停机状态下进行安装和卸载的热插拔功能,在上一篇文章《服务运行时动态挂载JavaAgent和插件------Sermant热插拔能力解析》中已经介绍了Sermant热插拔功能的实现原理。本篇文章将通过故障注入场景,来展示Sermant热插拔能力的应用价值。

二、 故障注入

1) 什么是故障注入?

故障注入是一种测试方法,它通过在系统中故意引入错误或故障,来测试系统对这些错误或故障的响应和恢复能力,并验证系统是否能够正常处理这些异常情况。下图是故障注入测试中的一些常见故障:

通过故障注入技术,测试人员可以在Java应用中模拟各种故障场景,以便评估应用的响应能力和恢复能力,还可以帮助提前拦截和发现Java应用潜在的可靠性问题,提升应用稳定性,避免现网出现重大质量事故。例如:

  • 通过在指定方法中抛出自定义异常可以测试系统在异常情况下的稳定性
  • 通过修改指定方法的返回值可以测试系统在异常数据情况下的处理能力
  • 通过数据库故障可以测试系统在异常情况下是否可以保持数据一致性等。

2) Java应用实现故障注入方案的难点是什么?

传统的故障注入方案是在应用中通过手动或者脚本的方式来引入故障,例如:修改代码、改变输入值、随机错误注入等。传统方式在进行故障注入测试时会存在以下问题:

  • 通过修改代码来注入故障时,每次注入新的故障都需要进行代码修改并重启服务,影响故障注入的效率
  • 通过开关配置来进行故障注入时,需要增加大量的开关逻辑判断

因此如何在不重启应用的情况下实现故障注入,并且可以重复的进行故障注入对提高故障注入测试的效率和全面性变得至关重要。

三、 Sermant热插拔功能在故障注入场景下的应用

Sermant热插拔功能可以在服务不停机状态下进行故障注入插件的安装和卸载,故障注入插件在插件安装的时候可以指定任意方法进行故障注入,在故障注入测试完成后可以卸载该插件来避免影响系统运行,故障注入插件卸载完成后可以重新安装故障注入插入进行其他方法的故障注入。

1) Sermant热插拔功能是什么?

Sermant热插拔功能是基于JavaAgent动态加载机制实现的,可以在服务不停机状态下进行Java Agent和插件的安装、卸载,而且安装、卸载插件时不会影响其他插件的正常运行。

下图为Sermant热插拔能力的示意图,Sermant可以在服务运行过程中进行Agent动态安装,Agent安装完成后可以通过动态安装、卸载插件来调整所需的微服务治理能力,也可以卸载整个Agent。

2) 基于Sermant实现故障注入插件

Sermant插件主要通过实现以下接口来实现字节码增强的功能:

  1. 通过实现PluginConfig来定义插件需要的配置。
  2. 通过实现AbstractPluginDeclarer来声明进行字节码增强的方法,类似于面向切面编程中的Joint point。
  3. 通过实现AbstractInterceptor来定义拦截器,类似于面向切面编程中的Advice。

Sermant插件的详细介绍请参考文章《开发者能力机制解析,玩转Sermant开发》

故障注入插件可以在PluginConfig实现类中接受Sermant动态安装时传递的参数信息。(基于Sermant热插拔功能进行动态安装时,可以通过Java Attach API传输的参数来设置PluginConfig实现类的属性值)。下面为配置类FaultInjectConfig的代码实现:

typescript 复制代码
@ConfigTypeKey("fault")  
public class FaultInjectConfig implements PluginConfig {  
    private String className;  
  
    private String methodName;  
  
    private String exceptionName;  
  
    public String getClassName() {  
        return className;  
    }  
  
    public void setClassName(String className) {  
        this.className = className;  
    }  
  
    public String getMethodName() {  
        return methodName;  
    }  
  
    public void setMethodName(String methodName) {  
        this.methodName = methodName;  
    }  
  
    public String getExceptionName() {  
        return exceptionName;  
    }  
  
    public void setExceptionName(String exceptionName) {  
        this.exceptionName = exceptionName;  
    }  
} 

故障注入插件可以在AbstractPluginDeclarer实现类中声明进行字节码增强的类和方法,即进行故障注入的类和方法。结合配置类FaultInjectConfig,故障注入插件可以在Sermant动态安装时调整故障注入的类和方法。如下面代码块所示(下面为故障注入声明器FaultInjectDeclarer的代码,基于配置类FaultInjectConfig声明故障注入的类和方法):

scala 复制代码
public class FaultInjectDeclarer extends AbstractPluginDeclarer {  

    private FaultInjectConfig faultInjectConfig = PluginConfigManager.getPluginConfig(FaultInjectConfig.class);  
  
    // 通过faultInjectConfig的className匹配需要进行字节码增强的类  
    @Override  
    public ClassMatcher getClassMatcher() {  
        return ClassMatcher.nameEquals(faultInjectConfig.getClassName());  
    }  
  
    // 拦截声明,通过faultInjectConfig的methodName匹配需要进行增强的方法,并声明拦截器为CustomExceptionInterceptor  
    @Override  
    public InterceptDeclarer[] getInterceptDeclarers(ClassLoader classLoader) {  
        return new InterceptDeclarer[]{  
                InterceptDeclarer.build(MethodMatcher.nameEquals(faultInjectConfig.getMethodName()),  
                        new CustomExceptionInterceptor())};  
    }  
} 

故障注入插件需要在AbstractInterceptor的实现类中定义字节码增强的逻辑,即进行故障注入,下面代码块为实现在方法执行前抛出自定义异常的故障注入逻辑:

scala 复制代码
public class CustomExceptionInterceptor extends AbstractInterceptor {  
    private FaultInjectConfig faultInjectConfig = PluginConfigManager.getPluginConfig(FaultInjectConfig.class);  
  
    @Override  
    public ExecuteContext before(ExecuteContext context) throws Exception {  
        // 实例化需要抛出的异常,并设置  
        Exception exception = (Exception) Class.forName(faultInjectConfig.getExceptionName())  
                .getConstructor(null).newInstance(null);  
        context.setThrowableOut(exception);  
  
        // 设置跳过方法原有执行逻辑  
        context.skip(new Object());  
        return context;  
    }  
  
    @Override  
    public ExecuteContext after(ExecuteContext context) throws Exception {  
        return context;  
    }  
} 

通过实现AbstractPluginDeclarer和AbstractInterceptor,故障注入插件可以在任何方法中注入想要的故障类型。

接下来我们以在ClassB的sayHello方法注入自定义异常这个故障为例来看Sermant是如何实现故障注入的。

3) 基于Sermant热插拔功能实现故障注入插件的安装

首先需要利用Java Attach API将Sermant加载到已运行的服务中,然后Sermant会解析Java Attach API传输的参数来执行命令解析,根据解析出来的命令类型来执行对应的命令。当命令类型为INSTALL-PLUGINS时,Sermant会执行安装命令。

Sermant热插拔功能会先获取故障注入插件包的路径并进行加载,Sermant采用自定义的类加载器PluginClassLoader和ServiceClassLoader对插件包中的类进行加载(Sermant类隔离架构解析可访问文章《Sermant类隔离架构解析------解决JavaAgent场景类冲突的实践》)。然后从Java Attach API传输的参数中解析需要增强的类名ClassB和方法信息sayHello,设置配置类FaultInjectConfig的属性className为ClassB、methodName为sayHello。

Sermant加载完成故障注入插件之后,会通过类文件转换器(ClassFileTransformer)对ClassB的sayHello方法进行字节码增强处理。为了避免对同一个方法进行多次字节码增强带来性能和资源损耗,Sermant只会对目标方法增强一次,第一次增强时会针对目标方法创建拦截器列表,并将拦截器放入其中,后续增强只需要将拦截器放入该增强方法对应的拦截器列表中即可。如下图所示,图中InterceptorA为其他插件对ClassA的print方法进行增强的拦截器,图中InterceptorB为其他插件对ClassB的sayHello方法进行增强的拦截器。

通过Sermant热插拔功能给ClassB的sayHello方法注入自定义异常的故障之后,在执行ClassB的sayHello方法时,拦截器就会拦截sayHello方法并执行故障注入逻辑,抛出自定义异常。如下图所示:

4) 基于Sermant热插拔功能实现故障注入的卸载

当故障注入测试结束后,Sermant热插拔功能的卸载能力可以将故障注入插件卸载,取消故障注入插件所有的字节码增强,将服务还原到增强前的状态。卸载之后还可以继续其他类型故障的注入测试。

当需要关闭故障注入插件时,可以通过Java Attach API来执行JavaAgent的动态机制,Sermant会解析Java Attach API传递的命令信息来执行对应的操作,当命令类型为UNINSTALL_PLUGINS时Sermant会执行卸载流程。

Sermant热插拔功能会先取消故障注入插件的字节码增强,并清除故障注入插件的插件信息:例如:插件加载时使用的自定义类加载器、加载时创建的Interceptor、故障注入插件的配置等。

最后Sermant会关闭类加载器、清除缓存的插件信息,将故障注入插件完全卸载。

卸载完成之后不会影响原服务的功能,而且故障注入插件可以再此安装,进行其他的故障测试。

5) 基于Sermant热插拔功能实现故障注入插件的第二次安装

故障注入插件卸载完成以后,还可以通过Sermant热插拔功能重新安装故障注入插件。故障注入插件支持通过调整FaultInjectConfig的属性配置来为其他方法注入故障。重新安装时,可以通过调整Java Attach API传递的参数来修改FaultInjectConfig的配置,通过配置不同的类名和方法名可以对其他的方法进行故障注入,例如:设置FaultInjectConfig中的类名和方法名为ClassC和printResult,就可以在ClassC的printResult方法中注入故障,重新安装故障注入插件流程和第一次安装没有任何区别,这里就不再赘述。

依赖于Sermant热插拔功能的插件卸载能力可以完全卸载故障注入插件,重新安装故障注入插件不需要进行任何特殊处理。重新安装故障注入插件之后,就可以针对新的方法进行故障注入测试。

四、 Sermant热插拔功能应用探索

通过Sermant热插拔功能在故障场景的应用,已经可以看出Sermant热插拔功能在故障注入场景下可以发挥巨大的作用,但Sermant热插拔功能除了在故障注入场景还可以在故障诊断、以及微服务应用的升级下发挥巨大的作用。例如:

  • 故障诊断:当服务出现故障时,可以通过Sermant热插拔功能动态安装插件去获取服务的关键信息,如线程堆栈跟踪、内存使用情况、方法运行时间等。这些信息可以帮助开发人员快速诊断应用程序的故障,并且可以在应用程序运行时进行修改和优化。
  • 微服务治理能力升级:当需要进行微服务治理能力升级时,也可以通过Sermant热插拔功能将升级的代码通过插件的形式动态的安装到微服务应用中,而不需要重启服务。

五、 总结

本篇文章介绍了Sermant热插拔功能在故障注入场景的应用,通过故障注入场景我们可以发现,Sermant热插拔功能在故障注入场景下可以发挥重大的作用。利用Sermant热插拔功能开发者和使用者可以在微服务运行过程中动态的进行故障注入,还可以多次注入不同的故障,帮助测试微服务的可靠性、稳定性。

Sermant热插拔功能不仅可用于故障注入,还可用故障诊断、以及微服务应用的升级等场景。Sermant热插拔功能不在微服务治理方面可以为开发者和使用者提供了更多的便利,帮助他们更有效地管理和维护微服务应用。



Sermant作为专注于服务治理领域的字节码增强框架,致力于提供高性能、可扩展、易接入、功能丰富的服务治理体验,并会在每个版本中做好性能、功能、体验的看护,广泛欢迎大家的加入。

相关推荐
weixin_46242847几秒前
使用 Caffeine 缓存并在业务方法上通过注解实现每3到5秒更新缓存
java·缓存
程序媛小果2 分钟前
基于java+SpringBoot+Vue的桂林旅游景点导游平台设计与实现
java·vue.js·spring boot
骑鱼过海的猫1234 分钟前
【java】java通过s3访问ceph报错
java·ceph·iphone
杨充10 分钟前
13.观察者模式设计思想
java·redis·观察者模式
Lizhihao_12 分钟前
JAVA-队列
java·开发语言
喵叔哟22 分钟前
重构代码之移动字段
java·数据库·重构
喵叔哟22 分钟前
重构代码之取消临时字段
java·前端·重构
fa_lsyk24 分钟前
maven环境搭建
java·maven
Daniel 大东43 分钟前
idea 解决缓存损坏问题
java·缓存·intellij-idea