转转流量录制与回放的原理及实践

1 需求背景

随着转转业务规模和复杂度不断提高,业务服务增加,服务之间依赖关系也逐渐复杂。在开发和测试中遇到如下问题:

  • 参数构造:转转接口测试平台可以很方便调用SCF(转转RPC框架),但是接口的参数很多是复杂model,手动构造参数成本不小,希望能够录制稳定测试环境流量,从流量中抽取接口参数,方便使用者选择参数进行接口测试。
  • 压测流量构造:转转是二手电商平台,有许多促销活动的压测需求,人工构造压测流量既不能模拟真实访问,又成本高昂。所以有录制线上流量的需求,然后压测平台通过策略二次加工形成压测case。
  • 自动化回归测试:业务迭代速度很快,每次迭代会不会影响原有逻辑?希望有一个平台能够提供筛选保存case,自动化回归,case通过率报告通知等能力。

这些问题每个互联网公司都会遇到,如何才能优雅解决这些问题呢?首先定义一下优雅:不增加业务成本,业务基本无感,对业务性能影响要足够小。阿里开源的jvm-sandbox-repeater(简称Repeater)正是为解决这些问题而生,能够做到业务无感,但是性能问题需要特别定制处理。本文重点介绍:

  • Repeater流量录制和回放业务无感实现原理(第2、3章节)
  • 线上服务流量录制时,如何减少对正常业务的性能影响(第4章节)

希望能够揭秘Repeater如何做到业务无感的流量录制和回放,进而让使用流量录制的同学对Repeater内部做了哪些工作以及对性能有哪些影响做到心中有数,最后介绍在流量录制时,为了保证对线上服务的性能影响相对可控,我们做了哪些工作,让大家会用敢用。

2 流量录制和回放概念

2.1 流量录制

对于Java调用,一次流量录制包括一次入口调用(entranceInvocation)(eg:HTTP/Dubbo/Java)和若干次子调用(subInvocations)。流量的录制过程就是把入口调用和子调用绑定成一次完整的记录。

java 复制代码
    /**
     * 获取商品价格,先从redis中获取,如果redis中没有,再用rpc调用获取,
     * @param productId
     * @return
     */
    public Integer getProductPrice(Long productId){ //入口调用

        //1.redis获取价格
        Integer price = redis.get(productId); //redis远程子调用
        if(Objects.isNull(price)){
            //2.远程调用获取价格
            price =  daoRpc.getProductCount(productId); //rpc远程子调用
            redis.set(productId, price); //redis远程子调用
        }
        //3.价格策略处理
        price = process(price); //本地子调用
        return price;

    }

    private Integer process(Long price){
        //价格策略远程调用
        return logicRpc.process(productId); //rpc远程子调用
    }

以获取产品价格方法为例,流量录制的内容简单来说是入口调用(getProductPrice)的入参和返回值,远程子调用(redis.get,daoRpc.getProductCount,redis.set,logicRpc.process)的入参和返回值,用于流量回放。注意并不会录制本地子调用(process)。

下图是转转流量回放平台录制好的单个流量的线上效果,帮助理解流量录制概念。

2.2 流量回放

流量回放,获取录制流量的入口调用入参,再次发起调用,并且对于子调用,直接使用流量录制时记录的入参和返回值,根据入参(简单来说)匹配子调用后,直接返回录制的数据。这样就还原了流量录制时的环境,如果回放后返回值和录制时返回值不一致,那么本条回放case标记为失败。 还以getProductPrice为例,假设录制时入口调用参数productId=1,返回值为1;redis.get子调用参数productId=1,返回值为1。那么回放时,redis.get不会访问redis,而是直接返回1。假设该函数有逻辑更新,回放返回值是2,与录制时返回值1不相等,那么次此流量回放标记为失败。

下图是转转流量回放平台的流量回放的线上效果,帮助理解流量回放概念

明白流量录制和回放概念后,下面看看业务无感实现流量录制和回放的实现原理。

