测试域: 流量回放-工具篇jvm-sandbox,jvm-sandbox-repeater,gs-rest-service

JVM-Sandbox

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 启动模式对比

模式优点不足
attach1. 不需要启停应用,即插即用,随时停止
2. 更新配置不需要重启应用
1. 进行 java 回放的时候可能由于无法获取到对应实例而回放失败
2. 如果需要对 repeater 进行 debug,需要将 repeater 代码嵌入到被录制应用的代码中,不方便 debug3. 不可进行录制应用名和录制环境的配置,会被默认标记为 unknown
agent1. 进行 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-SANDBOXjvm-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 快速开始

bootstrap.sh:

[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/fllexit_on_err()
{[[ ! -z "${2}" ]] && echo "${2}" 1>&2exit ${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}" ] ;thenecho ""
elseecho "found target pid exist, pid is ${PID}, kill it..."kill -9 ${PID}
fiif [ ! -f "${HOME}/sandbox/sandbox-module/repeater-bootstrap.jar" ]; thenecho "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

install-local.sh: 

[root@k8s-worker27-65 bin]# cat install-local.sh 
#!/usr/bin/env bash# repeater's target dir
REPEATER_TARGET_DIR=../target/repeatertypeset 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>&2exit ${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"

package.sh :

[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>&2exit ${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}/cfgcp ./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>&2exit ${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)completedhide-showmainno_cat_no_tocrefactorremotes/origin/HEAD -> origin/mainremotes/origin/autowired-ctorremotes/origin/boot-2.7remotes/origin/categoriesremotes/origin/completedremotes/origin/gregturn-masterremotes/origin/hide-showremotes/origin/mainremotes/origin/no_cat_no_tocremotes/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.jarIf 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 掉,看不出效果。下面为更正后的配置

[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.shpackage.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/fllexit_on_err()
{[[ ! -z "${2}" ]] && echo "${2}" 1>&2exit ${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}" ] ;thenecho ""
elseecho "found target pid exist, pid is ${PID}, kill it..."kill -9 ${PID}
fiif [ ! -f "${HOME}/sandbox/sandbox-module/repeater-bootstrap.jar" ]; thenecho "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/repeatertypeset 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>&2exit ${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 : defaultVERSION : 1.3.3MODE : ATTACHSERVER_ADDR : 0.0.0.0SERVER_PORT : 12580UNSAFE_SUPPORT : ENABLESANDBOX_HOME : /root/sandbox/bin/..SYSTEM_MODULE_LIB : /root/sandbox/bin/../moduleUSER_MODULE_LIB : /opt/data/fll/sandbox/sandbox-module;~/.sandbox-module;SYSTEM_PROVIDER_LIB : /root/sandbox/bin/../providerEVENT_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 开始录制和回放

录制几个请求:

[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 的日志增加了几个输出:

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-register2023-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

好了,试试回放。

录播的数据在本地,当然也可以纯在数据库里面,后面会存

[root@k8s-worker27-65 sandbox]# ls /opt/data/fll/sandbox/sandbox-module/repeater-data/record/
192168001065169511514921110001ed  192168001065169511515248010002ed  192168001065169511515821410003ed

如:其实是序列化之后存起来 

[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 传递的,必须录制和回放都用一样的参数

[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 也会对应输出日志:

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 拉取和消息投递的也可以自己实现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/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 吧。

特别说明:以下均为个人分析,并不保证正确哈。

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

[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 数据库地址 

[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

4. 创建数据库:

CREATE DATABASE IF NOT EXISTS repeaterDEFAULT CHARSET utf8COLLATE utf8_general_ci;
DROP TABLE IF EXISTS record;
CREATE TABLE record
(id             BIGINT(20)    NOT NULL AUTO_INCREMENT PRIMARY KEYCOMMENT '主键',gmt_create     DATETIME      NOT NULLCOMMENT '创建时间',gmt_record     DATETIME      NOT NULLcomment '录制时间',app_name       VARCHAR(255)  NOT NULLCOMMENT '应用名',environment    VARCHAR(255)  NOT NULLCOMMENT '环境信息',host           VARCHAR(36)   NOT NULLCOMMENT '机器IP',trace_id       VARCHAR(32)   NOT NULLCOMMENT '链路追踪ID',entrance_desc  VARCHAR(2000) NOT NULLCOMMENT '链路追踪ID',wrapper_record LONGTEXT      NOT NULLCOMMENT '记录序列化信息',request        LONGTEXT      NOT NULLCOMMENT '请求参数JSON',response       LONGTEXT      NOT NULLCOMMENT '返回值JSON'
)ENGINE = InnoDBCOMMENT = '录制信息'DEFAULT CHARSET = utf8AUTO_INCREMENT = 1;DROP TABLE IF EXISTS replay;
CREATE TABLE replay
(id              BIGINT(20)   NOT NULL AUTO_INCREMENT PRIMARY KEYCOMMENT '主键',gmt_create      DATETIME     NOT NULLCOMMENT '创建时间',gmt_modified    DATETIME     NOT NULLcomment '修改时间',app_name        VARCHAR(255) NOT NULLCOMMENT '应用名',environment     VARCHAR(255) NOT NULLCOMMENT '环境信息',ip              VARCHAR(36)  NOT NULLCOMMENT '机器IP',repeat_id       VARCHAR(32)  NOT NULLCOMMENT '回放ID',status          TINYINT      NOT NULLCOMMENT '回放状态',trace_id        VARCHAR(32)COMMENT '链路追踪ID',cost            BIGINT(20)COMMENT '回放耗时',diff_result     LONGTEXTCOMMENT 'diff结果',response        LONGTEXTCOMMENT '回放结果',mock_invocation LONGTEXTCOMMENT 'mock过程',success         BITCOMMENT '是否回放成功',record_id       BIGINT(20)COMMENT '外键')ENGINE = InnoDBCOMMENT = '回放信息'DEFAULT CHARSET = utf8AUTO_INCREMENT = 1;DROP TABLE IF EXISTS module_info;
CREATE TABLE module_info
(id           BIGINT(20)   NOT NULL AUTO_INCREMENT PRIMARY KEYCOMMENT '主键',gmt_create   DATETIME     NOT NULLCOMMENT '创建时间',gmt_modified DATETIME     NOT NULLcomment '修改时间',app_name     VARCHAR(255) NOT NULLCOMMENT '应用名',environment  VARCHAR(255) NOT NULLCOMMENT '环境信息',ip           VARCHAR(36)  NOT NULLCOMMENT '机器IP',port         VARCHAR(12)  NOT NULLCOMMENT '链路追踪ID',version      VARCHAR(128) NOT NULLCOMMENT '模块版本号',status       VARCHAR(36)  NOT NULLCOMMENT '模块状态'
)ENGINE = InnoDBCOMMENT = '在线模块信息'DEFAULT CHARSET = utf8AUTO_INCREMENT = 1;DROP TABLE IF EXISTS module_config;
CREATE TABLE module_config
(id           BIGINT(20)   NOT NULL AUTO_INCREMENT PRIMARY KEYCOMMENT '主键',gmt_create   DATETIME     NOT NULLCOMMENT '创建时间',gmt_modified DATETIME     NOT NULLcomment '录制时间',app_name     VARCHAR(255) NOT NULLCOMMENT '应用名',environment  VARCHAR(255) NOT NULLCOMMENT '环境信息',config       LONGTEXT     NOT NULLCOMMENT '配置信息'
)ENGINE = InnoDBCOMMENT = '模块配置信息'DEFAULT CHARSET = utf8AUTO_INCREMENT = 1;
 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"; ,去掉双引号里面第一个 /

[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";,去掉双引号里面第一个 /

[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";

6. 编译安装:

[root@k8s-worker27-65 bin]# ./install-local.sh

然后修改sandbox 的日志输位置

[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

[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  &

浏览器访问:

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/binNAMESPACE : defaultVERSION : 1.3.3MODE : ATTACHSERVER_ADDR : 0.0.0.0SERVER_PORT : 12580UNSAFE_SUPPORT : ENABLESANDBOX_HOME : /opt/data/fll/sandbox/bin/..SYSTEM_MODULE_LIB : /opt/data/fll/sandbox/bin/../moduleUSER_MODULE_LIB : /opt/data/fll/sandbox/sandbox-module;~/.sandbox-module;SYSTEM_PROVIDER_LIB : /opt/data/fll/sandbox/bin/../providerEVENT_POOL_SUPPORT : DISABLEsh ~/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 causejava.lang.NullPointerException: nullat 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 即可。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/87368.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构 | 树和二叉树

树 树是n&#xff08;n>0&#xff09;个结点的有限集。当n 0时&#xff0c;称为空树。在任意一棵非空树中应满足&#xff1a; 有且仅有一个特定的称为根的结点。当n>1时&#xff0c;其余节点可分为m&#xff08;m>0&#xff09;个互不相交的有限集T1,T2,…,Tm&#…

uni-app 之 去掉顶部导航

uni-app 之 去掉顶部导航 uniapp怎么样去掉顶部导航 uniapp去掉顶部导航的方法&#xff1a; 1、去掉所有导航栏&#xff1b; 2、单一页面去掉顶部导航栏。 image.png uniapp去掉顶部导航的方法&#xff1a; 1、去掉所有导航栏 "globalStyle": {"navigationBar…

Perceptual Compression与Semantic Compression的含义

这是我在读LDMS的学到的 Perceptual Compression 保留人类能够感知的重要信息&#xff0c;例如纹理&#xff0c;局部边缘等 Semantic Compression 保留数据的实际意义&#xff0c;例如图片包含了人物、建筑&#xff0c;人物之间的关系等

活动预告 | 中国数据库联盟(ACDU)中国行第三站定档成都,邀您探讨数据库前沿技术

数据库技术一直是信息时代中不可或缺的核心组成部分&#xff0c;随着信息量的爆炸式增长和数据的多样化&#xff0c;其重要性愈发凸显。作为中国数据库联盟&#xff08;ACDU&#xff09;的品牌活动之一&#xff0c;【ACDU 中国行】在线下汇集数据库领域的行业知名人士&#xff…

uniapp小程序点击按钮直接退出小程序效果demo(整理)

点击按钮直接退出小程序 <navigator target"miniProgram" open-type"exit">退出小程序</navigator>

支撑位和阻力位在Renko和烛台图如何使用?FPmarkets澳福3秒回答

很多投资者都知道&#xff0c;Renko图表和普通日本烛台都会采用相同的交易信号&#xff0c;即支撑位和阻力位。那么支撑位和阻力位在Renko和烛台图如何使用?FPmarkets澳福3秒回答。 这些信号在任何时间框架上都会出现&#xff0c;且在蜡烛图交易中颇受欢迎。对于Renko图表而言…

《DATASET CONDENSATION WITH GRADIENT MATCHING》

本文提出了一种用于数据效率学习的训练集合成技术&#xff0c;称为“数据集凝聚”(Dataset)&#xff0c;它学习将大数据集压缩成一个小的信息合成样本集&#xff0c;用于从头开始训练深度神经网络。我们将这个目标表述为在原始数据和合成数据上训练的深度神经网络权值的梯度之间…

[Linux] 2.Linux开发环境的搭建(Ubuntu)

虚拟机&#xff1a;VMare安装、Ubuntu、VitualBox 真机&#xff1a;公司的研发服务器 Linux虚拟机安装所需文件&#xff1a; 网盘资源&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1WN-tizjHpOgNF0tjbvcZsA?pwd2itd 提取码&#xff1a;2itd 文件解压&#xff…

十四、流式编程(4)

本章概要 终端操作 数组循环集合组合匹配查找信息数字流信息 终端操作 以下操作将会获取流的最终结果。至此我们无法再继续往后传递流。可以说&#xff0c;终端操作&#xff08;Terminal Operations&#xff09;总是我们在流管道中所做的最后一件事。 数组 toArray()&…

火山引擎DataLeap推出两款大模型应用: 对话式检索与开发 打破代码语言屏障

更多技术交流、求职机会&#xff0c;欢迎关注字节跳动数据平台微信公众号&#xff0c;回复【1】进入官方交流群 自上世50年代&#xff0c;以“计算机”作为代表性象征的信息革命开始&#xff0c;社会对于先进生产力的认知便开始逐步更迭——从信息化&#xff08;通常认为是把企…

Coupang真的好做吗?韩国Coupang入驻流程——站斧浏览器

coupang真的好做吗&#xff1f; Coupang自开放全球注册以来&#xff0c;一直备受跨境电商各平台卖家的关注&#xff0c;那么作为一颗跨境电商的新星&#xff0c;真的值得做吗&#xff1f; 不到一年的关注度遭到如此众多的跨境卖家追捧的平台&#xff0c;火是有他的原因的&…

【办公类-16-06】20230901大班运动场地分配表-斜线排列、5天循环、不跳节日,手动修改节日”(python 排班表系列)

背景需求&#xff1a; 大班组长发来一个“运动排班”的需求表&#xff1a;“就是和去年一样的每个班的运动排班&#xff0c;就因为今年大班变成7个班&#xff0c;删掉一个场地&#xff0c;就要重新做一份&#xff0c;不然我就用去年的那份了&#xff08;8个大班排班&#xff0…

【内网穿透】在Ubuntu搭建Web小游戏网站,并将其发布到公网访问

目录 前言 1. 本地环境服务搭建 2. 局域网测试访问 3. 内网穿透 3.1 ubuntu本地安装cpolar 3.2 创建隧道 3.3 测试公网访问 4. 配置固定二级子域名 4.1 保留一个二级子域名 4.2 配置二级子域名 4.3 测试访问公网固定二级子域名 前言 网&#xff1a;我们通常说的是互…

notepad++配合正则表达式分组模式处理文本转化为sql语句

一、正则分组知识点补充 正则分组和捕获 ()&#xff1a;用于分组和捕获子表达式。 大白话就是()匹配到的数据&#xff0c;通过美元符号加下标可以获取该数据&#xff0c;例如$1、$2, 下标从1开始。 下面的案例就采用该模式处理文本数据 二、使用正则的需求背景 有一份报表…

小米云原生文件存储平台化实践:支撑 AI 训练、大模型、容器平台多项业务

小米作为全球知名的科技巨头公司&#xff0c;已经在数百款产品中广泛应用了 AI 技术&#xff0c;这些产品包括手机、电视、智能音箱、儿童手表和翻译机等。这些 AI 应用主要都是通过小米的深度学习训练平台完成的。 在训练平台的存储方案中&#xff0c;小米曾尝试了多种不同的…

网络安全CTF比赛有哪些事?——《CTF那些事儿》告诉你

目录 前言 一、内容简介 二、读者对象 三、专家推荐 四、全书目录 前言 CTF比赛是快速提升网络安全实战技能的重要途径&#xff0c;已成为各个行业选拔网络安全人才的通用方法。但是&#xff0c;本书作者在从事CTF培训的过程中&#xff0c;发现存在几个突出的问题&#xff1…

ndoe.js、npm相关笔记

1、npm 全局安装 npm config get prefix 获取 npm 全局安装路径如果全局插件不能正常使用&#xff0c;看环境变量是否已经配置。没有配置则把全局安装路径配置到环境变量的path中

MS933NA适用于 1MP/60fps 摄像头、37.5MHz100MHz、10 位/12 位的串化器

MS933NA 是 10 位 /12 位串化器&#xff0c;支持 37.5MHz  100MHz 时钟&#xff0c; MS933NA 广泛应用于车载摄像、医疗设备、管道探测等领域。 主要特点 ◼ 支持输入 37.5MHz 到 100MHz 的图像时钟 ◼ 单个差分对互连 ◼ 可编程数据有效负载 10 位 /12 …

django 实现:闭包表—树状结构

闭包表—树状结构数据的数据库表设计 闭包表模型 闭包表&#xff08;Closure Table&#xff09;是一种通过空间换时间的模型&#xff0c;它是用一个专门的关系表&#xff08;其实这也是我们推荐的归一化方式&#xff09;来记录树上节点之间的层级关系以及距离。 场景 我们 …

什么是关系模型? 关系模型的基本概念

关系模型由IBM公司研究员Edgar Frank Codd于1970年发表的论文中提出&#xff0c;经过多年的发展&#xff0c;已经成为目前最常用、最重要的模型之一。 在关系模型中有一些基本的概念&#xff0c;具体如下。 (1)关系(Relation)。关系一词与数学领域有关&#xff0c;它是集合基…