Jvm-Sandbox-Repeater架构_小小平不平凡的博客-CSDN博客
https://www.cnblogs.com/hong-fithing/p/16222644.html
流量回放框架jvm-sandbox-repeater的实践_做人,最重要的就是开心嘛的博客-CSDN博客
[jvm-sandbox-repeater 学习笔记][入门使用篇] 2 配置说明 · TesterHome
流量回放框架jvm-sandbox-repeater的实践【入门使用篇】1 repeater安装与启动(初尝repeater-console) - 知乎
[jvm-sandbox-repeater 学习笔记][入门使用篇] 1 安装与启动 · TesterHome
jvm-sandbox-repeater
JVM沙箱容器,一种JVM的非侵入式运行期AOP解决方案,如jstack,jmap等都是attach方式,也就是进程之间通信。
沙箱常见应用场景
- 线上故障定位
- 线上系统流控
- 线上故障模拟
- 方法请求录制和结果回放
- 动态日志打印
- 安全信息监测和脱敏
JVM-SANDBOX还能帮助你做很多很多,取决于你的脑洞有多大了。
目标人群 - 面向测试开发工程师
- 线上有个用户请求一直不成功,我想在测试环境Debug一下,能帮我复现一下吗?
- 压测流量不知道怎么构造,数据结构太复杂,压测模型也难以评估,有什么好的办法吗?
- 不想写接口测试脚本了,我想做一个流量录制系统,把线上用户场景做业务回归,可能会接入很多服务系统,不想让每个系统都进行改造,有好的框架选择吗?
- 我想做一个业务监控系统,对线上核心接口采样之后做一些业务校验,实时监控业务正确性。
如果你有以上的想法或需求,jvm-sandbox-repeater 都将是你的不二选择方案;框架基于JVM-Sandbox,拥有JVM-Sandbox的一切特性,同时封装了以下能力:
- 录制/回放基础协议,可快速配置/编码实现一类中间件的录制/回放
- 开放数据上报,对于录制结果可上报到自己的服务端,进行监控、回归、问题排查等上层平台搭建
Repeater核心原理:
名词解释
- 录制:把一次请求的入参、出参、下游RPC、DB、缓存等序列化并存储的过程
- 回放:把录制数据还原,重新发起一次或N次请求,对特定的下游节点进行MOCK的过程
- 入口调用:入口调用一般是应用的流量来源,比如http/dubbo,在调用过程中录制调用入参、返回值。回放时作为流量发起和执行结果对比依据
- 子调用:子调用是调用执行过程中某次方法调用,区别于入口调用,该调用不作为回放发起录制时会记录该方法的入参、返回值,回放时用该返回值进行MOCK
- MOCK:在回放时,被拦截的子调用不会发生真实调用,利用Sandbox的流程干预能力,将录制时的返回值直接返回
Repeater特性
- 无侵入:无需修改代码,无需重启jvm
- 通用性:支持所有JVM类型语言
- 可插拔:随时启停、随时卸载
- 扩展性:简单几行代码即可适配一个常用插件
Repeater应用场景
- 业务回归
- 架构感知
- 问题排查
- 压测流量
- 线上监控
repeater-console体验
对性能的影响
结论:线上采样率控制在一定范围内,性能影响可接受
推荐使用agent方式还是attach方式
针对录制回放场景,推荐使用attach方式启动,更灵活,更可控,需要注意attach瞬间的影响
attach 和 agent 启动模式对比
模式 | 优点 | 不足 |
---|---|---|
attach | 1. 不需要启停应用,即插即用,随时停止 2. 更新配置不需要重启应用 | 1. 进行 java 回放的时候可能由于无法获取到对应实例而回放失败 2. 如果需要对 repeater 进行 debug,需要将 repeater 代码嵌入到被录制应用的代码中,不方便 debug3. 不可进行录制应用名和录制环境的配置,会被默认标记为 unknown |
agent | 1. 进行 java 回放的时候能够获取到对应实例能够正常回放 2. 启动应用时开启调试,即可远程调试 repeater3. 支持配置录制应用名以及录制环境,方便在录制记录中进行区分 | 1. 启动/停止都需要重启应用 2. 更新配置也需要重启应用 |
如何动态推送配置
配置变更后,用户主动推送,配置实时生效,无需重启
如何快速搭建测试平台
直接部署 repeater-console 到生产环境,把 repeater-module 分发到目标机器,录制后回放到指定环境
repeater的核心能力是什么?
1. 通用录制/回放能力
- 无侵入式录制HTTP/Java/Dubbo入参/返回值录制能力(业务系统无感知)
- 基于TTL提供多线程子调用追踪,完整追踪一次请求的调用路径
- 入口请求(HTTP/Dubbo/Java)流量回放、子调用(Java/Dubbo)返回值Mock能力
2. 快速可扩展API实现
- 录制/回放插件式架构
- 提供标准接口,可通过配置/简单编码实现一类通用插件
3. standalone工作模式
- 无需依赖任何服务端/存储,可以单机工作,提供录制/回放能力
repeater的可以应用到哪些场景?
1. 业务快速回归
- 基于线上流量的录制/回放,无需人肉准备自动化测试脚本、准备测试数据
2. 线上问题排查
- 录制回放提供"昨日重现"能力,还原线上真实场景到线下做问题排查和Debug
- 动态方法入参/返回值录制,提供线上快速问题定位
3. 压测流量准备
- 0成本录制HTTP/Dubbo等入口流量,作为压测流量模型进行压测
4. 实时业务监控
- 动态业务监控,基于核心接口数据录制回流到平台,对接口返回数据正确性进行校验和监控
二. jvm-sandbox-repeater简介
jvm-sandbox-repeater是阿里在19年7月份的时候开源的流量录制回放工具,代码提供了录制回放的能力,以及一个简单的repeater-console的demo示例。github 地址:GitHub - alibaba/jvm-sandbox-repeater: A Java server-side recording and playback solution based on JVM-Sandbox。
jvm-sandbox-repeater框架基于JVM-Sandbox,具备了JVM-Sandbox的所有特点封装了以下能力:
1.录制/回放基础协议,可快速配置/编码实现一类中间件的录制/回放
2.开放数据上报,对于录制结果可上报到自己的服务端,进行监控、回归、问题排查等上层平台搭建
基于它,我们可以在业务系统无感知的情况下,快速扩展 api ,实现自己的插件,对流量进行录制,入口请求(HTTP/Dubbo/Java)流量回放、子调用(Java/Dubbo)返回值Mock能力。详细介绍可以看官方说明。
录制回放主要原理如下:
录制:如图,当repeater启动对service A的录制后,有请求到service A,sandbox感知到请求后通知repeater。repeater对事件进行给过滤和采样计算,对满足录制条件的请求会记录请求、响应、子调用和响应,序列化成后通知repeater-console进行处理和保存。
回放:回放时,用户请求repeater-console的回放接口,明确需要回放哪条录制数据。然后repeater-console通过调用repeater提供的回放任务接收接口下发回放任务。repeater在执行回放任务的过程中,会反序列化记录的wrapperRecord,根据信息构造相同的请求,对被挂载的任务进行请求,并跟踪回放请求的处理流程,以便记录回放结果以及执行mock动作。如图,当我们启用了redis插件,录制时,service A到reids等的子请求方法、参数、响应将被录制下来,回放时,当service A再对reids发起请求时,repeater会先判断是否需要mock,当需要mock时会根据回放上下文中的信息拼接出MockRequest,通过mock策略计算获取MockResponse。目前源码中是获取相似度100%的请求的响应来进行mock。回放结束,repeater会将回放信息和结果序列化后通知repeater-console进行处理和保存。
重点词语解释
一、jvm-sandbox与jvm-sandbox-repeater
JVM-SANDBOX | jvm-sandbox-repeater | |
---|---|---|
简介 | JVM沙箱容器,一种JVM的非侵入式运行期AOP解决方案 | 基于JVM-Sandbox的录制/回放通用解决方案 jvm-sandbox-repeater是JVM-Sandbox生态体系下的重要模块,它具备了JVM-Sandbox的所有特点,插件式设计便于快速适配各种中间件,封装请求录制/回放基础协议,也提供了通用可扩展的各种丰富API。 |
目标群体 | 1. BTRACE好强大,也曾技痒想做一个更便捷、更适合自己的问题定位工具,既可支持线上链路监控排查,也可支持单机版问题定位。 2. 有时候突然一个问题反馈上来,需要入参才能完成定位,但恰恰没有任何日志,甚至出现在别人的代码里,好想开发一个工具可以根据需要动态添加日志,最好还能按照业务ID进行过滤。3. 系统间的异常模拟可以使用的工具很多,可是系统内的异常模拟怎么办,加开关或是用AOP在开发系统中实现,好想开发一个更优雅的异常模拟工具,既能模拟系统间的异常,又能模拟系统内的异常。4. 好想获取行调用链路数据,可以用它识别场景、覆盖率统计等等,覆盖率统计工具不能原生支持,统计链路数据不准确。想自己开发一个工具获取行链路数据。5. 我想开发录制回放、故障模拟、动态日志、行链路获取等等工具,就算我开发完成了,这些工具底层实现原理相同,同时使用,要怎么消除这些工具之间的影响,怎么保证这些工具动态加载,怎么保证动态加载/卸载之后不会影响其他工具,怎么保证在工具有问题的时候,快速消除影响,代码还原如果你有以上研发诉求,那么你就是JVM-SANDBOX(以下简称沙箱容器)的潜在客户。沙箱容器提供:1. 动态增强类你所指定的类,获取你想要的参数和行信息甚至改变方法执行2. 动态可插拔容器框架 | 1. 线上有个用户请求一直不成功,我想在测试环境Debug一下,能帮我复现一下吗? 2. 压测流量不知道怎么构造,数据结构太复杂,压测模型也难以评估,有什么好的办法吗?3. 不想写接口测试脚本了,我想做一个流量录制系统,把线上用户场景做业务回归,可能会接入很多服务系统,不想让每个系统都进行改造,有好的框架选择吗?4. 我想做一个业务监控系统,对线上核心接口采样之后做一些业务校验,实时监控业务正确性。如果你有以上的想法或需求,jvm-sandbox-repeater 都将是你的不二选择方案;框架基于JVM-Sandbox,拥有JVM-Sandbox的一切特性,同时封装了以下能力:1. 录制/回放基础协议,可快速配置/编码实现一类中间件的录制/回放2. 开放数据上报,对于录制结果可上报到自己的服务端,进行监控、回归、问题排查等上层平台搭建 |
项目简介 | JVM-SANDBOX(沙箱)实现了一种在不重启、不侵入目标JVM应用的AOP解决方案。 沙箱的特性无侵入:目标应用无需重启也无需感知沙箱的存在类隔离:沙箱以及沙箱的模块不会和目标应用的类相互干扰可插拔:沙箱以及沙箱的模块可以随时加载和卸载,不会在目标应用留下痕迹多租户:目标应用可以同时挂载不同租户下的沙箱并独立控制高兼容:支持JDK[6,11]沙箱常见应用场景线上故障定位线上系统流控线上故障模拟方法请求录制和结果回放动态日志打印安全信息监测和脱敏JVM-SANDBOX还能帮助你做很多很多,取决于你的脑洞有多大了。实时无侵入AOP框架在常见的AOP框架实现方案中,有静态编织和动态编织两种。静态编织:静态编织发生在字节码生成时根据一定框架的规则提前将AOP字节码插入到目标类和方法中,实现AOP;动态编织:动态编织则允许在JVM运行过程中完成指定方法的AOP字节码增强.常见的动态编织方案大多采用重命名原有方法,再新建一个同签名的方法来做代理的工作模式来完成AOP的功能(常见的实现方案如CgLib),但这种方式存在一些应用边界:侵入性:对被代理的目标类需要进行侵入式改造。比如:在Spring中必须是托管于Spring容器中的Bean固化性:目标代理方法在启动之后即固化,无法重新对一个已有方法进行AOP增强要解决无侵入的特性需要AOP框架具备 在运行时完成目标方法的增强和替换。在JDK的规范中运行期重定义一个类必须准循以下原则1. 不允许新增、修改和删除成员变量2. 不允许新增和删除方法3. 不允许修改方法签名JVM-SANDBOX属于基于Instrumentation的动态编织类的AOP框架,通过精心构造了字节码增强逻辑,使得沙箱的模块能在不违反JDK约束情况下实现对目标应用方法的无侵入运行时AOP拦截。 | repeater的核心能力是什么? 1. 通用录制/回放能力无侵入式录制HTTP/Java/Dubbo入参/返回值录制能力(业务系统无感知)基于TTL提供多线程子调用追踪,完整追踪一次请求的调用路径入口请求(HTTP/Dubbo/Java)流量回放、子调用(Java/Dubbo)返回值Mock能力2. 快速可扩展API实现录制/回放插件式架构提供标准接口,可通过配置/简单编码实现一类通用插件3. standalone工作模式无需依赖任何服务端/存储,可以单机工作,提供录制/回放能力repeater的可以应用到哪些场景?1. 业务快速回归基于线上流量的录制/回放,无需人肉准备自动化测试脚本、准备测试数据2. 线上问题排查录制回放提供"昨日重现"能力,还原线上真实场景到线下做问题排查和Debug动态方法入参/返回值录制,提供线上快速问题定位3. 压测流量准备0成本录制HTTP/Dubbo等入口流量,作为压测流量模型进行压测4. 实时业务监控动态业务监控,基于核心接口数据录制回流到平台,对接口返回数据正确性进行校验和监控 |
核心原理 | 事件驱动、类隔离策略、类增强策略 | 流量录制、流量回放 |
三 部署使用:
1.1 环境准备
安装包括 repeater 安装、repeater-console 安装
目前安装和使用,需要 mac 或者 linux 系统下进行,如果在 windows 下进行可能会遇到安装路径出错导致安装失败或者运行失败的情况。
- linux/Mac os
- jdk 1.8+
- maven 3.2+
- 数据库 mysql 5.7+(repeater-console 可能用到)
PS:如果只是想简单运行,可以直接使用官方版本,参考官方用户手册,以standalone
模式把玩。
下载源码:
[root@k8s-worker27-65 jvm-sandbox-repeater]# git clone https://github.com/alibaba/jvm-sandbox-repeater.git
standalone 快速开始
[root@k8s-worker27-65 bin]# cat bootstrap.sh
#!/usr/bin/env bash
# exit shell with err_code
# $1 : err_code
# $2 : err_msg
typeset HOME=/opt/data/fll
exit_on_err()
{
[[ ! -z "${2}" ]] && echo "${2}" 1>&2
exit ${1}
}
PID=$(ps -ef | grep "repeater-bootstrap.jar" | grep "java" | grep -v grep | awk '{print $2}')
expr ${PID} "+" 10 &> /dev/null
# if occurred error,exit
if [ ! $? -eq 0 ] || [ "" = "${PID}" ] ;then
echo ""
else
echo "found target pid exist, pid is ${PID}, kill it..."
kill -9 ${PID}
fi
if [ ! -f "${HOME}/sandbox/sandbox-module/repeater-bootstrap.jar" ]; then
echo "repeater-bootstrap.jar not found, try to install";
sh ./install-local.sh || exit_on_err 1 "install repeater failed"
fi
${JAVA_HOME}/bin/java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000 \
-javaagent:${HOME}/sandbox/lib/sandbox-agent.jar=server.port=8820\;server.ip=0.0.0.0 \
-Dapp.name=jettopro \
-Dapp.env=sit \
-jar ${HOME}/sandbox/sandbox-module/repeater-bootstrap.jar
[root@k8s-worker27-65 bin]# cat install-local.sh
#!/usr/bin/env bash
# repeater's target dir
REPEATER_TARGET_DIR=../target/repeater
typeset HOME=/opt/data/fll
typeset SANDBOX_HOME=/opt/data/fll/sandbox
# exit shell with err_code
# $1 : err_code
# $2 : err_msg
exit_on_err()
{
[[ ! -z "${2}" ]] && echo "${2}" 1>&2
exit ${1}
}
# package
sh ./package.sh || exit_on_err 1 "install failed cause package failed"
# extract sandbox to ${HOME}
#curl -s https://github.com/alibaba/jvm-sandbox-repeater/releases/download/v1.0.0/sandbox-1.3.3-bin.tar | tar xz -C ${HOME} || exit_on_err 1 "extract sandbox failed"
cat sandbox-1.3.3-bin.tar | tar xz -C ${HOME} || exit_on_err 1 "extract sandbox failed"
#cat sandbox-1.3.3-bin.tar | tar xz -C ${HOME} || exit_on_err 1 "extract sandbox failed"
# copy module to ~/.sandbox-module
#mkdir -p ${HOME}/.sandbox-module || exit_on_err 1 "permission denied, can not mkdir ~/.sandbox-module"
#cp -r ${REPEATER_TARGET_DIR}/* ${HOME}/.sandbox-module || exit_on_err 1 "permission denied, can not copy module to ~/.sandbox-module"
cp -r ${REPEATER_TARGET_DIR}/* ${SANDBOX_HOME}/sandbox-module || exit_on_err 1 "permission denied, can not copy module to ${SANDBOX_HOME}/sandbox-module"
[root@k8s-worker27-65 bin]# cat package.sh
#!/usr/bin/env bash
# repeater's target dir
REPEATER_TARGET_DIR=../target/repeater
# exit shell with err_code
# $1 : err_code
# $2 : err_msg
exit_on_err()
{
[[ ! -z "${2}" ]] && echo "${2}" 1>&2
exit ${1}
}
# maven package the sandbox
mvn clean package -Dmaven.test.skip=true -f ../pom.xml || exit_on_err 1 "package repeater failed."
mkdir -p ${REPEATER_TARGET_DIR}/plugins
mkdir -p ${REPEATER_TARGET_DIR}/cfg
cp ./repeater-logback.xml ${REPEATER_TARGET_DIR}/cfg/repeater-logback.xml \
&& cp ./repeater.properties ${REPEATER_TARGET_DIR}/cfg/repeater.properties \
&& cp ./repeater-config.json ${REPEATER_TARGET_DIR}/cfg/repeater-config.json \
&& cp ../repeater-module/target/repeater-module-*-jar-with-dependencies.jar ${REPEATER_TARGET_DIR}/repeater-module.jar \
&& cp ../repeater-console/repeater-console-start/target/repeater-console.jar ${REPEATER_TARGET_DIR}/repeater-bootstrap.jar \
&& cp ../repeater-plugins/ibatis-plugin/target/ibatis-plugin-*-jar-with-dependencies.jar ${REPEATER_TARGET_DIR}/plugins/ibatis-plugin.jar \
&& cp ../repeater-plugins/java-plugin/target/java-plugin-*-jar-with-dependencies.jar ${REPEATER_TARGET_DIR}/plugins/java-plugin.jar \
&& cp ../repeater-plugins/mybatis-plugin/target/mybatis-plugin-*-jar-with-dependencies.jar ${REPEATER_TARGET_DIR}/plugins/mybatis-plugin.jar \
&& cp ../repeater-plugins/dubbo-plugin/target/dubbo-plugin-*-jar-with-dependencies.jar ${REPEATER_TARGET_DIR}/plugins/dubbo-plugin.jar \
&& cp ../repeater-plugins/redis-plugin/target/redis-plugin-*-jar-with-dependencies.jar ${REPEATER_TARGET_DIR}/plugins/redis-plugin.jar \
&& cp ../repeater-plugins/http-plugin/target/http-plugin-*-jar-with-dependencies.jar ${REPEATER_TARGET_DIR}/plugins/http-plugin.jar \
&& cp ../repeater-plugins/hibernate-plugin/target/hibernate-plugin-*-jar-with-dependencies.jar ${REPEATER_TARGET_DIR}/plugins/hibernate-plugin.jar \
&& cp ../repeater-plugins/spring-data-jpa-plugin/target/spring-data-jpa-plugin-*-jar-with-dependencies.jar ${REPEATER_TARGET_DIR}/plugins/spring-data-jpa-plugin.jar
# tar the repeater.tar
cd ../target/
tar -zcvf repeater-stable-bin.tar repeater/
cd -
echo "package repeater-stable-bin.tar finish."
repeater-logback.xml :
[root@k8s-worker27-65 bin]# cat repeater-logback.xml
<?xml version="1.0" encoding="UTF-8" ?>
<configuration scan="true" scanPeriod="10000">
<appender name="REPEATER-FILE-APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/opt/data/fll/sandbox/logs/sandbox/repeater/repeater.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>/opt/data/fll/sandbox/logs/sandbox/repeater/repeater.log.%d{yyyy-MM-dd}</FileNamePattern>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="REPEATER-FILE-APPENDER"/>
</root>
</configuration>
[root@k8s-worker27-65 bin]# cat repeater-config.json
{
"useTtl" : true,
"degrade" : false,
"exceptionThreshold" : 1000,
"sampleRate" : 10000,
"pluginsPath" : null,
"httpEntrancePatterns" : [ "^/greeting.*$" ],
"javaEntranceBehaviors" : [ {
"classPattern" : "hello.GreetingController",
"methodPatterns" : [ "greeting" ],
"includeSubClasses" : false
} ],
"javaSubInvokeBehaviors" : [],
"pluginIdentities" : [ "http", "java-entrance", "java-subInvoke", "mybatis", "ibatis" ],
"repeatIdentities" : [ "java", "http" ]
}
repeater.properties:
[root@k8s-worker27-65 bin]# cat repeater.properties
# 录制消息投递地址
broadcaster.record.url=http://192.168.1.65:8001/facade/api/record/save
# 回放结果投递地址
broadcaster.repeat.url=http://192.168.1.65:8001/facade/api/repeat/save
# 回放消息取数据地址
repeat.record.url=http://192.168.1.65:8001/facade/api/record/%s/%s
# 配置文件拉取地址
repeat.config.url=http://192.168.1.65:8001/facade/api/config/%s/%s
# 心跳上报配置
repeat.heartbeat.url=http://192.168.1.65:8001/module/report.json
# 是否开启脱机工作模式
repeat.standalone.mode=false
# 是否开启spring advice拦截
repeat.spring.advice.switch=false;
启动:
启动之前:
[root@k8s-worker27-65 jvm-sandbox-repeater]# cat bin/repeater.properties
# 录制消息投递地址
broadcaster.record.url=http://127.0.0.1:8001/facade/api/record/save
# 回放结果投递地址
broadcaster.repeat.url=http://127.0.0.1:8001/facade/api/repeat/save
# 回放消息取数据地址
repeat.record.url=http://127.0.0.1:8001/facade/api/record/%s/%s
# 配置文件拉取地址
repeat.config.url=http://127.0.0.1:8001/facade/api/config/%s/%s
# 心跳上报配置
repeat.heartbeat.url=http://127.0.0.1:8001/module/report.json
# 是否开启脱机工作模式
repeat.standalone.mode=true
# 是否开启spring advice拦截
repeat.spring.advice.switch=false;
是否开启脱机工作模式
repeat.standalone.mode=true 单击模式,且
[root@k8s-worker27-65 bin]# cat install-local.sh
#!/usr/bin/env bash
# repeater's target dir
REPEATER_TARGET_DIR=../target/repeater
# exit shell with err_code
# $1 : err_code
# $2 : err_msg
exit_on_err()
{
[[ ! -z "${2}" ]] && echo "${2}" 1>&2
exit ${1}
}
# package
sh ./package.sh || exit_on_err 1 "install failed cause package failed"
# extract sandbox to ${HOME}
#curl -s https://github.com/alibaba/jvm-sandbox-repeater/releases/download/v1.0.0/sandbox-1.3.3-bin.tar | tar xz -C ${HOME} || exit_on_err 1 "extract sandbox failed"
cat sandbox-1.3.3-bin.tar | tar xz -C ${HOME} || exit_on_err 1 "extract sandbox failed"
# copy module to ~/.sandbox-module
mkdir -p ${HOME}/.sandbox-module || exit_on_err 1 "permission denied, can not mkdir ~/.sandbox-module"
cp -r ${REPEATER_TARGET_DIR}/* ${HOME}/.sandbox-module || exit_on_err 1 "permission denied, can not copy module to ~/.sandbox-module"
cat sandbox-1.3.3-bin.tar | tar xz -C ${HOME} || exit_on_err 1 "extract sandbox failed" curl下载不下来,自己想办法然后用本地的
[root@k8s-worker27-65 bin]# ./bootstrap.sh
step1 开始录制
[root@k8s-worker27-65 sandbox]# curl -s 'http://192.168.1.65:8001/regress/slogan?Repeat-TraceId=127000000001156034386424510000ed'
<h1 align="center" style="color:red;margin-top:300px">JAVA是世界上最好的语言!</h1>
执行结果如下:
访问链接时,repeater 插件通过 Repeat-TraceId=127000000001156034386424510000ed,唯一追踪到了这一次请求,后台服务返回了
JAVA是世界上最好的语言!
,repeater 把画面定格在了这一秒并将结果和 firstId 绑定
[root@k8s-worker27-65 sandbox]# curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId=127000000001156034386424510000ed'
<h1 align="center" style="color:red;margin-top:300px">Javascript是世界上最好的语言!</h1>[root@k8s-worker27-65 sandbox]#
[root@k8s-worker27-65 sandbox]# curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId=127000000001156034386424510000ed'
<h1 align="center" style="color:red;margin-top:300px">GO是世界上最好的语言!</h1>[root@k8s-worker27-65 sandbox]#
浏览器:
step2 开始回放
[root@k8s-worker27-65 sandbox]# curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId-X=127000000001156034386424510000ed'
<h1 align="center" style="color:red;margin-top:300px">GO是世界上最好的语言!</h1>[root@k8s-worker27-65 sandbox]#
[root@k8s-worker27-65 sandbox]# curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId-X=127000000001156034386424510000ed'
<h1 align="center" style="color:red;margin-top:300px">GO是世界上最好的语言!</h1>[root@k8s-worker27-65 sandbox]#
[root@k8s-worker27-65 sandbox]# curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId-X=127000000001156034386424510000ed'
<h1 align="center" style="color:red;margin-top:300px">GO是世界上最好的语言!</h1>[root@k8s-worker27-65 sandbox]#
浏览器:
无论我们多少次访问这个地址,都将返回 Repeat-TraceId=127000000001156034386424510000ed 绑定的录制信息JAVA是世界上最好的语言!
;如果重新访问Slogan后又会将最新的返回结果绑定到 Repeat-TraceId=127000000001156034386424510000ed(为了快速演示,将链路追踪的标志提到参数中进行透传了)
光是执行官方用例,当然不能满足我们需要啦。我们来测试下,如果有多个 Repeat-TraceId ,是否可以分别录制?
# 录制一个 128 开头的 traceId ,返回结果是 java
[root@k8s-worker27-65 sandbox]# curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId=128000000001156034386424510000ed'
<h1 align="center" style="color:red;margin-top:300px">JAVA是世界上最好的语言!</h1>您在 /var/spool/mail/root 中有新邮件
# 录制一个 129 开头的 traceId ,返回结果是 Python
[root@k8s-worker27-65 sandbox]# curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId=129000000001156034386424510000ed'
<h1 align="center" style="color:red;margin-top:300px">Python是世界上最好的语言!</h1>[root@k8s-worker27-65 sandbox]#
# 回放前面 128 2次 的流量 结果相同
[root@k8s-worker27-65 sandbox]# 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>[root@k8s-worker27-65 sandbox]#
[root@k8s-worker27-65 sandbox]# 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>[root@k8s-worker27-65 sandbox]#
# 回放前面 129 2次 的流量 结果相同
[root@k8s-worker27-65 sandbox]# 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>[root@k8s-worker27-65 sandbox]#
[root@k8s-worker27-65 sandbox]# 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>[root@k8s-worker27-65 sandbox]#
看来确实是有效的。
录制目标服务
前面只是个简单的练手,实际用不用得了,当然的实际项目说话啦。
为了简单,此处使用了几个 spring boot 的示例项目当做实际项目使用。
- restful-api
Getting Started | Building a RESTful Web Service
项目地址:https://github.com/chenhengjie123/gs-rest-service(官方文档:Getting Started | Building a RESTful Web Service ,在官方的基础上增加了请求日志打印的功能,便于查看回放效果)
clone 后,直接用 complete
里面的完整示例,当做被测程序。
[root@k8s-worker27-65 gs-rest-service]# git clone https://github.com/spring-guides/gs-rest-service.git
切换到2.1.6.RELEASE tag
[root@k8s-worker27-65 gs-rest-service]# git branch -a
* (分离自 2.1.6.RELEASE)
completed
hide-show
main
no_cat_no_toc
refactor
remotes/origin/HEAD -> origin/main
remotes/origin/autowired-ctor
remotes/origin/boot-2.7
remotes/origin/categories
remotes/origin/completed
remotes/origin/gregturn-master
remotes/origin/hide-show
remotes/origin/main
remotes/origin/no_cat_no_toc
remotes/origin/refactor
[root@k8s-worker27-65 gs-rest-service]# git tag
0.1.0
1.4.1.RELEASE
1.4.2.RELEASE
1.4.3.RELEASE
1.5.1.RELEASE
1.5.10.RELEASE
1.5.2.RELEASE
1.5.5.RELEASE
1.5.9.RELEASE
2.0.0.RELEASE
2.0.1.RELEASE
2.0.2.RELEASE
2.0.3.RELEASE
2.0.5.RELEASE
2.0.8.RELEASE
2.1.3.RELEASE
2.1.4.RELEASE
2.1.6.RELEASE
edgware.release
edgware.sr2
finchley.sr2
maven
编译:
Build an executable JAR
You can run the application from the command line with Gradle or Maven. You can also build a single executable JAR file that contains all the necessary dependencies, classes, and resources and run that. Building an executable jar makes it easy to ship, version, and deploy the service as an application throughout the development lifecycle, across different environments, and so forth.
If you use Gradle, you can run the application by using ./gradlew bootRun. Alternatively, you can build the JAR file by using ./gradlew build and then run the JAR file, as follows:
java -jar build/libs/gs-rest-service-0.1.0.jar
If you use Maven, you can run the application by using ./mvnw spring-boot:run. Alternatively, you can build the JAR file with ./mvnw clean package and then run the JAR file, as follows:
java -jar target/gs-rest-service-0.1.0.jar
访问:
http://localhost:8080/greeting
启动:
[root@k8s-worker27-65 target]# ls
classes generated-test-sources gs-rest-service-0.1.0.jar.original maven-status surefire-reports
generated-sources gs-rest-service-0.1.0.jar maven-archiver nohup.out test-classes
[root@k8s-worker27-65 target]# java -jar gs-rest-service-0.1.0.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.6.RELEASE)
2023-09-19 14:43:28.201 INFO 9844 --- [ main] hello.Application : Starting Application v0.1.0 on k8s-worker27-65 with PID 9844 (/root/work/traffic/app/gs-rest-service/complete/target/gs-rest-service-0.1.0.jar started by root in /root/work/traffic/app/gs-rest-service/complete/target)
2023-09-19 14:43:28.204 INFO 9844 --- [ main] hello.Application : The following profiles are active: dev
2023-09-19 14:43:29.328 INFO 9844 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2023-09-19 14:43:29.358 INFO 9844 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2023-09-19 14:43:29.358 INFO 9844 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.21]
2023-09-19 14:43:29.446 INFO 9844 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2023-09-19 14:43:29.446 INFO 9844 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1181 ms
2023-09-19 14:43:29.663 INFO 9844 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2023-09-19 14:43:29.856 INFO 9844 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2023-09-19 14:43:29.859 INFO 9844 --- [ main] hello.Application : Started Application in 2.142
浏览器访问:
http://192.168.1.65:8080/greeting
http://192.168.1.65:8080/greeting?name=wubo
程序本身功能:当请求 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
也可以在源码中:
[root@k8s-worker27-65 bin]# ./install-repeater.sh
可以省略,因为本地有源码直接编即可,无源码的时候需要从官网下载编译好的二进制jar
修改repeater配置文件:
源码目录:
[root@k8s-worker27-65 bin]# pwd
/root/work/traffic/wubo/jvm-sandbox-repeater/bin
[root@k8s-worker27-65 bin]# ls
bootstrap.sh install-local.sh package.sh repeater-logback.xml sandbox-1.3.3-bin.tar
health.sh install-repeater.sh repeater-config.json repeater.properties
注意:javaSubInvokeBehaviors,之前的配置遗漏了 javaSubInvokeBehaviors 的设定,会导致回放的时候返回没有被 mock 掉,看不出效果。下面为更正后的配置
javascript
[root@k8s-worker27-65 bin]# cat repeater-config.json
{
"useTtl" : true,
"degrade" : false,
"exceptionThreshold" : 1000,
"sampleRate" : 10000,
"pluginsPath" : null,
"httpEntrancePatterns" : [ "^/greeting.*$" ],
"javaEntranceBehaviors" : [],
"javaSubInvokeBehaviors" : [ {
"classPattern" : "hello.GreetingController",
"methodPatterns" : [ "greeting" ],
"includeSubClasses" : false
} ],
"pluginIdentities" : [ "http", "java-entrance", "java-subInvoke", "mybatis", "ibatis" ],
"repeatIdentities" : [ "java", "http" ]
}
录制回放配置字段这个配置,主要依赖com.alibaba.jvm.sandbox.repeater.plugin.domain.RepeaterConfig类,参考之前的博文,引用下字段配置说明,如下所示:具体的配置含义,官方提供的链接相对路径有问题。可以直接看这个链接:RepeaterConfig.java
配置名 | 配置含义 | 参数说明 | 备注 |
---|---|---|---|
pluginIdedentities | 录制所使用的插件列表,配置了相应的插件名称,才能启用对应类别插件类别的录制 | 插件名称有效值有:"http", "java-entrance", "java-subInvoke", "mybatis", "redis","ibatis","dubbo-consumer","dubbo-provider" | 1、插件配置生效还需要~/.sandbox-module/plugins/有对应的插件 jar 包。2、该参数有效值字段对应的取值是源码中实现了InvokePlugin的类的identity方法。 |
repeatIdentities | 回放所使用的插件列表,配置了对应的插件,才能进行对应类别的回放 | 插件名称有效值有:"http", java", "dubbo" | 1、插件配置生效还需要~/.sandbox-module/plugins/有对应的插件 jar 包。2、该参数有效值字段对应的取值是源码中实现了Repeater的类的identity方法。 |
httpEntrancePatterns | 需要录制和回放的 http 接口,需要同时在 pluginIdedentities 和 repeatIdentities 中都配置了http这个配置才生效 | 链接的路径 | 参数支持正则表达式:"^/alertService/.*$" |
javaSubInvokeBehaviors | 需要录制和 mock 的 java 方法的配置,需要 pluginIdedentities 配置了java-subInvoke这个配置才生效 | 类名、方法名、以及是否包含子方法(若为 true,则匹配该类下的所有子类或者实现类,实际是否可用,有待验证),支持正则表达式 | 如下配置的意思就是 com.test.server.utils 包下所有类和所有方法{"classPattern": "com.test.server.utils.","methodPatterns": [ "" ],"includeSubClasses": false} |
javaEntranceBehaviors | 需要录制和回放的 java 方法的入口,需要同时在 pluginIdedentities 配置了java-entrance以及 repeatIdentities 配置了java这个配置才生效 | 类名、方法名、以及是否包含子方法(若为 true,则匹配该类下的所有子类或者实现类,实际是否可用,有待验证),支持正则表达式 | 如下配置的意思就是 com.test.utils 包下所有类和所有方法{"classPattern": "com.test.utils.","methodPatterns": [ "" ],"includeSubClasses": false}如果该入口方法在某个 http 入口的调用链路下,可能不会被录制到,如 com.test.controller.hello() 方法,本身对应着 "/hello 的访问路径,则录制时无法录制到以这个 hello 方法为入口的 java 录制记录" |
pluginsPath | 插件路径 | String,默认填 null 即可 | 默认填 null 即可 |
exceptionThreshold | 异常发生阈值;默认 1000 当ExceptionAware感知到异常次数超过阈值后,会降级模块 | Integer,默认填 1000 即可 | 当前只使用过 1000,未出现过降级情况。当出现降级则不再进行任何录制。涉及的关键方法:com.alibaba.jvm.sandbox.repeater.plugin.core.impl.api.DefaultEventListener#access |
degrade | 开启之后,不进行录制,只处理回放请求 | boolean,默认填 false 即可 | 当前只使用过 false,按照字面理解就是当这个改为 true 之后,不再进行录制。涉及的关键方法:com.alibaba.jvm.sandbox.repeater.plugin.core.impl.api.DefaultEventListener#access |
useTtl | 是否开启 ttl 线程上下文切换,开启之后,才能将并发线程中发生的子调用记录下来,否则无法录制到并发子线程的子调用信息,原理是将住线程的 threadLocal 拷贝到子线程,执行任务完成后恢复 | boolean,默认填 true 即可 | 默认使用 true,开启线程跟踪 |
sampleRate | 采样率;最小粒度万分之一 | Integer 默认填 10000 即可 | 当前只使用过 10000,可以结合这个方法理解com.alibaba.jvm.sandbox.repeater.plugin.core.trace.TraceContext#inTimeSample |
在哪里调整录制回放配置
1)在非 standalone 模式下,会从 repeater-console 的 /facade/api/config/${appName}/${env}
接口中拉取配置。
2)在 standalone 模式下则读取~/.sandbox-module/cfg/repeater-config.json
下的配置。
非 standalone 模式下
按照官方提供的例子,修改为com.alibaba.repeater.console.start.controller.ConfigFacadeApi#getConfig
方法,重新组装RepeaterConfig
对象。
修改之后,repeater-console 获取配置的接口 需要重启后 才能返回修改后的配置内容。
package com.alibaba.repeater.console.start.controller;
/**
* {@link ConfigFacadeApi} Demo工程;作为repeater录制回放的配置管理服务
* <p>
*
* @author zhaoyb1990
*/
@RestController
@RequestMapping("/facade/api")
public class ConfigFacadeApi {
@RequestMapping("/config/{appName}/{env}")
public RepeaterResult<RepeaterConfig> getConfig(@PathVariable("appName") String appName,
@PathVariable("env") String env) {
// 自己存配置;目前直接Mock了一份
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("^/regress/.*$"));
// java入口方法
behaviors.add(new Behavior("com.alibaba.repeater.console.service.impl.RegressServiceImpl", "getRegress"));
config.setJavaEntranceBehaviors(behaviors);
List<Behavior> subBehaviors = Lists.newArrayList();
// java调用插件
subBehaviors.add(new Behavior("com.alibaba.repeater.console.service.impl.RegressServiceImpl", "getRegressInner"));
subBehaviors.add(new Behavior("com.alibaba.repeater.console.service.impl.RegressServiceImpl", "findPartner"));
subBehaviors.add(new Behavior("com.alibaba.repeater.console.service.impl.RegressServiceImpl", "slogan"));
config.setJavaSubInvokeBehaviors(subBehaviors);
config.setUseTtl(true);
return RepeaterResult.builder().success(true).message("operate success").data(config).build();
}
}
可以自行调整这个接口,改为读取文件的模式,这样可以做到修改配置不需要重启 repeater-console。
standalone 模式下
直接修改~/.sandbox-module/cfg/repeater-config.json
文件中的内容。
PS:修改后,如果本地重新安装了 repeater 则会恢复到没有修改的情况。如果想知道怎么样重新安装都不会被重置,那就看看 bin 目录下的install-local.sh
、package.sh
了解下安装过程都干了啥。
重启 repeater(可用)
repeater启动与关闭
章节中的启动与关闭相关命令,进行 repeater 重启。启动过程将会重新从 repeater-console 拉取配置。standalone 模式下也会重新读取配置。
使用 repeaterModule 中的接口更新配置(不可用,repeater 有缺陷有待完善)
在com.alibaba.jvm.sandbox.repeater.module.RepeaterModule
类中,实现了推送配置更新的接口。
可通过访问http://${repeater.ip}:${repeater.port}/sandbox/default/module/http/repeater/pushConfig
接口,将配置的内容序列化后传输过去。
但是由于 repeater 插件中只有 JavaSubInvokePlugin 插件实现了 onConfigChange 方法,所以这个接口功能并不完善。
repeater.properties
置文件主要是 repeater 是否以 standalone 模式运行,以及以非 standalone 模式运行时与 repeater-console 交互的 url 路径。
一般会在需要调整 repeater-console 地址的时候进行修改。repeat.standalone.mode
一般用 false,使用非 standalone 模式。
实际生效的配置是位于~/.sandbox-module/cfg 中的 repeater.properties。
在项目的 bin 目录下也有一份 repeater.properties,这份是在执行安装脚本的时候会被复制到~/.sandbox-module/cfg 下的。
每次修改这份配置,都需要重启 repeater 才能生效。
如下的配置,http://127.0.0.1:8001 为 repeater-console 的地址。
bootstrap.sh:需改目录结构
[root@k8s-worker27-65 bin]# mkdir -p /opt/data/fll/sandbox
[root@k8s-worker27-65 bin]# cat bootstrap.sh
#!/usr/bin/env bash
# exit shell with err_code
# $1 : err_code
# $2 : err_msg
typeset HOME=/opt/data/fll
exit_on_err()
{
[[ ! -z "${2}" ]] && echo "${2}" 1>&2
exit ${1}
}
PID=$(ps -ef | grep "repeater-bootstrap.jar" | grep "java" | grep -v grep | awk '{print $2}')
expr ${PID} "+" 10 &> /dev/null
# if occurred error,exit
if [ ! $? -eq 0 ] || [ "" = "${PID}" ] ;then
echo ""
else
echo "found target pid exist, pid is ${PID}, kill it..."
kill -9 ${PID}
fi
if [ ! -f "${HOME}/sandbox/sandbox-module/repeater-bootstrap.jar" ]; then
echo "repeater-bootstrap.jar not found, try to install";
sh ./install-local.sh || exit_on_err 1 "install repeater failed"
fi
${JAVA_HOME}/bin/java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000 \
-javaagent:${HOME}/sandbox/lib/sandbox-agent.jar=server.port=8820\;server.ip=0.0.0.0 \
-Dapp.name=jettopro \
-Dapp.env=sit \
-jar ${HOME}/sandbox/sandbox-module/repeater-bootstrap.jar
[root@k8s-worker27-65 bin]#
install-local.sh:修改目录结构
[root@k8s-worker27-65 bin]# cat install-local.sh
#!/usr/bin/env bash
# repeater's target dir
REPEATER_TARGET_DIR=../target/repeater
typeset HOME=/opt/data/fll
typeset SANDBOX_HOME=/opt/data/fll/sandbox
# exit shell with err_code
# $1 : err_code
# $2 : err_msg
exit_on_err()
{
[[ ! -z "${2}" ]] && echo "${2}" 1>&2
exit ${1}
}
# package
sh ./package.sh || exit_on_err 1 "install failed cause package failed"
# extract sandbox to ${HOME}
#curl -s https://github.com/alibaba/jvm-sandbox-repeater/releases/download/v1.0.0/sandbox-1.3.3-bin.tar | tar xz -C ${HOME} || exit_on_err 1 "extract sandbox failed"
cat sandbox-1.3.3-bin.tar | tar xz -C ${HOME} || exit_on_err 1 "extract sandbox failed"
# copy module to ~/.sandbox-module
#mkdir -p ${HOME}/.sandbox-module || exit_on_err 1 "permission denied, can not mkdir ~/.sandbox-module"
#cp -r ${REPEATER_TARGET_DIR}/* ${HOME}/.sandbox-module || exit_on_err 1 "permission denied, can not copy module to ~/.sandbox-module"
cp -r ${REPEATER_TARGET_DIR}/* ${SANDBOX_HOME}/sandbox-module || exit_on_err 1 "permission denied, can not copy module to ${SANDBOX_HOME}/sandbox-module"
repeater.properties:# 是否开启脱机工作模式
repeat.standalone.mode=true
[root@k8s-worker27-65 bin]# cat repeater.properties
# 录制消息投递地址
broadcaster.record.url=http://127.0.0.1:8001/facade/api/record/save
# 回放结果投递地址
broadcaster.repeat.url=http://127.0.0.1:8001/facade/api/repeat/save
# 回放消息取数据地址
repeat.record.url=http://127.0.0.1:8001/facade/api/record/%s/%s
# 配置文件拉取地址
repeat.config.url=http://127.0.0.1:8001/facade/api/config/%s/%s
# 心跳上报配置
repeat.heartbeat.url=http://127.0.0.1:8001/module/report.json
# 是否开启脱机工作模式
repeat.standalone.mode=true
# 是否开启spring advice拦截
repeat.spring.advice.switch=false;
配置说明
repeater-logback.xml
该配置文件主要是控制 repeater 的日志打印路径地址以及打印等级。
一般会在需要调整日志等级的时候修改。
实际生效的配置是位于~/.sandbox-module/cfg 中的 repeater-logback.xml。
在项目的 bin 目录下也有一份 repeater-logback.xml,这份是在执行安装脚本的时候会被复制到~/.sandbox-module/cfg 下的。
每次修改这份配置,都需要重启 repeater 才能生效。
可以修改日志级别,默认是info
[root@k8s-worker27-65 bin]# cat repeater-logback.xml
<?xml version="1.0" encoding="UTF-8" ?>
<configuration scan="true" scanPeriod="10000">
<appender name="REPEATER-FILE-APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/opt/data/fll/sandbox/logs/sandbox/repeater/repeater.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>/opt/data/fll/sandbox/logs/sandbox/repeater/repeater.log.%d{yyyy-MM-dd}</FileNamePattern>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<root level="info">
<appender-ref ref="REPEATER-FILE-APPENDER"/>
</root>
</configuration>
启动repeater-bootstrap.jar
[root@k8s-worker27-65 bin]# ./bootstrap.sh
检测日志: 如果没有日志,需要先启动一下step2 attach sandbox
[root@k8s-worker27-65 sandbox]# tailf /opt/data/fll/sandbox/logs/sandbox/repeater/repeater.log
step2 attach sandbox 到目标进程
监测被测系统方式:
先到刚才 clone spring boot 示例项目的根目录,启动被测应用
[root@k8s-worker27-65 ~]# ps -ef |grep java
root 12449 30686 4 15:11 pts/1 00:00:27 java -jar gs-rest-service-0.1.0.jar
root 13573 30679 36 15:20 pts/0 00:00:30 java -jar repeater-bootstrap.jar
root 13684 12208 0 15:21 pts/4 00:00:00 grep --color=auto java
可以看到,进程 id 为 12449。然后开始 attach
cd ~/sandbox/bin
# 假设目标JVM进程号为'7306' 。-P 是设定 jvm-sandbox 的端口号,后面回放需要用到
[root@k8s-worker27-65 bin]# ./sandbox.sh -p 12449 -P 12580
NAMESPACE : default
VERSION : 1.3.3
MODE : ATTACH
SERVER_ADDR : 0.0.0.0
SERVER_PORT : 12580
UNSAFE_SUPPORT : ENABLE
SANDBOX_HOME : /root/sandbox/bin/..
SYSTEM_MODULE_LIB : /root/sandbox/bin/../module
USER_MODULE_LIB : /opt/data/fll/sandbox/sandbox-module;~/.sandbox-module;
SYSTEM_PROVIDER_LIB : /root/sandbox/bin/../provider
EVENT_POOL_SUPPORT : DISABLE
[root@k8s-worker27-65 bin]#
小技巧:上述的找进程 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
查看 repeater 日志看模块和插件加载情况
[root@k8s-worker27-65 bin]# tailf -200 ~/logs/sandbox/repeater/repeater.log
或者agent方式:
配置文件:
{
"useTtl" : true,
"degrade" : false,
"exceptionThreshold" : 1000,
"sampleRate" : 10000,
"pluginsPath" : null,
"httpEntrancePatterns" : [ "^/jettopro-basic/.*$" ],
"javaEntranceBehaviors" : [],
"javaSubInvokeBehaviors" : [ {
"classPattern" : "com.cn.jettech.jettoprobasic.controller.basiccontroller02.BasicController201",
"methodPatterns" : [ "basichello20101" ],
"includeSubClasses" : false
} ],
"pluginIdentities" : [ "http", "java-entrance", "java-subInvoke", "mybatis", "ibatis" ],
"repeatIdentities" : [ "java", "http" ]
}
{
"useTtl" : true,
"degrade" : false,
"exceptionThreshold" : 1000,
"sampleRate" : 10000,
"pluginsPath" : null,
"httpEntrancePatterns" : [ "^/greeting.*$" ],
"javaEntranceBehaviors" : [],
"javaSubInvokeBehaviors" : [ {
"classPattern" : "hello.GreetingController",
"methodPatterns" : [ "greeting" ],
"includeSubClasses" : false
} ],
"pluginIdentities" : [ "http", "java-entrance", "java-subInvoke", "mybatis", "ibatis" ],
"repeatIdentities" : [ "java", "http" ]
}
注意事项:
1) sandbox-agent.jar :自己安装的位置进行配置
2)录制应用名、录制环境 :与console配置管理中的一致
3)repeater启动端口 :未在进程中使用过的
4)application.jar:被测应用
java
-javaagent:${HOME}/sandbox/lib/sandbox-agent.jar=server.port=${repeater启动端口}\;server.ip=0.0.0.0 \
-Dapp.name=${录制应用名} \
-Dapp.env=${录制环境} \
-jar application.jar
[root@k8s-worker27-65 target]# ls /opt/data/fll/sandbox/lib/
sandbox-agent.jar sandbox-core.jar sandbox-spy.jar
注册两个
[root@k8s-worker27-65 target]# nohup java -javaagent:/opt/data/fll/sandbox/lib/sandbox-agent.jar=server.port=12580\;server.ip=192.168.1.65 -Dapp.name=gs -Dapp.env=sit -jar /root/work/traffic/app/gs-rest-service/complete/target/gs-rest-service-0.1.0.jar &
[root@k8s-worker27-65 app]# nohup java -javaagent:/opt/data/fll/sandbox/lib/sandbox-agent.jar=server.port=12581\;server.ip=192.168.1.65 -Dapp.name=jettopro -Dapp.env=dev -jar /root/work/traffic/app/jettopro-basic-0.0.1-SNAPSHOT.jar &
此时已经注册上来了有心跳了
step3 开始录制和回放
录制几个请求:
javascript
[root@k8s-worker27-65 sandbox]# curl -s 'http://localhost:8080/greeting?name=jettech01'
{"id":57,"content":"Hello, jettech01!"}
[root@k8s-worker27-65 sandbox]# curl -s 'http://localhost:8080/greeting?name=jettech02'
{"id":58,"content":"Hello, jettech02!"}
[root@k8s-worker27-65 sandbox]# curl -s 'http://localhost:8080/greeting?name=jettech03'
{"id":59,"content":"Hello, jettech03!"}
对应看到 repeater 的日志增加了几个输出:
javascript
root@k8s-worker27-65 sandbox]# tailf /opt/data/fll/sandbox/logs/sandbox/repeater/repeater.log
at com.alibaba.jvm.sandbox.repeater.module.RepeaterModule.access$500(RepeaterModule.java:64)
at com.alibaba.jvm.sandbox.repeater.module.RepeaterModule$1.run(RepeaterModule.java:142)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
2023-09-19 16:58:13 INFO enable plugin mybatis success
2023-09-19 16:58:13 INFO add watcher success,type=mybatis,watcherId=1004
2023-09-19 16:58:13 INFO enable plugin http success
2023-09-19 16:58:13 INFO add watcher success,type=http,watcherId=1006
2023-09-19 16:58:13 INFO register event bus success in repeat-register
2023-09-19 17:19:09 INFO broadcast success,traceId=192168001065169511514921110001ed,resp=success
2023-09-19 17:19:12 INFO broadcast success,traceId=192168001065169511515248010002ed,resp=success
2023-09-19 17:19:18 INFO broadcast success,traceId=192168001065169511515821410003ed,resp=success
好了,试试回放。
录播的数据在本地,当然也可以纯在数据库里面,后面会存
javascript
[root@k8s-worker27-65 sandbox]# ls /opt/data/fll/sandbox/sandbox-module/repeater-data/record/
192168001065169511514921110001ed 192168001065169511515248010002ed 192168001065169511515821410003ed
如:其实是序列化之后存起来
javascript
[root@k8s-worker27-65 sandbox]# cat /opt/data/fll/sandbox/sandbox-module/repeater-data/record/192168001065169511514921110001ed
QzA6Y29tLmFsaWJhYmEuanZtLnNhbmRib3gucmVwZWF0ZXIucGx1Z2luLmRvbWFpbi5SZWNvcmRNb2RlbJcOc3ViSW52b2NhdGlvbnMSZW50cmFuY2VJbnZvY2F0aW9uB3RyYWNlSWQEaG9zdAtlbnZpcm9ubWVudAdhcHBOYW1lCXRpbWVzdGFtcGB5QzA5Y29tLmFsaWJhYmEuanZtLnNhbmRib3gucmVwZWF0ZXIucGx1Z2luLmRvbWFpbi5JbnZvY2F0aW9unQhpZGVudGl0eQR0eXBlDnNlcmlhbGl6ZVRva2VuA2VuZAVzdGFydBN0aHJvd2FibGVTZXJpYWxpemVkEnJlc3BvbnNlU2VyaWFsaXplZBFyZXF1ZXN0U2VyaWFsaXplZAhlbnRyYW5jZQVpbmRleAd0cmFjZUlkCXByb2Nlc3NJZAhpbnZva2VJZGFDMDdjb20uYWxpYmFiYS5qdm0uc2FuZGJveC5yZXBlYXRlci5wbHVnaW4uZG9tYWluLklkZW50aXR5kQN1cmliME1qYXZhOi8vaGVsbG8uR3JlZXRpbmdDb250cm9sbGVyL2dyZWV0aW5nfihMamF2YS9sYW5nL1N0cmluZzspTGhlbGxvL0dyZWV0aW5nO0MwOWNvbS5hbGliYWJhLmp2bS5zYW5kYm94LnJlcGVhdGVyLnBsdWdpbi5kb21haW4uSW52b2tlVHlwZZEEbmFtZWMEamF2YTA2b3JnLnNwcmluZ2ZyYW1ld29yay5ib290LmxvYWRlci5MYXVuY2hlZFVSTENsYXNzTG9hZGVyTAAAAYqsvH/VTAAAAYqsvH+nTjBEUXc1b1pXeHNieTVIY21WbGRHbHVaNUlIWTI5dWRHVnVkQUpwWkdBUlNHVnNiRzhzSUdwbGRIUmxZMmd3TVNINE9RPT0cY1FkYmIySnFaV04wQ1dwbGRIUmxZMmd3TVE9PUaRMCAxOTIxNjgwMDEwNjUxNjk1MTE1MTQ5MjExMTAwMDFlZMvqy+pDMD1jb20uYWxpYmFiYS5qdm0uc2FuZGJveC5yZXBlYXRlci5wbHVnaW4uZG9tYWluLkh0dHBJbnZvY2F0aW9upghpZGVudGl0eQR0eXBlCXBhcmFtc01hcAdoZWFkZXJzDnNlcmlhbGl6ZVRva2VuA2VuZAVzdGFydBN0aHJvd2FibGVTZXJpYWxpemVkEnJlc3BvbnNlU2VyaWFsaXplZBFyZXF1ZXN0U2VyaWFsaXplZAhlbnRyYW5jZQVpbmRleAd0cmFjZUlkCXByb2Nlc3NJZAhpbnZva2VJZAVhc3luYwRib2R5C2NvbnRlbnRUeXBlBm1ldGhvZARwb3J0CnJlcXVlc3RVUkkKcmVxdWVzdFVSTGRiEWh0dHA6Ly8vZ3JlZXRpbmcvYwRodHRwSARuYW1lcQdbc3RyaW5nCWpldHRlY2gwMVpIBGhvc3QObG9jYWxob3N0OjgwODAKdXNlci1hZ2VudAtjdXJsLzcuMjkuMAZhY2NlcHQDKi8qWjA2b3JnLnNwcmluZ2ZyYW1ld29yay5ib290LmxvYWRlci5MYXVuY2hlZFVSTENsYXNzTG9hZGVyTAAAAYqsvH/ZTAAAAYqsvH+kTjA4TUNkN0ltbGtJam8xTnl3aVkyOXVkR1Z1ZENJNklraGxiR3h2TENCcVpYUjBaV05vTURFaEluMD0xHGNRZGJiMkpxWldOMFNBZG9aV0ZrWlhKelNBUm9iM04wRG14dlkyRnNhRzl6ZERvNE1EZ3dDblZ6WlhJdFlXZGxiblFMWTNWeWJDODNMakk1TGpBR1lXTmpaWEIwQXlvdktsb0pjR0Z5WVcxelRXRndTQVJ1WVcxbGNRZGJjM1J5YVc1bkNXcGxkSFJsWTJnd01Wb0diV1YwYUc5a0EwZEZWQVJ3YjNKMDFCK1FDbkpsY1hWbGMzUlZVa3dlYUhSMGNEb3ZMMnh2WTJGc2FHOXpkRG80TURnd0wyZHlaV1YwYVc1bkNuSmxjWFZsYzNSVlVra0pMMmR5WldWMGFXNW5CR0p2WkhrQUMyTnZiblJsYm5SVWVYQmxUbG89VJEwIDE5MjE2ODAwMTA2NTE2OTUxMTUxNDkyMTExMDAwMWVky+nL6UYATgNHRVTUH5AJL2dyZWV0aW5nHmh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9ncmVldGluZzAgMTkyMTY4MDAxMDY1MTY5NTExNTE0OTIxMTEwMDAxZWQMMTkyLjE2OC4xLjY1B3Vua25vd24HdW5rbm93bkwAAAGKrLx/pA==[root@k8s-worker27-65 sandbox]#
方式一:利用模块暴露的 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 传递的,必须录制和回放都用一样的参数)
javascript
[root@k8s-worker27-65 sandbox]# curl -s 'http://localhost:8080/greeting?name=jettech01' -H "Repeat-TraceId-X:192168001065169511514921110001ed"
{"id":57,"content":"Hello, jettech01!"}
[root@k8s-worker27-65 sandbox]# curl -s 'http://localhost:8080/greeting?name=jettech02' -H "Repeat-TraceId-X:192168001065169511515248010002ed"
{"id":58,"content":"Hello, jettech02!"}
[root@k8s-worker27-65 sandbox]# curl -s 'http://localhost:8080/greeting?name=jettech03' -H "Repeat-TraceId-X:192168001065169511515821410003ed"
{"id":59,"content":"Hello, jettech03!"}
id 还在递增,回放没生效。但看了下 plugin 的源码 ,确实是有这样的逻辑。而且上面两个请求发出的时候, repeater.log 并没有输出录制到请求的日志。
20190710 更新:问题已解决,原因是前面的 repeater.json 配置不正确,遗漏了 javaSubInvokeBehaviors
相关配置,导致返回值没有被录制到。
修正后,已经可以输出正确的返回了。此时 repeater.log 也会对应输出日志:
javascript
tailf /opt/data/fll/sandbox/logs/sandbox/repeater/repeater.log
2023-09-19 17:19:09 INFO broadcast success,traceId=192168001065169511514921110001ed,resp=success
2023-09-19 17:19:12 INFO broadcast success,traceId=192168001065169511515248010002ed,resp=success
2023-09-19 17:19:18 INFO broadcast success,traceId=192168001065169511515821410003ed,resp=success
2023-09-19 17:22:18 INFO find target invocation by PARAMETER_MATCH,identity=java://hello.GreetingController/greeting~(Ljava/lang/String;)Lhello/Greeting;,invocation=com.alibaba.jvm.sandbox.repeater.plugin.domain.Invocation@7fe7d33e
2023-09-19 17:22:34 INFO find target invocation by PARAMETER_MATCH,identity=java://hello.GreetingController/greeting~(Ljava/lang/String;)Lhello/Greeting;,invocation=com.alibaba.jvm.sandbox.repeater.plugin.domain.Invocation@f5ea90e
2023-09-19 17:22:50 INFO find target invocation by PARAMETER_MATCH,identity=java://hello.GreetingController/greeting~(Ljava/lang/String;)Lhello/Greeting;,invocation=com.alibaba.jvm.sandbox.repeater.plugin.domain.Invocation@1ae27c65
最后三行就是对应返回录制的 response 了
方式三:使用 repeater-console 做回放
官方文档没有明确给出这个方式,但通过查看 repeater-console 里面的 readme ,可以看到它也是有暴露接口供调用的。因此也试试。
结果看了下,里面提供的 standalone 和 mysql 两种数据存储方式,都不支持前面回放的存储方法(存在 ~/.sandbox-module/repeater-data/record
中)。还得调整录制方式才能进行回放。
整体结构还是比较清晰的,有 plugin 目录,便于扩展。也有 console 提供最简要的流量管理。更详细的,后续再慢慢研究。
repeater-console 简介
官方的说明:
jvm-sandbox-repeater 仅仅提供了录制回放的能力,如果需要完成
业务回归
、实时监控
、压测
等平台,后面须要有一个数据中心
负责采集数据的加工、存储、搜索,repeater-console 提供了简单的 demo 示例;一个模块管理
平台负责管理 JVM-Sandbox 各模块生命周期;一个配置管理
平台负责维护和推送 jvm-sandbox-repeater 采集所须要的各种配置变更注意:目前项目代码默认启动 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
javascript
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/xxxxxxxxxxxxxxxxx-H "RepeatId:xxxxxxxxxxxxxxxxx"
curl -s http://127.0.0.1:8001/facade/api/repeat/callback/xxxxxxxxxxxxxxxxx
所以只能通过解读源码来反推用法咯。
个人的源码阅读三步骤:明确阅读目的、了解整体架构、细读目标功能
step 0 明确阅读目的
目的很简单,使用 repeater-console ,在目前的 demo 项目上完成批量流量录制回放的功能
step 1 了解整体架构
为了便于描述,还是用 tree 吧。
特别说明:以下均为个人分析,并不保证正确哈。
javascript
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
-
添加录制的记录
javascript@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(); } }
-
添加回放的结果
javascript@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 ,获取序列化后的录制数据
javascript@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 执行回放记录
javascript@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 获取回放执行结果
javascript@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
javascript
[root@k8s-worker27-65 bin]# pwd
/root/work/traffic/wubo/jvm-sandbox-repeater/bin
[root@k8s-worker27-65 bin]# cat repeater.properties
# 录制消息投递地址
broadcaster.record.url=http://127.0.0.1:8001/facade/api/record/save
# 回放结果投递地址
broadcaster.repeat.url=http://127.0.0.1:8001/facade/api/repeat/save
# 回放消息取数据地址
repeat.record.url=http://127.0.0.1:8001/facade/api/record/%s/%s
# 配置文件拉取地址
repeat.config.url=http://127.0.0.1:8001/facade/api/config/%s/%s
# 心跳上报配置
repeat.heartbeat.url=http://127.0.0.1:8001/module/report.json
# 是否开启脱机工作模式
repeat.standalone.mode=false
# 是否开启spring advice拦截
repeat.spring.advice.switch=false;
3.修改:repeater-console/repeater-console-start/src/main/resources/application.properties 数据库地址
javascript
[root@k8s-worker27-65 jvm-sandbox-repeater]# cat repeater-console/repeater-console-start/src/main/resources/application.properties
spring.application.name=repeater-server
server.port=8001
mybatis.type-aliases-package=com.alibaba.repeater.console.dal.model
# 本地mysql数据源测试
spring.datasource.url=jdbc:mysql://192.168.1.65:3306/repeater?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456aA
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect
spring.jpa.show-sql=false
spring.jpa.hibernate.ddl-auto=none
# 使用本地数据源进行测试
console.use.localCache =false
# 示例回放地址(工程使用需要维护repeater插件的ip:port替换,指定ip发起回放)
repeat.repeat.url=http://%s:%s/sandbox/default/module/http/repeater/repeat
# 示例配置地址(工程使用需要维护repeater插件的ip:port替换,指定ip发起回放)
repeat.config.url=http://%s:%s/sandbox/default/module/http/repeater/pushConfig
# 示例重载地址(工程使用需要维护repeater插件的ip:port替换,指定ip发起回放)
repeat.reload.url=http://%s:%s/sandbox/default/module/http/repeater/reload
# velocity
spring.velocity.cache= false
spring.velocity.charset=UTF-8
spring.velocity.check-template-location=true
spring.velocity.content-type=text/html
spring.velocity.enabled=true
spring.velocity.resource-loader-path=classpath:/velocity/templates
spring.velocity.prefix=/velocity/templates/
spring.velocity.toolboxConfigLocation=/velocity/toolbox.xml
spring.velocity.suffix=.vm
-
创建数据库:
CREATE DATABASE IF NOT EXISTS repeater
DEFAULT CHARSET utf8
COLLATE utf8_general_ci;
DROP TABLE IF EXISTS record;
CREATE TABLE record
(
id BIGINT(20) NOT NULL AUTO_INCREMENT PRIMARY KEY
COMMENT '主键',
gmt_create DATETIME NOT NULL
COMMENT '创建时间',
gmt_record DATETIME NOT NULL
comment '录制时间',
app_name VARCHAR(255) NOT NULL
COMMENT '应用名',
environment VARCHAR(255) NOT NULL
COMMENT '环境信息',
host VARCHAR(36) NOT NULL
COMMENT '机器IP',
trace_id VARCHAR(32) NOT NULL
COMMENT '链路追踪ID',
entrance_desc VARCHAR(2000) NOT NULL
COMMENT '链路追踪ID',
wrapper_record LONGTEXT NOT NULL
COMMENT '记录序列化信息',
request LONGTEXT NOT NULL
COMMENT '请求参数JSON',
response LONGTEXT NOT NULL
COMMENT '返回值JSON'
)
ENGINE = InnoDB
COMMENT = '录制信息'
DEFAULT CHARSET = utf8
AUTO_INCREMENT = 1;DROP TABLE IF EXISTS replay;
CREATE TABLE replay
(
id BIGINT(20) NOT NULL AUTO_INCREMENT PRIMARY KEY
COMMENT '主键',
gmt_create DATETIME NOT NULL
COMMENT '创建时间',
gmt_modified DATETIME NOT NULL
comment '修改时间',
app_name VARCHAR(255) NOT NULL
COMMENT '应用名',
environment VARCHAR(255) NOT NULL
COMMENT '环境信息',
ip VARCHAR(36) NOT NULL
COMMENT '机器IP',
repeat_id VARCHAR(32) NOT NULL
COMMENT '回放ID',
status TINYINT NOT NULL
COMMENT '回放状态',
trace_id VARCHAR(32)
COMMENT '链路追踪ID',
cost BIGINT(20)
COMMENT '回放耗时',
diff_result LONGTEXT
COMMENT 'diff结果',
response LONGTEXT
COMMENT '回放结果',
mock_invocation LONGTEXT
COMMENT 'mock过程',
success BIT
COMMENT '是否回放成功',
record_id BIGINT(20)
COMMENT '外键')
ENGINE = InnoDB
COMMENT = '回放信息'
DEFAULT CHARSET = utf8
AUTO_INCREMENT = 1;DROP TABLE IF EXISTS module_info;
CREATE TABLE module_info
(
id BIGINT(20) NOT NULL AUTO_INCREMENT PRIMARY KEY
COMMENT '主键',
gmt_create DATETIME NOT NULL
COMMENT '创建时间',
gmt_modified DATETIME NOT NULL
comment '修改时间',
app_name VARCHAR(255) NOT NULL
COMMENT '应用名',
environment VARCHAR(255) NOT NULL
COMMENT '环境信息',
ip VARCHAR(36) NOT NULL
COMMENT '机器IP',
port VARCHAR(12) NOT NULL
COMMENT '链路追踪ID',
version VARCHAR(128) NOT NULL
COMMENT '模块版本号',
status VARCHAR(36) NOT NULL
COMMENT '模块状态'
)
ENGINE = InnoDB
COMMENT = '在线模块信息'
DEFAULT CHARSET = utf8
AUTO_INCREMENT = 1;DROP TABLE IF EXISTS module_config;
CREATE TABLE module_config
(
id BIGINT(20) NOT NULL AUTO_INCREMENT PRIMARY KEY
COMMENT '主键',
gmt_create DATETIME NOT NULL
COMMENT '创建时间',
gmt_modified DATETIME NOT NULL
comment '录制时间',
app_name VARCHAR(255) NOT NULL
COMMENT '应用名',
environment VARCHAR(255) NOT NULL
COMMENT '环境信息',
config LONGTEXT NOT NULL
COMMENT '配置信息'
)
ENGINE = InnoDB
COMMENT = '模块配置信息'
DEFAULT CHARSET = utf8
AUTO_INCREMENT = 1;
javascript
repeater-console/repeater-console-dal/src/main/resources/database.sql
5、修复官方仓库里 console 一些代码问题。
5.1 把 repeater-console/repeater-console-start/src/main/resources/velocity
下面的所有文件,查找 #parse("/blocks
,统一改替换为 #parse("blocks
。原有代码最前面带上 /
会导致引用找不到报错
5.2 修改 repeater-console/repeater-console-start/src/main/java/com/alibaba/repeater/console/start/controller/page/ReplayController.java
中的 return "/replay/detail";
,改为 return "replay/detail";
,去掉双引号里面第一个 /
javascript
[root@k8s-worker27-65 jvm-sandbox-repeater]# cat repeater-console/repeater-console-start/src/main/java/com/alibaba/repeater/console/start/controller/page/ReplayController.java |grep "replay/detail"
//return "/replay/detail";
return "replay/detail";
5.3
修改 repeater-console/repeater-console-start/src/main/java/com/alibaba/repeater/console/start/controller/test/RegressPageController.java
中的 return "/regress/index";
,改为 return "regress/index";
,去掉双引号里面第一个 /
javascript
[root@k8s-worker27-65 jvm-sandbox-repeater]# cat repeater-console/repeater-console-start/src/main/java/com/alibaba/repeater/console/start/controller/test/RegressPageController.java |grep "regress/index"
//return "/regress/index";
return "regress/index";
- 编译安装:
javascript
[root@k8s-worker27-65 bin]# ./install-local.sh
然后修改sandbox 的日志输位置
javascript
[root@k8s-worker27-65 sandbox]# cat /opt/data/fll/sandbox/cfg/sandbox-logback.xml
<?xml version="1.0" encoding="UTF-8" ?>
<configuration scan="true" scanPeriod="10000">
<appender name="SANDBOX-FILE-APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/opt/data/fll/sandbox/logs/sandbox/sandbox.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>/opt/data/fll/sandbox/logs/sandbox/sandbox.log.%d{yyyy-MM-dd}</FileNamePattern>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %SANDBOX_NAMESPACE %-5level %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<root level="info">
<appender-ref ref="SANDBOX-FILE-APPENDER"/>
</root>
</configuration>
7.启动console
javascript
[root@k8s-worker27-65 sandbox-module]# pwd
/opt/data/fll/sandbox/sandbox-module
[root@k8s-worker27-65 sandbox-module]# nohup java -jar repeater-bootstrap.jar &
浏览器访问:
javascript
http://192.168.1.65:8001/regress/index.htm
8.现在,借助界面来做一次录制回放吧。基本套路还是一样的:
8.1、在 console 增加配置,用于对接应用(注意这个和之前纯命令行有点不同,命令行是先完成第二步)
8.2、让 repeater 注入到被测应用,上报数据到 console
8.3、在 console 中操作,进行录制和回放
接下来,一步一步操作。
8.1、在 console 增加配置,用于对接应用
点击左侧的【配置管理】,添加如下配置:
应用名:unknown
环境:unknown
{
"useTtl" : true,
"degrade" : false,
"exceptionThreshold" : 1000,
"sampleRate" : 10000,
"pluginsPath" : null,
"httpEntrancePatterns" : [ "^/greeting.*$" ],
"javaEntranceBehaviors" : [],
"javaSubInvokeBehaviors" : [ {
"classPattern" : "hello.GreetingController",
"methodPatterns" : [ "greeting" ],
"includeSubClasses" : false
} ],
"pluginIdentities" : [ "http", "java-entrance", "java-subInvoke", "mybatis", "ibatis" ],
"repeatIdentities" : [ "java", "http" ]
}
点击【保存】,存下配置
8.2、让 repeater 注入到被测应用
[root@k8s-worker27-65 bin]# pwd;./sandbox.sh -p 12449 -P 12580
/opt/data/fll/sandbox/bin
NAMESPACE : default
VERSION : 1.3.3
MODE : ATTACH
SERVER_ADDR : 0.0.0.0
SERVER_PORT : 12580
UNSAFE_SUPPORT : ENABLE
SANDBOX_HOME : /opt/data/fll/sandbox/bin/..
SYSTEM_MODULE_LIB : /opt/data/fll/sandbox/bin/../module
USER_MODULE_LIB : /opt/data/fll/sandbox/sandbox-module;~/.sandbox-module;
SYSTEM_PROVIDER_LIB : /opt/data/fll/sandbox/bin/../provider
EVENT_POOL_SUPPORT : DISABLE
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 的【在线模块】,应该能看到增加了当前这个被测应用的心跳记录:
8.3、开始录制。给这个被测应用输送一些流量
手动发出2条请求,也可以在浏览器中出发
[root@k8s-worker27-65 bin]# curl -s 'http://localhost:8080/greeting'
{"id":60,"content":"Hello, World!"}您在 /var/spool/mail/root 中有新邮件
[root@k8s-worker27-65 bin]# curl -s 'http://localhost:8080/greeting?name=wubo1'
{"id":61,"content":"Hello, wubo1!"}
然后打开 console 的【在线流量】,能看到刚发出的两条请求已经录制下来了:
数据库:
8.4、回放请求。直接点击第一行末尾的回放按钮,进行回放:
详情:
回放:
然后,就可以看到回放结果了。稍等几秒后刷新下回放结果界面,就能看到执行结果
请求参数:
返回结果
子调用:
总结
官方的文档还是一如既往的少,代码里面也有点坑(对 velocity 不熟悉,上面的代码只是按自己理解改的,如果有更正确的修改姿势欢迎分享),界面和技术栈都用的比较小众和比较久远的的(spring-boot 17 年已经去掉对 velocity 模板引擎的支持了)。 而且一个批量回放功能还是只有按钮实际没做的。。。
不过也算是给到大家一个真正示例控制台该有的样子,把需要的元素和界面设计都基本给出了。如果想要开箱即用,对 http 接口进行简单的录制回放,可以使用这个带界面的 console 来试用一下。
附录:过程中的报错及解决
1、注入 repeater 到被测应用后,console 报错:
39333 --- [nio-8001-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.NullPointerException] with root cause
java.lang.NullPointerException: null
at com.alibaba.repeater.console.start.controller.api.ConfigFacadeApi.getConfig(ConfigFacadeApi.java:34) ~[classes!/:na]
at sun.reflect.GeneratedMethodAccessor77.invoke(Unknown Source) ~[na:na]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_25]
原因:上报心跳包后,appName 和 environment 和配置对不上。
解决:请确认有至少一个配置,appName 和 environment 都是 unknown
2、报错 org.apache.velocity.exception.ResourceNotFoundException: Unable to find resource '/blocks/pager.vm'] with root cause ,且界面打不开
原因:没有按照前面所述修改 console 源码,导致引用其他模板的部分根目录不正确。
解决:按照前面描述,把 #parse("/blocks
,统一改替换为 #parse("blocks
即可。