【性能测试】ChaosTesting(混沌测试)&ChaosBlade(混沌实验工具)(五)-jvm混沌实验

6. chaosblade-jvm实验场景

6.1 挂载 java agent

blade prepare jvm

6.1.1 介绍

挂载 java agent,执行 java 实验场景必要步骤

6.1.2 参数

-j, --javaHome string: 指定 JAVA_HOME 路径,用于指定 java bin 和 tools.jar,如果不添加此参数,默认会优先获取 JAVA_HOME 环境变量,如果获取失败,会解析指定进程参数获取 JAVA_HOME,获取失败,会使用 chaosblade 自带的 tools.jar
--pid string: java 进程ID
-P, --port int: java agent 暴露服务的本地端口,用于下发实验命令
-p, --process string: java 进程关键词,用于定位 java 进程
-d, --debug: 开启 debug 模式

6.1.3 案例

bash 复制代码
# 指定 pid 执行 java agent 挂载
blade prepare jvm --pid 26652
# 命令也可简写为
blade p jvm --pid 26652

执行成功,会返回实验准备的 UID,例如:

bash 复制代码
{"code":200,"success":true,"result":"2552c05c6066dde5"}

2552c05c6066dde5 就是实验准备对象的 UID,执行卸载操作需要用到此 UID,例如

bash 复制代码
blade revoke 2552c05c6066dde5
# 命令也可简写为
blade r 2552c05c6066dde5

如果 UID 忘记,可通过以下命令查询

bash 复制代码
blade status --type prepare --target jvm
# 命令也可简写为:
blade s --type p --target jvm

挂载 java agent 操作是个比较耗时的过程,在未返回结果前请耐心等待

6.0 创建jvm

blade create jvm

6.0.1 介绍

jvm 本身相关场景,以及可以指定类,方法注入延迟、返回值、异常故障场景,也可以编写 groovy 和 java 脚本来实现复杂的场景。目前支持的场景如下

  1. blade create jvm CodeCacheFilling(blade create jvm CodeCacheFilling.md) 填充 jvm code cache
  2. blade create jvm OutOfMemoryError(blade create jvm OutOfMemoryError.md) 内存溢出,支持堆、栈、metaspace 区溢出
  3. blade create jvm cpufullload(blade create jvm cpufullload.md) java 进程 CPU 使用率满载
  4. blade create jvm delay(blade create jvm delay.md) 方法延迟
  5. blade create jvm return(blade create jvm return.md) 指定返回值
  6. blade create jvm script(blade create jvm script.md) 编写 groovy 和 java 实现场景
  7. blade create jvm throwCustomException(blade create jvm throwCustomException.md) 抛自定义异常场景

6.0.2 参数

此处列举 jvm 支持的通用参数:
--pid string: 指定 java 进程号
--process string: 指定 java 进程名,如果同时填写
--timeout string:
设定运行时长,单位是秒,通用参数

JVM 方法级别的故障场景通用参数:
--classname string: 指定类名,必须是实现类,带全包名,例如 com.xxx.xxx.XController (必填项)
--methodname string: 指定方法名,注意相同方法名的方法都会被注入相同故障 (必填项)
--after: 方法执行完成返回前注入故障,比如修改复杂的返回对象
--effect-count string: 限制影响数量
--effect-percent string: 限制影响百分比

各场景还有自身所独有的参数,可以在每个场景文档中查看

6.0.3 案例

此处举个简单的例子:当前 Java 进程 CPU 使用率满载

bash 复制代码
# 先执行 prepare 操作
blade prepare jvm --process tomcat
{"code":200,"success":true,"result":"af9ec083eaf32e26"}

# 执行进程内 CPU 满载
blade create jvm cpufullload --process tomcat
{"code":200,"success":true,"result":"2a97b8c2fe9d7c01"}
# 验证结果:见下图
-w461
Copy
# 停止实验
blade destroy 2a97b8c2fe9d7c01

