故障注入原理探究

前言

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

1.chaosblade整体介绍

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


2.chaosblade-exec-jvm2.1系统设计

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

2.2工程架构

2.3模块管理

2.4实现原理

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

2.5实验步骤

2.5.1Agent挂载

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

publicvoidonLoad()throwsThrowable{("loadchaosblademodule");().setPluginLifecycleListener(this);();();//ChansBlade模块激活实现}publicvoidonActive()throwsThrowable{("activechaosblademodule");loadPlugins();}

Plugin加载方式:•SandboxModuleonActive()事件•bladecreate命令CreateHandler;SandboxModuleonActive()事件,会注册ModelSpec;Plugin加载时,创建事件监听器(plugin),监听器会监听感兴趣的事件,如BeforeAdvice、AfterAdvice等,具体实现如下:

//加载插件publicvoidadd(PluginBeanplugin){PointCutpointCut=();if(pointCut==null){return;}StringenhancerName=().getClass().getSimpleName();//创建filterPointCut匹配Filterfilter=(enhancerName,pointCut);if(()){//事件监听intwatcherId=(filter,(plugin),,);((plugin),watcherId);}else{intwatcherId=(filter,(plugin),);((plugin),watcherId);}}

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

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

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

(StringtargetName,ClassLoaderclassLoader,StringclassName,Objectobject,Methodmethod,Object[]methodArguments)throwsException{//StatusManagerif(!().expExists(targetName)){return;}EnhancerModelmodel=doBeforeAdvice(classLoader,className,object,method,methodArguments);if(model==null){return;}(targetName).setMethod(method).setObject(object).setMethodArguments(methodArguments);(model);}
2.5.2创建混沌实验

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

privateResponsehandleInjection(Stringsuid,Modelmodel,ModelSpecmodelSpec){//注册RegisterResultresult=(suid,model);if(()){//handleinjectiontry{applyPreInjectionModelHandler(suid,modelSpec,model);}catch(ExperimentExceptionex){(suid);(_ERROR,());}(());}(_INJECTION,"theexperimentexists");}

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

2.5.3故障能力注入

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

publicclassServletEnhancerextsBeforeEnhancer{privatestaticfinalLoggerLOOGER=();@OverridepublicEnhancerModeldoBeforeAdvice(ClassLoaderclassLoader,StringclassName,Objectobject,Methodmethod,Object[]methodArguments,StringtargetName)throwsException{//获取原方法的一些参数Objectrequest=methodArguments[0];StringqueryString=(request,"getQueryString",newObject[]{},false);StringcontextPath=(request,"getContextPath",newObject[]{},false);StringrequestURI=(request,"getRequestURI",newObject[]{},false);StringrequestMethod=(request,"getMethod",newObject[]{},false);StringrequestPath=(contextPath)?requestURI:(contextPath,"");//MatcherModelmatcherModel=newMatcherModel();(_STRING_KEY,queryString);(_KEY,requestMethod);(_PATH_KEY,requestPath);returnnewEnhancerModel(classLoader,matcherModel);}}

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

publicstaticvoidinject(EnhancerModelenhancerModel)throwsInterruptProcessException{Stringtarget=();ListStatusMetricstatusMetrics=().getExpByTarget(target);for(StatusMetricstatusMetric:statusMetrics){Modelmodel=();if(!compare(model,enhancerModel)){continue;}try{booleanpass=limitAndIncrease(statusMetric);if(!pass){("Limitedby:{}",(model));break;}("Matchrule:{}",(model));(model);ModelSpecmodelSpec=().getModelSpec(target);ActionSpecactionSpec=(());().run(enhancerModel);}catch(InterruptProcessExceptione){throwe;}catch(UnsupportedReturnTypeExceptione){("unsupportedreturntypeforreturnexperiment",e);();}catch(Throwablee){("injectexception",e);();}break;}}

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

publicvoidrun(EnhancerModelenhancerModel)throwsException{Stringtime=(());IntegersleepTimeInMillis=(time);intoffset=0;StringoffsetTime=(());if(!(offsetTime)){offset=(offsetTime);}TimeoutExecutortimeoutExecutor=();if(timeoutExecutor!=null){longtimeoutInMillis=();if(timeoutInMillis0timeoutInMillissleepTimeInMillis){sleep(timeoutInMillis,0);(enhancerModel);return;}}sleep(sleepTimeInMillis,offset);}publicvoidsleep(longtimeInMillis,intoffsetInMillis){Randomrandom=newRandom();intoffset=0;if(offsetInMillis0){offset=(offsetInMillis);}if(offset%2==0){timeInMillis=timeInMillis+offset;}else{timeInMillis=timeInMillis-offset;}if(timeInMillis=0){timeInMillis=offsetInMillis;}try{//触发延迟(timeInMillis);}catch(InterruptedExceptione){("runningdelayactioninterrupted",e);}}
2.5.4销毁

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

publicResponsehandle(Requestrequest){Stringuid=("suid");Stringtarget=("target");Stringaction=("action");if((uid)){if((target)||(action)){(_PARAMETER,"lessnecessaryparameters,suchasuid,targetand"+"action");}//注销statusreturndestroy(target,action);}returndestroy(uid);}
2.5.5卸载Agent

./bladerevoke98e792c9a9a5dfea该命令下发后,触发SandboxModuleunload()事件,同时插件卸载。publicvoidonUnload()throwsThrowable{("unloadchaosblademodule");();();();("unloadchaosblademodulesuccessfully");}

总结

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

作者介绍

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

出处:

发布于 2025-04-03
199
目录

    推荐阅读