3 Repeater实现原理

  • Repeater Console模块
    • 流量录制和回放的配置管理
    • 心跳管理
    • 录制和回放调用入口
  • Repeater agent plugin模块:Repeater核心功能是流量录制回放,其实现核心是agent插件,开源框架已经实现redis、mybatis、http、okhttp、dubbo、mq、guava cache等插件。由于录制和回放逻辑是以字节码增强的方式在程序运行时织入,所以无需业务编码。换句话说,agent技术是业务无感的关键。

下面我们就进入无感的关键环节,介绍Repeater如何织入流量录制和回放逻辑代码,以及梳理流量录制和回放的核心代码。

3.1 流量录制和回放逻辑如何织入

用一句话来说,Repeater本身并没有实现代码织入功能,它依赖另一个阿里开源项目JVM-Sandbox。详细来讲,Repeater的核心逻辑录制协议基于JVM-Sandbox的BEFORERETRUNTHROW事件机制进行录制流程控制。本质上来说,JVM-Sandbox实现了java agent级别的spring aop功能,是一个通用增强框架。JVM-Sandbox的基于字节码增强的事件机制原理见下图:

上图以add方法为例,揭示JVM-Sandbox增强前后的代码变化,方便大家理解。下面的代码是对图中增强代码相关重点的注释

java 复制代码
public int add(int a, int b) {
        try {
            Object[] params = new Object[]{a, b};
            //BEFORE事件
            Spy.Ret retOnBefore = Spy.onBefore(10001,
                    "com.taobao.test.Test", "add", this, params);
            //BEFORE结果可以直接返回结果或者抛出异常,是实现mock(阻断真实远程调用)的关键
            if (retOnBefore.state == I_RETURN) return (int) retOnBefore.object;
            if (retOnBefore.state == I_THROWS) throws(Throwable) retOnBefore.object;
            a = params[0];
            b = params[1];
            int r = a + b;
            //RETRUN事件
            Spy.Ret retOnReturn = Spy.onReturn(10001, r);
            if (retOnReturn.state == I RETURN)return (int) retOnReturn.object;
            if (retOnReturn.state == I_THROWS) throws(Throwable) retOnReturn.object;
            return r;
        } catch (Throwable cause) {
            //THROW事件
            Spy.Ret retOnIhrows = Spy.onThrows(10001, cause);
            if (retOnThrows.state == I RETURN)return (int) retOnThrows.object;
            if (retOnThrows.state == I THROWS) throws(Throwable) retOnThrows.object;
        throws cause;
        }
    }

由上可知,Repeater是利用jvm agent字节码增强技术为目标方法织入BEFORERETRUNTHROW逻辑。

3.2 流量录制和回放的核心代码

既然Repeater利用JVM-Sandbox aop框架编写流量录制和回放逻辑,那么让我们看看它的核心代码doBefore。先来一张流程图。

再重点介绍doBeforedoMock的核心代码,它们是实现录制和回放的关键,注意阅读注释。为了方便理解,我对开源代码做了大量删减,只保留核心逻辑。

java 复制代码
    /**
     * 处理before事件
     * 流量录制时记录函数元信息和参数,缓存录制数据
     * 流量回放时,调用回放逻辑,直接返回录制时的数据,后面会对processor.doMock进行展开讲解
     * @param event before事件
     */
