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类,完整的代码请下载本书相关资源文件获取。