探索如何利用流量回放,将线上真实的数据流转化为覆盖全面的回归测试用例。在调研了业内比较知名的流量回放方案,如 TcpReplay,TcpCopy 后。发现这些方案均只是服务端入口流量的 Copy 组件,虽然的确能将线上流量「复制 & 引流」至目标服务,如果仅仅是复制 HTTP 入口的流量,那么接收流量回放的服务必须也要配套和被流量录制服务业务数据一致的缓存,数据库,第三方服务等,
能录制/回放应用调用链路入口(通常为 HTTP)的 Request/Response
能录制/回放应用调用链路内部对的 DB,Redis 及其他服务的 Request/Response。
能串联整个调用链路期间所有相关的录制/回放(一般都是考虑基于 Trace)
能无限回放至任意环境(包括线上,线下,指定主机等)应用代码无侵入及录制过程对服务极低的性能耗损 。
除了入口 HTTP Request/Response 处于链路的首位和末尾外。调用链路在应用内部的顺序是不确定的。比如可能先调用 DB 后调用 Redis,也可能先调用 Redis 后调用同样的 DB SQL 多次,是一个没有任何规律(也无法推测出规律)的调用顺序。
所有录制的 Endpoint 都会同时被录制一个基于当前上下文的调用编号。由于回放时的调用链路与录制时的调用链路是一致的,所以可以通过编号准确的找到当前步骤需要回放的 Endpoint 及其数据。JVM-SANDBOX 来进行 AOP 方式的录制,看起来的确是切实可行的。
jvm-sandbox-repeater
jvm-sandbox-repeater框架基于JVM-Sandbox,具备了JVM-Sandbox的所有特点封装了以下能力:
1.录制/回放基础协议,可快速配置/编码实现一类中间件的录制/回放
2.开放数据上报,对于录制结果可上报到自己的服务端,进行监控、回归、问题排查等上层平台搭建
基于它,我们可以在业务系统无感知的情况下,快速扩展 api ,实现自己的插件,对流量进行录制,入口请求(HTTP/Dubbo/Java)流量回放、子调用(Java/Dubbo)返回值Mock能力。详细介绍可以看官方说明。
录制回放主要原理如下:
repeater作为sandbox一个子模块加载,进行流量录制发送console,console存储流量并且,调用repeater将流量在目标jvm上回放。
录制:如图,当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进行处理和保存。
通过对 DB(MyBatis),Redis(RedisTemlate)等进行 AOP 拦截;
在相关 Endpoint 进行网络交互前记录(序列化)请求,并在网络交互后记录响应;
在请求/响应/及上文提到的调用编号均完备的情况下,使用 Json 序列为包含元数据(比如 Class 信息,数组或者集合的元素类型等)的字符串后推送至消息中间件(如 Kafka);repeater 是通过http发送同步请求(不过有内存队列)。随后通过 Repeat Service 异步消费后存入 DB。
流量录制回放代码入口
流量录制回放配置
@RequestMapping("/facade/api")
public class ConfigFacadeApi {
@RequestMapping("/config/{appName}/{env}")
public RepeaterResult<RepeaterConfig> getConfig(@PathVariable("appName") String appName,
@PathVariable("env") String env) {
// 改为了可以适用于 gs-rest-service(自己的应用) 的配置
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("^/greeting.*$"));
// java入口方法
behaviors.add(new Behavior("hello.GreetingController", "greeting"));
config.setJavaEntranceBehaviors(behaviors);
List<Behavior> subBehaviors = Lists.newArrayList();
// java调用插件
config.setJavaSubInvokeBehaviors(subBehaviors);
config.setUseTtl(true);
return RepeaterResult.builder().success(true).message("operate success").data(config).build();
}
}
代码repeater-module负责流量的录制工作,我们从repeater-module模块中,RepeaterModule.class来进行分析。
RepeaterModule实现了ModuleLifecycle接口,在sandbox进行模块加载时,对于实现该接口的实例会一次执行其onLoad方法、onActive方法、onCompleted方法,一起来看下onCompleted方法:
- 在线程池中执行,配置拉取,pullConfig实际上就是调用console提供的接口能力
- 根据配置进行初始化
- 启动心跳检测
public void loadCompleted() {
ExecutorInner.execute(new Runnable() {
@Override
public void run() {
configManager = StandaloneSwitch.instance().getConfigManager();
broadcaster = StandaloneSwitch.instance().getBroadcaster();
invocationListener = new DefaultInvocationListener(broadcaster);
RepeaterResult<RepeaterConfig> pr = configManager.pullConfig();
if (pr.isSuccess()) {
log.info("pull repeater config success,config={}", pr.getData());
ClassloaderBridge.init(loadedClassDataSource);
initialize(pr.getData());
}
}
});
heartbeatHandler = new HeartbeatHandler(configInfo, moduleManager);
heartbeatHandler.start();
}