前端开发入门到精通的在线学习网站

网站首页 > 资源文章 正文

故障注入原理探究(故障灌注)

qiguaw 2024-09-04 17:09:41 资源文章 27 ℃ 0 评论

前言

随着渠道API接入的渠道越来越多, 用户量也在日益递增, 由于渠道API本身的业务复杂性, 以及依赖的中台服务之多, 很有可能出现的问题会带来巨大的影响; 只是通过常用的单元测试, 集成测试, 性能测试等来验证服务的稳定性已经远远不够; 因此去年在平台的混沌工程基础上完成了渠道API和流量变现平台在mysql延迟,mq延迟,请求延迟, 异常等场景下的故障注入演练; 在演练中提前识别了潜在的问题并加以解决, 同时在使用过程中对故障注入的原理进行了解并记录, 方便后续根据业务本身的独特性对故障演练场景进行定制化生成。

1.chaosblade 整体介绍

实际上 chaosblade 是一个聚合的父项目,只是把所有实验场景入口封装到一起实现一个命令行工具,底层又去调用了各种场景下的具体实现,它将场景按领域实现封装成一个个单独的项目,这也符合不同平台、语言存在实现差异的情况,不仅可以使领域内场景标准化实现,而且非常方便场景水平和垂直扩展,通过遵循混沌实验模型,实现 chaosblade cli 统一调用。目前包含的项目如下:

2.chaosblade-exec-jvm

2.1 系统设计

Chaosblade-exec-jvm 是通过 JavaAgent attach 方式来实现类的 transform 注入故障,底层使用了 jvm-sandbox 实现,通过插件的可拔插设计来扩展对不同 java 应用的支持。所以 chaosblade-exec-jvm 其实只是一个 java agent 模块,不是一个可执行的工程,必须依赖 jvm-sandbox。

2.2 工程架构

2.3 模块管理

2.4 实现原理

以 servlet,api 的/test 接口延迟为例

2.5 实验步骤

2.5.1 Agent 挂载

该命令下发后,将在目标 jvm 进程挂载 Agent,触发 SandboxModule onLoad()事件,初始化 PluginLifecycleListener 来管理插件的生命周期,同时也触发 SandboxModule onActive() 事件,加载部分插件,加载插件对应的 ModelSpec.

public void onLoad() throws Throwable {
LOGGER.info("load chaosblade module");
 ManagerFactory.getListenerManager().setPluginLifecycleListener(this);
 dispatchService.load();
 ManagerFactory.load();//ChansBlade 模块激活实现
}
public void onActive() throws Throwable {
LOGGER.info("active chaosblade module");
loadPlugins();
}

Plugin 加载方式: ? SandboxModule onActive()事件 ? blade create 命令 CreateHandler; SandboxModule onActive()事件,会注册 ModelSpec;Plugin 加载时,创建事件监听器 SandboxEnhancerFactory.createAfterEventListener(plugin),监听器会监听感兴趣的事件,如 BeforeAdvice、AfterAdvice 等,具体实现如下:

// 加载插件
public void add(PluginBean plugin) {
PointCut pointCut = plugin.getPointCut();
if (pointCut == null) {
return;
}
String enhancerName = plugin.getEnhancer().getClass().getSimpleName();
// 创建 filter PointCut 匹配
Filter filter = SandboxEnhancerFactory.createFilter(enhancerName, pointCut);


        if (plugin.isAfterEvent()) {
            // 事件监听
            int watcherId = moduleEventWatcher.watch(filter, SandboxEnhancerFactory.createAfterEventListener(plugin),
                Type.BEFORE, Type.RETURN);
            watchIds.put(PluginUtil.getIdentifierForAfterEvent(plugin), watcherId);
        } else {
            int watcherId = moduleEventWatcher.watch(
                filter, SandboxEnhancerFactory.createBeforeEventListener(plugin), Event.Type.BEFORE);
            watchIds.put(PluginUtil.getIdentifier(plugin), watcherId);
        }
    }

PointCut 匹配 SandboxModule onActive()事件触发 Plugin 加载后,SandboxEnhancerFactory 创建 filter,filter 内部通过 PointCut 的 ClassMatcher 和 MethodMatcher 过滤。

触发 Enhancer 如果已经加载插件,此时目标应用匹配能匹配到 filter 后,EventListener 已经可以被触发,但是 chaosblade-exec-jvm 内部通过 StatusManager 管理状态,所以故障能力不会被触发。

例如 BeforeEventListener 触发调用 BeforeEnhancer 的 beforeAdvice 方法,在 ManagerFactory.getStatusManager().expExists(targetName)判断时候被中断,具体的实现如下:

