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 案例
# 指定 pid 执行 java agent 挂载
blade prepare jvm --pid 26652
# 命令也可简写为
blade p jvm --pid 26652
执行成功,会返回实验准备的 UID,例如:
{"code":200,"success":true,"result":"2552c05c6066dde5"}
2552c05c6066dde5 就是实验准备对象的 UID,执行卸载操作需要用到此 UID,例如
blade revoke 2552c05c6066dde5
# 命令也可简写为
blade r 2552c05c6066dde5
如果 UID 忘记,可通过以下命令查询
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 脚本来实现复杂的场景。目前支持的场景如下
- [blade create jvm CodeCacheFilling](blade create jvm CodeCacheFilling.md) 填充 jvm code cache
- [blade create jvm OutOfMemoryError](blade create jvm OutOfMemoryError.md) 内存溢出,支持堆、栈、metaspace 区溢出
- [blade create jvm cpufullload](blade create jvm cpufullload.md) java 进程 CPU 使用率满载
- [blade create jvm delay](blade create jvm delay.md) 方法延迟
- [blade create jvm return](blade create jvm return.md) 指定返回值
- [blade create jvm script](blade create jvm script.md) 编写 groovy 和 java 实现场景
- [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 使用率满载
# 先执行 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 获取返回值,代码如下:
@RequestMapping(value = "async")
@ResponseBody
public String asyncHello(final String name, long timeout) {if (timeout == 0) {timeout = 3000;}try {FutureTask futureTask = new FutureTask(new Callable() {@Overridepublic 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) 会发生超时返回:
blade c jvm delay --time 4000 --classname=com.example.controller.DubboController --methodname=sayHello --process tomcat
{"code":200,"success":true,"result":"d6ebea0dc28b6ab3"}
注入故障前:
注入故障后:
停止实验:
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”
@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";
}
故障注入命令如下:
blade c jvm return --value hello-chaosblade --classname com.example.controller.DubboController --methodname hello --process tomcat
故障注入之前:
故障注入之后:
停止实验:
blade d d31e24dea782a275
上述代码调用 sayHello 方法,我们对 sayHello 方法注入返回 null 故障,sayHello 方法如下:
private String sayHello(String name) throws BeansException {demoService = (DemoService)SpringContextUtil.getBean("demoService");StringBuilder result = new StringBuilder();result.append(demoService.sayHello(name));return result.toString();
}
执行以下命令:
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 引擎解析器。
blade c jvm script --classname com.example.controller.DubboController --methodname call --script-content aW1wb3J0IGphdmEudXRpbC5NYXA7CgppbXBvcnQgY29tLmV4YW1wbGUuY29udHJvbGxlci5DdXN0b21FeGNlcHRpb247CgovKioKICogQGF1dGhvciBDaGFuZ2p1biBYaWFvCiAqLwpwdWJsaWMgY2xhc3MgRXhjZXB0aW9uU2NyaXB0IHsKICAgIHB1YmxpYyBPYmplY3QgcnVuKE1hcDxTdHJpbmcsIE9iamVjdD4gcGFyYW1zKSB0aHJvd3MgQ3VzdG9tRXhjZXB0aW9uIHsKICAgICAgICBwYXJhbXMucHV0KCIxIiwgMTExTCk7CiAgICAgICAgLy9yZXR1cm4gIk1vY2sgVmFsdWUiOwogICAgICAgIC8vdGhyb3cgbmV3IEN1c3RvbUV4Y2VwdGlvbigiaGVsbG8iKTsKICAgICAgICByZXR1cm4gbnVsbDsKICAgIH0KfQo= --script-name exception
使用 script-file 参数指定文件演练:
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 参数。如
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 案例
对以下业务类做修改返回值实验场景:
@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 方法做返回值修改:
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 参数来指定编码后的内容。
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 案例
指定全部核满载
blade c jvm cfl --process tomcat
{"code":200,"success":true,"result":"48d70f01e65f68f7"}
查看该进程 CPU 使用率:
停止实验:
blade d 48d70f01e65f68f7
指定两个核满载(测试机器是 8 个核)
blade c jvm cfl --cpu-count 2 --process tomcat
{"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 案例
堆内存占用:
blade c jvm oom --area HEAP --wild-mode true --process tomcat{"code":200,"success":true,"result":"99b9228b9632e043"}
故障注入之前:
故障注入之后:
停止 HEAP 内存占用:
blade d 99b9228b9632e043
创建 Metaspace 区内存占用,注意,执行完此场景后,需要重启应用!!!!:
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 满故障:
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 常见问题
- 由于需要编译和“加热”代码,所以在填充的过程中CPU占用率会很高;并且会持续一段时间(测试中,默认大小的情况下,从无占用到填充满约5分钟,实际情况下,CodeCache都会有一定的使用率,所以时间不会那么长);
- 由于“加热”过程中需要实例化大量的class,会有大量对象一直无法被GC回收,有概率导致Metaspace满而产生OOM;
- 由于无法直接判断JIT编译是否关闭,所以只能根据CodeCache占用量来判断,但是JIT编译关闭时,CodeCache占用量的阈值并不能精准获取,所以是通过CodeCache的增长来判断的,如果5秒内CodeCache占用量都无变化,即判断JIT编译关闭(JIT编译关闭后,CodeCache占用量不再变化);
- 目前是根据CodeCache的默认大小来设计的(生成class数量等),即240M(jdk8 64bit),如果设置更大的CodeCache(-XX:ReservedCodeCacheSize)的话,持续时间会更长,甚至由于动态产生的class数量不够而导致无法填充满;
- 由于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,业务代码如下:
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 异常,影响两条请求,命令如下
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"}
验证结果:
注入前:
注入后:
第三次请求后恢复正常:
停止实验:
blade d 3abbe6fe97d6bc75