流量回放-SandboxRepeater

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”),目录结构如下

流量回放-SandboxRepeater

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是否是所有订阅事件都发一遍消息(插件可以重新定义事件监听)

5. 参考资料

JVM-Sandbox-Repeater

jvm-sandbox-repeater 学习笔记

上一篇:微信测试号


下一篇:Vineyard 加入 CNCF Sandbox,将继续瞄准云原生大数据分析领域