如果故障选择了你……


作者 | 叶飞、穹谷


导读:总以为混沌工程离你很远?但发生故障的那一刻不是由你来选择的,而是那一刻来选择你,你能做的就是为之做好准备。混沌工程在阿里内部已经应用多年,而ChaosBlade这个开源项目是阿里多年来通过注入故障来对抗故障的经验结晶。为使大家更深入的了解其实现原理以及如何扩展自己所需要的组件故障注入,我们准备了一个系列对其做详细技术剖析:架构篇、模型篇、协议篇、字节码篇、插件篇以及实战篇。
原文标题《技术剖析 Java 场景混沌工程实现系列(一)| 架构篇》

前言

在分布式系统架构下,服务间的依赖日益复杂,很难评估单个服务故障对整个系统的影响,并且请求链路长,监控告警的不完善导致发现问题、定位问题难度增大,同时业务和技术迭代快,如何持续保障系统的稳定性和高可用性受到很大的挑战。

我们知道发生故障的那一刻不是由你来选择的,而是那一刻来选择你,你能做的就是为之做好准备。所以构建稳定性系统很重要的一环是混沌工程,在可控范围或环境下,通过故障注入,来持续提升系统的稳定性和高可用能力。

ChaosBlade(Github 地址:https://github.com/chaosblade-io/chaosblade) 是一款遵循混沌工程实验原理,提供丰富故障场景实现,帮助分布式系统提升容错性和可恢复性的混沌工程工具,可实现底层故障的注入,特点是操作简洁、无侵入、扩展性强。 其中 chaosblade-exec-jvm (Github 地址:https://github.com/chaosblade- ... c-jvm )项目实现了零成本对 Java 应用服务故障注入。其不仅支持主流的框架组件,如 Dubbo、Servlet、RocketMQ 等,还支持指定任意类和方法注入延迟、异常以及通过编写 Java 和 Groovy 脚本来实现复杂的实验场景。

为使大家更深入的了解其实现原理以及如何扩展自己所需要的组件故障注入,分为六篇文章对其做详细技术剖析:架构篇、模型篇、协议篇、字节码篇、插件篇以及实战篇。本文将详细介绍 chaosblade-exec-jvm 的整体架构设计,使用户对 chaosblade-exec-jvm 有一定的了解。

系统设计

1.png


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秒,步骤如下:

```shell
// 挂载 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
```

1. 执行过程

以下通过 Servlet 请求延迟为例,详细介绍故障注入的过程。

2.png

  1. ChaosBlade 下发挂载命令,挂载 Sandbox 到应用进程,激活 Java Agent,例如blade p jvm --pid 888
  2. 挂载 Sandbox 后加载 chaosblade-exec-jvm 模块,加载插件,如 ServletPlugin、DubboPlugin 等。
  3. 匹配 ServletPlugin 插件的切点、注册事件监听,HttpServlet 的 doPost、doGet 方法。
  4. ChaosBlade 下发故障规则命令blade create servlet --requestpath=/topic delay --time=3000 --method=post
  5. 匹配故障规则,如 --requestpath=/topic ,访问 http://127.0.0.1/topic 规则匹配成功。
  6. 匹配故障规则成功后,触发故障,如延迟故障、自定义异常抛出等。
  7. ChaosBlade 下发命令卸载 JavaAgent,如blade revoke 98e792c9a9a5dfea


2. 代码剖析

1)挂载 Agent

shell
blade p jvm --pid 888


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

java
// Agent 加载事件
public void onLoad() throws Throwable {
ManagerFactory.getListenerManager().setPluginLifecycleListener(this);
dispatchService.load();
ManagerFactory.load();
}
// ChaosBlade 模块激活实现
public void onActive() throws Throwable {
loadPlugins();
}


2)加载 Plugin

3.jpg


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

```java
// 加载插件
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);
}
```

3)匹配 PointCut

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

```java
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();
...
};
}
```

4)触发 Enhancer

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

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

```java
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);
}
```

5)创建混沌实验

shell
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 故障能力等。

```java
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);
}
}
```

ModelSpec

  • com.alibaba.chaosblade.exec.common.model.handler.PreCreateInjectionModelHandler预创建
  • com.alibaba.chaosblade.exec.common.model.handler.PreDestroyInjectionModelHandler预销毁


java
private void applyPreInjectionModelHandler(String suid, ModelSpec modelSpec, Model model)
throws ExperimentException {
if (modelSpec instanceof PreCreateInjectionModelHandler) {
((PreCreateInjectionModelHandler)modelSpec).preCreate(suid, model);
}
}
...


DirectlyInjectionAction

如果 ModelSpec 是 PreCreateInjectionModelHandler 类型,且 ActionSpec 的类型是 DirectlyInjectionAction 类型,将直接进行故障能力注入,比如 JvmOom 故障能力,ActionSpec 的类型不是 DirectlyInjectionAction 类型,将加载插件。

4.jpg