# 卸载 agent
blade revoke af9ec083eaf32e26

6.2 指定类方法调用延迟

blade create jvm delay

6.2.1 参数

以下是此场景特有参数,通用参数详见:blade create jvm(blade create jvm.md)
--effect-count string: 影响的请求条数
--effect-percent string: 影响的请求百分比
--time string: 延迟时间,单位是毫秒,必填项
--offset string: 延迟时间上下偏移量,比如 --time 3000 --offset 1000,则延迟时间范围是 2000-4000 毫秒

6.2.2 案例

业务方法通过 future 获取返回值,代码如下:

java 复制代码
@RequestMapping(value = "async")
@ResponseBody
public String asyncHello(final String name, long timeout) {
    if (timeout == 0) {
        timeout = 3000;
    }
    try {
        FutureTask futureTask = new FutureTask(new Callable() {
            @Override
            public Object call() throws Exception {
                return sayHello(name);
            }
        });
        new Thread(futureTask).start();
        return (String)futureTask.get(timeout, TimeUnit.MILLISECONDS);
    } catch (TimeoutException e) {
        return "timeout, " + e.getMessage() + "\n";
    } catch (Exception e) {
        return e.getMessage() + "\n";
    }
}

我们对 sayHello 方法调用注入 4 秒延迟故障,futureTask.get(2000, TimeUnit.MILLISECONDS) 会发生超时返回:

bash 复制代码
blade c jvm delay --time 4000 --classname=com.example.controller.DubboController --methodname=sayHello --process tomcat
bash 复制代码
{"code":200,"success":true,"result":"d6ebea0dc28b6ab3"}

注入故障前:

注入故障后:

停止实验:

bash 复制代码
blade d d6ebea0dc28b6ab3

6.3 指定类方法的返回值

blade create jvm return

6.3.1 介绍

指定类方法的返回值,仅支持基本类型、null 和 String 类型的返回值。

6.3.2 参数

以下是此场景特有参数,通用参数详见:blade create jvm(blade create jvm.md)
--effect-count string: 影响的请求条数
--effect-percent string: 影响的请求百分比
--value string: 返回指定值,仅支持基本类型和字符串类型,如果想返回 null,可以设置为 --value null 。必选项

6.3.3 案例

指定com.example.controller.DubboController类,下面业务方法返回 "hello-chaosblade"

java 复制代码
@RequestMapping(value = "hello")
@ResponseBody
public String hello(String name, int code) {
    if (name == null) {
        name = "friend";
    }
    StringBuilder result = null;
    try {
        result = new StringBuilder(sayHello(name));
    } catch (Exception e) {
        return e.getMessage() + "\n";
    }
    return result.toString() + "\n";
}

故障注入命令如下:

bash 复制代码
blade c jvm return --value hello-chaosblade --classname com.example.controller.DubboController --methodname hello --process tomcat

故障注入之前:

故障注入之后:

停止实验:

bash 复制代码
blade d d31e24dea782a275

上述代码调用 sayHello 方法,我们对 sayHello 方法注入返回 null 故障,sayHello 方法如下:

java 复制代码
private String sayHello(String name) throws BeansException {
    demoService = (DemoService)SpringContextUtil.getBean("demoService");
    StringBuilder result = new StringBuilder();
    result.append(demoService.sayHello(name));
    return result.toString();
}

执行以下命令:

bash 复制代码
blade c jvm return --value null --classname com.example.controller.DubboController --methodname sayHello --process tomcat

故障注入之后:

6.4 编写 java 或者 groovy 脚本实现复杂的故障场景

** blade create jvm script**

6.4.1 介绍

编写 java 或者 groovy 脚本实现复杂的故障场景,比如篡改参数、修改返回值、抛自定义异常等

6.4.2 参数

