1. 介绍
1.1 简介
SandboxRepeater是一套流量回放工具,开源的应该基本不更新了,阿里收费的项目正在出,还在公测阶段。目前开源出来的流量回放框架有滴滴的Rdebub和阿里的SandboxRepeater等,不过其它像滴滴的这套是PHP框架,除非完美适配,不然的话SpringBoot项目如果要使用也只能基于SandboxRepeater修改。
1.2 架构
- 插件Plugin
- 控制台界面Console
1.3 如何启动Sandbox Repeater客户端
2. 源码
2.1 安装Sandbox
- 远程安装:通过命令远程下载install-repeater文件进行安装客户端,这个在阿里开源的项目中bin目录下有,可直接运行。
curl -s http://sandbox-ecological.oss-cn-hangzhou.aliyuncs.com/install-repeater.sh | sh
下载完毕后,会根据install-repeater中的命令安装到${HOME}/sandbox目录下(“/User/apple/sanbox”),目录结构如下
2.2 启动Sandbox
sandbox主要是利用java agent原理,启动方式分为attach、agent两种,一种是脱离目标应用的命令方式启动,一种是作为目标应用的JVM参数启动,两种方式启动后的特性不一样。Sandbox启动需要占用一个端口。
2.2.1 attach模式
attach 模式下,录制应用名和录制环境这两个参数都会被默认为unknown。
# 启动命令
~/sandbox/bin/sandbox.sh -p ${被录制应用进程号} -P ${repeater启动端口}
# 关闭命令
~/sandbox/bin/sandbox.sh -S ${被录制应用进程号}
(1) 为什么默认录制应用名和录制环境会设备unknown
Repeater中获取了目标应用的系统参数,如果按照attach方式启动的话,目标应用一般不会事先配置好Repeater需要的参数。流量回放主要还是模拟真实环境数据进行定位问题,真正要去使用的话还是第一种方式比较友好,主要是完全无浸入目标应用,可以随时开启或者关闭录制,而第二种的话随目标应用生命周期一致。
// com.alibaba.jvm.sandbox.repeater.plugin.core.model.ApplicationModel#ApplicationModel
private ApplicationModel() {
// for example, you can define it your self
this.appName = getSystemPropertyOrDefault("app.name", "unknown");
this.environment = getSystemPropertyOrDefault("app.env", "unknown");
try {
this.host = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
// default value for disaster
this.host = "127.0.0.1";
}
}
2.2.2 agent模式
JVM参数配置如下:
-javaagent:${HOME}/Desktop/sandboxb/lib/sandbox-agent.jar=server.port=8820;server.ip=0.0.0.0
-Dapp.name=unknown
-Dapp.env=unknown
2.3 Repeater
Sandbox会根据参数server.port以及server.ip进行启动,命令的方式可以配置,也可以从cfg中sandbox.properties中配置(具体生效哪个,怎么生效没看)。Sandbox只是作为一个入口,它会根据配置sanbox.properties读取user_module信息,加载该文件夹下的jar包信息。Repeater会去加载改目录下的repeater.properties信息(这个文件包含一些心跳上报、配置拉取、回放结果投递等等的配置信息)
#
# this properties file define the sandbox's config
# @author luanjia@taobao.com
# @date 2016-10-24
#
# define the sandbox's ${SYSTEM_MODULE} dir
## system_module=../module
# define the sandbox's ${USER_MODULE} dir, multi values, use ',' split
## user_module=~/.sandbox-module;~/.sandbox-module-1;~/.sandbox-module-2;~/.sandbox-module-n;
user_module=~/.sandbox-module;
#user_module=/home/staragent/plugins/jvm-sandbox-module/sandbox-module;/home/staragent/plugins/monkeyking;
# define the sandbox's ${PROVIDER_LIB} dir
## provider=../provider
# define the network interface
## server.ip=0.0.0.0
# define the network port
## server.port=4769
# switch the sandbox can enhance system class
unsafe.enable=true
2.4 匹配方法拦截条件
方法匹配有两种方式,一种是通配符(a.b.c.*),一种是正则匹配。匹配方式一般默认是通配符方式。
{
"useTtl" : true,
"degrade" : false,
"exceptionThreshold" : 1000,
"sampleRate" : 0,
"pluginsPath" : null,
"httpEntrancePatterns" : [ "^/regress/.*$" ],
"javaEntranceBehaviors" : [ {
"classPattern" : "com.alibaba.repeater.console.service.impl.*",
"methodPatterns" : [ "*" ],
"includeSubClasses" : false
} ],
"javaSubInvokeBehaviors" : [ {
"classPattern" : "com.alibaba.repeater.console.service.impl.RegressServiceImpl",
"methodPatterns" : [ "getRegressInner", "findPartner", "slogan" ],
"includeSubClasses" : false
} ],
"pluginIdentities" : [ "http", "java-entrance", "java-subInvoke" ],
"repeatIdentities" : [ "java", "http" ]
}
(1)通配符比较
// com.alibaba.jvm.sandbox.api.util.GaStringUtils#matching(java.lang.String, java.lang.String)
public static boolean matching(final String string, final String wildcard) {
return null != wildcard
&& null != string
&& matching(string, wildcard, 0, 0);
}
// com.alibaba.jvm.sandbox.api.util.GaStringUtils#matching(java.lang.String, java.lang.String, int, int)
private static boolean matching(String string, String wildcard, int stringStartNdx, int patternStartNdx) {
int pNdx = patternStartNdx;
int sNdx = stringStartNdx;
int pLen = wildcard.length();
if (pLen == 1) {
if (wildcard.charAt(0) == '*') { // speed-up
return true;
}
}
int sLen = string.length();
boolean nextIsNotWildcard = false;
while (true) {
// check if end of string and/or pattern occurred
if ((sNdx >= sLen)) { // end of string still may have pending '*' callback pattern
while ((pNdx < pLen) && (wildcard.charAt(pNdx) == '*')) {
pNdx++;
}
return pNdx >= pLen;
}
if (pNdx >= pLen) { // end of pattern, but not end of the string
return false;
}
char p = wildcard.charAt(pNdx); // pattern char
// perform logic
if (!nextIsNotWildcard) {
if (p == '\\') {
pNdx++;
nextIsNotWildcard = true;
continue;
}
if (p == '?') {
sNdx++;
pNdx++;
continue;
}
if (p == '*') {
char pnext = 0; // next pattern char
if (pNdx + 1 < pLen) {
pnext = wildcard.charAt(pNdx + 1);
}
if (pnext == '*') { // double '*' have the same effect as one '*'
pNdx++;
continue;
}
int i;
pNdx++;
// find recursively if there is any substring from the end of the
// line that matches the rest of the pattern !!!
for (i = string.length(); i >= sNdx; i--) {
if (matching(string, wildcard, i, pNdx)) {
return true;
}
}
return false;
}
} else {
nextIsNotWildcard = false;
}
// check if pattern char and string char are equals
if (p != string.charAt(sNdx)) {
return false;
}
// everything matches for now, continue
sNdx++;
pNdx++;
}
}
2.5 事件监听
任何方法增强的地方分为三种:方法前,方法后以及异常时。sandbox抽象了该事件行为,对外提供事件监听行为,Repeater就是如此实现。插件可以重写事件监听进行自定义处理。
// com.alibaba.jvm.sandbox.repeater.plugin.core.impl.api.DefaultEventListener#onEvent
public void onEvent(Event event) throws Throwable {
try {
/*
* event过滤;针对单个listener,只处理top的事件
*/
if (!isTopEvent(event)) {
// ...
return;
}
/*
* 初始化Tracer
*/
initContext(event);
/*
* 执行基础过滤
*/
if (!access(event)) {
// ...
return;
}
/*
* 执行采样计算(只有entrance插件负责计算采样,子调用插件不计算),traceId不变,而采样计算根据traceId计算,所以一次完整的调用不会出现部分事件采样通过,部分不通过的情况
*/
if (!sample(event)) {
// ...
return;
}
/*
* processor filter
*/
if (processor != null && processor.ignoreEvent((InvokeEvent) event)) {
// ...
return;
}
/*
* 分发事件处理(对于一次around事件可以收集到入参/返回值的可以直接使用;需要从多次before实践获取的)
*/
switch (event.type) {
case BEFORE:
// 记录调用信息
doBefore((BeforeEvent) event);
break;
case RETURN:
// 记录调用信息,所有闭环后HTTP调用录制接口保存相关调用信息,地址信息事从repeater.properties读取(线程池异步处理,保存的时候需要注意序列化方式)
doReturn((ReturnEvent) event);
break;
case THROWS:
// 同return
doThrow((ThrowsEvent) event);
break;
default:
break;
}
} catch (ProcessControlException pe) {
/*
* sandbox流程干预
*/
// process control 会中断事件,不会有return/throw事件过来,因此需要清除偏移量
eventOffset.remove();
throw pe;
} catch (Throwable throwable) {
// uncaught exception
log.error("[Error-0000]-uncaught exception occurred when dispatch event,type={},event={}", invokeType, event, throwable);
ApplicationModel.instance().exceptionOverflow(throwable);
} finally {
/*
* 入口插件 && 完成事件
*/
clearContext(event);
}
}
2.6 回放
如果是回放流量,则发起mock,构建mock信息。
// com.alibaba.jvm.sandbox.repeater.plugin.core.impl.api.DefaultEventListener#doBefore
protected void doBefore(BeforeEvent event) throws ProcessControlException {
// 回放流量;如果是入口则放弃;子调用则进行mock
if (RepeatCache.isRepeatFlow(Tracer.getTraceId())) {
processor.doMock(event, entrance, invokeType);
return;
}
// ...
}
3. 实践
4. FAQ
4.1 Redis插件只能拦截Jedis连接方式,如果使用Lettuce方式则无法拦截
4.2 SandboxRepeater可以录制JAVA方法入参及返回结果,但是前提是这些录制的对象具备被序列化和反序列化的资质,如Hession、Json等,目前如果参数中带有HttpServerletRequest,则无法进行录制(录制的入参信息会缺失),
那么HttpServerletRequest是否有手段存储进行回放。
4.3 录制有一个采样率的配置,根据traceId%10000 < 采样基数。
4.4 有的时候也仅仅是作为工具小用一下,并不想大搞。目前看这套用起来还是有麻烦,Arthas貌似有部分功能重叠,下回可以看看(TBD)。
4.5 为什么只针对top事件, 子调用的方法怎么区分
4.6 TTL
4.7 sandbox是否是所有订阅事件都发一遍消息(插件可以重新定义事件监听)