通用技术 通用流量录制回放工具 jvm-sandbox-repeater 尝鲜 (二)——repeater-console 使用 (已完成)

陈恒捷 for PPmoney · 2019年07月12日 · 最后由 孙武牧羊 回复于 2020年10月20日 · 13833 次阅读
本帖已被设为精华帖!

为了避免文章过长,此文章单独记录 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 拉取和消息投递的也可以自己实现BroadcasterConfigManager。稍后我们会公布一份录制回放所需的完整架构图以及 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(刚好项目也需要用到)
  • 批量回放,还得自行实现。这套录制回放要在项目中用上还有很多东西要做。
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 34 条回复 时间 点赞
026 将本帖设为了精华贴 07月12日 11:11

给恒捷点赞。

刘晓光 回复

哇哇,受宠若惊,谢谢点赞。

陈恒捷 回复

正在琢磨大规模搞起。

仅楼主可见
梦梦GO 回复

Failed to execute goal on project repeater-console-service: Could not resolve dependencies for project com.alibaba.jvm.sandbox:repeater-console-service:jar:1.0.0-SNAPSHOT: Could not find artifact com.alibaba.jvm.sandbox:repeater-plugin-core:jar:1.0.0-SNAPSHOT -> [Help 1]

缺少依赖,估计是因为你之前没 build 过 repeater 的其它 module 。
可以先 cd 到 repeater 项目根目录,执行 mvn install ,再按文中步骤到 repeater-console 执行这个命令试试?

陈恒捷 回复

谢谢楼主,退了一级目录执行 mvn install -DskipTests, 后再进到 repeater-console 去执行就成功启动了。特别感谢,期望紧跟楼主步伐 哈哈

1.在 Linux 上使用 git 下载 repeater 的源码
git clone git://github.com/alibaba/jvm-sandbox-repeater.git
2.在源码上修改 properties 文件
vim repeater-console/repeater-console-start/src/main/resources/application.properties

3.在 linux 的 repeater 的源码上修改 ConfigFacadeApi.java 文件
vim repeater-console/repeater-console-start/src/main/java/com/alibaba/repeater/console/start/controller/ConfigFacadeApi.java

4.修改 linux 上的 peteater 的源码的,RepeaterBridge.java 文件
vim repeater-plugin-core/src/main/java/com/alibaba/jvm/sandbox/repeater/plugin/core/bridge/RepeaterBridge.java

5.修改在 linux 上使用 下载安装的 sandbox 的配置文件
curl -s http://sandbox-ecological.oss-cn-hangzhou.aliyuncs.com/install-repeater.sh | sh
vim ~/.sandbox-module/cfg/repeater.properties

6.进入在 Linux 上源码的 bin 文件夹,执行./install-local.sh
cd /root/shifeng/git/jvm-sandbox-repeater/bin
./install-local.sh

7.进入 repetaer 的跟目录,进行清空,编译,打包
mvn clean

mvn compile -DskipTests

mvn install -DskipTests

8.进入 repeater-console 执行清空,编译,打包
mvn clean

mvn compile -DskipTests

mvn install -DskipTests

9.修改 sandbox 的配置文件开启 console
vim ~/.sandbox-module/cfg/repeater.properties