com.alibaba.chaosblade.exec.common.aop.BeforeEnhancer


public void beforeAdvice(String targetName,
ClassLoader classLoader,
String className,
Object object,
Method method,
Object[] methodArguments) throws Exception {
// StatusManager
if (!ManagerFactory.getStatusManager().expExists(targetName)) {
return;
}
EnhancerModel model = doBeforeAdvice(classLoader, className, object, method, methodArguments);
if (model == null) {
return;
}
model.setTarget(targetName).setMethod(method).setObject(object).setMethodArguments(methodArguments);
Injector.inject(model);
}

2.5.2 创建混沌实验

./blade create servlet --requestpath=/topic delay --time=3000 该命令下发后,触发 SandboxModule @Http("/create")注解标记的方法,将事件分发给 com.alibaba.chaosblade.exec.service.handler.CreateHandler 处理 在判断必要的 uid、target、action、model 参数后调用 handleInjection,handleInjection 通过状态管理器注册本次实验,如果插件类型是 PreCreateInjectionModelHandler 的类型,将预处理一些东西。同时如果 Action 类型是 DirectlyInjectionAction,那么将直接进行故障能力注入,如 jvm oom 等,如果不是将加载插件。如果 ModelSpec 是 PreCreateInjectionModelHandler 类型,且 ActionSpec 的类型是 DirectlyInjectionAction 类型,将直接进行故障能力注入,比如 JvmOom 故障能力,ActionSpec 的类型不是 DirectlyInjectionAction 类型,将直接加载插件。

private Response handleInjection(String suid, Model model, ModelSpec modelSpec) {
// 注册
RegisterResult result = this.statusManager.registerExp(suid, model);
if (result.isSuccess()) {
// handle injection
try {
applyPreInjectionModelHandler(suid, modelSpec, model);
} catch (ExperimentException ex) {
this.statusManager.removeExp(suid);
return Response.ofFailure(Response.Code.SERVER_ERROR, ex.getMessage());
}


            return Response.ofSuccess(model.toString());
        }
        return Response.ofFailure(Response.Code.DUPLICATE_INJECTION, "the experiment exists");
    }

注册成功后返回 uid,如果本阶段直接进行故障能力注入了,或者自定义 Enhancer advice 返回 null,那么不通过 Inject 类触发故障。

2.5.3 故障能力注入

故障能力注入可以通过 Inject 注入, 也可以通过 DirectlyInjectionAction 直接注入, 直接注入不经过 Inject 类调用阶段,如 jvm oom 等; 匹配参数包装 自定义的 Enhancer,如 ServletEnhancer,把一些需要与命令行匹配的参数 包装在 MatcherModel 里面,然后包装 EnhancerModel 返回,比如 --requestpath = /index,那么 requestpath 等于 requestURI 去除 contextPath。参数匹配在 Injector.inject(model)阶段判断。

public class ServletEnhancer extends BeforeEnhancer {


    private static final Logger LOOGER = LoggerFactory.getLogger(ServletEnhancer.class);


    @Override
    public EnhancerModel doBeforeAdvice(ClassLoader classLoader, String className, Object object,
                                        Method method, Object[] methodArguments,String targetName)
        throws Exception {
        // 获取原方法的一些参数
        Object request = methodArguments[0];
        String queryString = ReflectUtil.invokeMethod(request, "getQueryString", new Object[] {}, false);
        String contextPath = ReflectUtil.invokeMethod(request, "getContextPath", new Object[] {}, false);
        String requestURI = ReflectUtil.invokeMethod(request, "getRequestURI", new Object[] {}, false);
        String requestMethod = ReflectUtil.invokeMethod(request, "getMethod", new Object[] {}, false);


        String requestPath = StringUtils.isBlank(contextPath) ? requestURI : requestURI.replaceFirst(contextPath, "");


        //
        MatcherModel matcherModel = new MatcherModel();
        matcherModel.add(ServletConstant.QUERY_STRING_KEY, queryString);
        matcherModel.add(ServletConstant.METHOD_KEY, requestMethod);
        matcherModel.add(ServletConstant.REQUEST_PATH_KEY, requestPath);
        return new EnhancerModel(classLoader, matcherModel);
    }


}

参数匹配和能力注入(Inject 调用) inject 阶段首先获取 StatusManager 注册的实验,compare(model, enhancerModel)经常参数比对,失败后 return,limitAndIncrease(statusMetric)判断 --effect-count --effect-percent 来控制影响的次数和百分比

