作者 | 叶飞、穹谷
**导读:**总以为混沌工程离你很远?但发生故障的那一刻不是由你来选择的,而是那一刻来选择你,你能做的就是为之做好准备。混沌工程在阿里内部已经应用多年,而ChaosBlade这个开源项目是阿里多年来通过注入故障来对抗故障的经验结晶。为使大家更深入的了解其实现原理以及如何扩展自己所需要的组件故障注入,我们准备了一个系列对其做详细技术剖析:架构篇、模型篇、协议篇、字节码篇、插件篇以及实战篇。
原文标题《技术剖析 Java 场景混沌工程实现系列(一)| 架构篇》
在分布式系统架构下,服务间的依赖日益复杂,很难评估单个服务故障对整个系统的影响,并且请求链路长,监控告警的不完善导致发现问题、定位问题难度增大,同时业务和技术迭代快,如何持续保障系统的稳定性和高可用性受到很大的挑战。
我们知道发生故障的那一刻不是由你来选择的,而是那一刻来选择你,你能做的就是为之做好准备。所以构建稳定性系统很重要的一环是混沌工程,在可控范围或环境下,通过故障注入,来持续提升系统的稳定性和高可用能力。
ChaosBlade(Github 地址:https://github.com/chaosblade-io/chaosblade) 是一款遵循混沌工程实验原理,提供丰富故障场景实现,帮助分布式系统提升容错性和可恢复性的混沌工程工具,可实现底层故障的注入,特点是操作简洁、无侵入、扩展性强。 其中 chaosblade-exec-jvm (Github 地址:https://github.com/chaosblade-io/chaosblade-exec-jvm )项目实现了零成本对 Java 应用服务故障注入。其不仅支持主流的框架组件,如 Dubbo、Servlet、RocketMQ 等,还支持指定任意类和方法注入延迟、异常以及通过编写 Java 和 Groovy 脚本来实现复杂的实验场景。
为使大家更深入的了解其实现原理以及如何扩展自己所需要的组件故障注入,分为六篇文章对其做详细技术剖析:架构篇、模型篇、协议篇、字节码篇、插件篇以及实战篇。本文将详细介绍 chaosblade-exec-jvm 的整体架构设计,使用户对 chaosblade-exec-jvm 有一定的了解。
Chaosblade-exec-jvm 基于 JVM-Sanbox 做字节码修改,执行 ChaosBlade 工具可实现将故障注入的 Java Agent 挂载到指定的应用进程中。Java Agent 遵循混沌实验模型设计,通过插件的可拔插设计来扩展对不同 Java 组件的支持,可以很方便的扩展插件来支持更多的故障场景,插件基于 AOP 的设计定义通知Advice
、增强类Enhancer
、切点PointCut
,同时结合混沌实验模型定模型ModelSpec
、实验靶点Target
、匹配方式Matcher
、攻击动作Action
。
Chaosblade-exec-jvm 在由make build
编译打包时下载 JVM-Sanbox relase 包,编译打包后 chaosblade-exec-jvm 做为 JVM-Sandbox 的模块。在加载 Agent 后,同时监听 JVM-Sanbox 的事件来管理整个混沌实验流程,通过Java Agent 技术来实现类的 transform 注入故障。
在日常后台应用开发中,我们经常需要提供 API 接口给客户端,而这些 API 接口不可避免的由于网络、系统负载等原因存在超时、异常等情况。使用 Java 语言时,HTTP 协议我们通常使用 Servlet 来提供 API 接口,chaosblade-exec-jvm 支持 Servlet 插件,注入超时、自定义异常等故障能力。本篇将通过给 Servlet API 接口 注入延迟故障能力为例,分析 chaosblade-exec-jvm 故障注入的流程。
对 Servlet API 接口/topic
延迟3秒,步骤如下:
// 挂载 Agent
blade prepare jvm --pid 888
{
"code":200,"success":true,"result":"98e792c9a9a5dfea"}
// 注入故障能力
blade create servlet --requestpath=/topic delay --time=3000 --method=post
{
"code":200,"success":true,"result":"52a27bafc252beee"}
// 撤销故障能力
blade destroy 52a27bafc252beee
// 卸载 Agent
blade revoke 98e792c9a9a5dfea
以下通过 Servlet 请求延迟为例,详细介绍故障注入的过程。
blade p jvm --pid 888
。blade create servlet --requestpath=/topic delay --time=3000 --method=post
。blade revoke 98e792c9a9a5dfea
。blade p jvm --pid 888
该命令下发后,将在目标 Java 应用进程挂在 Agent ,触发 SandboxModule onLoad() 事件,初始化 PluginLifecycleListener 来管理插件的生命周期,同时也触发 SandboxModule onActive() 事件,加载部分插件,加载插件对应的 ModelSpec。
// Agent 加载事件
public void onLoad() throws Throwable {
ManagerFactory.getListenerManager().setPluginLifecycleListener(this);
dispatchService.load();
ManagerFactory.load();
}
// ChaosBlade 模块激活实现
public void onActive() throws Throwable {
loadPlugins();
}
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);
// 事件监听
int watcherId = moduleEventWatcher.watch(filter, SandboxEnhancerFactory.createBeforeEventListener(plugin), Event.Type.BEFORE);
watchIds.put(PluginUtil.getIdentifier(plugin), watcherId);
}
SandboxModule onActive() 事件触发 Plugin 加载后,SandboxEnhancerFactory 创建 Filter,Filter 内部通过 PointCut 的 ClassMatcher 和 MethodMatcher 过滤。
public static Filter createFilter(final String enhancerClassName, final PointCut pointCut) {
return new Filter() {
@Override
public boolean doClassFilter(int access, String javaClassName, String superClassTypeJavaClassName,
String[] interfaceTypeJavaClassNameArray,
String[] annotationTypeJavaClassNameArray
) {
// ClassMatcher 匹配
ClassMatcher classMatcher = pointCut.getClassMatcher();
...
}
@Override
public boolean doMethodFilter(int access, String javaMethodName,
String[] parameterTypeJavaClassNameArray,
String[] throwsTypeJavaClassNameArray,
String[] annotationTypeJavaClassNameArray) {
// MethodMatcher 匹配
MethodMatcher methodMatcher = pointCut.getMethodMatcher();
...
};
}
如果已经加载插件,此时目标应用匹配能匹配到 Filter 后,EventListener 已经可以被触发,但是 chaosblade-exec-jvm 内部通过 StatusManager 管理状态,所以故障能力不会被触发。
例如 BeforeEventListener 触发调用 BeforeEnhancer 的 beforeAdvice() 方法,在ManagerFactory.getStatusManager().expExists(targetName) 判断时候被中断,具体的实现如下:
public void beforeAdvice(String targetName,
ClassLoader classLoader,
String className,
Object object,
Method method,
Object[] methodArguments) throws Exception {
// 判断实验的状态
if (!ManagerFactory.getStatusManager().expExists(targetName)) {
return;
}
EnhancerModel model = doBeforeAdvice(classLoader, className, object, method, methodArguments);
if (model == null) {
return;
}
...
// 注入阶段
Injector.inject(model);
}
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,那么将直接进行故障能力注入,且不需要走 Enhancer,如 JVM OOM 故障能力等。
public Response handle(Request request) {
if (unloaded) {
return Response.ofFailure(Code.ILLEGAL_STATE, "the agent is uninstalling");
}
// 检查 suid,suid 是一次实验的上下文ID
String suid = request.getParam("suid");
...
return handleInjection(suid, model, modelSpec);
}
private Response handleInjection(String suid, Model model, ModelSpec modelSpec) {
RegisterResult result = this.statusManager.registerExp(suid, model);
if (result.isSuccess()) {
// 判断是否预创建
applyPreInjectionModelHandler(suid, modelSpec, model);
}
}
com.alibaba.chaosblade.exec.common.model.handler.PreCreateInjectionModelHandler
预创建com.alibaba.chaosblade.exec.common.model.handler.PreDestroyInjectionModelHandler
预销毁private void applyPreInjectionModelHandler(String suid, ModelSpec modelSpec, Model model)
throws ExperimentException {
if (modelSpec instanceof PreCreateInjectionModelHandler) {
((PreCreateInjectionModelHandler)modelSpec).preCreate(suid, model);
}
}
...
如果 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 类触发故障。
故障能力注入的方式,最终都是调用 ActionExecutor 执行故障能力。
DirectlyInjectionAction 直接注入不经过Enhancer参数包装匹配直接到故障触发 ActionExecutor 执行阶段,如果是Injector 注入此时因为 StatusManager 已经注册了实验,当事件再次出发后ManagerFactory.getStatusManager().expExists(targetName) 的判断不会被中断,继续往下走,到了自定义的 Enhancer ,在自定义的 Enhancer 里面可以拿到原方法的参数、类型等,甚至可以反射调原类型的其他方法,这样做风险较大,一般在这里往往是取一些成员变量或者 get 方法等,用于 Inject 阶段参数匹配。
自定义的 Enhancer,如 ServletEnhancer,把一些需要与命令行匹配的参数 包装在 MatcherMode 里面,然后包装 EnhancerModel 返回,比如 --requestpath = /index ,那么requestpath 等于 requestURI;–querystring=“name=xx” 做自定义匹配。参数包装好后,在 Injector.inject(model) 阶段判断。
public EnhancerModel doBeforeAdvice(ClassLoader classLoader, String className, Object object,
Method method, Object[] methodArguments)
throws Exception {
Object request = methodArguments[0];
String requestURI = ReflectUtil.invokeMethod(request, ServletConstant.GET_REQUEST_URI, new Object[]{
}, false);
String requestMethod = ReflectUtil.invokeMethod(request, ServletConstant.GET_METHOD, new Object[]{
}, false);
MatcherModel matcherModel = new MatcherModel();
matcherModel.add(ServletConstant.METHOD_KEY, requestMethod);
matcherModel.add(ServletConstant.REQUEST_PATH_KEY, requestURI);
Map<String, Object> queryString = getQueryString(requestMethod, request);
EnhancerModel enhancerModel = new EnhancerModel(classLoader, matcherModel);
// 自定义参数匹配
enhancerModel.addCustomMatcher(ServletConstant.QUERY_STRING_KEY, queryString, ServletParamsMatcher.getInstance());
return enhancerModel;
}
Inject 阶段首先获取 StatusManage 注册的实验,compare(model, enhancerModel) 做参数比对,比对失败返回,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;
}
// 累加攻击次数和判断攻击次数是否到达 effect count
boolean pass = limitAndIncrease(statusMetric);
if (!pass) {
break;
}
enhancerModel.merge(model);
ModelSpec modelSpec = ManagerFactory.getModelSpecManager().getModelSpec(target);
ActionSpec actionSpec = modelSpec.getActionSpec(model.getActionName());
// ActionExecutor执行故障能力
actionSpec.getActionExecutor().run(enhancerModel);
break;
}
}
由 Inject 触发,或者由 DirectlyInjectionAction 直接触发,最后调用自定义的 ActionExecutor 生成故障,如 DefaultDelayExecutor ,此时故障能力已经生效了。
public void run(EnhancerModel enhancerModel) throws Exception {
String time = enhancerModel.getActionFlag(timeFlagSpec.getName());
Integer sleepTimeInMillis = Integer.valueOf(time);
// 触发延迟
TimeUnit.MILLISECONDS.sleep(sleepTimeInMillis);
}
blade destroy 52a27bafc252beee
该命令下发后,触发 SandboxModule @Http("/destory") 注解标记的方法,将事件分发给 com.alibaba.chaosblade.exec.service.handler.DestroyHandler 处理,注销本次故障的状态,此时再次触发 Enchaner 后,StatusManger判定实验状态已经销毁,不会在进行故障能力注入
// StatusManger 判断实验状态
if (!ManagerFactory.getStatusManager().expExists(targetName)) {
return;
}
如果插件的 ModelSpec 是 PreDestroyInjectionModelHandler 类型,且 ActionSpec 的类型是 DirectlyInjectionAction 类型,停止故障能力注入,ActionSpec 的类型不是 DirectlyInjectionAction 类型,将卸载插件。
// DestroyHandler 注销实验状态
public Response handle(Request request) {
String uid = request.getParam("suid");
...
// 判断 uid
if (StringUtil.isBlank(uid)) {
if (StringUtil.isBlank(target) || StringUtil.isBlank(action)) {
return false;
}
// 注销status
return destroy(target, action);
}
return destroy(uid);
}
blade revoke 98e792c9a9a5dfea
该命令下发后,触发 SandboxModule unload() 事件,同时插件卸载,完全回收 Agent 创建的各种资源。
public void onUnload() throws Throwable {
dispatchService.unload();
ManagerFactory.unload();
watchIds.clear();
}
本文以 Servlet 场景为例,详细介绍了 chaosblade-exec-jvm 项目架构设计和实现原理,后续将通过模型篇、协议篇、字节码篇、插件篇以及实战篇深入介绍此项目,使读者达到可以快速扩展自己所需插件的目的。
ChaosBlade 项目作为一个混沌工程实验工具,不仅使用简洁,而且还支持丰富的实验场景且扩展场景简单,支持的场景领域如下:
ChaosBlade 社区欢迎各位加入,我们一起讨论混沌工程领域实践或者在使用 ChaosBlade 过程中产生的任何想法和问题。
叶飞:Github @tiny-x,开源社区爱好者,ChaosBlade Committer,参与推动 ChaosBlade 混沌工程生态建设。
穹谷:Github @xcaspar,ChaosBlade 项目负责人,混沌工程布道师。
“阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的公众号。”