背景

流量录制回放近几年已经越来越火。如阿里开放的 doom 、滴滴开源的 RDebug 。而公司随着业务的快速发展,回归测试的耗时越来越长,而且可以留给脚本维护的时间也并不多。因此也期望通过流量录制回放,提高回归测试的效率。

而刚好前几天看到阿里技术公众号推送的一篇 jvm-sandbox 的文章,里面有提到开源了其中做流量录制回放的 repeater 模块,所以尝鲜一下。

为了方便记录和阅读,目前已经调整为系列文章。导航地址如下:

通用流量录制回放工具 jvm-sandbox-repeater 尝鲜记录(本文)
通用流量录制回放工具 jvm-sandbox-repeater 尝鲜 (二)——repeater-console 使用
通用流量录制回放工具 jvm-sandbox-repeater 尝鲜 (三)—— repeater plugin 开发
通用流量录制回放工具 jvm-sandbox-repeater 尝鲜 (四)——新版带界面 console 的使用

jvm-sandbox-repeater 简介

直接搬运官方文档里面的介绍吧

image_1df2dcu799j9pif16k3kje1s1j9.png-187.1kB
image_1df2ddd0jf2sk7rvjlqkduavm.png-61.8kB

github 地址:https://github.com/alibaba/jvm-sandbox-repeater

简单的说,就是一个可以在不修改程序的情况下,进行一个服务 http、java、dubbo 入参及返回值的录制。也支持快速扩展 api ,实现自己的插件。

standalone 快速开始

step0 安装 sandbox/启动 bootstrap

cd bin
./bootstrap.sh

等待 SpringBoot 应用启动完成 -> Started Application in 4.797 seconds (JVM running for 6.586)

step1 开始录制

curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId=127000000001156034386424510000ed'

执行结果如下:

访问链接时,repeater 插件通过 Repeat-TraceId=127000000001156034386424510000ed,唯一追踪到了这一次请求,后台服务返回了JAVA是世界上最好的语言!,repeater 把画面定格在了这一秒并将结果和 firstId 绑定

step2 开始回放

curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId-X=127000000001156034386424510000ed'

无论我们多少次访问这个地址,都将返回 Repeat-TraceId=127000000001156034386424510000ed 绑定的录制信息JAVA是世界上最好的语言!;如果重新访问Slogan后又会将最新的返回结果绑定到 Repeat-TraceId=127000000001156034386424510000ed(为了快速演示,将链路追踪的标志提到参数中进行透传了)

完整执行结果如下:

➜  bin git:(master) curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId=127000000001156034386424510000ed'
<h1 align="center" style="color:red;margin-top:300px">JAVA是世界上最好的语言!</h1>%                                           

➜  bin git:(master) curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId-X=127000000001156034386424510000ed'
<h1 align="center" style="color:red;margin-top:300px">JAVA是世界上最好的语言!</h1>%

光是执行官方用例,当然不能满足我们需要啦。我们来测试下,如果有多个 Repeat-TraceId ,是否可以分别录制?

测试一下:

# 录制一个 128 开头的 traceId ,返回结果是 java
➜  bin git:(master) curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId=128000000001156034386424510000ed'
<h1 align="center" style="color:red;margin-top:300px">JAVA是世界上最好的语言!</h1>%                                                                           
# 录制一个 129 开头的 traceId ,返回结果是 Python
➜  bin git:(master) curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId=129000000001156034386424510000ed'
<h1 align="center" style="color:red;margin-top:300px">Python是世界上最好的语言!</h1>%                                                                         
# 回放前面 128 的流量
➜  bin git:(master) curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId-X=128000000001156034386424510000ed'
<h1 align="center" style="color:red;margin-top:300px">JAVA是世界上最好的语言!</h1>%                                                                           
# 回放前面 129 的流量
➜  bin git:(master) curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId-X=129000000001156034386424510000ed'
<h1 align="center" style="color:red;margin-top:300px">Python是世界上最好的语言!</h1>%  

看来确实是有效的。

录制目标服务

前面只是个简单的练手,实际用不用得了,当然的实际项目说话啦。

为了简单,此处使用了几个 spring boot 的示例项目当做实际项目使用。

项目地址:https://github.com/chenhengjie123/gs-rest-service(官方文档:https://spring.io/guides/gs/rest-service/ ,在官方的基础上增加了请求日志打印的功能,便于查看回放效果)