以下是此场景特有参数,通用参数详见:blade create jvm(blade create jvm.md)
--effect-count string: 影响的请求条数
--effect-percent string: 影响的请求百分比
--script-content string: 脚本内容,是 Base64 编码后的内容,相关工具类 Base64Util。注意,不能和 script-file 同时使用。
--script-file string: 脚本文件,文件绝对路径
--script-name string: 脚本名称,日志记录用,可不填写。
--script-type string: 脚本类型,取值为 java 或 groovy,默认为 java。

使用 script-content 指定演练脚本内容,不添加 script-type 参数,默认为 java 脚本,将调用 java 引擎解析器。

bash 复制代码
blade c jvm script --classname com.example.controller.DubboController --methodname call --script-content aW1wb3J0IGphdmEudXRpbC5NYXA7CgppbXBvcnQgY29tLmV4YW1wbGUuY29udHJvbGxlci5DdXN0b21FeGNlcHRpb247CgovKioKICogQGF1dGhvciBDaGFuZ2p1biBYaWFvCiAqLwpwdWJsaWMgY2xhc3MgRXhjZXB0aW9uU2NyaXB0IHsKICAgIHB1YmxpYyBPYmplY3QgcnVuKE1hcDxTdHJpbmcsIE9iamVjdD4gcGFyYW1zKSB0aHJvd3MgQ3VzdG9tRXhjZXB0aW9uIHsKICAgICAgICBwYXJhbXMucHV0KCIxIiwgMTExTCk7CiAgICAgICAgLy9yZXR1cm4gIk1vY2sgVmFsdWUiOwogICAgICAgIC8vdGhyb3cgbmV3IEN1c3RvbUV4Y2VwdGlvbigiaGVsbG8iKTsKICAgICAgICByZXR1cm4gbnVsbDsKICAgIH0KfQo=  --script-name exception

使用 script-file 参数指定文件演练:

bash 复制代码
blade c jvm script --classname com.example.controller.DubboController --methodname call --script-file /Users/Shared/IdeaProjects/Workspace_WebApp/dubbodemo/src/main/java/com/example/controller/ExceptionScript.java --script-name exception

执行 groovy 脚本实验场景,参数同上,但必须添加 --script-type groovy 参数。如

bash 复制代码
blade c jvm script --classname com.example.controller.DubboController --methodname call --script-file /Users/Shared/IdeaProjects/Workspace_WebApp/dubbodemo/src/main/java/com/example/controller/GroovyScript.groovy --script-name exception --script-type groovy 

脚本规范

必须创建一个类,对类名和包名没有要求,其中所依赖的类,必须是目标应用所具备的类。

同包下的类引用,必须写全包名,比如故障脚本类是 com.example.controller.ExceptionScript,类中引入了同包下的 DubboController 类,则 DubboController 必须添加 com.example.controller.DubboController。引入非同包下的类,无需写全包名。

必须添加 public Object run(Map<String, Object> params) 方法,其中 params 对象中包含目标方法参数,key 是参数索引下标,从 0 开始,比如目标方法是 public String call(Object obj1, Object obj2){},则 params.get("0")则返回的是 obj1 对象,可以执行params.put("0", ) 来修改目标方法参数(目标方法及 --classname 和 --methodname 所指定的类方法)。

上述方法返回的对象如果不为空,则会根据脚本中返回的对象来修改目标方法返回值,注意类型必须和目标方法返回值一致。如果上述方法返回 null,则不会修改目标方法返回值。

6.4.3 案例

对以下业务类做修改返回值实验场景:

java 复制代码
@RestController
@RequestMapping("/pet")
public class PetController {