10.启动刚刚打好 jar 包的 repeater-console
java -jar repeater-console-start/target/*.jar

11.启动要录制的项目
java -jar gs-rest-service-0.1.0.jar

12.将启动项目的进程,绑定到指定的 12580 端口

查看 repeater-console 项目日志

查看 sandbox 日志

  1. 进行录制的请求访问 curl -s 'http://localhost:8080/greeting' 录制项目正确打印,返回了结果 但是 repeater-console 和 sandbox 并没有进行录制

您好,看了您的文章收获了很多,非常感谢!还有个问题想请教,mock 回放是不是只和 repeat-config 的配置相关,与回放的方式无关,即采用 repeat-console 的方式也能够进行 mock 的回放?

是的。

这篇文章说的就是 repeater-console 这种方式如何进行 mock 和回放。

13楼 已删除
14楼 已删除

问下,case 筛选是咋做的?录制下来后,肯定会有重复的请求

Wang 回复

这个自己二次开发 console 。
而且说实话,重复的请求如果不是非常多,不会额外造成太多的执行耗时,其实重复又何妨?

curl 窗口报错:

repeater 日志(只有第一次发送那一条):

求教是什么问题啊?

qiugang250 回复

把你的完整步骤(包括配置项怎么改、用的什么操作系统等)发下?信息太少,无法定位问题原因。

陈恒捷 回复

非常感谢如此迅速的回复,问题配置和步骤如下(这次问题和坑中的问题表现一致,之前截图问题未再次出现):
1、配置:
1)用的 vmware 虚拟机

2)操作系统版本:

3)脱机模式关闭

4)修改端口:

5)修改配置:

6)修改坑中描述类型:

完成以上配置后,执行步骤
2、操作步骤:
1) cd /opt/git/gs-rest-service/complete && mvn install && java -jar target/.jar

2) cd /opt/git/jvm-sandbox-repeater/repeater-console && mvn install -DskipTests && java -jar repeater-console-start/target/
.jar

3) 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

4) 录制和回放
curl -s 'http://127.0.0.1:8001/facade/api/repeat/unknown/010000002015157182403926310001ed'报成功,服务端也收到请求并响应;
curl -s 'http://127.0.0.1:8001/facade/api/repeat/callback/010000002015157182403926310001ed'报错
curl -s 'http://127.0.0.1:8001/facade/api/repeat/unknownrepeater 日志中也中间打印了一段,看起来貌似是成功的,但是怎么又出现了一条 TraceId 不同的请求?

qiugang250 回复

从你前面配置步骤,没看出太大问题。但你的截图里总是有些像乱码一样的奇怪字符,不知道是不是你的系统默认字符集不大对。

另外,有个很奇怪的点,你倒数第二个截图里面,repeat 请求返回的 data 值,没见到具体值是啥,展示不完整。

然后最后的 callback 里用的 id 是必须用 repeat 返回的 data 里面带有的 id 的,因为它查询的是回放结果,直接用和请求 repeat 一样的 id 是不正确的。我刚刚也微调了下正文里的内容,强调一下这个点。

陈恒捷 回复

非常感谢,确实是没注意,以为都用发送的那个 traceId,现在已经通了。

我们是想把线上流量引到线下环境使用,并比较两次返回区别,所以图示中 diff 字段非常关键,但是开源代码并有实现(可以自己实现,一些字段可能得加白名单不比对,此外还涉及一些脱敏字段部分);另外,我们如果要应用,必须要实现以上接口的自动发送和存储,非常期待大神有时间的时候出一下以上两块的文章,大赞!!!

仅楼主可见
zbmc 回复

repeater-console 里没有对应方法,要用 repeater 里面序列化库的反序列化方法。

qiugang250 回复

这块其实是工程化的部分了,并不复杂,所以暂时没有计划专门写文章分享这部分。建议可以自己探索学习下。

repeater 是一个底层组件,还是需要不少探索改造才能在项目上实际使用的,建议要做好自己要额外开发一些配套东西的准备。

shifeng5131425 回复

我也遇到相同的问题,请问现在解决了吗?

后面须要有一个数据中心负责采集数据的加工、存储、搜索 -- 请教大神,请问录制的流量是以序列化的方式存储的吗?序列化存储的数据可以直接加工吗?

simple [精彩盘点] TesterHome 社区 2019 年 度精华帖 中提及了此贴 12月24日 23:00
simple [精彩盘点] TesterHome 社区 2019 年 度精华帖 中提及了此贴 12月24日 23:00
折耳猫 回复

是的,用的是 repeater 自带的 hessian 做序列化,具体算法没有去细研究,但从序列化后的文本看应该是做了一些转换的。

序列化后的数据建议还是反序列化后重新序列化成类似 json 之类的格式再加工,避免直接加工出问题导致数据损坏。

陈恒捷 回复

序列化后的数据建议还是反序列化后重新序列化成类似 json 之类的格式再加工,避免直接加工出问题导致数据损坏。
-- 序列化成类似 json 格式加工后的数据能用 repeater 回放成功吗?是不是还需要再转换一次?

折耳猫 回复

应该是要转换。

repeater 内部正常只能使用 hessian 序列化的数据,当然你也可以通过二次开发让它使用其它的序列化工具。

我是 jvm-sandbox-repeater-master.zip 进行解压,然后 mvn install ,再做以下操作

1、源码文件 repeater-console/repeater-console-start/src/main/resources/application.properties 中 repeat.repeat.url=http://127.0.0.1:12580/sandbox/default/module/http/repeater/repeat 端口号改成 12580
2、源码文件中 ConfigFacadeApi 类,以下两个地方改成如下
// 白名单列表
config.setHttpEntrancePatterns(Lists.newArrayList("/crmredis/.*$"));
// java 入口方法
behaviors.add(new Behavior("yxitest.controllers.CrmRedisController", "getVa"));

3、 ~/.sandbox-module/cfg 下的 repeater.properties 文件中的字段改成 false,如下
是否开启脱机工作模式
repeat.standalone.mode=false

4、启动 repeater-console 服务 sandbox-repeater/jvm-sandbox-repeater-master/repeater-console 下的 java -jar repeater-console-start/target/*.jar

5、~/sandbox/bin 下,使用 sh sandbox.sh -p 5119 -P 12580 命令

说明:5119 为自己的服务进程

现在出现一个报错,不太理解是什么原因,求教

现在出现一个报错,不太理解是什么原因,求教

2020-02-05 22:41:00 ERROR Error occurred serialize
com.alibaba.jvm.sandbox.repeater.plugin.core.serialize.SerializeException: [Error-1001]-hessian-serialize-error
at com.alibaba.jvm.sandbox.repeater.plugin.core.serialize.HessianSerializer.serialize(HessianSerializer.java:43)
at com.alibaba.jvm.sandbox.repeater.plugin.core.serialize.AbstractSerializerAdapter.serialize2String(AbstractSerializerAdapter.java:30)
at com.alibaba.jvm.sandbox.repeater.plugin.core.wrapper.SerializerWrapper.inTimeSerialize(SerializerWrapper.java:102)
at com.alibaba.jvm.sandbox.repeater.plugin.core.impl.api.DefaultEventListener.doBefore(DefaultEventListener.java:170)
at com.alibaba.jvm.sandbox.repeater.plugin.core.impl.api.DefaultEventListener.onEvent(DefaultEventListener.java:114)
at com.alibaba.jvm.sandbox.core.enhance.weaver.EventListenerHandlers.handleEvent(EventListenerHandlers.java:102)
at com.alibaba.jvm.sandbox.core.enhance.weaver.EventListenerHandlers.handleOnBefore(EventListenerHandlers.java:342)
at com.alibaba.jvm.sandbox.core.enhance.weaver.EventListenerHandlers.onBefore(EventListenerHandlers.java:565)
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 java.com.alibaba.jvm.sandbox.spy.Spy.spyMethodOnBefore(Spy.java:193)
at yxitest.controllers.CrmRedisController.getVa(CrmRedisController.java)
at sun.reflect.GeneratedMethodAccessor61.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:221)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:137)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:111)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:806)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:729)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:959)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:893)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:635)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:85)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:504)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81)
at org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:677)
at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:650)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:803)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:790)
at org.apache.tomcat.util.net.AprEndpoint$SocketWithOptionsProcessor.run(AprEndpoint.java:2351)
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 org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.NoClassDefFoundError: org/slf4j/Marker
at java.lang.Class.getDeclaredMethods0(Native Method)
at java.lang.Class.privateGetDeclaredMethods(Class.java:2701)
at java.lang.Class.getDeclaredMethods(Class.java:1975)
at com.caucho.hessian.io.JavaSerializer.getWriteReplace(JavaSerializer.java:161)
at com.caucho.hessian.io.SerializerFactory.loadSerializer(SerializerFactory.java:310)
at com.caucho.hessian.io.SerializerFactory.getSerializer(SerializerFactory.java:253)
at com.caucho.hessian.io.SerializerFactory.getObjectSerializer(SerializerFactory.java:208)
at com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:463)
at com.caucho.hessian.io.UnsafeSerializer$ObjectFieldSerializer.serialize(UnsafeSerializer.java:296)
at com.caucho.hessian.io.UnsafeSerializer.writeInstance(UnsafeSerializer.java:215)
at com.caucho.hessian.io.UnsafeSerializer.writeObject(UnsafeSerializer.java:174)
at com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:465)
at com.caucho.hessian.io.UnsafeSerializer$ObjectFieldSerializer.serialize(UnsafeSerializer.java:296)
at com.caucho.hessian.io.UnsafeSerializer.writeInstance(UnsafeSerializer.java:215)
at com.caucho.hessian.io.UnsafeSerializer.writeObject(UnsafeSerializer.java:169)
at com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:465)
at com.caucho.hessian.io.ArraySerializer.writeObject(ArraySerializer.java:69)
at com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:465)
at com.caucho.hessian.io.UnsafeSerializer$ObjectFieldSerializer.serialize(UnsafeSerializer.java:296)
at com.caucho.hessian.io.UnsafeSerializer.writeInstance(UnsafeSerializer.java:215)
at com.caucho.hessian.io.UnsafeSerializer.writeObject(UnsafeSerializer.java:169)
at com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:465)
at com.caucho.hessian.io.UnsafeSerializer$ObjectFieldSerializer.serialize(UnsafeSerializer.java:296)
at com.caucho.hessian.io.UnsafeSerializer.writeInstance(UnsafeSerializer.java:215)
at com.caucho.hessian.io.UnsafeSerializer.writeObject(UnsafeSerializer.java:174)
at com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:465)
at com.caucho.hessian.io.UnsafeSerializer$ObjectFieldSerializer.serialize(UnsafeSerializer.java:296)
at com.caucho.hessian.io.UnsafeSerializer.writeInstance(UnsafeSerializer.java:215)
at com.caucho.hessian.io.UnsafeSerializer.writeObject(UnsafeSerializer.java:174)
at com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:465)
at com.caucho.hessian.io.UnsafeSerializer$ObjectFieldSerializer.serialize(UnsafeSerializer.java:296)
at com.caucho.hessian.io.UnsafeSerializer.writeInstance(UnsafeSerializer.java:215)
at com.caucho.hessian.io.UnsafeSerializer.writeObject(UnsafeSerializer.java:174)
at com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:465)
at com.caucho.hessian.io.CollectionSerializer.writeObject(CollectionSerializer.java:117)
at com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:465)
at com.caucho.hessian.io.UnsafeSerializer$ObjectFieldSerializer.serialize(UnsafeSerializer.java:296)
at com.caucho.hessian.io.UnsafeSerializer.writeInstance(UnsafeSerializer.java:215)
at com.caucho.hessian.io.UnsafeSerializer.writeObject(UnsafeSerializer.java:174)
at com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:465)
at com.caucho.hessian.io.UnsafeSerializer$ObjectFieldSerializer.serialize(UnsafeSerializer.java:296)
at com.caucho.hessian.io.UnsafeSerializer.writeInstance(UnsafeSerializer.java:215)
at com.caucho.hessian.io.UnsafeSerializer.writeObject(UnsafeSerializer.java:174)
at com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:465)
at com.caucho.hessian.io.UnsafeSerializer$ObjectFieldSerializer.serialize(UnsafeSerializer.java:296)
at com.caucho.hessian.io.UnsafeSerializer.writeInstance(UnsafeSerializer.java:215)
at com.caucho.hessian.io.UnsafeSerializer.writeObject(UnsafeSerializer.java:174)
at com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:465)
at com.caucho.hessian.io.UnsafeSerializer$ObjectFieldSerializer.serialize(UnsafeSerializer.java:296)
at com.caucho.hessian.io.UnsafeSerializer.writeInstance(UnsafeSerializer.java:215)
at com.caucho.hessian.io.UnsafeSerializer.writeObject(UnsafeSerializer.java:174)
at com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:465)
at com.caucho.hessian.io.UnsafeSerializer$ObjectFieldSerializer.serialize(UnsafeSerializer.java:296)
at com.caucho.hessian.io.UnsafeSerializer.writeInstance(UnsafeSerializer.java:215)
at com.caucho.hessian.io.UnsafeSerializer.writeObject(UnsafeSerializer.java:174)
at com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:465)
at com.caucho.hessian.io.UnsafeSerializer$ObjectFieldSerializer.serialize(UnsafeSerializer.java:296)
at com.caucho.hessian.io.UnsafeSerializer.writeInstance(UnsafeSerializer.java:215)
at com.caucho.hessian.io.UnsafeSerializer.writeObject(UnsafeSerializer.java:174)
at com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:465)
at com.caucho.hessian.io.UnsafeSerializer$ObjectFieldSerializer.serialize(UnsafeSerializer.java:296)
at com.caucho.hessian.io.UnsafeSerializer.writeInstance(UnsafeSerializer.java:215)
at com.caucho.hessian.io.UnsafeSerializer.writeObject(UnsafeSerializer.java:174)
at com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:465)
at com.caucho.hessian.io.BasicSerializer.writeObject(BasicSerializer.java:303)
at com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:465)
at com.alibaba.jvm.sandbox.repeater.plugin.core.serialize.HessianSerializer.serialize(HessianSerializer.java:39)
... 56 common frames omitted
Caused by: java.lang.ClassNotFoundException: org.slf4j.Marker
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at com.alibaba.jvm.sandbox.agent.SandboxClassLoader.loadClass(SandboxClassLoader.java:68)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 123 common frames omitted

qiuqiu0802 回复

这个报错看起来是序列化相关部分有异常。

你是在进行到第五步时出现的吗?这个异常是出现在哪个日志文件里?

请高手帮我分析一下,有三点没搞懂,应用项目 gs-rest-service 中
1、触发回放时请求的 appName 为什么传入 unknown

2、回放时,返回的 ID 还是递增的,没理解

3、回放时,除传入的 traceId,其他参数传与不传返回的结果不一样,是在真实的应用中回放,都要带上参数,还是有其他方式可以,我理解这个录制应该是有把传参也录制了下来的,是不支持还是进行二次开发在回放时不需要传参

大佬 你好我昨天晚上通过录制的 ID curl -s 'http://127.0.0.1:8001/facade/api/repeat/unknown/192168015059156326967477410002ed' 执行这个后返回结果是 500,提示 getID 不存在,这会不会是最近代码导致的呢 ?而且跟你现在的代码不一致了

shifeng5131425 回复


需要加下白名单和 behavior

qiuqiu0802 回复

大佬,这个序列化报错原因找到了吗?我也出现了

ServenCoding 回复

最近没怎么研究这个了,建议你到官方提下 issue 吧

majf2015 回复

1、触发回放时请求的 appName 为什么传入 unknown

这个是默认值,因为我们没有特意配置这个 app 的名称。

2、回放时,返回的 ID 还是递增的,没理解

不知道你具体是哪个 id ,方便发下具体的请求和返回报文么?

3、回放时,除传入的 traceId,其他参数传与不传返回的结果不一样,是在真实的应用中回放,都要带上参数,还是有其他方式可以,我理解这个录制应该是有把传参也录制了下来的,是不支持还是进行二次开发在回放时不需要传参

没太理解你的问题。回放时有两个地方有请求参数,一个是给 console 一个执行回放的请求(facade/api/repeat/unknown/192168015059156326967477410002ed),另一个是 console 执行回放时产生的给被测应用的请求。第二个给被测应用的请求里面的参数,console 会自动把它按照录制时一模一样地发出,没有提供可以调整的入口。

楼主最近还有研究吗?按这套流程走不通了好像,到获取回放执行回放那块就会有问题的,源码也改掉了的😭

源码现在有许多变动了,楼主这套流程走下来目前发现这几个问题:
1.需要改动源码,添加回放的 ip,若本地录制 + 回放,填 127.0.0.1 即可


2.获取回放结果的接口有改动,callback 现在只返回 null
当前可查询回放结果的接口:curl -s 'http://127.0.0.1:8001/facade/api/record/unknown/192168015059156326967477410002edtraceId)('

3.回放结果记录字段 wrapper_record 存在乱码情况(初步判定为录制时出错 RecordServiceImpl#saveRecord,暂未找到出错原因,待大佬们帮忙解决)

42楼 已删除
陈恒捷 怎么去学习开源项目? 中提及了此贴 07月29日 10:25
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册