```java
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 类触发故障。

6)注入故障能力

故障能力注入的方式,最终都是调用 ActionExecutor 执行故障能力。
  • 通过 Injector 注入;
  • DirectlyInjectionAction 直接注入,直接注入不进过 Inject 类调用阶段,如果  JVM OOM 故障能力等。


DirectlyInjectionAction 直接注入不经过Enhancer参数包装匹配直接到故障触发 ActionExecutor 执行阶段,如果是Injector 注入此时因为 StatusManager 已经注册了实验,当事件再次出发后ManagerFactory.getStatusManager().expExists(targetName) 的判断不会被中断,继续往下走,到了自定义的 Enhancer ,在自定义的 Enhancer 里面可以拿到原方法的参数、类型等,甚至可以反射调原类型的其他方法,这样做风险较大,一般在这里往往是取一些成员变量或者 get 方法等,用于 Inject 阶段参数匹配。

7)包装匹配参数

自定义的 Enhancer,如 ServletEnhancer,把一些需要与命令行匹配的参数 包装在 MatcherMode 里面,然后包装 EnhancerModel 返回,比如  --requestpath = /index ,那么requestpath 等于 requestURI;--querystring="name=xx" 做自定义匹配。参数包装好后,在 Injector.inject(model) 阶段判断。

```java
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;
}
```

8)判断前置条件

Inject 阶段首先获取 StatusManage 注册的实验,compare(model, enhancerModel) 做参数比对,比对失败返回,limitAndIncrease(statusMetric) 判断 --effect-count --effect-percent 来控制影响的次数和百分比

java
public static void inject(EnhancerModel enhancerModel) throws InterruptProcessException {
String target = enhancerModel.getTarget();
List&lt;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;
}
}


9)触发故障能力

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

java
public void run(EnhancerModel enhancerModel) throws Exception {
String time = enhancerModel.getActionFlag(timeFlagSpec.getName());
Integer sleepTimeInMillis = Integer.valueOf(time);
// 触发延迟
TimeUnit.MILLISECONDS.sleep(sleepTimeInMillis);
}


3. 销毁实验

shell
blade destroy 52a27bafc252beee


该命令下发后,触发 SandboxModule @Http("/destory") 注解标记的方法,将事件分发给 com.alibaba.chaosblade.exec.service.handler.DestroyHandler 处理,注销本次故障的状态,此时再次触发 Enchaner 后,StatusManger判定实验状态已经销毁,不会在进行故障能力注入

java
// StatusManger 判断实验状态
if (!ManagerFactory.getStatusManager().expExists(targetName)) {
return;
}


如果插件的 ModelSpec 是 PreDestroyInjectionModelHandler 类型,且 ActionSpec 的类型是 DirectlyInjectionAction 类型,停止故障能力注入,ActionSpec 的类型不是 DirectlyInjectionAction 类型,将卸载插件。

java
// DestroyHandler 注销实验状态
public Response handle(Request request) {
String uid = request.getParam(&quot;suid&quot;);
...
// 判断 uid
if (StringUtil.isBlank(uid)) {
if (StringUtil.isBlank(target) || StringUtil.isBlank(action)) {
return false;
}
// 注销status
return destroy(target, action);
}
return destroy(uid);
}


4. 卸载 Agent

shell
blade revoke 98e792c9a9a5dfea


该命令下发后,触发 SandboxModule unload() 事件,同时插件卸载,完全回收 Agent 创建的各种资源。

java
public void onUnload() throws Throwable {
dispatchService.unload();
ManagerFactory.unload();
watchIds.clear();
}


总结

本文以 Servlet 场景为例,详细介绍了 chaosblade-exec-jvm 项目架构设计和实现原理,后续将通过模型篇、协议篇、字节码篇、插件篇以及实战篇深入介绍此项目,使读者达到可以快速扩展自己所需插件的目的。

ChaosBlade 项目作为一个混沌工程实验工具,不仅使用简洁,而且还支持丰富的实验场景且扩展场景简单,支持的场景领域如下:
  • 基础资源:比如 CPU、内存、网络、磁盘、进程等实验场景;
  • Java 应用:比如数据库、缓存、消息、JVM 本身、微服务等,还可以指定任意类方法注入各种复杂的实验场景;
  • C++ 应用:比如指定任意方法或某行代码注入延迟、变量和返回值篡改等实验场景;
  • Docker 容器:比如杀容器、容器内 CPU、内存、网络、磁盘、进程等实验场景;
  • Kubernetes 平台:比如节点上 CPU、内存、网络、磁盘、进程实验场景,Pod 网络和 Pod 本身实验场景如杀 Pod,Pod IO 异常,容器的实验场景如上述的 Docker 容器实验场景;
  • 云资源:比如阿里云 ECS 宕机等实验场景。


ChaosBlade 社区欢迎各位加入,我们一起讨论混沌工程领域实践或者在使用 ChaosBlade 过程中产生的任何想法和问题。

作者简介

叶飞:Github @tiny-x,开源社区爱好者,ChaosBlade Committer,参与推动 ChaosBlade 混沌工程生态建设。
穹谷:Github @xcaspar,ChaosBlade 项目负责人,混沌工程布道师。

阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的公众号。”

0 个评论

要回复文章请先登录注册