clone 后,直接用 complete 里面的完整示例,当做被测程序。

程序本身功能:当请求 http://localhost:8080/greeting?name=User 时,返回 {"id":2,"content":"Hello, User!"} 。其中 Hello 后面的名称根据请求参数的 name 自动替换,id 会自动递增。

接下来,按照官方的说明,进行操作:

step0 安装 sandbox 和插件到应用服务器

curl -s http://sandbox-ecological.oss-cn-hangzhou.aliyuncs.com/install-repeater.sh | sh

正常日志输出:

======  begin to install sandbox and repeater module       ======
======  step 0 begin to download sandbox package           ======
======  step 1 begin to download repeater module package   ======
======                 install finished                    ======

step1 修改 repeater-config.json,启用拦截点和插件信息

根据需要修改repeater-config.json配置文件,具体配置含义参见:RepeaterConfig.java

repeater-config.json 配置文件,位置是 ~/.sandbox-module/cfg/repeater-config.json

具体的配置含义,官方提供的链接相对路径有问题,无法跳转。可以直接看这个链接:RepeaterConfig.java

此处根据这个被测项目的需要,进行了调整:

20190710 更新:之前的配置遗漏了 javaSubInvokeBehaviors 的设定,会导致回放的时候返回没有被 mock 掉,看不出效果。下面为更正后的配置

{
  "degrade": false, 
  "exceptionThreshold": 1000,
  "httpEntrancePatterns": [
    "^/greeting.*$"
  ],
  "javaEntranceBehaviors": [
  ],
  "javaSubInvokeBehaviors": [
    {
      "classPattern": "hello.GreetingController",
      "includeSubClasses": false,
      "methodPatterns": [
        "greeting"
      ]
    }
  ],
  "pluginIdentities": [
    "http",
    "java-subInvoke"
  ],
  "repeatIdentities": [
    "java",
    "http"
  ],
  "sampleRate": 10000,
  "useTtl": false
}

step2 attach sandbox 到目标进程

先到刚才 clone spring boot 示例项目的根目录,启动被测应用

