头图

前言

随着渠道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.1Agent挂载

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

 

 

<p
data-id="p1dr9soow-55yusgsaettds"></p>

publicvoidonLoad() throwsThrowable{

LOGGER.info("load
chaosblade module");       

ManagerFactory.getListenerManager().setPluginLifecycleListener(this);   

dispatchService.load();  

ManagerFactory.load();//ChansBlade模块激活实现

}

publicvoidonActive()
throwsThrowable{

LOGGER.info("active
chaosblade module");

loadPlugins();

}

 

Plugin加载方式:

●  SandboxModule onActive()事件 

●  blade create命令CreateHandler;

 

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

<p
data-id="p1dr9soow-7fch97a058xs"></p>

// 加载插件

publicvoidadd(PluginBeanplugin)
{

PointCutpointCut=plugin.getPointCut();

if(pointCut==null) {

return;

}

StringenhancerName=plugin.getEnhancer().getClass().getSimpleName();

// 创建filter PointCut匹配

Filterfilter=SandboxEnhancerFactory.createFilter(enhancerName,
pointCut);

 

if(plugin.isAfterEvent())
{

// 事件监听

intwatcherId=moduleEventWatcher.watch(filter,
SandboxEnhancerFactory.createAfterEventListener(plugin),

Type.BEFORE,
Type.RETURN);

watchIds.put(PluginUtil.getIdentifierForAfterEvent(plugin),
watcherId);

} else{

intwatcherId=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)判断时候被中断,具体的实现如下:

<p
data-id="p1dr9soow-fgkqeelalcqv4"></p>

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

 

publicvoidbeforeAdvice(StringtargetName,

ClassLoaderclassLoader,

StringclassName,

Objectobject,

Methodmethod,

Object[]
methodArguments) throwsException{

//
StatusManager

if(!ManagerFactory.getStatusManager().expExists(targetName))
{

return;

}

EnhancerModelmodel=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类型,将直接加载插件。

<p
data-id="p1dr9soow-3vw0o16cbr75s"></p>

privateResponsehandleInjection(Stringsuid,
Modelmodel, ModelSpecmodelSpec) {

// 注册

RegisterResultresult=this.statusManager.registerExp(suid,
model);

if(result.isSuccess())
{

// handle
injection

try{

applyPreInjectionModelHandler(suid,
modelSpec, model);

}
catch(ExperimentExceptionex) {

this.statusManager.removeExp(suid);

returnResponse.ofFailure(Response.Code.SERVER_ERROR,
ex.getMessage());

}

 

returnResponse.ofSuccess(model.toString());

}

returnResponse.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)阶段判断。

<p
data-id="p1dr9soow-6mxifwpxrre9s"></p>

publicclassServletEnhancerextendsBeforeEnhancer{

 

privatestaticfinalLoggerLOOGER=LoggerFactory.getLogger(ServletEnhancer.class);

 

@Override

publicEnhancerModeldoBeforeAdvice(ClassLoaderclassLoader,
StringclassName,
Objectobject,

Methodmethod,
Object[] methodArguments,StringtargetName)

throwsException{

// 获取原方法的一些参数

Objectrequest=methodArguments[0];

StringqueryString=ReflectUtil.invokeMethod(request,
"getQueryString",
newObject[]
{}, false);

StringcontextPath=ReflectUtil.invokeMethod(request,
"getContextPath",
newObject[]
{}, false);

StringrequestURI=ReflectUtil.invokeMethod(request,
"getRequestURI",
newObject[]
{}, false);

StringrequestMethod=ReflectUtil.invokeMethod(request,
"getMethod",
newObject[]
{}, false);

 

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

 

//

MatcherModelmatcherModel=newMatcherModel();

matcherModel.add(ServletConstant.QUERY_STRING_KEY,
queryString);

matcherModel.add(ServletConstant.METHOD_KEY,
requestMethod);

matcherModel.add(ServletConstant.REQUEST_PATH_KEY,
requestPath);

returnnewEnhancerModel(classLoader,
matcherModel);

}

}

 

参数匹配和能力注入(Inject调用)

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

<p
data-id="p1dr9soow-bcseiivqbirr4"></p>

publicstaticvoidinject(EnhancerModelenhancerModel)
throwsInterruptProcessException{

Stringtarget=enhancerModel.getTarget();

List<StatusMetric>statusMetrics=ManagerFactory.getStatusManager().getExpByTarget(

target);

for(StatusMetricstatusMetric:
statusMetrics) {

Modelmodel=statusMetric.getModel();

if(!compare(model,
enhancerModel)) {

continue;

}

try{

booleanpass=limitAndIncrease(statusMetric);

if(!pass)
{

LOGGER.info("Limited
by: {}", JSON.toJSONString(model));

break;

}

LOGGER.info("Match
rule: {}", JSON.toJSONString(model));

enhancerModel.merge(model);

ModelSpecmodelSpec=ManagerFactory.getModelSpecManager().getModelSpec(target);

ActionSpecactionSpec=modelSpec.getActionSpec(model.getActionName());

actionSpec.getActionExecutor().run(enhancerModel);

}
catch(InterruptProcessExceptione) {

throwe;

}
catch(UnsupportedReturnTypeExceptione) {

LOGGER.warn("unsupported
return type for return experiment", e);

statusMetric.decrease();

} catch(Throwablee)
{

LOGGER.warn("inject
exception", e);         

statusMetric.decrease();

}

break;

}

}

 

故障触发

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

<p
data-id="p1dr9soow-jh40presgd62o"></p>

publicvoidrun(EnhancerModelenhancerModel)
throwsException{

Stringtime=enhancerModel.getActionFlag(timeFlagSpec.getName());

IntegersleepTimeInMillis=Integer.valueOf(time);

intoffset=0;

StringoffsetTime=enhancerModel.getActionFlag(timeOffsetFlagSpec.getName());

if(!StringUtil.isBlank(offsetTime))
{

offset=Integer.valueOf(offsetTime);

}

TimeoutExecutortimeoutExecutor=enhancerModel.getTimeoutExecutor();

if(timeoutExecutor!=null) {

longtimeoutInMillis=timeoutExecutor.getTimeoutInMillis();

if(timeoutInMillis>0&&timeoutInMillis<sleepTimeInMillis)
{

sleep(timeoutInMillis,
0);

timeoutExecutor.run(enhancerModel);

return;

}

}

sleep(sleepTimeInMillis,
offset);

}

 

publicvoidsleep(longtimeInMillis,
intoffsetInMillis) {

Randomrandom=newRandom();

intoffset=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(InterruptedExceptione)
{

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类型,将卸载插件。

<p
data-id="p1dr9soow-8z0gumkt4ulfk"></p>

publicResponsehandle(Requestrequest)
{

Stringuid=request.getParam("suid");

Stringtarget=request.getParam("target");

Stringaction=request.getParam("action");

if(StringUtil.isBlank(uid))
{

if(StringUtil.isBlank(target)
||StringUtil.isBlank(action)) {

returnResponse.ofFailure(Code.ILLEGAL_PARAMETER,
"less
necessary parameters, such as uid, target and"

+"
action");

}

// 注销status

returndestroy(target,
action);

}

returndestroy(uid);

}

 

2.5.5卸载Agent

./blade
revoke 98e792c9a9a5dfea

该命令下发后,触发SandboxModule
unload()事件,同时插件卸载。

<p
data-id="p1dr9soow-1kpmrng9k6h34"></p>

publicvoidonUnload()
throwsThrowable{

LOGGER.info("unload
chaosblade module");

dispatchService.unload();

ManagerFactory.unload();

watchIds.clear();

LOGGER.info("unload
chaosblade module successfully");

}

 

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

 

结束语

在分布式系统, 面对故障没有很好的办法, 唯一能够做的就是为之做好准备, 通过故障发现的方式, 主动发现系统的隐患, 将其消灭在萌芽状态。 故障处理不是单一某一种方式就能解决,而是通过故障发现、参照故障处理原则、故障处理等多个手段就行组合,才有可能将隐患消灭。因此在了解故障注入原理后, 就可以根据系统本身特有的性质以及业务本身更容易出问题的场景来个性化定制演练来达到增强项目韧性的目的, 降低正式环境出问题的概率。


信也科技布道师
12 声望10 粉丝