【性能测试】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
相关推荐
不写八个10 分钟前
Python办公自动化教程(005):Word添加段落
开发语言·python·word
_.Switch28 分钟前
Python机器学习框架介绍和入门案例:Scikit-learn、TensorFlow与Keras、PyTorch
python·机器学习·架构·tensorflow·keras·scikit-learn
赵荏苒39 分钟前
Python小白之Pandas1
开发语言·python
一眼万里*e1 小时前
fish-speech语音大模型本地部署
python·flask·大模型
结衣结衣.2 小时前
python中的函数介绍
java·c语言·开发语言·前端·笔记·python·学习
茫茫人海一粒沙2 小时前
Python 代码编写规范
开发语言·python
林浩2332 小时前
Python——异常处理机制
python
数据分析螺丝钉2 小时前
力扣第240题“搜索二维矩阵 II”
经验分享·python·算法·leetcode·面试
小蜗笔记3 小时前
在Python中实现多目标优化问题(7)模拟退火算法的调用
开发语言·python·模拟退火算法
TANGLONG2223 小时前
【C语言】数据在内存中的存储(万字解析)
java·c语言·c++·python·考研·面试·蓝桥杯