# 在示例项目 clone 后的根目录中运行
cd complete 
mvn install && java -jar target/*.jar

# 查看进程 id 
ps aux | grep target/gs-rest-service-0.1.0.jar
hengjiechen       7737   0.0  0.0  4267932    616 s007  R+   10:23AM   0:00.00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn rest-service
hengjiechen       7306   0.0  3.2  7935464 269884 s000  S+   10:23AM   0:14.84 /usr/bin/java -jar target/gs-rest-service-0.1.0.jar

可以看到,进程 id 为 7306 。然后开始 attach

cd ~/sandbox/bin

# 假设目标JVM进程号为'7306' 。-P 是设定 jvm-sandbox 的端口号,后面回放需要用到
./sandbox.sh -p 7306 -P 12580

小技巧:上述的找进程 id + attach 过程,可以用这个命令一键达成:

# -P 是设定 jvm-sandbox 的端口号,后面回放需要用到
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

控制台输出:

          NAMESPACE : default
            VERSION : 1.2.1
               MODE : ATTACH
        SERVER_ADDR : account.jetbrains.com
        SERVER_PORT : 53866
     UNSAFE_SUPPORT : ENABLE
       SANDBOX_HOME : /Users/hengjiechen/sandbox/bin/..
  SYSTEM_MODULE_LIB : /Users/hengjiechen/sandbox/bin/../module
    USER_MODULE_LIB : /Users/hengjiechen/sandbox/sandbox-module;~/.sandbox-module;
SYSTEM_PROVIDER_LIB : /Users/hengjiechen/sandbox/bin/../provider
 EVENT_POOL_SUPPORT : DISABLE

查看 repeater 日志看模块和插件加载情况

$ tail -200f ~/logs/sandbox/repeater/repeater.log

...
2019-07-07 10:24:14 INFO  initializing logback success. file=/Users/hengjiechen/.sandbox-module/cfg/repeater-logback.xml;
2019-07-07 10:24:14 INFO  module on loaded,id=repeater,version=1.0.0,mode=ATTACH
2019-07-07 10:24:14 INFO  onActive
2019-07-07 10:24:14 INFO  pull repeater config success,config=com.alibaba.jvm.sandbox.repeater.plugin.domain.RepeaterConfig@4dddeb36
2019-07-07 10:24:15 INFO  enable plugin http success
2019-07-07 10:24:15 INFO  add watcher success,type=http,watcherId=1000
2019-07-07 10:24:16 INFO  register event bus success in repeat-register

step3 开始录制和回放

录制几个请求:

➜  complete git:(master) ✗ curl -s 'http://localhost:8080/greeting?name=User'
{"id":1,"content":"Hello, User!"}%                                                                                                                            
➜  complete git:(master) ✗ curl -s 'http://localhost:8080/greeting?name=User2'
{"id":2,"content":"Hello, User2!"}%                                                                                                                           
➜  complete git:(master) ✗ curl -s 'http://localhost:8080/greeting?name=User3'
{"id":3,"content":"Hello, User3!"}%                                                                                                                           
➜  complete git:(master) ✗ curl -s 'http://localhost:8080/greeting'
{"id":4,"content":"Hello, World!"}%

对应看到 repeater 的日志增加了几个输出:

...
2019-07-09 16:31:20 INFO  broadcast success,traceId=192168015059156266108005510001ed,resp=success
2019-07-09 16:31:26 INFO  broadcast success,traceId=192168015059156266108604210002ed,resp=success
2019-07-09 16:31:31 INFO  broadcast success,traceId=192168015059156266109135010003ed,resp=success
2019-07-09 16:31:36 INFO  broadcast success,traceId=192168015059156266109622310004ed,resp=success

好了,试试回放。

怎么回放?文档完全没提到,一脸懵逼。。。等官方文档更新把。

20190709 更新:官方已经更新回放文档啦,接着进行下去。

方式一:利用模块暴露的 http 接口发起回放

官方的说明:

模块暴露了回放接口,用于服务端发起远程回放,具体如下:

url : http://ip:port/sandbox/default/module/http/repeater/repeat
params : _data

其中 port 是 jvm-sandbox 启动时候绑定的 port,可以在 attach sandbox 时增加-P
12580 指定,或者执行~/sandbox/bin/sandbox.sh -p {pid} -v 查看 SERVER_PORT _data
是由 RepeatMeta 经过 hessian 序列化之后的值,具体调用方式参见 AbstractRecordService
和 RecordFacadeApi

没说明是用什么 http 方法(后面通过看 AbstractRecordService.java 看出是 post ),而且 _data 需要用程序做 RepeatMeta 的 hessian 序列化。。。看起来就不是给我们这种命令行触发用的。先跳过。

方式二:针对 HTTP 接口,可以像 Slogan Demo 一样进行参数或者 Header 透传方式进行 MOCK 回放

从前面的 repeater 日志,找到了几个 traceId 。对应把它填到 Repeat-TraceId-X 参数中。(特别留意:回放会根据录制时的 url 进行匹配。如果有参数是通过 url 传递的,必须录制和回放都用一样的参数

# 第一种写法:
$ curl -s 'http://localhost:8080/greeting' -H "Repeat-TraceId-X:192168015059156266109622310004ed"
{"id":4,"content":"Hello, World!"}%  
$ curl -s 'http://localhost:8080/greeting?name=User3' -H "Repeat-TraceId-X:192168015059156266109135010003ed"
{"id":3,"content":"Hello, User3!"}%     

# 第二种写法:
$ curl -s 'http://localhost:8080/greeting?Repeat-TraceId-X=192168015059156266109622310004ed'
{"id":4,"content":"Hello, World!"}%  

id 还在递增,回放没生效。但看了下 plugin 的源码 ,确实是有这样的逻辑。而且上面两个请求发出的时候, repeater.log 并没有输出录制到请求的日志。

20190710 更新:问题已解决,原因是前面的 repeater.json 配置不正确,遗漏了 javaSubInvokeBehaviors 相关配置,导致返回值没有被录制到。

修正后,已经可以输出正确的返回了。此时 repeater.log 也会对应输出日志:

2019-07-10 17:19:25 INFO  initializing logback success. file=/Users/chenhengjie/.sandbox-module/cfg/repeater-logback.xml;
2019-07-10 17:19:25 INFO  module on loaded,id=repeater,version=1.0.0,mode=ATTACH
2019-07-10 17:19:25 INFO  onActive
2019-07-10 17:19:25 INFO  pull repeater config success,config=com.alibaba.jvm.sandbox.repeater.plugin.domain.RepeaterConfig@61e09144
2019-07-10 17:19:25 INFO  enable plugin http success
2019-07-10 17:19:26 INFO  add watcher success,type=http,watcherId=1000
2019-07-10 17:19:26 INFO  enable plugin java-subInvoke success
2019-07-10 17:19:26 INFO  add watcher success,type=java,watcherId=1003
2019-07-10 17:19:27 INFO  register event bus success in repeat-register
2019-07-10 17:19:31 INFO  broadcast success,traceId=192168015059156275037036610001ed,resp=success
2019-07-10 17:19:32 INFO  broadcast success,traceId=192168015059156275037271710002ed,resp=success
2019-07-10 17:19:41 INFO  find target invocation by PARAMETER_MATCH,identity=java://hello.GreetingController/greeting~S,invocation=com.alibaba.jvm.sandbox.repeater.plugin.domain.Invocation@3af687c7
2019-07-10 17:19:52 INFO  find target invocation by PARAMETER_MATCH,identity=java://hello.GreetingController/greeting~S,invocation=com.alibaba.jvm.sandbox.repeater.plugin.domain.Invocation@6b6f0533

最后两行就是对应返回录制的 response 了。

方式三:使用 repeater-console 做回放

官方文档没有明确给出这个方式,但通过查看 repeater-console 里面的 readme ,可以看到它也是有暴露接口供调用的。因此也试试。

结果看了下,里面提供的 standalone 和 mysql 两种数据存储方式,都不支持前面回放的存储方法(存在 ~/.sandbox-module/repeater-data/record 中)。还得调整录制方式才能进行回放。

7.16 更新:此回放方式已跑通。详细的记录,请参照:通用流量录制回放工具 jvm-sandbox-repeater 尝鲜 (二)——repeater-console 使用

待补充

待补充

源码结构简析

项目的源码目录如下:

$ tree -L 2 | grep -v iml
.
├── LICENSE
├── Readme.md
├── bin
│   ├── bootstrap.sh
│   ├── health.sh
│   ├── install-local.sh
│   ├── install-repeater.sh
│   ├── package.sh
│   ├── repeater-config.json
│   ├── repeater-logback.xml
│   └── repeater.properties
├── docs
│   ├── plugin-development.md
│   ├── slogan-demo.md
│   └── user-guide-cn.md
├── hessian-lite
│   ├── pom.xml
│   └── src
├── pom.xml
├── repeater-client
│   ├── pom.xml
│   └── src
├── repeater-console
│   ├── Readme.md
│   ├── pom.xml
│   ├── repeater-console-common
│   ├── repeater-console-dal
│   ├── repeater-console-service
│   ├── repeater-console-start
├── repeater-module
│   ├── pom.xml
│   └── src
├── repeater-plugin-api
│   ├── pom.xml
│   └── src
├── repeater-plugin-core
│   ├── pom.xml
│   └── src
├── repeater-plugins
│   ├── dubbo-plugin
│   ├── http-plugin
│   ├── ibatis-plugin
│   ├── java-plugin
│   ├── mybatis-plugin
│   ├── pom.xml
│   ├── redis-plugin
└── travis.sh

整体结构还是比较清晰的,有 plugin 目录,便于扩展。也有 console 提供最简要的流量管理。更详细的,后续再慢慢研究。

吐槽

20190709 更新:之前提到的 3 个吐槽点官方都第一时间修复了,效率很高。

这次补充两个点:

1、repeater-console 的说明还是太少,虽然通过阅读源码大致了解了它的功能,但不如有文档方便,对新手不大友好。建议补充对应的说明。

2、回放提供的 2 个方式都不是太友好,方式二比较简单,但不支持批量,不适合项目使用。方式一基本上只能通过编程方式进行,无法直接通过接口进行。

建议提供一个命令行工具,可以直接通过命令行参数自动组装和序列化输出 _data 参数,便于用最简单的方式调用回放功能。

小结

这个开源项目是 7 月 4 日 出来的,由于是直接在 jvm 层控制,从原理上是通用性、扩展性最强的。而且目前也提供了 mybatis、http、dubbo 、Java 等插件,redis 预计 7 月放出,基本上覆盖了项目中最常用的几个中间件了。

虽然目前文档还不是非常完整,但看得出来是有在用心维护的,昨天周六也有更新了一版文档,补充了关于原理方面的说明。相信只要有足够的时间,会变得更完善的。

最后,非常感谢阿里能开源一个这么强大的组件,这样一些质量技术方面没法有太多资源投入的公司,也可以把流量录制回放搞起来了。


↙↙↙阅读原文可查看相关链接,并与作者交流