高可用可伸缩微服务架构:基于Dubbo、Spring Cloud和Service Mesh
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

3.3 基于Dubbo的自动化Mock系统

目前有很多公司在进行微服务改造时使用Dubbo框架,在业务不断发展的情况下,服务之间的调用链路越来越冗长,每个服务又是单独的团队在维护,每个团队又在不断地演进和维护各个服务,对测试人员来说将是非常大的挑战。

测试人员每次进行功能测试的时候,测试用例都需要重新写一遍,无法将测试用例的数据沉淀,尤其是做自动化测试的时候,测试人员准备测试数据就需要很长时间,效率非常低。

接口自动化测试框架也多种多样,包括Testng、JUnit、Fitnesse等,但都需要测试人员具备测试代码编写能力,如果要做到和手工接口测试一样的效果,则自动化测试需要大量的代码,后期维护代码的成本非常大。因此做成简单配置用例流,无须编写测试代码的系统更贴合实际工作要求。

在平常的工作中,测试人员又是如何验证数据的呢?

● 接口返回值

通过肉眼分析比对接口返回值的内容,判断业务逻辑的正确性。

● 数据库验证

测试接口的输入值需要通过手工编写数据库SQL来获取,接口调用完成后,需要通过大量的SQL验证数据库值的正确性。

● 日志验证

通过返回值和数据库不能确保代码实现了预期的逻辑,只能通过肉眼观察日志以确认代码的实际运行逻辑。

● 测试报告

人工记录用例结果,人工编写报告,耗时耗力,难以准确定位代码问题。

可以看到在大规模的微服务测试场景下,对测试人员的要求是非常高的,测试人员在大部分情况下还是要通过手工来进行测试。以互联网支付系统来说,某个团队新增了支付交易的需求,这时要进行测试,测试人员除了要测试支付交易需求本身是否正确,同时要结合上下游的服务整体进行回归测试,这时开发人员往往在支付交易系统中采用“硬编码”的方式对上下游的系统进行“挡板”,如果测试人员对测试数据有所调整,那么“挡板”也要跟着调整,同时在项目正式上线的时候,如果开发人员没有将“挡板”程序去除干净,则将面临严重的线上问题。整个过程烦琐、冗长、效率低并且容易出错。

3.3.1 Mock模拟系统的产生

业务系统调用众多其他系统完成功能逻辑,而想要得到其他系统接口的特定输出,就需要做相应的运营配置,增加很多的沟通成本;甚至偶发性bug只能在特定的环境状况下复现,所以只能作为不可测的逻辑。

以风控系统为例,如果业务系统需要测试某个商品类别下的累积限额,则需要风控的同事配合不断修改限额阈值。目前的情况是多个业务系统都在接入风控,配合测试的人力成本和时间成本是很高的。为此设计了挡板模拟系统,其功能结构如图3-36所示。

图3-36

针对测试人员测试用例数据无法沉淀和复用的问题,我们将采用“用例与日志锚点库”的方案:

● 用例库的建立可以实现对以往测试规则的记录与复用,改变每次回归测试都要重复编写用例与准备数据的状况。

● 日志锚点库是对代码执行流程的有效验证,除了可以应用在测试环境中,还可基于大数据日志中心对生产代码的运行做日常监控。

● 交易与支付系统业务逻辑复杂,靠人脑和文档记忆功能关系难免疏漏,而用例库和日志锚点库会随着业务的变更测试而随即维护,是一部“活文档”。

3.3.2 Dubbo Mock的使用

Dubbo自带的Mock功能首先是为了做服务降级,比如某验权服务,当服务提供方全部“挂掉”后,客户端不抛出异常,而是通过Mock数据返回授权失败。

我们以官网上的一个例子来进行说明:

        <dubbo:reference interface="com.foo.BarService" mock="force" />

我们可以在期望的reference标签上加一个mock="force",就可以将当前服务设置为Mock。但是设置完Mock属性后还没有结束,需要有一个Mock类对应我们的服务接口类。

规则如下:

接口名 + Mock后缀,服务接口调用失败Mock实现类,该Mock类必须有一个无参构造函数。

对应到com.foo.BarService的话,则创建BarServiceMock类:

        public class BarServiceMock implements BarService {

            public String sayHello(String name){
              //可以伪造容错数据,此方法只在出现RpcException时被执行
              return "容错数据";
            }
        }

经过以上设置后,当调用BarService进行远程调用时,直接请求到BarServiceMock类上面进行模拟测试。

3.3.3 Dubbo Mock的原理解析