protected void doBefore(BeforeEvent event) throws ProcessControlException {
        // 回放流量;如果是入口则放弃;子调用则进行mock
        if (RepeatCache.isRepeatFlow(Tracer.getTraceId())) {
            processor.doMock(event, entrance, invokeType);
            return;
        }
        //非回放流量,进行流量录制,主要元信息、参数、返回值
        Invocation invocation = initInvocation(event);
        //记录是否为入口流量
        invocation.setEntrance(entrance);
        //记录参数
        invocation.setRequest(processor.assembleRequest(event));   
        //记录返回值
        invocation.setResponse(processor.assembleResponse(event));

    }
    
     @Override
    public void doMock(BeforeEvent event, Boolean entrance, InvokeType type) throws ProcessControlException {
    
      try {
          
          //通过录制数据构建mock请求 
          final MockRequest request = MockRequest.builder().build();
          //执行mock动作
          final MockResponse mr = StrategyProvider.instance().provide(context.getMeta().getStrategyType()).execute(request);
          //根据mock结果,阻断真实远程调用
          switch (mr.action) {
              case SKIP_IMMEDIATELY:
                  break;
              case THROWS_IMMEDIATELY:
                  //直接抛出异常,映射到JVM-Sandbox的事件机制原理的add函数
                  //也就是代码走到if (retOnBefore.state == I_THROWS) throws(Throwable) retOnBefore.object;
                  //而不再执行后面的代码(JVM-Sandbox框架机制,调用如下代码会触发阻断真实调用)
                  ProcessControlException.throwThrowsImmediately(mr.throwable);
                  break;
              case RETURN_IMMEDIATELY:
              //直接返回录制结果,映射到JVM-Sandbox的事件机制原理的add函数,同理,也不再执行后面的代码(阻断真实调用)
                  ProcessControlException.throwReturnImmediately(assembleMockResponse(event, mr.invocation));
                  break;
              default:
                  ProcessControlException.throwThrowsImmediately(new RepeatException("invalid action"));
                  break;
          }
      } catch (ProcessControlException pce) {
          throw pce;
      } catch (Throwable throwable) {
          ProcessControlException.throwThrowsImmediately(new RepeatException("unexpected code snippet here.", throwable));
      }
    }

通过上面的2、3章节介绍了Repeater流量录制和回放业务无感的实现原理,下面说一下应用过程中需要哪些改造点。

4 Repeater落地实践

4.1 改造点

  • Rpeater开源管理后台仅仅是个Demo,需要重新设计和实现。
  • SCF(转转RPC框架)插件扩展,支持SCF应用的流量录制和回放。
  • DB由MySQL改造为ES,Repeater原生使用MySQL作为流量录制和回放的数据库,仅用于Demo演示,性能和容量无法满足实际需求。
  • Docker环境下频繁更换ip时不中断录制。
  • 回放结果Diff支持字段过滤。
  • 大批量回放。
  • 线上环境录制。

4.2 线上环境录制

流量录制很大一部分应用场景在线下,但是线上也有录制场景。从流量录制的原理可知,由于要记录入口调用和各种远程子调用,开启流量录制后,对于该请求占用内存资源会大大增加,并且会增加耗cpu的序列化操作(用于上报流量录制结果)。既然流量录制是一个天然的耗内存和性能操作,对于线上服务的录制除了保持敬畏之心之外,还有设计一种机制减少录制时对线上服务的性能影响。下面开始介绍如果做到录制时减少对线上服务性能的影响。

线上录制减少性能影响的方案:

  • 从流程上,线上录制需要申请。

  • 从技术上,与发布系统联动,为录制服务增加专门的节点进行录制,并且设置权重为正常节点的1/10,正常节点不会挂载流量录制代码。

  • 从回滚上,如果线上录制节点遇到问题,可以从发布系统直接删除录制节点。

5 总结

本文旨在介绍Repeater流量录制和回放的实现原理,以及在落地过程中改造点,希望达到让大家懂原理、会使用、敢使用的目的。


关于作者

梁会彬,架构部,资深开发工程师。负责云平台、配置中心、权限系统、Repeater等基础组件。

转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。 关注公众号「转转技术」(综合性)、「大转转FE」(专注于FE)、「转转QA」(专注于QA),更多干货实践,欢迎交流分享~

相关推荐
瓜牛_gn21 分钟前
依赖注入注解
java·后端·spring
Estar.Lee38 分钟前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
喜欢猪猪40 分钟前
Django:从入门到精通
后端·python·django
一个小坑货40 分钟前
Cargo Rust 的包管理器
开发语言·后端·rust
bluebonnet2744 分钟前
【Rust练习】22.HashMap
开发语言·后端·rust
uhakadotcom1 小时前
如何实现一个基于CLI终端的AI 聊天机器人?
后端
Iced_Sheep2 小时前
干掉 if else 之策略模式
后端·设计模式
XINGTECODE2 小时前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
程序猿进阶2 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺2 小时前
Spring Boot框架Starter组件整理
java·spring boot·后端