本文作者陈恒捷是TesterHome社区主编,第十届MTSC大会上海站-开源专场出品人。先后在PP助手、PPmoney、荔枝等公司从事测试效能提升相关工作,在测试技术及效率提升方面有丰富的经验积累。
为了避免文章过长,此文单独记录 repeater-console 部分的使用。对于 jvm-sandbox-repeater 普通用法的尝鲜记录,请参照 通用流量录制回放工具 jvm-sandbox-repeater 实践记录 (一)
repeater-console 简介
官方的说明:
jvm-sandbox-repeater 仅仅提供了录制回放的能力,如果需要完成
业务回归
、实时监控
、压测
等平台,后面须要有一个数据中心
负责采集数据的加工、存储、搜索,repeater-console 提供了简单的demo示例;一个模块管理
平台负责管理JVM-Sandbox各模块生命周期;一个配置管理
平台负责维护和推送jvm-sandbox-repeater采集所须要的各种配置变更在阿里集团淘系技术质量内部,已有一套完整的体系在持续运行,从17年开始支持了淘系技术质量部的CI、建站、系统重构等多方面质量保障任务,后续如有需要也会考虑把更多的东西开源回馈社区
注意:目前项目代码默认启动standalone模式,不需要依赖任何服务端和存储,能够简单快速的实现单机的录制回放,控制单机模式的开关在
~/.sandbox-module/cfg/repeater.properties
文件中的repeat.standalone.mode=true //开启或关闭单机工作模式
,关闭单机模式后,配置拉取/消息投递等都依赖repeater.properties中配置的具体url;如不想通过http拉取和消息投递的也可以自己实现Broadcaster
和ConfigManager
。稍后我们会公布一份录制回放所需的完整架构图以及jvm-sandbox-repeater在整个体系中的位置供大家工程使用做参考。
个人理解,要想在业务中使用,我们还得搞下 数据中心、模块管理 和 配置管理 。
【数据中心】:你存了那么多流量,总得有个存储和管理的地方吧,数据中心就是干这个活。要不光靠官方提供的那个透传 repeatId 的回放方法,只能回放单个流量,实际项目不够用。
【模块管理】:这个还不是太了解,个人理解是各个 plugin 的管理?
【配置管理】:就是之前试用时说过的只有一个 ~/.sandbox-module/cfg/repeater-config.json
配置文件,是不可能满足多个项目同时使用的需要的。所以需要有个配置管理,提供这方面配置的存储和修改能力。
源码熟悉
由于目前官方对于这个 console 只有一份非常简单的文档:
repeater-console工程集成录制/回放的配置管理;数据存储/数据对比等具备多种能力,因各系统架构差异较大,目前仅开源简单的demo工程,后续会提供统一的工程,也希望有能力和时间的同学来提PR
curl -s http://127.0.0.1:8001/regress/getAsync/repeater -H
'Repeat-TraceId:030010083212156034386424510101ed'
curl -s http://127.0.0.1:8001/facade/api/repeat/repeater/030010083212156034386424510101ed -H "RepeatId:030010083212156034386424510201ed"
curl -s http://127.0.0.1:8001/facade/api/repeat/callback/030010083212156034386424510201ed
所以只能通过解读源码来反推用法咯。
个人的源码阅读三步骤:明确阅读目的、了解整体架构、细读目标功能
step 0 明确阅读目的
目的很简单,使用 repeater-console ,在目前的 demo 项目上完成批量流量录制回放的功能
step 1 了解整体架构
为了便于描述,还是用 tree 吧。
特别说明:以下均为个人分析,并不保证正确哈。
tree -L 10 | grep -v iml | grep -v target
.
├── Readme.md
├── pom.xml
├── repeater-console-common // 存放公共方法的模块
│ ├── pom.xml
│ └── src
│ └── main
│ └── java
│ └── com
│ └── alibaba
│ └── repeater
│ └── console
│ └── common
│ ├── PackageInfo.java // 一个空的类,应该是预留用的
│ └── domain // 目前只有一个名为 Regress 的 java bean ,代表单条回放记录
├── repeater-console-dal // 和数据库打交道的存储模块,model 层
│ ├── pom.xml
│ └── src
│ └── main
│ ├── java
│ │ └── com
│ │ └── alibaba
│ │ └── repeater
│ │ └── console
│ │ └── dal
│ │ ├── mapper // mybatis 的 mapper 映射类,存放数据库操作犯法
│ │ └── model // mybatis 的 model 类,和数据库表结构对应
│ └── resources
│ └── database.sql // 数据库初始化语句
├── repeater-console-service // 主要逻辑实现的模块,service 层
│ ├── pom.xml
│ └── src
│ └── main
│ └── java
│ └── com
│ └── alibaba
│ └── repeater
│ └── console
│ └── service
│ ├── RecordService.java // 存储服务,提供存储录制、存储回放、获取记录、执行回放、查看回放结果接口的定义
│ ├── RegressService.java // 回归服务,提供获取单个回放、多个回放、找到你的小伙伴、slogan喊口号4个接口的定义(最后两个接口不知道是什么鬼。。。)
│ ├── impl
│ │ ├── AbstractRecordService.java // 存储服务一个抽象实现,提供了 repeat 方法和 jvm-sandbox-repeater 进行交互,触发回放
│ │ ├── RecordServiceLocalImpl.java // 存储服务的本地存储实现。使用一个 ConcurrentHashMap 把所有数据存到内存中。
│ │ ├── RecordServiceMysqlImpl.java // 存储服务 mysql 存储的实现。使用前面存储模块和 mysql 数据库交互,进行存储。
│ │ ├── RecordServiceProxyImpl.java // 存储服务的代理类,根据配置文件值来决定用哪个实现类进行存储服务的实现
│ │ └── RegressServiceImpl.java // 回归服务的实现类。包含了官方提供的 slogan 服务的实现。
│ └── util
│ └── ConvertUtil.java // 给原始录制记录加上一些元数据(如 appName,environment 等),并转换成一个完整的录制记录的工具类。转换方法目前各个存储服务用的都是 hessian 序列化。
├── repeater-console-start // 最外部的层,controller 层。直接暴露接口和提供 main 入口。我们最前面 slogan 示例看到的 repeater-bootstrap.jar 包,实际就是用这里源码打出来的包。
│ ├── pom.xml
│ └── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── alibaba
│ │ │ └── repeater
│ │ │ └── console
│ │ │ └── start
│ │ │ ├── Application.java // 标准的 spring boot 启动类
│ │ │ ├── ConfigurationBean.java // java 回放用的感知 spring context 的 hook
│ │ │ └── controller
│ │ │ ├── ConfigFacadeApi.java // 配置管理服务 api 设计的示例。仅提供了获取配置的方法,而且直接 hard code 了一份配置。
│ │ │ ├── RecordFacadeApi.java // 存储服务 api 设计的示例,提供了存储录制、存储回放、获取记录、执行回放、查看回放结果五个 api 接口
│ │ │ └── RegressController.java // 回归服务,相当于一个示例的被测服务。官方的 slogan 例子用的就是这里的接口。
│ │ └── resources
│ │ └── application.properties // 配置文件。需要留意的是,里面有个 `repeat.repeat.url` 配置项,需要和 sandbox 的监听 port 保持一致。
│ └── test
│ └── java
│ └── com
│ └── alibaba
│ └── repeater
│ └── console
│ └── start
│ └── RegressTest.java // 一个自动化集成测试用例,如果在 idea 里面跑的话,需要先手动启动 console 服务才能运行,且测试了下,全部用例都是 fail 的。先忽略。
简单小结:
1、console 划分为了4个子模块,除了一个是公共模块外,剩余三个分别是数据层、service逻辑层和最外部的 controller 层,基本是一个标准 spring boot 程序。
2、里面主要提供了3个服务:存储服务,配置管理服务,回归服务(本质上就是个示例,估计是给自动化测试用的)
3、需要重点关注的是存储服务,里面包含了存储录制、存储回放、获取记录、执行回放、查看回放结果五个 api 接口。
step 3 细读目标功能
从上一步已经明确了,目标功能是存储服务。因此进一步细看对应的代码。主要关注存储服务的实现。为了简便理解,主要针对 local 这个本地存储的实现进行解读。
里面涉及几个 model 定义,为了方便理解,先说明下:
-
RecordWrapper
: repeater 提供的一个完整的录制记录。包括appName、环境名、主机名、traceId、入口描述、入口调用记录、子调用记录。 -
RepeatModel
: repeater 提供的一个回放结果记录。包括 repeatId、是否完成、实际返回值、原始返回值、diff记录、耗时、traceId。 -
Record
: console 提供的单个录制记录的描述,包括创建时间、录制时间、appName、主机名、traceId、原始录制记录。用途估计是后续用来过滤筛选记录。
下面的解读主要涉及上述3个类,更详细的领域模型划分,建议参考 domain
- 添加录制的记录
@Override
public RepeaterResult<String> saveRecord(String body) {
try {
// 把输入值反序列化成 RecordWrapper 对象
RecordWrapper wrapper = SerializerWrapper.hessianDeserialize(body, RecordWrapper.class);
// 如果反序列化失败,直接返回错误
if (wrapper == null || StringUtils.isEmpty(wrapper.getAppName())) {
return RepeaterResult.builder().success(false).message("invalid request").build();
}
// 把 wrapper + 原始传入的 body ,组合成 record 。主要是添加了一个创建日期、大部分 wrapper 和 record 一一对应地存储,以及把整个 body 放到 wrapperRecord 对象中作为存档
Record record = ConvertUtil.convertWrapper(wrapper, body);
// 存到record的缓存里,key 是 appName + traceId 组合而成,value 就是 record 对象
recordCache.put(buildUniqueKey(wrapper.getAppName(), wrapper.getTraceId()), record);
// 保存成功,就可以返回了
return RepeaterResult.builder().success(true).message("operate success").data("-/-").build();
} catch (Throwable throwable) {
return RepeaterResult.builder().success(false).message(throwable.getMessage()).build();
}
}
- 添加回放的结果
@Override
public RepeaterResult<String> saveRepeat(String body) {
try {
// 相同的套路,先反序列化出 RepeatModel 对象
RepeatModel rm = SerializerWrapper.hessianDeserialize(body, RepeatModel.class);
// 从缓存中根据 repeatId 获取到录制的记录。特别留意,虽然 value 类型一样,但 record 和 repeat 是两个分别独立的缓存,所以这里的调整是不会影响上面 record 的调整的。
Record record = repeatCache.remove(rm.getRepeatId());
// 如果找不到记录,那就认为无效(repeatCached的记录添加,在执行回放的接口里会进行。所以如果找不到记录,说明这次回放的执行不是通过这个服务进行的,所以也没必要记录它的回放结果)
if (record == null) {
return RepeaterResult.builder().success(false).message("invalid repeatId:" + rm.getRepeatId()).build();
}
// 校验确认这个回放是通过这个服务执行后,取出原始的回放记录,并转成 RecordWrapper 对象,便于获取更多信息
RecordWrapper wrapper = SerializerWrapper.hessianDeserialize(record.getWrapperRecord(), RecordWrapper.class);
// 添加原始 response 信息
rm.setOriginResponse(SerializerWrapper.hessianDeserialize(wrapper.getEntranceInvocation().getResponseSerialized()));
// 把 repeatModel 记录到缓存
repeatModelCache.put(rm.getRepeatId(), rm);
} catch (Throwable throwable) {
return RepeaterResult.builder().success(false).message(throwable.getMessage()).build();
}
return RepeaterResult.builder().success(true).message("operate success").data("-/-").build();
}
- 根据应用名和 traceId ,获取序列化后的录制数据
@Override
public RepeaterResult<String> get(String appName, String traceId) {
// 从缓存中找数据,找不到就返回失败
Record record = recordCache.get(buildUniqueKey(appName, traceId));
if (record == null) {
return RepeaterResult.builder().success(false).message("data not exits").build();
}
// 返回成功,数据为 wrapperRecord ,即序列化后的数据
return RepeaterResult.builder().success(true).message("operate success").data(record.getWrapperRecord()).build();
}
- 根据 appName、traceId、repeatId 执行回放记录
@Override
public RepeaterResult<String> repeat(String appName, String traceId, String repeatId) {
// 从录制记录里获取录制信息,如果找不到,返回失败
final Record record = recordCache.get(buildUniqueKey(appName, traceId));
if (record == null) {
return RepeaterResult.builder().success(false).message("data does not exist").build();
}
// 执行回放
RepeaterResult<String> pr = repeat(record, repeatId);
// 如果成功,以执行结果的 data 字段(成功时是 repeatId)为 key ,录制记录为 value ,记录到 repeatCache 中
if (pr.isSuccess()) {
repeatCache.put(pr.getData(), record);
}
return pr;
}
- 根据 repeatId 获取回放执行结果
@Override
public RepeaterResult<RepeatModel> callback(String repeatId) {
// 因为保存回放记录时会移除 repeatCache 里的记录。如果发现里面没被移除,说明回放未结束,返回还在进行中
if (repeatCache.containsKey(repeatId)) {
return RepeaterResult.builder().success(true).message("operate is going on").build();
}
// 从 repeatModelCache 获取到完整的回放结果记录
RepeatModel rm = repeatModelCache.get(repeatId);
// 如果取不到,返回错误
if (rm == null) {
return RepeaterResult.builder().success(false).message("invalid repeatId:" + repeatId).build();
}
// 返回完整的回放结果记录
return RepeaterResult.builder().success(true).message("operate success").data(rm).build();
}
小结:
- 从接口上看,调用顺序必须是 saveRecord -> repeat -> saveRepeat -> callback 。如果不对会导致后续接口调用失败。
- 通过一个 repeatCached 的中间缓存,巧妙解决了回放还在进行中,查找回放结果时需要返回进行中这个场景。
- 正常情况下 console 存储服务主要关注的是 Record 对象,缓存主要用的也是它。完整录制记录,由 RecordWrapper 负责。完整的回放结果记录,由 RepeatModel 负责。
实际使用
step 0 调整模式重新启动
上面分析了整个 console 服务的使用,主要提供的是存储服务、配置获取服务。很遗憾,里面并没有提供批量回放的接口,后续需要另行开发。
但上面终究只是从源码的推测,不实际跑下怎么知道是不是真的是这样呢?
根据官方的 用户使用手册 只需要把 ~/.sandbox-module/cfg/repeater.properties
里面的 repeat.standalone.mode
的值,从 true 改为 false 即可改为用 console 进行存储和配置获取。
同时,console 的一些配置项也要对应调整下,否则端口号和 repeater 的对不上,repeater-config 不正确,也会出问题。
具体步骤:
1、杀掉原来的进程,关闭应用
2、修改 ~/.sandbox-module/cfg/repeater.properties
的值,repeat.standalone.mode
改为 false
3、修改源码目录中的 jvm-sandbox-repeater/repeater-console/repeater-console-start/src/main/resources/application.properties
,把 repeat.repeat.url
中的 8820
端口号,改为 12580
4、修改 jvm-sandbox-repeater/repeater-console/repeater-console-start/src/main/java/com/alibaba/repeater/console/start/controller/ConfigFacadeApi.java
,内容基本参照以前的配置,但需要微调下,把 javaEntranceBehaviors 的加上(否则 request 就会没录制下来),去掉 javaSubInvokeBehaviors (response 不要用 Mock ,用真实的)。修改后内容如下:
@RequestMapping("/config/{appName}/{env}")
public RepeaterResult<RepeaterConfig> getConfig(@PathVariable("appName") String appName,
@PathVariable("env") String env) {
// 改为了可以适用于 gs-rest-service 的配置
RepeaterConfig config = new RepeaterConfig();
List<Behavior> behaviors = Lists.newArrayList();
config.setPluginIdentities(Lists.newArrayList("http", "java-entrance", "java-subInvoke", "mybatis", "ibatis"));
// 回放器
config.setRepeatIdentities(Lists.newArrayList("java", "http"));
// 白名单列表
config.setHttpEntrancePatterns(Lists.newArrayList("^/greeting.*$"));
// java入口方法
behaviors.add(new Behavior("hello.GreetingController", "greeting"));
config.setJavaEntranceBehaviors(behaviors);
List<Behavior> subBehaviors = Lists.newArrayList();
// java调用插件
config.setJavaSubInvokeBehaviors(subBehaviors);
config.setUseTtl(true);
return RepeaterResult.builder().success(true).message("operate success").data(config).build();
}
5、启动 repeater-console
# 进入源码根目录,再执行以下命令(跳过测试的原因是,这个不是单测,必须用指定脚本运行才行)
$ cd repeater-console
$ mvn install -DskipTests && java -jar repeater-console-start/target/*.jar
...
2019-07-16 17:26:53.028 INFO 30001 --- [ main] c.a.repeater.console.start.Application : Started Application in 7.092 seconds (JVM running for 7.774)
6、启动应用、启动 sandbox 并 attach 到进程中。务必记住,后续每次重启,sandbox 的 attach 必须放在 console 之后,因为它只有启动时会去获取一次配置。
# 启动被录制的那个应用。如果不知道这个文件夹哪里来,请查看系列文章第一篇的内容,搜索下一行用到的命令。
cd complete
mvn install && java -jar target/*.jar
# sandbox attach
sh ~/sandbox/bin/sandbox.sh -p `ps -ef | grep "target/gs-rest-service-0.1.0.jar" | grep -v grep | awk '{print $2}'` -P 12580
如果启动正常,且确实改为了从 console 中读取配置,~/logs/sandbox/repeater/repeater.log
会出现如下日志:
2019-07-16 17:32:06 INFO module on loaded,id=repeater,version=1.0.0,mode=ATTACH
2019-07-16 17:32:06 INFO onActive
2019-07-16 17:32:07 INFO pull repeater config success,config=com.alibaba.jvm.sandbox.repeater.plugin.domain.RepeaterConfig@c1faf51
2019-07-16 17:32:07 INFO enable plugin http success
2019-07-16 17:32:08 INFO add watcher success,type=http,watcherId=1000
2019-07-16 17:32:08 INFO enable plugin ibatis success
2019-07-16 17:32:08 INFO add watcher success,type=ibatis,watcherId=1003
2019-07-16 17:32:08 INFO enable plugin java-entrance success
2019-07-16 17:32:09 INFO add watcher success,type=java,watcherId=1005
2019-07-16 17:32:09 INFO enable plugin java-subInvoke success
2019-07-16 17:32:09 INFO watch plugin occurred error
com.alibaba.jvm.sandbox.repeater.plugin.exception.PluginLifeCycleException: enhance models is empty, plugin type is java-subInvoke
at com.alibaba.jvm.sandbox.repeater.plugin.core.impl.AbstractInvokePluginAdapter.watchIfNecessary(AbstractInvokePluginAdapter.java:117)
at com.alibaba.jvm.sandbox.repeater.plugin.core.impl.AbstractInvokePluginAdapter.watch(AbstractInvokePluginAdapter.java:62)
at com.alibaba.jvm.sandbox.repeater.module.RepeaterModule.initialize(RepeaterModule.java:186)
at com.alibaba.jvm.sandbox.repeater.module.RepeaterModule.access$500(RepeaterModule.java:60)
at com.alibaba.jvm.sandbox.repeater.module.RepeaterModule$1.run(RepeaterModule.java:132)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
2019-07-16 17:32:09 INFO enable plugin mybatis success
2019-07-16 17:32:09 INFO add watcher success,type=mybatis,watcherId=1007
2019-07-16 17:32:09 INFO register event bus success in repeat-register
repeater-console 服务的命令行也会输出:
2019-07-16 17:29:01.016 INFO 30001 --- [nio-8001-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization started
2019-07-16 17:29:01.054 INFO 30001 --- [nio-8001-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization completed in 38 ms
step 1 录制回放
启动完毕后,就可以开始测试录制和回放了。在此之前,请阅读下一章节的坑1,按指引修改源码并更新本地安装,否则很有可能会被这个坑卡住:
# 手动发出2条请求
$ curl -s 'http://localhost:8080/greeting'
{"id":1,"content":"Hello, World!"}%
$ curl -s 'http://localhost:8080/greeting?name=User'
{"id":2,"content":"Hello, User!"}%
# 查看对应的 traceId
$ tail -2 ~/logs/sandbox/repeater/repeater.log
2019-07-16 17:34:04 INFO broadcast success,traceId=192168015059156326964361610001ed,resp=HttpUtil.Resp(code=200, body={"success":true,"data":"-/-","message":"operate success"}, message=null)
2019-07-16 17:34:34 INFO broadcast success,traceId=192168015059156326967477410002ed,resp=HttpUtil.Resp(code=200, body={"success":true,"data":"-/-","message":"operate success"}, message=null)
# 调用 console 接口,触发回放
$ curl -s 'http://127.0.0.1:8001/facade/api/repeat/unknown/192168015059156326967477410002ed'
{"success":true,"data":"192168015059156328015008810009ed","message":"operate success"}%
# 同时查看被测应用日志,确实有收到请求
# 调用 console 接口,查看回放结果。==请特别留意,此处用的 id 是上一步服务端返回 body 里面 data 字段的值,而非原来录制请求时的 traceId 。原因是查询回放结果时,一个录制可能对应多个回放结果,所以需要用回放结果的 id 进行查询==
$ curl -s 'http://127.0.0.1:8001/facade/api/repeat/callback/192168015059156328015008810009ed'
{"success":true,"data":{"repeatId":"192168015059156328015008810009ed","finish":true,"response":"{\"id\":3,\"content\":\"Hello, User!\"}","originResponse":"{\"id\":2,\"content\":\"Hello, User!\"}","diff":null,"cost":13,"traceId":"192168015059156328015025810003ed","mockInvocations":null},"message":"operate success"}%
至此,基于 console 进行录制回放,也完成了。虽然没达到批量回放的目的,但总算把整体流程跑通了。
踩坑
坑1:请求 facade/api/repeat/
接口进行回放时,接口返回 success ,但被测应用日志显示没有收到任何新的请求。
解决:把 com.alibaba.jvm.sandbox.repeater.plugin.core.bridge.RepeaterBridge
里面的 HashMap key ,从 InvokeType 改为 String ,put 和 get 方法也对应调整,即可解决。
参考 diff:
diff --git a/repeater-plugin-core/src/main/java/com/alibaba/jvm/sandbox/repeater/plugin/core/bridge/RepeaterBridge.java b/repeater-plugin-core/src/main/java/com/alibaba/jvm/sandbox/repeater/plugin/core/bridge/RepeaterBridge.java
index 26c0218..6f2102d 100644
--- a/repeater-plugin-core/src/main/java/com/alibaba/jvm/sandbox/repeater/plugin/core/bridge/RepeaterBridge.java
+++ b/repeater-plugin-core/src/main/java/com/alibaba/jvm/sandbox/repeater/plugin/core/bridge/RepeaterBridge.java
@@ -17,7 +17,7 @@ public class RepeaterBridge {
private RepeaterBridge() {}
- private volatile Map<InvokeType, Repeater> cached = new HashMap<InvokeType, Repeater>();
+ private volatile Map<String, Repeater> cached = new HashMap<String, Repeater>();
public static RepeaterBridge instance() {
return RepeaterBridge.LazyInstanceHolder.INSTANCE;
@@ -28,7 +28,7 @@ public class RepeaterBridge {
// reset repeat'er container
cached.clear();
for (Repeater repeater : rs) {
- cached.put(repeater.getType(), repeater);
+ cached.put(repeater.getType().name(), repeater);
}
}
@@ -43,6 +43,6 @@ public class RepeaterBridge {
* @return 回放器
*/
public Repeater select(InvokeType type) {
- return cached.get(type);
+ return cached.get(type.name());
}
}
修改后,请务必执行源码目录的 bin/install-local.sh
把源码更新安装到本地,并重新调整配置项。
问题定位过程:
1、查看 ~/logs/sandbox/repeater/repeater.log
,看到如下错误:
2019-07-16 17:44:23 INFO subscribe success params={_data=QzA5Y29tLmFsaWJhYmEuanZtLnNhbmRib3gucmVwZWF0ZXIucGx1Z2luLmRvbWFpbi5SZXBlYXRNZXRhmAdhcHBOYW1lB3RyYWNlSWQEbW9jawhyZXBlYXRJZA9tYXRjaFBlcmNlbnRhZ2UKZGF0YXNvdXJjZQxzdHJhdGVneVR5cGUJZXh0ZW5zaW9uYAd1bmtub3duMCAxOTIxNjgwMTUwNTkxNTYzMjcwMjI3NDQ4MTAwMDFlZFQwIDE5MjE2ODAxNTA1OTE1NjMyNzAyNjI3OTkxMDAwMWVkXWROQzBFY29tLmFsaWJhYmEuanZtLnNhbmRib3gucmVwZWF0ZXIucGx1Z2luLnNwaS5Nb2NrU3RyYXRlZ3kkU3RyYXRlZ3lUeXBlkQRuYW1lYQ9QQVJBTUVURVJfTUFUQ0hIWg==}
2019-07-16 17:44:23 ERROR [Error-0000]-uncaught exception occurred when register repeat event, req={_data=QzA5Y29tLmFsaWJhYmEuanZtLnNhbmRib3gucmVwZWF0ZXIucGx1Z2luLmRvbWFpbi5SZXBlYXRNZXRhmAdhcHBOYW1lB3RyYWNlSWQEbW9jawhyZXBlYXRJZA9tYXRjaFBlcmNlbnRhZ2UKZGF0YXNvdXJjZQxzdHJhdGVneVR5cGUJZXh0ZW5zaW9uYAd1bmtub3duMCAxOTIxNjgwMTUwNTkxNTYzMjcwMjI3NDQ4MTAwMDFlZFQwIDE5MjE2ODAxNTA1OTE1NjMyNzAyNjI3OTkxMDAwMWVkXWROQzBFY29tLmFsaWJhYmEuanZtLnNhbmRib3gucmVwZWF0ZXIucGx1Z2luLnNwaS5Nb2NrU3RyYXRlZ3kkU3RyYXRlZ3lUeXBlkQRuYW1lYQ9QQVJBTUVURVJfTUFUQ0hIWg==}
com.alibaba.jvm.sandbox.repeater.plugin.exception.RepeatException: no valid repeat found for invoke type:com.alibaba.jvm.sandbox.repeater.plugin.domain.InvokeType$1@7cefc025
at com.alibaba.jvm.sandbox.repeater.plugin.core.impl.api.DefaultFlowDispatcher.dispatch(DefaultFlowDispatcher.java:38)
at com.alibaba.jvm.sandbox.repeater.plugin.core.impl.spi.RepeatSubscribeSupporter.onSubscribe(RepeatSubscribeSupporter.java:59)
at com.alibaba.jvm.sandbox.repeater.plugin.core.impl.spi.RepeatSubscribeSupporter.onSubscribe(RepeatSubscribeSupporter.java:26)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.google.common.eventbus.Subscriber.invokeSubscriberMethod(Subscriber.java:95)
at com.google.common.eventbus.Subscriber$1.run(Subscriber.java:80)
at com.alibaba.ttl.TtlRunnable.run(TtlRunnable.java:51)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
在代码中查了下,错误日志对应的关键代码如下:
@Override
public void dispatch(RepeatMeta meta, RecordModel recordModel) throws RepeatException {
if (recordModel == null || recordModel.getEntranceInvocation() == null || recordModel.getEntranceInvocation().getType() == null) {
throw new RepeatException("invalid request, record or root invocation is null");
}
// 从 RepeaterBridge 中寻找对应的 type 记录
Repeater repeater = RepeaterBridge.instance().select(recordModel.getEntranceInvocation().getType());
if (repeater == null) {
throw new RepeatException("no valid repeat found for invoke type:" + recordModel.getEntranceInvocation().getType().name());
}
RepeatContext context = new RepeatContext(meta, recordModel, TraceGenerator.generate());
// 放置到回放缓存中
RepeatCache.putRepeatContext(context);
repeater.repeat(context);
}
看起来是从 RepeaterBridge 中找不到对应的 type 记录。再细看下它具体是怎么找的:
public class RepeaterBridge {
private RepeaterBridge() {}
private volatile Map<InvokeType, Repeater> cached = new HashMap<InvokeType, Repeater>();
public static RepeaterBridge instance() {
return RepeaterBridge.LazyInstanceHolder.INSTANCE;
}
public void build(List<Repeater> rs) {
if (rs == null || rs.isEmpty()) { return; }
// reset repeat'er container
cached.clear();
for (Repeater repeater : rs) {
cached.put(repeater.getType(), repeater);
}
}
private final static class LazyInstanceHolder {
private final static RepeaterBridge INSTANCE = new RepeaterBridge();
}
/**
* 选择合适的回放器
*
* @param type 调用类型
* @return 回放器
*/
public Repeater select(InvokeType type) {
return cached.get(type);
}
}
恩,内部有一个 cached ,以 InvokeType 为 key ,repeater 为 value 缓存数据。那为何会对不上呢?各个有可能的地方都加些日志看看:
public class RepeaterBridge {
protected final static Logger log = LoggerFactory.getLogger(RepeaterBridge.class);
private RepeaterBridge() {}
private volatile Map<InvokeType, Repeater> cached = new HashMap<InvokeType, Repeater>();
public static RepeaterBridge instance() {
return RepeaterBridge.LazyInstanceHolder.INSTANCE;
}
public void build(List<Repeater> rs) {
log.info("进入 RepeaterBridge 的 build 方法,参数为: {}", JSONObject.toJSONString(rs));
if (rs == null || rs.isEmpty()) {
log.info("rs 为 null 或空,直接返回");
return;
}
// reset repeat'er container
log.info("清空缓存");
cached.clear();
for (Repeater repeater : rs) {
log.info("往 cached 中添加记录: {}", JSONObject.toJSONString(rs));
log.info("记录的 type 为:{}", repeater.getType().name());
cached.put(repeater.getType(), repeater);
}
log.info("完成 build 所有处理");
}
private final static class LazyInstanceHolder {
private final static RepeaterBridge INSTANCE = new RepeaterBridge();
}
/**
* 选择合适的回放器
*
* @param type 调用类型
* @return 回放器
*/
public Repeater select(InvokeType type) {
log.info("传入 select 的 type name: {}", type.name());
log.info("RepeaterBridge 中的所有 cached: {}", JSONObject.toJSONString(cached));
for (InvokeType invokeType : cached.keySet()) {
log.info("cached 中具有的 type name: {}", invokeType.name());
log.info("cached 中的 type {} 和传入的 type {} 作 equals 的结果: {}", invokeType, type, invokeType.equals(type));
}
return cached.get(type);
}
}
调整后,通过源码目录的 bin/install-local.sh
把源码更新安装到本地,并重新调整配置项、重启被测应用、sandbox后,查看日志,发现一个非常诡异的结果:
2019-07-16 19:53:36 INFO 传入 select 的 type: http
2019-07-16 19:53:36 INFO RepeaterBridge 中的所有 cached: {{}:{"type":{"$ref":"$.null"}},{}:{"type":{"$ref":"$.null"}},{}:{"type":{"$ref":"$.null"}}}
2019-07-16 19:53:36 INFO cached 中具有的 type: dubbo
2019-07-16 19:53:36 INFO cached 中的 type com.alibaba.jvm.sandbox.repeater.plugin.domain.InvokeType$6@7c418ee0 和传入的 type com.alibaba.jvm.sandbox.repeater.plugin.domain.InvokeType$1@f23bf1 作 equals 的结果: false
2019-07-16 19:53:36 INFO cached 中具有的 type: http
2019-07-16 19:53:36 INFO cached 中的 type com.alibaba.jvm.sandbox.repeater.plugin.domain.InvokeType$1@1469dba0 和传入的 type com.alibaba.jvm.sandbox.repeater.plugin.domain.InvokeType$1@f23bf1 作 equals 的结果: false
2019-07-16 19:53:36 INFO cached 中具有的 type: java
2019-07-16 19:53:36 INFO cached 中的 type com.alibaba.jvm.sandbox.repeater.plugin.domain.InvokeType$2@3d4fcd8 和传入的 type com.alibaba.jvm.sandbox.repeater.plugin.domain.InvokeType$1@f23bf1 作 equals 的结果: false
2019-07-16 19:53:36 ERROR [Error-0000]-uncaught exception occurred when register repeat event, req={_data=QzA5Y29tLmFsaWJhYmEuanZtLnNhbmRib3gucmVwZWF0ZXIucGx1Z2luLmRvbWFpbi5SZXBlYXRNZXRhmAdhcHBOYW1lB3RyYWNlSWQEbW9jawhyZXBlYXRJZA9tYXRjaFBlcmNlbnRhZ2UKZGF0YXNvdXJjZQxzdHJhdGVneVR5cGUJZXh0ZW5zaW9uYAd1bmtub3duMCAxOTIxNjgwMTUwNTkxNTYzMjc3MTA4MjUxMTAwMDFlZFQwIDE5MjE2ODAxNTA1OTE1NjMyNzgwMTYwNDExMDAwNWVkXWROQzBFY29tLmFsaWJhYmEuanZtLnNhbmRib3gucmVwZWF0ZXIucGx1Z2luLnNwaS5Nb2NrU3RyYXRlZ3kkU3RyYXRlZ3lUeXBlkQRuYW1lYQ9QQVJBTUVURVJfTUFUQ0hIWg==}
当 cached 中具有的 type: http
这个情况下时,传入的值和缓存的值内存地址不一样,且 equals 的结果也是 false ,所以导致了虽然 name 一样,但缓存查找 key 的时候匹配不上。
本地测试了下,不管怎么初始化,都不会出现内存地址不一样的情况。因此具体原因还在细究中,估计是一些 java 的暗坑。
总结
- repeater 底层本身就已经提供了接口对接配套的存储服务、配置获取服务,可以直接复用。
- repeater 负责录制和存储原始录制数据,配合 console 控制录制和回放,可以实现强大的功能满足各类需要,且各个模块职责清晰。这个设计值得学习。
- repeater 内部实现包含很多和录制回放息息相关的信息,后续还需要继续了解 repeater-plugin ,下一步的目标是写出一个 rabbitmq 的 plugin (刚好项目也需要用到)
- 批量回放,还得自行实现。这套录制回放要在项目中用上还有很多东西要做。
本文首发于TesterHome社区,点此链接可查看原文并与作者直接交流。
今日份的知识已摄入~
想了解更多前沿测试开发技术:欢迎关注「第十届MTSC大会·上海」>>>
1个主会场+11大专场,大咖云集精英齐聚