在Dubbo的配置文件classpath:/META-INF/dubbo/internal/com.alibaba.dubbo.rpc. cluster.Cluster中可以看到如下配置列表:

        mock=com.alibaba.dubbo.rpc.cluster.support.wrapper.MockClusterWrapper
        failover=com.alibaba.dubbo.rpc.cluster.support.FailoverCluster
        failfast=com.alibaba.dubbo.rpc.cluster.support.FailfastCluster
        failsafe=com.alibaba.dubbo.rpc.cluster.support.FailsafeCluster
        failback=com.alibaba.dubbo.rpc.cluster.support.FailbackCluster
        forking=com.alibaba.dubbo.rpc.cluster.support.ForkingCluster
        available=com.alibaba.dubbo.rpc.cluster.support.AvailableCluster
        switch=com.alibaba.dubbo.rpc.cluster.support.SwitchCluster
        mergeable=com.alibaba.dubbo.rpc.cluster.support.MergeableCluster
        broadcast=com.alibaba.dubbo.rpc.cluster.support.BroadcastCluster

配置文件中实际上有五大路由策略。

AvailableCluster:获取可用的调用。遍历所有Invokers并判断Invoker.isAvalible,只要一个有为true则直接调用返回,不管成不成功。

BroadcastCluster:广播调用。遍历所有Invokers, “catch”住每一个Invoker的异常,而不影响其他Invoker调用。

FailbackCluster:失败自动恢复,对于Invoker调用失败,后台记录失败请求,任务定时重发,通常用于通知。

FailfastCluster:快速失败,只发起一次调用,失败立即报错,通常用于非幂等性操作。

FailoverCluster:失败转移,当出现失败时,重试其他服务器,通常用于读操作,但重试会带来更长的延迟。

Dubbo中默认使用的是FailoverCluster策略,而在实际执行的过程中FailoverCluster会先被注入MockClusterWrapper,过程如下:

        Cluster$Adaptive → 定位到内部key为failover的对象 → FailoverCluster → 注入MockClusterWrapper

MockClusterWrapper内部会创建一个MockClusterInvoker对象。实际创建是封装了FailoverClusterInvoker的MockClusterInvoker,这样就成功地在Invoker中植入了Mock机制。

我们来看MockClusterInvoker的内部实现:

● 如果在配置中没有设置Mock,那么直接把方法调用转发给实际的Invoker(也就是FailoverClusterInvoker)。

        String mockValue = directory.getUrl().getMethodParameter(
              invocation.getMethodName(), Constants.MOCK_KEY, Boolean.FALSE.toString()).trim();
            if(mockValue.length()== 0 || mockValue.equalsIgnoreCase("false"))
            {
              //no mock
              result = this.invoker.invoke(invocation);
            }

● 如果配置了强制执行Mock,比如发生服务降级,那么直接按照配置执行Mock之后返回。

        else if(mockValue.startsWith("force"))
        {
              if(logger.isWarnEnabled())
              {
                logger.info("force-mock: " + invocation.getMethodName()+ " force-mock enabled , url: " +  directory.getUrl());
              }
            //force:direct mock
             result = doMockInvoke(invocation, null);
      }

● 如果是其他的情况,比如配置的是mock=fail:return null,那么就是在正常的调用出现异常的时候按照配置执行Mock。

        try
        {
          result = this.invoker.invoke(invocation);
        }
        catch(RpcException rpcException) {
            if(rpcException.isBiz()) {
              throw rpcException;
            }
            else
            {
            if(logger.isWarnEnabled()) {
              logger.info("fail-mock:  "  +  invocation.getMethodName() +  "  fail-mock enabled , url : "
            +  directory.getUrl(), rpcException);
            }
              result = doMockInvoke(invocation, rpcException);
            }
        }

Dubbo的Mock功能主要用于服务降级,服务提供方在客户端执行容错逻辑,在出现RpcException(比如网络失败、超时等)时进行容错,然后执行降级Mock逻辑,基于自身特性并不适合做Mock测试系统。

3.3.4 自动化Mock系统的实现

基于前面提到的测试人员手工测试,以及Dubbo自有Mock功能的不足,我们提出了建设基于Dubbo的自动化Mock系统的想法,整体构思用例图如图3-37所示。

图3-37展示的是Mock系统的主要功能,主要两大块内容是:测试属性设置和测试规则设置。

图3-37

1.系统整体结构

系统的整体结构如图3-38所示。

图3-38