    @GetMapping("/list")
    public Result<List<PetVO>> getPets() {
        Map<Long, Discount> petDiscount = discountManager
            .getPetDiscounts()
            .stream()
            .filter(discount -> discount.getExpired() == 0)
            .collect(Collectors.toMap(
                Discount::getPetId,
                Function.identity()
            ));

        List<PetVO> pets = petManager
            .getPets()
            .stream()
            .map(pet -> {
                PetVO petVO = PetVO.from(pet);
                Discount discount = petDiscount.get(pet.getId());

                if (null != discount && null != discount.getDiscountPrice() && discount.getDiscountPrice() > 0L) {
                    petVO.setDiscountPrice(discount.getDiscountPrice());
                }

                return petVO;
            })
            .collect(Collectors.toList());

       return Result.success(pets);
    }

则编写 Java 脚本,实现对 getPets 方法做返回值修改:

java 复制代码
package com.alibaba.csp.monkeyking.controller;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import com.alibaba.csp.monkeyking.demo.model.Pet;
import com.alibaba.csp.monkeyking.model.PetVO;
import com.alibaba.csp.monkeyking.model.Result;

public class ChaosController {

    public Object run(Map<String, Object> params) {
        ArrayList<PetVO> petVOS = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            Pet pet = new Pet();
            pet.setName("test_" + i);
            PetVO petVO = PetVO.from(pet);
            petVOS.add(petVO);
        }
        Result<List<PetVO>> results = Result.success(petVOS);
        return results;
    }
}

保存文件后,通过上面 使用方式 部分的命令来调用,也可以将其进行 Base64 编码,通过指定 script-content 参数来指定编码后的内容。

bash 复制代码
blade c jvm script --classname com.alibaba.csp.monkeyking.controller.PetController --methodname getPets --script-file /Users/Shared/IdeaProjects/Workspace_WebApp/dubbodemo/src/main/java/com/alibaba/csp/monkeyking/controller/ChaosController --script-name specifyReturnObj

未执行实验之前页面:

执行实验之后:

6.5 指定 java 进程 CPU 满载

blade create jvm cpufullload

6.5.1 介绍

指定 java 进程 CPU 满载,可以简写为 blade c jvm cfl

6.5.2 参数

以下是此场景特有参数,通用参数详见:blade create jvm(blade create jvm.md)
--cpu-count string: 绑定的 CPU 核数,即指定几个核满载

6.5.3 案例

指定全部核满载

bash 复制代码
blade c jvm cfl --process tomcat 
bash 复制代码
{"code":200,"success":true,"result":"48d70f01e65f68f7"}

查看该进程 CPU 使用率:

停止实验:

bash 复制代码
blade d 48d70f01e65f68f7

指定两个核满载(测试机器是 8 个核)

bash 复制代码
blade c jvm cfl --cpu-count 2 --process tomcat
bash 复制代码
{"code":200,"success":true,"result":"a929157644688b15"}

查看进程 CPU 使用率是满核的四分之一:

6.6 内存溢出场景

blade create jvm OutOfMemoryError

6.6.1 介绍

内存溢出场景,命令可以简写为:blade c jvm oom

6.6.2 参数

以下是此场景特有参数,通用参数详见:blade create jvm(blade create jvm.md)
--area string: JVM 内存区,目前支持 HEAP, NOHEAP, OFFHEAP,必填项。用Heap来表示Eden+Old,,用NOHEAP来表示metaspace,用OFFHEAP来表示堆外内存
--block string: 指定对象大小,仅支持 HEAP 和 OFFHEAP 区,单位是 MB
--interval string: 单位ms,默认500两次oom异常间的时间间隔,只有在非暴力模式才生效,可以减缓gc的频率,不用担心进程会无响应
--wild-mode string: 默认false,是否开启暴力模式,如果是暴力模式,在OOM发生之后也不会释放之前创建的内存,可能会引起应用进程无响应

6.6.3 案例

堆内存占用:

bash 复制代码
blade c jvm oom --area HEAP --wild-mode true --process tomcat

{"code":200,"success":true,"result":"99b9228b9632e043"}

故障注入之前:

故障注入之后:

停止 HEAP 内存占用:

bash 复制代码
blade d 99b9228b9632e043

创建 Metaspace 区内存占用,注意,执行完此场景后,需要重启应用!!!!:

bash 复制代码
blade c jvm oom --area NOHEAP --wild-mode true --process tomcat

{"code":200,"success":true,"result":"93264dd07149cf54"}

故障注入后:

6.6.4 实现原理

根据不同区注入
java.lang.OutOfMemoryError: Java heap space

创建 Heap的话分为Young,Old,这块区域的oom是最好重现,只需要不断的创建对象就可以,如果内存使用达到了 Xmx或者Xmn所规定的大小,并且gc回收不了,就会触发oom错误。

检查 • 可以通过 jmap -heap pid 来查看当前堆占用情况是否到了100% • 可以通过jstat -gcutil pid 来查看是否发生了gc,因为会一直创建新的对象,所以会频繁触发gc操作

恢复 当演练终止后,会停止产生新的对象,但此时不一定heap就恢复了,因为恢复需要触发gc才可以进行回收,当然也可以通过手动调用 System.gc()来强行触发gc,但是如果你的启动参数里面有 -XX:+DisableExplicitGC 那么这个命令就无法生效了.

注意 触发OOM的时候可能会导致进程被操作系统所kill,这个原因是因为你的Xmx设置的不合理,比如操作系统内存只有3G,但是你Xmx会设置了3G甚至更多,那么就会因为系统内存不足,而被os kill掉进程,所以这里务必要注意Xmx大小

java.lang.OutOfMemoryError: Metaspace

创建 Metaspace可以通过不断的加载类对象来创建,当大小超过了 -XX:MaxMetaspaceSize 并且无法进行gc回收就会抛出 oom错误了

检查 • 可以通过jstat -gcutil pid 来查看 M区的使用情况以及gc的次数

恢复 类对象的回收条件在jvm里面比较苛刻,需要满足很多条件,就算满足了条件,触发gc了也不一定回收,只要有下面任何一个条件就无法被回收. • objects of that class are still reachable. • the Class object representing the class is still reachable • the ClassLoader that loaded the class is still reachable • other classes loaded by the ClassLoader are still reachable 因此最好的办法就是重启应用.

java.lang.OutOfMemoryError: Direct buffer memoryDirectBuffer

创建 堆外内存可以直接通过ByteBuffer.allocateDirect 来产生,并且会一直消耗系统内存.

检查 • 因为堆外内存不属于堆里面,所以你通过jmap命令很难发现,但是可以通过 jstat -gcutil pid 来查看,如果频发出发了fullgc,但是e,O,M区都没发生变化, 那就是进行堆外内存回收 • 可以通过free -m 查看内存使用情况

注意 同样,如果没有设置最大堆外内存大小,同样会因为OS的memory耗尽而导致进程被杀,所以需要配置比如下面的参数: -XX:MaxDirectMemorySize=100M

6.7 调用频率比较高的代码

blade create jvm CodeCacheFilling

6.7.1 介绍

CodeCache主要用于存放native code,其中主要是JIT编译后的代码。被JIT编译的一般都是"热代码",简单说就是调用频率比较高的代码,JIT编译后,代码的执行效率会变高,CodeCache满会导致JVM关闭JIT编译且不可再开启,那么CodeCache满会引起系统运行效率降低,导致系统最大负载下降,当系统流量较大时,可表现为RT增高、QPS下降等。 命令可以简写为:blade c jvm ccf

6.7.2 参数

此场景无特有参数,通用参数详见:blade create jvm(blade create jvm.md)

6.7.3 案例

6.7.3.1 注入 CodeCache 满故障:
bash 复制代码
blade c jvm CodeCacheFilling --process tomcat                                                                          

{"code":200,"success":true,"result":"f0e896f38c704894"}
6.7.3.2 实现原理

由于CodeCache主要存放JIT编译的结果,所以填充CodeCache分为两步,第一步是生成用于触发JIT编译的class,方式是通过动态编译生成大量的class;第二步是编译后生成的class进行实例化和频繁调用("加热"),直到触发JIT编译后进入CodeCache区。通过这样方式不停的填充CodeCache,直到JIT编译关闭