public static void inject(EnhancerModel enhancerModel) throws InterruptProcessException {
String target = enhancerModel.getTarget();
List<StatusMetric> statusMetrics = ManagerFactory.getStatusManager().getExpByTarget(
target);
for (StatusMetric statusMetric : statusMetrics) {
Model model = statusMetric.getModel();
if (!compare(model, enhancerModel)) {
continue;
}
try {
boolean pass = limitAndIncrease(statusMetric);
if (!pass) {
LOGGER.info("Limited by: {}", JSON.toJSONString(model));
break;
}
LOGGER.info("Match rule: {}", JSON.toJSONString(model));
enhancerModel.merge(model);
ModelSpec modelSpec = ManagerFactory.getModelSpecManager().getModelSpec(target);
ActionSpec actionSpec = modelSpec.getActionSpec(model.getActionName());
actionSpec.getActionExecutor().run(enhancerModel);
} catch (InterruptProcessException e) {
throw e;
} catch (UnsupportedReturnTypeException e) {
LOGGER.warn("unsupported return type for return experiment", e);
statusMetric.decrease();
} catch (Throwable e) {
LOGGER.warn("inject exception", e);
 statusMetric.decrease();
}
break;
}
}

故障触发 由 Inject 触发,或者由 DirectlyInjectionAction 直接触发,最后调用自定义的 ActionExecutor 生成故障,如 DefaultDelayExecutor,此时故障能力已经生效了。

public void run(EnhancerModel enhancerModel) throws Exception {
String time = enhancerModel.getActionFlag(timeFlagSpec.getName());
Integer sleepTimeInMillis = Integer.valueOf(time);
int offset = 0;
String offsetTime = enhancerModel.getActionFlag(timeOffsetFlagSpec.getName());
if (!StringUtil.isBlank(offsetTime)) {
offset = Integer.valueOf(offsetTime);
}
TimeoutExecutor timeoutExecutor = enhancerModel.getTimeoutExecutor();
if (timeoutExecutor != null) {
long timeoutInMillis = timeoutExecutor.getTimeoutInMillis();
if (timeoutInMillis > 0 && timeoutInMillis < sleepTimeInMillis) {
sleep(timeoutInMillis, 0);
timeoutExecutor.run(enhancerModel);
return;
}
}
sleep(sleepTimeInMillis, offset);
}


public void sleep(long timeInMillis, int offsetInMillis) {
Random random = new Random();
int offset = 0;
if (offsetInMillis > 0) {
offset = random.nextInt(offsetInMillis);
}
if (offset % 2 == 0) {
timeInMillis = timeInMillis + offset;
} else {
timeInMillis = timeInMillis - offset;
}
if (timeInMillis <= 0) {
timeInMillis = offsetInMillis;
}
try {
// 触发延迟
TimeUnit.MILLISECONDS.sleep(timeInMillis);
} catch (InterruptedException e) {
LOGGER.error("running delay action interrupted", e);
}
}

2.5.4 销毁

./blade destroy 52a27bafc252beee 该命令下发后,触发 SandboxModule @Http("/destory")注解标记的方法,将事件分发给 com.alibaba.chaosblade.exec.service.handler.DestroyHandler 处理。注销本次故障的状态。如果插件的 ModelSpec 是 PreDestroyInjectionModelHandler 类型,且 ActionSpec 的类型是 DirectlyInjectionAction 类型,停止故障能力注入,ActionSpec 的类型不是 DirectlyInjectionAction 类型,将卸载插件。

public Response handle(Request request) {
String uid = request.getParam("suid");
String target = request.getParam("target");
String action = request.getParam("action");
if (StringUtil.isBlank(uid)) {
if (StringUtil.isBlank(target) || StringUtil.isBlank(action)) {
return Response.ofFailure(Code.ILLEGAL_PARAMETER, "less necessary parameters, such as uid, target and" + " action");
}
// 注销 status
return destroy(target, action);
}
return destroy(uid);
}

2.5.5 卸载 Agent

./blade revoke 98e792c9a9a5dfea 该命令下发后,触发 SandboxModule unload()事件,同时插件卸载。public void onUnload() throws Throwable { LOGGER.info("unload chaosblade module"); dispatchService.unload(); ManagerFactory.unload(); watchIds.clear(); LOGGER.info("unload chaosblade module successfully"); }

总结

以上便是chaosblade-exec-jvm的总体流程, 同时也支持在chaosblade-exec-plugin模块下自定义自己的插件, 结合自己的项目来定制化演练场景, 通过模拟各种可能的故障情况, 及早发现存在的漏洞和弱点, 从而进行改进和完善。

作者介绍

Hippo,信也科技服务端研发专家

来源:微信公众号:拍码场

出处:https://mp.weixin.qq.com/s/KziZ9hAHD0p5fCxrLcV7pg


Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表