基于Dubbo实现Mock功能,需要对Dubbo源码进行一些必要的修改,通过上面的结构图我们可以看到,实际上我们正是利用了Dubbo的Filter chain过滤器链这一机制实现的。下面将简单介绍Dubbo的Filter机制。

2. Dubbo Filter机制

Filter是一种递归的链式调用,用来在远程调用真正执行的前后加入一些逻辑,跟AOP的拦截器Servlet中的filter概念一样。

Filter接口定义如下:

        @SPI

        public interface Filter {

          Result invoke(Invoker<? > invoker, Invocation invocation)throws RpcException;

        }

Filter的实现类需要打上@Activate注解,@Activate的group属性是一个string数组,我们可以通过这个属性来指定这个Filter是在consumer还是在provider下激活,当然二者可以同时激活,所谓激活就是能够被获取,组成Filter链。

        List<Filter>  filters  =ExtensionLoader.getExtensionLoader(Filter.class).getAct ivateExtension(invoker.getUrl(), key, group);

        //Key就是SERVICE_FILTER_KEY或REFERENCE_FILTER_KEY

        //Group就是consumer或provider

ProtocolFilterWrapper:在服务的暴露与引用的过程中根据Key是provider还是consumer来构建服务提供者与消费者的调用过滤器链,Filter最终都要被封装到Wrapper中。

        public <T> Exporter<T> export(Invoker<T>invoker)throws RpcException {

        return protocol.export(buildInvokerChain(invoker, Constants.SERVICE_FILTER_KEY,Constants.PROVIDER));
        }

        public <T> Invoker<T> refer(Class<T> type, URL url)throws RpcException {

            return buildInvokerChain(protocol.refer(type, url), Constants.REFERENCE_FILTER_KEY,Constants.CONSUMER);
        }

构建Filter链,当获取激活的Filter集合后就通过ProtocolFilterWrapper类中的buildInvokerChain方法来进行构建:

        for(int i = filters.size()-1; i >= 0; i --){
              final Filter filter = filters.get(i);
              final Invoker<T> next = last;
              last = new Invoker<T>(){
                  public Result invoke(Invocation invocation)throws RpcException {
                      return filter.invoke(next, invocation);
                  }
                  ……//其他方法
              };
         }

3.自动化Mock系统的流程

流程如图3-39所示。

用户通过Dubbo正常访问服务,经过自定义的Filter过滤器,在Filter中判断当前请求是Mock请求还是正常请求。

(1)如果是Mock请求则进行拦截,然后通过HTTP协议转发到Mock系统中,Mock系统根据传递进来的服务名、应用名、方法名和IP地址找到对应的Mock规则和模拟配置数据,并返回预设的数据给Dubbo,从而完成整个调用过程。

(2)如果是正常请求,则Filter不拦截请求,正常放行。

4. Dubbo源码的改造

初步想法是通过在Dubbo配置文件中添加配置属性,用来标注某个服务是Mock服务,来看一个简单的例子,如图3-40所示。

在application标签中添加一个mockurl属性,用来标注Mock系统的地址,在reference标签中添加一个Mock属性,用来标注该服务添加了挡板。

图3-39

图3-40

具体改造过程如下:

在ApplicationConfig中添加mockurl属性,用来存放配置文件解析后的属性值,代码如下:

        public class ApplicationConfig extends AbstractConfig {

            private static final long   serialVersionUID = 5508512956753757169L;

            //应用名称
            private String             name;
            //模块版本
            private String             version;
            //应用负责人
            private String             owner;
            //组织名(BU或部门)
            private String             organization;
            //分层
            private String             architecture;
            //环境,如dev/test/run
            private String             environment;
            //Java代码编译器
            private String             compiler;
            //日志输出方式
            private String             logger;
            //注册中心
            private List<RegistryConfig> registries;
            //服务监控
            private MonitorConfig       monitor;
            //是否为默认
            private Boolean            isDefault;
            //Mock系统地址
            private String mockurl;
        }

与reference标签对应的ReferenceConfig类中有Mock属性值,所以不用再添加Mock属性。Mock属性在ReferenceConfig继承的父类中,继承关系如下:

        AbstractReferenceConfig -> AbstractInterfaceConfig -> AbstractMethodConfig

Mock属性在AbstractMethodConfig类中。

我们都知道Dubbo之间的数据通信主要是通过URL进行的,在执行Filter过滤链之前已经将URL数据进行了解析和封装,在Filter过滤器中可以根据Invocation类直接获取URL属性值。我们新增一个具有Mock功能的Filter类,完整的代码请下载本书相关资源文件获取。