6.7.3.3 常见问题
  1. 由于需要编译和"加热"代码,所以在填充的过程中CPU占用率会很高;并且会持续一段时间(测试中,默认大小的情况下,从无占用到填充满约5分钟,实际情况下,CodeCache都会有一定的使用率,所以时间不会那么长);
  2. 由于"加热"过程中需要实例化大量的class,会有大量对象一直无法被GC回收,有概率导致Metaspace满而产生OOM;
  3. 由于无法直接判断JIT编译是否关闭,所以只能根据CodeCache占用量来判断,但是JIT编译关闭时,CodeCache占用量的阈值并不能精准获取,所以是通过CodeCache的增长来判断的,如果5秒内CodeCache占用量都无变化,即判断JIT编译关闭(JIT编译关闭后,CodeCache占用量不再变化);
  4. 目前是根据CodeCache的默认大小来设计的(生成class数量等),即240M(jdk8 64bit),如果设置更大的CodeCache(-XX:ReservedCodeCacheSize)的话,持续时间会更长,甚至由于动态产生的class数量不够而导致无法填充满;
  5. 由于JIT编译关闭后不可再手工开启,所以该故障无法直接恢复,需要用户手工重启应用系统来恢复;

6.8 指定类方法抛自定义异常

blade create jvm throwCustomException

6.8.1 介绍

指定类方法抛自定义异常,命令可以简写为 blade c jvm tce

6.8.2 参数

以下是此场景特有参数,通用参数详见:blade create jvm(blade create jvm.md)
--effect-count string: 影响的请求条数
--effect-percent string: 影响的请求百分比
--exception string: 异常类,带全包名,必须继承 java.lang.Exception 或 java.lang.Exception 本身
--exception-message string: 指定异常类信息,默认值是 chaosblade-mock-exception

6.8.3 案例

类名:com.example.controller.DubboController,业务代码如下:

java 复制代码
private String sayHello(String name) throws BeansException {
    demoService = (DemoService)SpringContextUtil.getBean("demoService");
    StringBuilder result = new StringBuilder();
    result.append(demoService.sayHello(name));
    return result.toString();
}

指定以上方法抛出 java.lang.Exception 异常,影响两条请求,命令如下

bash 复制代码
blade c jvm throwCustomException --exception java.lang.Exception --classname com.example.controller.DubboController --methodname sayHello --process tomcat --effect-count 2

{"code":200,"success":true,"result":"3abbe6fe97d6bc75"}

验证结果:

注入前:

注入后:

第三次请求后恢复正常:

停止实验:

bash 复制代码
blade d 3abbe6fe97d6bc75
相关推荐
ZhengEnCi6 分钟前
P2M-Matplotlib折线图完全指南-从数据可视化到趋势分析的Python绘图利器
python·matlab·数据可视化
ZhengEnCi2 小时前
P2L-Matplotlib饼图完全指南-从数据可视化到图表定制的Python绘图利器
python·matlab
曲幽2 小时前
你的REST接口还在“过度投喂”数据吗?——FastAPI + GraphQL实战避坑指南
python·fastapi·web·graphql·route·cors·rest·strawberry
用户8358086187913 小时前
基于 Self-RAG 与列表级重排序的进阶 RAG 系统设计与实现
python
Warson_L19 小时前
Python `Annotated` 与 LangGraph Reducer 学习笔记
python
韩师傅19 小时前
海天线算法的前世今生
python·计算机视觉
韩师傅19 小时前
当你的甲方设备过烂,要如何快速出效果?
python·计算机视觉
Warson_L20 小时前
LangGraph的MessageState and HumanMessage
python
韩师傅20 小时前
当你的甲方吐槽天空不够蓝,你应该如何应对
python·计算机视觉
Warson_L21 小时前
python的类&继承
python