admin管理员组

文章数量:1031308

微服务测试

微服务测试

近年来,随着云原生基础技术的提升,包括容器化、服务编排、服务网格、微服务化等技术,应用环境逐步从传统的物理机架构演变到现在的云原生架构。微服务架构作为实现云原生应用的重要条件之一,越来越多的互联网应用采用了微服务架构作为开发应用的架构形式。微服务虽然使得应用更容易开发、更容易扩展,但因为其本质上是一种通过网络进行通信的分布式架构模式,与传统的单体应用相比,会面临更多的故障点。为此,我们需要构建一种面向微服务的测试体系。本节我们将深入了解微服务的多种特性,并针对这些特性制定相应的测试策略。

1. 云原生和微服务

1.1 云原生

什么是云原生?我们看看云原生计算基金会(CNCF)的定义。

云原生技术有利于各组织在公有云、私有云和混合云等新型动态环境中,构建和运行可弹性扩展的应用。云原生的代表技术包括容器、服务网格、微服务、不可变基础设施和声明式API。

这些技术能够构建容错性好、易于管理和便于观察的松耦合系统。结合可靠的自动化手段,云原生技术使工程师能够轻松地对系统作出频繁和可预测的重大变更。

简单说,云原生的最终目的是帮助企业在云上搭建应用,使企业应用能够持续交付、持续部署,可快速扩展,有着更好的容错性等好处。那么为了实现云原生,需要哪些关键技术呢?我们可以参考下CNCF的云原生的技术全景图,参见图1。

读者可以参考CNCF官方网址获取最新的全景图:/。

图1. CNCF云原生技术全景图

从图1我们可以看到为了实现云原生,需要很多技术的支持,包括:数据存储、消息服务、镜像、容器、服务编排、服务发现、服务间调用技术、API网关、服务网格等诸多技术。

按照分层的方式,我们简单看下云原生各层的含义。

1.供给层(Provisioning): 供给层是CNCF的第一层,它主要用于构建云原生平台和应用程序的基础设置,包括:自动化和配置工具用于加快底层计算资源的创建和配置(虚拟机、网络、防火墙规则、负载均衡等),镜像仓库等。

2.运行时层(Runtime): 这一层提供了容器在云原生下运行的所有工具,包括:云原生存储帮助应用轻松快速访问数据、容器运行时用于执行容器化应用、云原生网络用于帮助容器化应用间的互联。

3.编排和管理层(Orchestration & Managment):这一层包含了如何将各个独立的容器化服务作为一个整体进行管理的多类工具,包括:用于调度和管理容器的Kubernetes、用于服务发现的etcd、远程过程调用(RPC)的gPRC、API网关、服务网格等。

4.应用定义和开发层(App Definition & Development):这一层包含的是帮助工程师完成云原生应用开发的各类工具,包括:用于存储和检索数据的数据库(MySQL, TiKV,MongoDB等)、支持松耦合、编排架构的流式处理和消息传递工具(Spark, Kafka等)、以及CI/CD工具等。

5.可观测和分析工具集(Observability and Analysis):CNCF中还包含了这个重要的工具集,其中有:用于监控和采集容器节点健康指标的监控工具(如:Prometheus),用于收集、存储和处理日志消息的日志采集工具,用于跟踪服务间调用的分布式系统跟踪工具(如:Jaeger, OpenTracing等)。

1.2 微服务

从云原生的定义,我们可以看到微服务是云原生里的一个重要的概念。那什么是微服务呢?微服务是一种构建应用的架构方案,应用是由一系列的小型的服务组成,服务间通过明确定义的协议API相互通信。微服务使得应用更容易开发、更容易扩展。

微服务最终要在云上部署、执行,所以微服务应用几乎依赖于所有云原生的技术,可以说云原生对微服务具有天然的友好性,能够更好的支撑微服务体系。

下面我们举个简单的例子来对比下传统的单体服务和微服务架构的区别。

我们有个购物网站,用户通过网站购买商品,这个业务有三个功能模块:用户模块,用于用户管理;订单模块:用于处理订单信息;商品模块,用于商品管理。

图2是用传统的单体服务实现的方案,也就是我们常见的三层实现模式,接入层,业务逻辑层和数据层,同时这些模块会被部署到一台机器上。

图2 单体服务分层架构示例

在业务规模较小的情况下,单体模式有一定的优点:

(1)应用开发比较简单,调试容易:因为模块代码比较集中,同时是单机部署,所以在开发环境搭建上比较容易,所有模块的日志可以直接输出到本机,定位问题也比较容易。

(2)部署比较容易:因为是单体,可以把所有的模块打包到一起,然后直接发布。

但随着业务需求复杂性的增大,单体逐步暴露出很多局限性,比如:

(1)代码复杂性增加,代码难于理解和维护:业务需求复杂性的增加,导致相应的模块代码的复杂性随之增加,模块间的耦合性越来越大,进而导致代码难于理解和维护。

(2)容灾能力差:多个模块因为在同一个进程空间,当某个模块有问题,比如:内存泄露,或是指针越界错误,可能会导致整个进程异常退出,进而影响了其他模块的正常工作。

(3)研发效率降低:在编码阶段,因为代码复杂性的增加,导致研发人员对代码的理解、代码的变更也会增加难度。同时,模块间的强耦合,存在模块间相互等待对方开发完成等问题,不利于团队间的并行开发,整体上拖慢了整个团队的研发效率。

(4)不利于技术栈的升级:单体模式下要求所有模块使用同一个技术栈,当技术栈需要升级时,无法模块化的逐步升级技术栈,需要整体调整技术栈,这也意味着整个项目的同时重构,变更内容过多,存在比较大的风险,给技术升级带来很大的挑战和阻力。

图3是基于微服务架构的实现方案,我们对三个模块分别用三个独立的微服务实现,每个微服务完成服务注册。用户通过网关层访问服务,网关层首先通过注册中心查找到要访问的服务信息,然后再将请求发送给相应的微服务。微服务间的调用也是先通过服务注册中心查找要访问的服务信息,然后再进行通信。

图3 基于微服务架构的实现方案

微服务架构有效的解决了单体服务的上述难点。

(1)降低了代码的复杂度:通过将原来的复杂模块分拆成一些小的微服务,每个服务完成特定的功能,服务间通过协议交互,降低了原来代码的耦合度,有利于代码的理解和维护。

(2)容灾能力强:各个微服务独立部署和运行,某个微服务的异常退出不会影响其他微服务的正常工作。

(3)提升研发效率:因为单个微服务可以独立开发,服务间的调试前期可以通过契约测试等手段来完成,可以让多个小团队并行开发,从而提升了研发效率。

(4)技术栈的升级更容易:各个微服务通过标准的协议进行交互,比如:gRPC、REST等协议,每个服务所用的开发语言、技术栈等可以完全不同,只要完成约定的契约协议即可。技术栈升级时,可以按照微服务的粒度进行逐步升级,技术风险也更小了。

当然微服务架构同时也带来了一些挑战:

(1)分布式特性引入了开发的复杂性:因为服务被拆分成多个独立部署和运行的微服务,服务间的交互变成了跨网络的交互,包括:服务路由、服务间的通信机制、数据一致性、网络及服务的不稳定性、网络延迟等诸多挑战,使得系统开发的复杂性提升很多。

(2)部署更复杂:对于单体服务只需要部署一组文件到某台服务器即可,但微服务架构下的应用通常由很多不同的微服务组成,而每个微服务又可能有多个运行实例,每个运行实例需要可能需要单独配置、监控等,这些都导致微服务的部署更加的复杂。

(3)测试难度加大:和单体服务相比,各个微服务需要独立部署和管理维护。在测试过程中,需要部署上下游服务,需要保证上下游服务的环境稳定性。同时因为日志分散在多个不同服务环境下,导致日志查看难、效率低,需要事先建设一些基础能力来提升测试效率,包括:聚合各个微服务的日志、拼接调用链路等手段。

下面我们总结下单体模式和微服务架构在技术上不同点,这些不同点将是我们针对微服务的测试重点。

表1:微服务架构测试重点

单体模式

微服务架构

微服务架构测试重点

服务生命期管理

服务自己管理

服务注册中心统一管理

单微服务接口测试微服务生命期测试微服务实例漂移测试微服务配置测试

服务间通信

同主机进程内通信

跨主机的网络通信、服务发现及服务路由

链路中断重入测试服务间强弱依赖测试服务间消息异常测试

服务间协作

基于函数API方式进程内调用

基于rpc协议/http协议等契约方式提供服务

协议一致性测试

数据存储

单主机事务

分布式事务

分布式事务测试

1.3 微服务的日志聚合及分布式跟踪

微服务架构下,服务被拆分成很多不同的微服务,服务间的调用越来越多,服务链路更长。当某次服务请求失败时,定位问题将非常的困难。一般是通过日志聚合及分布式跟踪技术来解决这个问题。

1.2.1 日志聚合

因为各个微服务被部署到不同的机器/容器内独立运行,其生成的日志也会分散在各台机器/容器内,而且容器的生命期结束后,其存储的日志也可能会随着一起销毁。当我们定位问题时,需要登录到各个不同的机器/容器内检查日志,这种方式显然是效率低下的。针对这个问题的一种解决方案是构建日志聚合能力。简答说,就是让各个微服务的日志分别上报到统一的日志服务器上。云原生下的一种通用的解决方案是如图4所示的基于Elastic Stack实现的。

图4 基于Elastic Stack的日志聚合方案

Elastic Stack包含了如下关键模块。

(1)日志文件: 微服务生成的日志文件,也就是我们的采集源。

(2)Beats:一种轻量型数据采集器。Beats从环境中收集日志和指标,然后再传输到 Elastic Stack 中。

(3)Logstash:服务器端数据处理管道,能够同时从多个来源采集数据,转换数据,然后将数据发送到Elasticsearch中进行存储。

(4)Elasticsearch:一种搜索和分析引擎。

(5)Kibana:一种数据可视化工具,可以让用户在 Elasticsearch 中使用图形和图表对数据进行可视化展示。

1.3.2 分布式追踪

有了日志聚合后,我们仍需要一种机制,将同一个服务请求分别经由哪些微服务处理串联起来,这样当请求失败时,我们可以快速定位到失败的微服务,同时结合着聚合的日志查看更多的服务上下文日志信息,帮助我们快速定位问题。这种机制就是分布式追踪技术。一种典型的方案是基于zipkin构建一套分布式追踪系统,这里不再展开讲解。

分布式追踪的基本原理如图5所以,给每个请求分配一个唯一的trace_id,在微服务调用时,将这个trace_id传递下去,同时给每个服务设定一个span_id用来标识调用的上下游服务,最终形成一个调用链。

图5 分布式追踪技术示例

有了这个分布式追踪后,我们能做什么呢?下面是一些典型的应用场景。

(1)快速定位问题:当遇到服务失败时,通过调用链路,我们可以快速看到哪个服务失败了,进而帮助我们快速定位问题,包括查看其日志输出、上下游请求/响应等信息。

(2)发现服务间的强弱依赖关系:通过调用链路图,我们可以看到服务间的依赖关系,可以进一步识别和测试哪些服务是关键服务、热点服务,如果失败了,业务上是否有降级方案等。

(3)发现并优化瓶颈服务:通过统计调用链路每个环节的调用耗时,将端到端的耗时分解到每个服务上,进而发现一些耗时较长的服务,并对其进行优化。

搭建完日志采集和链路分析平台后,下面的章节我们将看看如何对基于微服务架构的业务进行测试。

2. 微服务测试体系建设

2.1 单服务接口测试

既然基于微服务架构的业务是由一系列小型的微服务组成,借鉴分层测试的思路,我们首先可以做的是针对每个微服务进行单独的接口测试。下面我们看看常见的接口测试有哪些维度。

1.功能测试:确保请求和响应符合业务功能预期。

2.协议字段测试:包括:

(1)字段类型异常测试:比如:int类型的入参传入"abc"字符型的值;

(2)字段取值范围测试:比如:对于枚举类型的字段传入不存在的值,对接口要求大于0的值传入负数,对字段长度有要求的输入超长字符串等。

(3)必填字段测试:对必填字段不传值等。

(4)默认值测试:对有默认值的字段不传入值,查看默认值参数是否正确。

(5)文本类的字符编码测试:对于一些文本类的接口字段,传入不同的编码字符,包括:中英文、半角、全角、emoji字符等。

3.安全测试:包括:

(1)前置条件测试:确保调用该接口时的前置条件是满足的,才可以调用该接口,比如:对于一些关键信息的查询接口,用户必须登录经过鉴权后才能查询,避免裸接口暴露敏感信息。

(2)关键字段失效性测试:比如:登录session、订单是否有过期时间等。

(3)防篡改能力:比如:对关键字段是否有防篡改校验,如:重入时修改支付金额/支付币种等。

(4)重入验证:接口是否支持重入,重入时是否做了关键字段的幂等验证,确保关键字段重入时没有被篡改。

(5)敏感字段:是否有需要加密传输/存储的字段,比如:个人身份证、银行卡、密码等敏感信息。

(6)是否存在越权:比如:通过修改请求参数访问非该用户的信息。

(7)查询接口是否防遍历:接口设计时是否有防遍历的能力,比如:某些入参ID不能是简单递增的,接口访问有频率限制等。

4.性能测试:可以通过将下游服务mock的方式来单独收集该服务自身的性能数据,也可以通过全链路采集性能数据,然后根据调用链路切分出每个服务的处理时长,找到有性能瓶颈的服务。

5.协议升级测试:包括协议及数据的兼容性等维度的测试。

(1)新老接口协议的兼容性测试:新接口是否需要兼容老接口的服务请求调用,老接口如果收到新接口的服务调用将如何处理等。

这里特别注意的是ProtoBuf(PB)协议消息字段序列号的不变性,当PB协议升级时,不要修改原有的消息字段的序列号,因为PB反序列化时,是按照id来进行字段映射的,否则可能会导致消息解析时字段顺序错乱。

举个实际业务中遇到的案例,如图6所示,我们的服务A调用服务B,服务B将PB协议升级,将其中的PB的一个id=14的字段删除了,然后将原来id=15的字段的id修改成14,把id=16的字段修改成id=15,但未通知服务A升级PB协议,结果服务B收到服务A发送过来的旧的消息包时,按照新的PB协议解析,取到的id=14字段bk_operation的值其实是bk_direction的,取得的id=15字段bk_type其实是bk_operation字段的值,进而造成业务上的bug。

图6 PB字段序列号变更举例

(2)新老接口的数据兼容性测试:这个可能是容易忽略的测试场景,比如:老接口生成的未到终态的数据是否可以由新接口来继续处理。

我们举个实际业务中遇到的案例:企业用户注册模块由一系列的注册步骤组成,比如:登记企业基本信息、上传营业执照、银行卡身份验证等,因为注册流程比较长,任何一个步骤,用户都有可能临时中断,之后再继续注册。期间我们进行了注册接口升级,变更了计算敏感数据签名的算法,当新接口加载之前老接口的数据进行验证时,验证失败,导致尚未完成注册的企业无法继续后续的注册流程。

2.2 微服务生命期测试

本节我们首先了解下微服务的生命期有哪些状态,基于此设计我们的测试场景。

图7 微服务的生命期

图7 显示了微服务的典型的生命期过程,在研发阶段结束后,我们将获得一个可以部署和运行的微服务构建物,接着是给微服务分配一个运行时环境,比如:拉起一个docker镜像,然后是启动微服务,进入启动状态后微服务会向注册中心发起注册,其他微服务就可以查询和调用该微服务了。当服务需要终止时,比如服务下线,在进入终止状态前微服务会向注册中心发起注销信息,从服务注册中心下线,最后其执行环境会被释放。

这个阶段需要关注的测试点包括:

(1)微服务注册:验证微服务上线后,是否能够自动注册到服务注册中心并且被其他服务发现。

(2)微服务注销:微服务服务退出,调用方是否能够及时感知到?服务注册中心一般有两种机制来感知服务下线,一是上面提到的微服务退出前主动上报,这种是最及时的;同时,服务注册中心还会对微服务进行心跳检测,如果发现一段时间内没有收到微服务的心跳包,则认为该微服务下线了,比如当微服务异常退出的场景,就依赖心跳检测机制。无论是哪种情况,调用方都需要处理被调服务退出的场景,是否有合理的处理机制,比如重试或是进入服务降级等操作。同时,我们还要关注微服务是否有自我异常退出的检测机制,如果发现异常退出了,是否需要重新拉起,是否需要从服务注册中心注销服务等操作。

2.3 微服务实例漂移测试

在介绍微服务实例漂移之前,我们先介绍下什么是有状态服务和无状态服务。在云原生环境下,一般是推荐无状态服务。

无状态服务(stateless service):服务对单次请求的处理,不依赖于之前的请求,服务本身不存储任何信息。比如:我们常用的HTTP协议本身是无状态的服务,每个请求之间是不相互依赖的。有状态服务(stateful service):与无状态服务相反,它会保存一些请求的上下文信息,从而先后的请求是有一定的关联性。例如,我们经常会使用Session 来保存用户的一些信息,虽然HTTP协议是无状态的,但是借助于Session,我们将HTTP服务转换成了有状态服务。

下面我们看个例子。

用户通过负载均衡访问微服务A实例,完成登录后会获取一个token,下次再访问该服务时(如:查看账单),请求里会带上这个token,当微服务实例收到请求后,会再次验证token的有效性,如果有效则返回响应信息。

图8是一种有状态的实现方式,因为微服务A实例1异常退出,下次再次访问微服务A时,将访问请求转发到了实例2上,即发生了实例漂移,但因为实例2没有存储之前的token信息,导致token验证失败。

为解决这个问题,图9通过一个公共DB实现了状态化服务,但微服务实例本身是无状态的。微服务A实例1首先将token信息保存到了DB中,那么当实例2需要验证token信息时,会访问公共DB查询token信息进行验证。

图8 有状态服务举例

图9 无状态服务,通过外部DB来实现状态化服务

微服务设计上一般建议设计成无状态的,主要是考虑动态可伸缩性,可以更好的应对实例漂移。在云原生环境下,微服务实例可能会被频繁的创建和销毁。如果server是无状态的,那么对于客户端来说,就可以将请求发送到任意一个服务实例上,然后通过负载均衡等手段,实现水平扩展。但如果server是有状态的,那么就无法很容易地实现水平扩展,因为需要始终把请求发送到同一个服务实例上才行。

在微服务测试中,我们要关注这类微服务的服务状态设计。

(1)如果是有状态服务,要验证同一个请求在中断后,下次重入时,是否能够路由到同一个服务实例上。

(2)如果是无状态服务,要验证同一个请求在中断后,下次重入时,如果路由到同一个服务的其他服务实例上,请求是否能够继续正确的处理。

2.5 微服务的配置测试

图10 微服务生命期,含加载配置

图10所示,微服务启动时会加载应用配置。关于配置加载需要关注以下几点:

(1)配置来源:需要梳理服务依赖的配置来源有哪些,在执行环境下是否正确配置了,是否做到了环境隔离。微服务的配置来源可以是所在运行环境的环境变量、可以是来自于配置中心的配置变量。

对于运行环境的环境变量要做到服务间的环境隔离,可以梳理环境变量的来源,构造环境变量缺失、环境变量配置错误等场景来验证当环境变量异常时微服务的运行情况。

对于来自于配置中心的配置项,需要特别关注的一点配置项是否是通用配置,要和微服务实例不相关,比如:将微服务所在容器的IP配置到配置项内,显然是不合理的,因为其他微服务实例读取到的这个IP配置和其自身的容器IP地址是不同的。像这类变量可以通过容器初始化时,注入到容器的环境变量里。

(2)配置加载时机:一般是在微服务启动时加载,还有一种配置持续更新的机制,一般是通过一个定时任务,不断查询配置项是否有变更,或是每次用到配置项的时候都去读取最新的数据。建议在加载配置时,对配置项进行合法性校验,而不是在使用时才发现配置异常,进而导致业务出现问题。

(3)上下游关联配置的合理性:因为每个微服务都有一个自己单独的配置项,服务间相关的配置的合理性容易被忽略。一个典型的案例:关于服务超时timeout配置,上游服务的timeout至少要大于下游的timeout配置,否则会出现下游服务有结果返回时,上游服务已经给调用方返回超时了。

(4)配置内容测试:主要是针对配置项进行常规的测试,包括:类型、长度、配置项缺失(key或value或都缺少)、默认值等,这里需要注意当配置项异常时,是否应该影响业务主流程。

2.6 服务调用链路异常测试

上面分布式跟踪章节中,我们提到通过trace_id和span_id我们可以构建出服务的调用链路。考虑到微服务间的调用都是通过网络调用完成的。网络可能会存在抖动、延迟、丢包等问题。所以在测试微服务业务时,我们需要特别关注这类场景。下面我们梳理下有哪些典型的测试场景。

2.6.1 链路中断重入测试

首先我们看下为什么要做中断重入测试。

我们举个支付流程的例子。如图11所示,我们一个简单的支付流程包括查单->下单-> 校验 -> 扣款。每一步都是由一个微服务来完成的,那么可能存在多种中断的场景。一般的业务都会允许用户重试支付,也就是重入。对于中断3和中断4场景,下单成功后,用户重新发起支付流程,我们期望支付成功并且扣款金额是正确的。但如果中断重入处理不当,可能会带来一定的资金损失。比如:用户完成支付后,扣款服务没有返回结果,用户再次发起支付,是否会导致用户被扣了两次款?

除了支付流程外,如果我们引入其他的流程,也同样会存在更多的资金风险场景。比如:

(1)退款流程:用户支付完成后,申请退款,申请退款时,链路发生中断,用户再次发起退款请求,退款行为是否会被执行两次,也就是用户收到两份退款?

(2)提现流程:用户提现时,链路发生中断,用户再次发起提现,是否会收到两份提现金?

图11 中断重入场景举例

通过以上的举例,我们可以看到链路中断重入测试是非常有必要的。链路中断重入测试的核心流程如下:

(1)梳理服务间的调用链路,可以通过上文提到的分布式跟踪技术来完成。

(2)依次模拟每个微服务中断的场景,微服务中断的模拟可以有多种方式,简单的一种可以通过修改微服务的名称配置,进而导致服务发现失败来模拟。第一次服务访问失败后,接着进行重入测试,将相同的访问请求再发送一遍,如果业务支持重入(幂等),第二次的重入请求会被正确处理。

这里我们再重点讲解下幂等性,这个和重入测试是十分相关的。

幂等性:简单说就是对一个接口,相同的请求不管调用多少次,对系统的影响与一次执行是相同的,对调用方的返回每次也是相同的。

下面举两个实际业务中的bug来说明幂等性:

(1)举例1:微服务AcceptOrder重入时,返回的order_time和第一次调用时返回的格式不一致。

bug说明:我们有个AcceptOrder微服务,该服务用于处理传入的订单,上游服务调用该服务接口时,会返回一个订单order对象,其中一个字段是订单时间accept_time。order对象会保存到DB中,下次接口重入时,会从DB查询该订单是否存在,如果存在,则直接返回DB中的order对象,而不用重新创建一个新的订单。

我们预期两次返回的order对象的属性都是一样的。但实际中发现accpet_time在第一次返回的是“2022-10-25 15:08:14”,重入时返回的是“2022-10-25T15:08:14+08:00”。两次返回结果不一致,进而导致上层服务做日期解析时报错。

原因分析:第一次调用AcceptOrder服务时,这个accept_time是AcceptOrder服务从内存中获取当前时间“2022-10-25 15:08:14”,然后就返给了上游服务。在保存到DB时,保存的是“2022-10-25T15:08:14+08:00”这种格式(包含了时区信息)。重入时,是从DB中读取的该字段。两次时间格式的不一致,导致重入行为的不一致,进而违反了接口的幂等性原则。

(2)举例2:AcceptServer调用BookingServer成功后,但在更新账单状态时失败,第一次调用和重入时返回结果不一致。

bug说明:我们有三个微服务UserServer,AcceptServer和BookingServer,UserServer用来接收用户的订单请求,然后将订单请求转发给AcceptServer来处理,AcceptServer会先生成一个记账单,然后调用BookingServer完成记账。第一次AcceptServer调用BookingServer进行记账时,BookingServer记账失败了,就返回了一个错误码给到AcceptServer,然后AcceptServer透传错误码给UserServer。

但当UserServer收到错误码,再次调用AcceptServer进行重入时,AcceptServer从DB中查到该重入订单,不再去调用BookingServer进行记账,然后就直接返回了成功的返回码。但实际上这个订单在DB中的记账单的状态仍然是失败的,在相同的数据下,两次请求返回的结果不一致。

原因分析:当AcceptServer第一次记账时,会在DB里记录了一条记账单,这个记账单的状态是处于初始态的,同时整个系统还有另外一个批跑程序来直接读取DB中的未到终态的记账单并将其做到终态。当AcceptServer重入时,查到DB中有记账单时,它也不会去调用BookingServer去记账了,即使是记账单还没有被批跑程序做到终态,AcceptServer都会直接返回成功码给调用方。在记账单处于相同状态下,却有两种不同的返回码,这种行为也是违反了幂等原则。

2.6.2 服务间强弱依赖测试

我们首先看下服务间的强弱依赖的定义。

假定我们有两个微服务A和B,A会调用B。

强依赖:如果微服务B出现故障不可用时,微服务A也不可用,这种依赖就是强依赖。

弱依赖:如果微服务B出现故障不可用时,针对B有个降级策略,微服务A仍然可用,那这种依赖就是弱依赖。

梳理出微服务间的强弱依赖关系,有什么用处呢?有了这个强弱依赖,我们提前发现因为依赖问题可能导致的故障,避免依赖故障影响系统的可用性。

举个例子,如图12所示,某后台下载服务,网关收到前端App的下载文件请求后,首先进行鉴权,如果鉴权成功,接着在下载文件前,会先调用CDN竞速服务,连接几个CDN节点竞速,获得最快的一个CDN节点,最后调用下载服务从该节点下载文件。对外的后台下载网关主要依赖于鉴权服务、CDN竞速服务、文件下载服务。接着我们来分析下哪些服务对后台下载网关是强依赖的、哪些是弱依赖的。显然鉴权服务和文件下载服务时强依赖的,如果鉴权服务异常,我们无法验证前端App的身份,不允许匿名下载的;同样,如果文件下载服务异常,我们也将无法提供下载能力。而CDN竞速服务则是一个弱依赖服务,当CDN节点竞速服务失败时,从系统健壮性及用户体检的角度,我们需要将这个依赖降级为弱依赖,给下载服务提供一个降级服务,比如可以从一个默认的CDN节点下载文件。

图12 下载服务的强弱依赖图

具体我们该如何实施强弱依赖测试呢?通过我们上文提到的分布式追踪技术,我们可以快速的生成微服务间的调用链路,即服务间的依赖关系。然后借助于中断重入测试,通过依次构造每个服务的异常,如果业务不受影响,说明异常的服务是个弱依赖的服务,这样我们就可以得到服务间的强弱依赖关系拓扑了。接着我们会结合业务场景分析当前的强弱依赖关系是否合理,找到优化点。

2.6.3 服务间消息异常测试

在微服务架构下,服务间都是通过网络进行消息传递,因为网络传输的不确定性,存在消息丢失、消息延迟、消息乱序等情况出现。在微服务业务测试中,我们需要覆盖各类消息异常场景的测试。大致有如下的手段:

(1)基于混沌工程:混沌工程中有网络相关的异常模拟工具,可以模拟网络抖动、延迟、丢包等异常。这里可以参考本书的16.4节内容。

(2)基于时序建模:混沌工程的方式是采用的一种随机的方式来模拟各种时序上的异常,无法确保消息时序异常场景模拟的完备性,故而存在一定的局限性。时序建模测试是通过梳理出一个分布式系统的时序模型图,生成向量时钟图,并且基于向量比较和一定的策略生成时序测试用例,从而覆盖更完备的时序场景。这部分内容可以参考本书的20.4章节。

2.7 协议一致性测试

首先我们先拓展下协议的概念,除了微服务本身的服务协议文件外,如:ProtoBuf(PB)协议,一般微服务还会和DB有交互,DB的字段定义也算是一种协议。归纳起来有如下三类协议不一致的场景,如图13所示。

图13 协议不一致场景

2.7.1 PB之间字段定义的一致性

这里我们重点关注PB协议字段的名称、类型和取值范围,在多个不同PB间是否一致。

syntax = "proto3"; import "validate.proto"; message QueryReq { string rest_code = 1 [(validate.rules).string = {min_len: 4, max_len: 4}]; string order_id = 2 [(validate.rules).string = {min_len: 1, max_len: 64}]; int32 from_type = 3 [(validate.rules).int32 = {gt: 0, lt: 10}];// }

代码段 1 :Protobuf 示例

代码段1是基于proto3的并且包含字段校验定义的PB协议的示例,从这个示例里我们可以看到rest_code是字符串类型,并且长度是必须是4;order_id也是字符串类型,长度是1到64之间;from_type是一个int32类型的,取值范围是(0,10)。

通过搜集出上下游相关微服务的PB协议的定义,我们就可以比对它们之间相同字段的定义是否要满足一定的一致性条件。当不一致时,是否可能会有服务异常。

比如:上游服务A调用下游服务B,B接着调用服务C,A和B之间的PB协议中的某个字段name长度为20,而B和C之间的PB协议中相同的字段name的长度为10。那么就存在从A传入的name长度大于10时,传递到C的name可能会被截断,导致业务bug出现。

2.7.2 PB和DB之间字段定义的一致性

对于一些和DB有交互的微服务,在接收到请求消息后,经过一系列逻辑处理后,会在DB中存储一些相关信息。这些信息有些是直接来自于请求消息,需要我们关注的是DB的字段定义和相应的PB字段定义之间是否要满足一定的一致性要求。当不一致时,同样可能会有服务异常。

对于DB字段来说,我们会重点关注:名称、类型、字符集、长度、默认值等信息。

CREATE TABLE `t_restaurant` ( `Fid` bigint NOT NULL AUTO_INCREMENT COMMENT '自增主键', `Frest_code` varchar(4) NOT NULL COMMENT '餐厅代号', `Forder_id` varchar(64) NOT NULL COMMENT '订单ID', `Ffrom_type` smallint NOT NULL COMMENT '订单来源', `Fstate` smallint DEFAULT '0' COMMENT '逻辑删除 1-删除 0-未删除', `Fmemo` varchar(500) DEFAULT NULL COMMENT '备注', `Fcreate_time` datetime NOT NULL COMMENT '创建时间', `Fmodify_time` datetime NOT NULL COMMENT '修改时间', PRIMARY KEY (`Fid`), KEY `idx_modify_time` (`Fmodify_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='餐厅订单表';

代码段2:t_restaurant表的sql创建语句

代码段2是一个t_restaurant表的sql创建语句,从这个sql语句中,我们可以看到表的各个字段的定义。有了DB字段的定义和上节我们提到的PB的协议定义后,我们就可以对比他们之间的差异,找到一些不一致的点,发现一些潜在的问题。

(1)名称是否一致:建议规则:DB表字段的名称是以F开头的,去掉F后的名称则是PB协议中的字段名称。遵照这个规则,DB和PB间的字段就建立起了映射关系,可以非常方便我们编写脚本来自动做对比和监控,而无需人工做比对了,特别是当微服务和DB表数量比较大的时候,这个规则就变得尤其重要了,最好是在项目初期研发人员就按照这个规则来设计PB和DB表,将会极大的提升我们的比对效率和准确度。

(2)定义类型是否一致:需要我们梳理出PB中的字段定义类型和DB的字段定义类型的映射关系,读者可以查询PB官网的字段类型定义和DB(如MySQL)的字段类型定义,确保两者的映射关系是正确的。比如:PB中定义的是varchar,而DB中用的bigint类型,如果在DB操作时,未考虑到两者的差异,可能会引入bug。如:"10" 映射成bigint是10,而"10ab"映射成bigint时也是"10",就存在信息的丢失了。

(3)字段长度是否一致:这一点也需要我们特别的关注,如果DB字段的长度比PB的字段长度短,当数据超过DB字段长度时,在保存DB数据时,就存在数据被截断的情况,进而导致存储的数据有误的bug。

2.7.3 DB之间字段定义的一致性

这一项主要是确保整个系统的DB之间存储的相同的字段要做到统一一致,字段上主要关注名称、类型、字符集、长度、默认值等信息的一致性。

DB一般是为微服务使用的,当存在不一致时,就可能会导致一些风险。

比如(以下举例以MySQL DB为例):

(1)字符集不一致:表1用utf8mb4,表2用utf8(即utf8mb3),对于一些特殊的4字节的emoji表情字符,同一份数据在表1可以正常存储,在表2会被截断成3字节存储,如果微服务读取表1的数据,然后保存到表2时,就会出现数据不一致的情况。

(2)定义类型不一致:可能会遇到mysql隐式转换的问题,比如:一个表里保存的是int类型,另外一个表是varchar类型,当用int类型值来匹配varchar类型的数据时,mysql会将varchar转化成int类型进行匹配,导致匹配结果有误,如,第7行的'a5'被隐式转换成0,从而匹配成功。

mysql> SELECT 1 > '5a'; -> 0 mysql> SELECT 7 > '5a'; -> 1 mysql> SELECT 0 > 'a5'; -> 0 mysql> SELECT 0 = 'a5'; -> 1

代码段3:sql隐式转换举例

(3)字段长度不一致:会导致从一个表读取的数据,保存到另外一个表时可能会被截断的风险。

(4)默认值不一致:当相同的字段数据保存到不同的表中,对于有默认值的相同字段,要确保默认值的一致性。

2.8 分布式事务测试

事务的概念是指操作各种数据项的一个操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。在单机系统下可以通过DB来完成一次事务操作。在分布式系统下,服务之间需要通过网络远程协作完成的事务称之为分布式事务。

在分布式事务场景下,有多种解决方案,包括:2PC两阶段提交,TCC(try-confirm-cancel)模式,Saga模式以及最大努力等模式。其中两阶段提交存在单点故障及同步阻塞等问题,故不适合高并发的互联网业务。TCC模式,需要业务遵循TCC开发模式,导致开发成本较高,同时,对于业务流程长的场景,事务边界长,加锁时间长,TCC模式将会影响并发性能。所以对于业务流程长的微服务业务,Saga模式将更适用一些。

Saga 事务基本协议如下,参见图14:

每个 Saga 事务由一系列有序本地子事务(sub-transaction) T1,T2,…,Ti,…,Tn组成。

本地子事务操作本地存储,其事务性可以通过DB来实现,当本地子事务成功后,会发送通知下一个子事务执行。

图14 Saga模式举例

如果 T1 到 Tn 均成功提交,那么事务就可以顺利完成。否则,就要采取恢复策略,恢复策略分为向前恢复和向后恢复两种。

向前恢复的执行模式为,T1,T2,…,Ti(失败),Ti(重试)…,Ti+1,…,Tn。Ti失败时,会一直重试Ti直到成功,接着执行后续的事务,参加图15。

图15 Saga模式之向前恢复机制

向后恢复的执行模式为:T1,T2,…,Ti(失败),Ci(补偿),…,C2,C1。Ti失败时,会反向执行Ci及其之前的每个补偿动作。每个 Ti都有对应的幂等补偿动作C1,C2,…,Ci,…,Cn,补偿动作用于撤销 T1,T2,…,Ti,…,Tn造成的结果,参见图16。

图16 Saga模式之向后恢复机制

结合表2的第三方平台网上购物场景举例,我们可以看到本地子事务分为三类:

(1)可补偿性事务(Compensatable transactions):可以使用补偿事务回滚的事务。比如:网上购物时,客户下单后,一直未完成支付,那么下单服务对应的补偿事务将完成将订单关闭的动作。

(2)关键性事务(Pivot transactions):Saga执行过程的关键点。如果关键性事务成功,则Saga将一直运行到完成。比如:网上购物时,买家完成了支付,卖家发货,买家确认收货。买家确认收货是一个关键性事务,其之后的事务要保证成功,如:最终确保第三方支付平台将收款转到卖家。

(3)可重复性事务(Retriable transactions):在关键性事务之后的事务,可通过重复执行确保成功,这就要求服务的幂等性。如:表2的第6步遇到失败时,反复向卖家转账直到转账成功。

表2 分布式事务场景举例

步骤

微服务名称

微服务行为

事务类别

备注

1

下单服务

买家下单服务

可补偿性事务

补偿行为:关闭订单

2

账号服务

买卖家身份验证服务

-

只是做验证,无需补偿

3

支付服务

买家支付到第三方平台

可补偿性事务

补偿行为:对买家退款

4

发货服务

卖家发货

可补偿性事务

补偿行为:取消订单

5

收货服务

买家确认收货

关键性事务

买家确认收货后,需要确保剩下的事务操作成功

6

转账服务

第三方平台转账到卖家

重复性事务

可重复多次,确保转账成功

在理解了分布式事务Saga模式后,我们的测试思路和重点如下:

(1)首先构建业务的Saga模型,包含本地子事务的调用序列、每个子事务的类型(可补偿性事务/关键性事务/可重复性事务)。

(2)评估Saga模型的合理性,比如:调用序列、每个子事务类型是否正确等。

(3)验证向前恢复,对可重复性事务进行幂等验证。

(4)验证向后恢复,这点是针对可补偿性事务,按照调用事务链,从关键性事务点依次往前回退验证。

另外,关于补偿性事务和原事务要特别关注两个时序问题:

(1)原事务未执行,补偿事务执行了:比如原事务的请求因为网络丢包没被执行,导致整个事务回滚,原事务的补偿事务会被执行,这个时候要确保补偿事务做正确的校验并执行正确。

(2)补偿事务先于原事务执行:原事务的请求因为网络拥塞,请求超时,导致整个事务回滚,原事务的补偿事务被执行,接着原事务的请求又到达了,这时对原事务的请求是要禁止执行的。

3. 总结

本节介绍了云原生及微服务相关的基本概念。通过对比单体架构和微服务架构,我们了解到微服务架构的特点以及微服务架构的分布式特性带来的测试挑战。接着针对这些挑战,我们梳理出了微服务测试体系,覆盖了微服务特有的多种测试维度,包括:微服务生命期测试,调用链路异常测试、协议一致性测试以及分布式事务测试等,从而为微服务的产品质量提供更全面的保障。

4. 参考文献

[1] CNCF官网:/

[2] 克里斯·理查森(Chris Richardson):<<微服务架构设计模式>>

[3] 科妮莉亚·戴维斯(Cornelia Davis):<<云原生模式>>

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。原始发表:2025-02-09,如有侵权请联系 cloudcommunity@tencent 删除测试服务配置事务微服务

微服务测试

微服务测试

近年来,随着云原生基础技术的提升,包括容器化、服务编排、服务网格、微服务化等技术,应用环境逐步从传统的物理机架构演变到现在的云原生架构。微服务架构作为实现云原生应用的重要条件之一,越来越多的互联网应用采用了微服务架构作为开发应用的架构形式。微服务虽然使得应用更容易开发、更容易扩展,但因为其本质上是一种通过网络进行通信的分布式架构模式,与传统的单体应用相比,会面临更多的故障点。为此,我们需要构建一种面向微服务的测试体系。本节我们将深入了解微服务的多种特性,并针对这些特性制定相应的测试策略。

1. 云原生和微服务

1.1 云原生

什么是云原生?我们看看云原生计算基金会(CNCF)的定义。

云原生技术有利于各组织在公有云、私有云和混合云等新型动态环境中,构建和运行可弹性扩展的应用。云原生的代表技术包括容器、服务网格、微服务、不可变基础设施和声明式API。

这些技术能够构建容错性好、易于管理和便于观察的松耦合系统。结合可靠的自动化手段,云原生技术使工程师能够轻松地对系统作出频繁和可预测的重大变更。

简单说,云原生的最终目的是帮助企业在云上搭建应用,使企业应用能够持续交付、持续部署,可快速扩展,有着更好的容错性等好处。那么为了实现云原生,需要哪些关键技术呢?我们可以参考下CNCF的云原生的技术全景图,参见图1。

读者可以参考CNCF官方网址获取最新的全景图:/。

图1. CNCF云原生技术全景图

从图1我们可以看到为了实现云原生,需要很多技术的支持,包括:数据存储、消息服务、镜像、容器、服务编排、服务发现、服务间调用技术、API网关、服务网格等诸多技术。

按照分层的方式,我们简单看下云原生各层的含义。

1.供给层(Provisioning): 供给层是CNCF的第一层,它主要用于构建云原生平台和应用程序的基础设置,包括:自动化和配置工具用于加快底层计算资源的创建和配置(虚拟机、网络、防火墙规则、负载均衡等),镜像仓库等。

2.运行时层(Runtime): 这一层提供了容器在云原生下运行的所有工具,包括:云原生存储帮助应用轻松快速访问数据、容器运行时用于执行容器化应用、云原生网络用于帮助容器化应用间的互联。

3.编排和管理层(Orchestration & Managment):这一层包含了如何将各个独立的容器化服务作为一个整体进行管理的多类工具,包括:用于调度和管理容器的Kubernetes、用于服务发现的etcd、远程过程调用(RPC)的gPRC、API网关、服务网格等。

4.应用定义和开发层(App Definition & Development):这一层包含的是帮助工程师完成云原生应用开发的各类工具,包括:用于存储和检索数据的数据库(MySQL, TiKV,MongoDB等)、支持松耦合、编排架构的流式处理和消息传递工具(Spark, Kafka等)、以及CI/CD工具等。

5.可观测和分析工具集(Observability and Analysis):CNCF中还包含了这个重要的工具集,其中有:用于监控和采集容器节点健康指标的监控工具(如:Prometheus),用于收集、存储和处理日志消息的日志采集工具,用于跟踪服务间调用的分布式系统跟踪工具(如:Jaeger, OpenTracing等)。

1.2 微服务

从云原生的定义,我们可以看到微服务是云原生里的一个重要的概念。那什么是微服务呢?微服务是一种构建应用的架构方案,应用是由一系列的小型的服务组成,服务间通过明确定义的协议API相互通信。微服务使得应用更容易开发、更容易扩展。

微服务最终要在云上部署、执行,所以微服务应用几乎依赖于所有云原生的技术,可以说云原生对微服务具有天然的友好性,能够更好的支撑微服务体系。

下面我们举个简单的例子来对比下传统的单体服务和微服务架构的区别。

我们有个购物网站,用户通过网站购买商品,这个业务有三个功能模块:用户模块,用于用户管理;订单模块:用于处理订单信息;商品模块,用于商品管理。

图2是用传统的单体服务实现的方案,也就是我们常见的三层实现模式,接入层,业务逻辑层和数据层,同时这些模块会被部署到一台机器上。

图2 单体服务分层架构示例

在业务规模较小的情况下,单体模式有一定的优点:

(1)应用开发比较简单,调试容易:因为模块代码比较集中,同时是单机部署,所以在开发环境搭建上比较容易,所有模块的日志可以直接输出到本机,定位问题也比较容易。

(2)部署比较容易:因为是单体,可以把所有的模块打包到一起,然后直接发布。

但随着业务需求复杂性的增大,单体逐步暴露出很多局限性,比如:

(1)代码复杂性增加,代码难于理解和维护:业务需求复杂性的增加,导致相应的模块代码的复杂性随之增加,模块间的耦合性越来越大,进而导致代码难于理解和维护。

(2)容灾能力差:多个模块因为在同一个进程空间,当某个模块有问题,比如:内存泄露,或是指针越界错误,可能会导致整个进程异常退出,进而影响了其他模块的正常工作。

(3)研发效率降低:在编码阶段,因为代码复杂性的增加,导致研发人员对代码的理解、代码的变更也会增加难度。同时,模块间的强耦合,存在模块间相互等待对方开发完成等问题,不利于团队间的并行开发,整体上拖慢了整个团队的研发效率。

(4)不利于技术栈的升级:单体模式下要求所有模块使用同一个技术栈,当技术栈需要升级时,无法模块化的逐步升级技术栈,需要整体调整技术栈,这也意味着整个项目的同时重构,变更内容过多,存在比较大的风险,给技术升级带来很大的挑战和阻力。

图3是基于微服务架构的实现方案,我们对三个模块分别用三个独立的微服务实现,每个微服务完成服务注册。用户通过网关层访问服务,网关层首先通过注册中心查找到要访问的服务信息,然后再将请求发送给相应的微服务。微服务间的调用也是先通过服务注册中心查找要访问的服务信息,然后再进行通信。

图3 基于微服务架构的实现方案

微服务架构有效的解决了单体服务的上述难点。

(1)降低了代码的复杂度:通过将原来的复杂模块分拆成一些小的微服务,每个服务完成特定的功能,服务间通过协议交互,降低了原来代码的耦合度,有利于代码的理解和维护。

(2)容灾能力强:各个微服务独立部署和运行,某个微服务的异常退出不会影响其他微服务的正常工作。

(3)提升研发效率:因为单个微服务可以独立开发,服务间的调试前期可以通过契约测试等手段来完成,可以让多个小团队并行开发,从而提升了研发效率。

(4)技术栈的升级更容易:各个微服务通过标准的协议进行交互,比如:gRPC、REST等协议,每个服务所用的开发语言、技术栈等可以完全不同,只要完成约定的契约协议即可。技术栈升级时,可以按照微服务的粒度进行逐步升级,技术风险也更小了。

当然微服务架构同时也带来了一些挑战:

(1)分布式特性引入了开发的复杂性:因为服务被拆分成多个独立部署和运行的微服务,服务间的交互变成了跨网络的交互,包括:服务路由、服务间的通信机制、数据一致性、网络及服务的不稳定性、网络延迟等诸多挑战,使得系统开发的复杂性提升很多。

(2)部署更复杂:对于单体服务只需要部署一组文件到某台服务器即可,但微服务架构下的应用通常由很多不同的微服务组成,而每个微服务又可能有多个运行实例,每个运行实例需要可能需要单独配置、监控等,这些都导致微服务的部署更加的复杂。

(3)测试难度加大:和单体服务相比,各个微服务需要独立部署和管理维护。在测试过程中,需要部署上下游服务,需要保证上下游服务的环境稳定性。同时因为日志分散在多个不同服务环境下,导致日志查看难、效率低,需要事先建设一些基础能力来提升测试效率,包括:聚合各个微服务的日志、拼接调用链路等手段。

下面我们总结下单体模式和微服务架构在技术上不同点,这些不同点将是我们针对微服务的测试重点。

表1:微服务架构测试重点

单体模式

微服务架构

微服务架构测试重点

服务生命期管理

服务自己管理

服务注册中心统一管理

单微服务接口测试微服务生命期测试微服务实例漂移测试微服务配置测试

服务间通信

同主机进程内通信

跨主机的网络通信、服务发现及服务路由

链路中断重入测试服务间强弱依赖测试服务间消息异常测试

服务间协作

基于函数API方式进程内调用

基于rpc协议/http协议等契约方式提供服务

协议一致性测试

数据存储

单主机事务

分布式事务

分布式事务测试

1.3 微服务的日志聚合及分布式跟踪

微服务架构下,服务被拆分成很多不同的微服务,服务间的调用越来越多,服务链路更长。当某次服务请求失败时,定位问题将非常的困难。一般是通过日志聚合及分布式跟踪技术来解决这个问题。

1.2.1 日志聚合

因为各个微服务被部署到不同的机器/容器内独立运行,其生成的日志也会分散在各台机器/容器内,而且容器的生命期结束后,其存储的日志也可能会随着一起销毁。当我们定位问题时,需要登录到各个不同的机器/容器内检查日志,这种方式显然是效率低下的。针对这个问题的一种解决方案是构建日志聚合能力。简答说,就是让各个微服务的日志分别上报到统一的日志服务器上。云原生下的一种通用的解决方案是如图4所示的基于Elastic Stack实现的。

图4 基于Elastic Stack的日志聚合方案

Elastic Stack包含了如下关键模块。

(1)日志文件: 微服务生成的日志文件,也就是我们的采集源。

(2)Beats:一种轻量型数据采集器。Beats从环境中收集日志和指标,然后再传输到 Elastic Stack 中。

(3)Logstash:服务器端数据处理管道,能够同时从多个来源采集数据,转换数据,然后将数据发送到Elasticsearch中进行存储。

(4)Elasticsearch:一种搜索和分析引擎。

(5)Kibana:一种数据可视化工具,可以让用户在 Elasticsearch 中使用图形和图表对数据进行可视化展示。

1.3.2 分布式追踪

有了日志聚合后,我们仍需要一种机制,将同一个服务请求分别经由哪些微服务处理串联起来,这样当请求失败时,我们可以快速定位到失败的微服务,同时结合着聚合的日志查看更多的服务上下文日志信息,帮助我们快速定位问题。这种机制就是分布式追踪技术。一种典型的方案是基于zipkin构建一套分布式追踪系统,这里不再展开讲解。

分布式追踪的基本原理如图5所以,给每个请求分配一个唯一的trace_id,在微服务调用时,将这个trace_id传递下去,同时给每个服务设定一个span_id用来标识调用的上下游服务,最终形成一个调用链。

图5 分布式追踪技术示例

有了这个分布式追踪后,我们能做什么呢?下面是一些典型的应用场景。

(1)快速定位问题:当遇到服务失败时,通过调用链路,我们可以快速看到哪个服务失败了,进而帮助我们快速定位问题,包括查看其日志输出、上下游请求/响应等信息。

(2)发现服务间的强弱依赖关系:通过调用链路图,我们可以看到服务间的依赖关系,可以进一步识别和测试哪些服务是关键服务、热点服务,如果失败了,业务上是否有降级方案等。

(3)发现并优化瓶颈服务:通过统计调用链路每个环节的调用耗时,将端到端的耗时分解到每个服务上,进而发现一些耗时较长的服务,并对其进行优化。

搭建完日志采集和链路分析平台后,下面的章节我们将看看如何对基于微服务架构的业务进行测试。

2. 微服务测试体系建设

2.1 单服务接口测试

既然基于微服务架构的业务是由一系列小型的微服务组成,借鉴分层测试的思路,我们首先可以做的是针对每个微服务进行单独的接口测试。下面我们看看常见的接口测试有哪些维度。

1.功能测试:确保请求和响应符合业务功能预期。

2.协议字段测试:包括:

(1)字段类型异常测试:比如:int类型的入参传入"abc"字符型的值;

(2)字段取值范围测试:比如:对于枚举类型的字段传入不存在的值,对接口要求大于0的值传入负数,对字段长度有要求的输入超长字符串等。

(3)必填字段测试:对必填字段不传值等。

(4)默认值测试:对有默认值的字段不传入值,查看默认值参数是否正确。

(5)文本类的字符编码测试:对于一些文本类的接口字段,传入不同的编码字符,包括:中英文、半角、全角、emoji字符等。

3.安全测试:包括:

(1)前置条件测试:确保调用该接口时的前置条件是满足的,才可以调用该接口,比如:对于一些关键信息的查询接口,用户必须登录经过鉴权后才能查询,避免裸接口暴露敏感信息。

(2)关键字段失效性测试:比如:登录session、订单是否有过期时间等。

(3)防篡改能力:比如:对关键字段是否有防篡改校验,如:重入时修改支付金额/支付币种等。

(4)重入验证:接口是否支持重入,重入时是否做了关键字段的幂等验证,确保关键字段重入时没有被篡改。

(5)敏感字段:是否有需要加密传输/存储的字段,比如:个人身份证、银行卡、密码等敏感信息。

(6)是否存在越权:比如:通过修改请求参数访问非该用户的信息。

(7)查询接口是否防遍历:接口设计时是否有防遍历的能力,比如:某些入参ID不能是简单递增的,接口访问有频率限制等。

4.性能测试:可以通过将下游服务mock的方式来单独收集该服务自身的性能数据,也可以通过全链路采集性能数据,然后根据调用链路切分出每个服务的处理时长,找到有性能瓶颈的服务。

5.协议升级测试:包括协议及数据的兼容性等维度的测试。

(1)新老接口协议的兼容性测试:新接口是否需要兼容老接口的服务请求调用,老接口如果收到新接口的服务调用将如何处理等。

这里特别注意的是ProtoBuf(PB)协议消息字段序列号的不变性,当PB协议升级时,不要修改原有的消息字段的序列号,因为PB反序列化时,是按照id来进行字段映射的,否则可能会导致消息解析时字段顺序错乱。

举个实际业务中遇到的案例,如图6所示,我们的服务A调用服务B,服务B将PB协议升级,将其中的PB的一个id=14的字段删除了,然后将原来id=15的字段的id修改成14,把id=16的字段修改成id=15,但未通知服务A升级PB协议,结果服务B收到服务A发送过来的旧的消息包时,按照新的PB协议解析,取到的id=14字段bk_operation的值其实是bk_direction的,取得的id=15字段bk_type其实是bk_operation字段的值,进而造成业务上的bug。

图6 PB字段序列号变更举例

(2)新老接口的数据兼容性测试:这个可能是容易忽略的测试场景,比如:老接口生成的未到终态的数据是否可以由新接口来继续处理。

我们举个实际业务中遇到的案例:企业用户注册模块由一系列的注册步骤组成,比如:登记企业基本信息、上传营业执照、银行卡身份验证等,因为注册流程比较长,任何一个步骤,用户都有可能临时中断,之后再继续注册。期间我们进行了注册接口升级,变更了计算敏感数据签名的算法,当新接口加载之前老接口的数据进行验证时,验证失败,导致尚未完成注册的企业无法继续后续的注册流程。

2.2 微服务生命期测试

本节我们首先了解下微服务的生命期有哪些状态,基于此设计我们的测试场景。

图7 微服务的生命期

图7 显示了微服务的典型的生命期过程,在研发阶段结束后,我们将获得一个可以部署和运行的微服务构建物,接着是给微服务分配一个运行时环境,比如:拉起一个docker镜像,然后是启动微服务,进入启动状态后微服务会向注册中心发起注册,其他微服务就可以查询和调用该微服务了。当服务需要终止时,比如服务下线,在进入终止状态前微服务会向注册中心发起注销信息,从服务注册中心下线,最后其执行环境会被释放。

这个阶段需要关注的测试点包括:

(1)微服务注册:验证微服务上线后,是否能够自动注册到服务注册中心并且被其他服务发现。

(2)微服务注销:微服务服务退出,调用方是否能够及时感知到?服务注册中心一般有两种机制来感知服务下线,一是上面提到的微服务退出前主动上报,这种是最及时的;同时,服务注册中心还会对微服务进行心跳检测,如果发现一段时间内没有收到微服务的心跳包,则认为该微服务下线了,比如当微服务异常退出的场景,就依赖心跳检测机制。无论是哪种情况,调用方都需要处理被调服务退出的场景,是否有合理的处理机制,比如重试或是进入服务降级等操作。同时,我们还要关注微服务是否有自我异常退出的检测机制,如果发现异常退出了,是否需要重新拉起,是否需要从服务注册中心注销服务等操作。

2.3 微服务实例漂移测试

在介绍微服务实例漂移之前,我们先介绍下什么是有状态服务和无状态服务。在云原生环境下,一般是推荐无状态服务。

无状态服务(stateless service):服务对单次请求的处理,不依赖于之前的请求,服务本身不存储任何信息。比如:我们常用的HTTP协议本身是无状态的服务,每个请求之间是不相互依赖的。有状态服务(stateful service):与无状态服务相反,它会保存一些请求的上下文信息,从而先后的请求是有一定的关联性。例如,我们经常会使用Session 来保存用户的一些信息,虽然HTTP协议是无状态的,但是借助于Session,我们将HTTP服务转换成了有状态服务。

下面我们看个例子。

用户通过负载均衡访问微服务A实例,完成登录后会获取一个token,下次再访问该服务时(如:查看账单),请求里会带上这个token,当微服务实例收到请求后,会再次验证token的有效性,如果有效则返回响应信息。

图8是一种有状态的实现方式,因为微服务A实例1异常退出,下次再次访问微服务A时,将访问请求转发到了实例2上,即发生了实例漂移,但因为实例2没有存储之前的token信息,导致token验证失败。

为解决这个问题,图9通过一个公共DB实现了状态化服务,但微服务实例本身是无状态的。微服务A实例1首先将token信息保存到了DB中,那么当实例2需要验证token信息时,会访问公共DB查询token信息进行验证。

图8 有状态服务举例

图9 无状态服务,通过外部DB来实现状态化服务

微服务设计上一般建议设计成无状态的,主要是考虑动态可伸缩性,可以更好的应对实例漂移。在云原生环境下,微服务实例可能会被频繁的创建和销毁。如果server是无状态的,那么对于客户端来说,就可以将请求发送到任意一个服务实例上,然后通过负载均衡等手段,实现水平扩展。但如果server是有状态的,那么就无法很容易地实现水平扩展,因为需要始终把请求发送到同一个服务实例上才行。

在微服务测试中,我们要关注这类微服务的服务状态设计。

(1)如果是有状态服务,要验证同一个请求在中断后,下次重入时,是否能够路由到同一个服务实例上。

(2)如果是无状态服务,要验证同一个请求在中断后,下次重入时,如果路由到同一个服务的其他服务实例上,请求是否能够继续正确的处理。

2.5 微服务的配置测试

图10 微服务生命期,含加载配置

图10所示,微服务启动时会加载应用配置。关于配置加载需要关注以下几点:

(1)配置来源:需要梳理服务依赖的配置来源有哪些,在执行环境下是否正确配置了,是否做到了环境隔离。微服务的配置来源可以是所在运行环境的环境变量、可以是来自于配置中心的配置变量。

对于运行环境的环境变量要做到服务间的环境隔离,可以梳理环境变量的来源,构造环境变量缺失、环境变量配置错误等场景来验证当环境变量异常时微服务的运行情况。

对于来自于配置中心的配置项,需要特别关注的一点配置项是否是通用配置,要和微服务实例不相关,比如:将微服务所在容器的IP配置到配置项内,显然是不合理的,因为其他微服务实例读取到的这个IP配置和其自身的容器IP地址是不同的。像这类变量可以通过容器初始化时,注入到容器的环境变量里。

(2)配置加载时机:一般是在微服务启动时加载,还有一种配置持续更新的机制,一般是通过一个定时任务,不断查询配置项是否有变更,或是每次用到配置项的时候都去读取最新的数据。建议在加载配置时,对配置项进行合法性校验,而不是在使用时才发现配置异常,进而导致业务出现问题。

(3)上下游关联配置的合理性:因为每个微服务都有一个自己单独的配置项,服务间相关的配置的合理性容易被忽略。一个典型的案例:关于服务超时timeout配置,上游服务的timeout至少要大于下游的timeout配置,否则会出现下游服务有结果返回时,上游服务已经给调用方返回超时了。

(4)配置内容测试:主要是针对配置项进行常规的测试,包括:类型、长度、配置项缺失(key或value或都缺少)、默认值等,这里需要注意当配置项异常时,是否应该影响业务主流程。

2.6 服务调用链路异常测试

上面分布式跟踪章节中,我们提到通过trace_id和span_id我们可以构建出服务的调用链路。考虑到微服务间的调用都是通过网络调用完成的。网络可能会存在抖动、延迟、丢包等问题。所以在测试微服务业务时,我们需要特别关注这类场景。下面我们梳理下有哪些典型的测试场景。

2.6.1 链路中断重入测试

首先我们看下为什么要做中断重入测试。

我们举个支付流程的例子。如图11所示,我们一个简单的支付流程包括查单->下单-> 校验 -> 扣款。每一步都是由一个微服务来完成的,那么可能存在多种中断的场景。一般的业务都会允许用户重试支付,也就是重入。对于中断3和中断4场景,下单成功后,用户重新发起支付流程,我们期望支付成功并且扣款金额是正确的。但如果中断重入处理不当,可能会带来一定的资金损失。比如:用户完成支付后,扣款服务没有返回结果,用户再次发起支付,是否会导致用户被扣了两次款?

除了支付流程外,如果我们引入其他的流程,也同样会存在更多的资金风险场景。比如:

(1)退款流程:用户支付完成后,申请退款,申请退款时,链路发生中断,用户再次发起退款请求,退款行为是否会被执行两次,也就是用户收到两份退款?

(2)提现流程:用户提现时,链路发生中断,用户再次发起提现,是否会收到两份提现金?

图11 中断重入场景举例

通过以上的举例,我们可以看到链路中断重入测试是非常有必要的。链路中断重入测试的核心流程如下:

(1)梳理服务间的调用链路,可以通过上文提到的分布式跟踪技术来完成。

(2)依次模拟每个微服务中断的场景,微服务中断的模拟可以有多种方式,简单的一种可以通过修改微服务的名称配置,进而导致服务发现失败来模拟。第一次服务访问失败后,接着进行重入测试,将相同的访问请求再发送一遍,如果业务支持重入(幂等),第二次的重入请求会被正确处理。

这里我们再重点讲解下幂等性,这个和重入测试是十分相关的。

幂等性:简单说就是对一个接口,相同的请求不管调用多少次,对系统的影响与一次执行是相同的,对调用方的返回每次也是相同的。

下面举两个实际业务中的bug来说明幂等性:

(1)举例1:微服务AcceptOrder重入时,返回的order_time和第一次调用时返回的格式不一致。

bug说明:我们有个AcceptOrder微服务,该服务用于处理传入的订单,上游服务调用该服务接口时,会返回一个订单order对象,其中一个字段是订单时间accept_time。order对象会保存到DB中,下次接口重入时,会从DB查询该订单是否存在,如果存在,则直接返回DB中的order对象,而不用重新创建一个新的订单。

我们预期两次返回的order对象的属性都是一样的。但实际中发现accpet_time在第一次返回的是“2022-10-25 15:08:14”,重入时返回的是“2022-10-25T15:08:14+08:00”。两次返回结果不一致,进而导致上层服务做日期解析时报错。

原因分析:第一次调用AcceptOrder服务时,这个accept_time是AcceptOrder服务从内存中获取当前时间“2022-10-25 15:08:14”,然后就返给了上游服务。在保存到DB时,保存的是“2022-10-25T15:08:14+08:00”这种格式(包含了时区信息)。重入时,是从DB中读取的该字段。两次时间格式的不一致,导致重入行为的不一致,进而违反了接口的幂等性原则。

(2)举例2:AcceptServer调用BookingServer成功后,但在更新账单状态时失败,第一次调用和重入时返回结果不一致。

bug说明:我们有三个微服务UserServer,AcceptServer和BookingServer,UserServer用来接收用户的订单请求,然后将订单请求转发给AcceptServer来处理,AcceptServer会先生成一个记账单,然后调用BookingServer完成记账。第一次AcceptServer调用BookingServer进行记账时,BookingServer记账失败了,就返回了一个错误码给到AcceptServer,然后AcceptServer透传错误码给UserServer。

但当UserServer收到错误码,再次调用AcceptServer进行重入时,AcceptServer从DB中查到该重入订单,不再去调用BookingServer进行记账,然后就直接返回了成功的返回码。但实际上这个订单在DB中的记账单的状态仍然是失败的,在相同的数据下,两次请求返回的结果不一致。

原因分析:当AcceptServer第一次记账时,会在DB里记录了一条记账单,这个记账单的状态是处于初始态的,同时整个系统还有另外一个批跑程序来直接读取DB中的未到终态的记账单并将其做到终态。当AcceptServer重入时,查到DB中有记账单时,它也不会去调用BookingServer去记账了,即使是记账单还没有被批跑程序做到终态,AcceptServer都会直接返回成功码给调用方。在记账单处于相同状态下,却有两种不同的返回码,这种行为也是违反了幂等原则。

2.6.2 服务间强弱依赖测试

我们首先看下服务间的强弱依赖的定义。

假定我们有两个微服务A和B,A会调用B。

强依赖:如果微服务B出现故障不可用时,微服务A也不可用,这种依赖就是强依赖。

弱依赖:如果微服务B出现故障不可用时,针对B有个降级策略,微服务A仍然可用,那这种依赖就是弱依赖。

梳理出微服务间的强弱依赖关系,有什么用处呢?有了这个强弱依赖,我们提前发现因为依赖问题可能导致的故障,避免依赖故障影响系统的可用性。

举个例子,如图12所示,某后台下载服务,网关收到前端App的下载文件请求后,首先进行鉴权,如果鉴权成功,接着在下载文件前,会先调用CDN竞速服务,连接几个CDN节点竞速,获得最快的一个CDN节点,最后调用下载服务从该节点下载文件。对外的后台下载网关主要依赖于鉴权服务、CDN竞速服务、文件下载服务。接着我们来分析下哪些服务对后台下载网关是强依赖的、哪些是弱依赖的。显然鉴权服务和文件下载服务时强依赖的,如果鉴权服务异常,我们无法验证前端App的身份,不允许匿名下载的;同样,如果文件下载服务异常,我们也将无法提供下载能力。而CDN竞速服务则是一个弱依赖服务,当CDN节点竞速服务失败时,从系统健壮性及用户体检的角度,我们需要将这个依赖降级为弱依赖,给下载服务提供一个降级服务,比如可以从一个默认的CDN节点下载文件。

图12 下载服务的强弱依赖图

具体我们该如何实施强弱依赖测试呢?通过我们上文提到的分布式追踪技术,我们可以快速的生成微服务间的调用链路,即服务间的依赖关系。然后借助于中断重入测试,通过依次构造每个服务的异常,如果业务不受影响,说明异常的服务是个弱依赖的服务,这样我们就可以得到服务间的强弱依赖关系拓扑了。接着我们会结合业务场景分析当前的强弱依赖关系是否合理,找到优化点。

2.6.3 服务间消息异常测试

在微服务架构下,服务间都是通过网络进行消息传递,因为网络传输的不确定性,存在消息丢失、消息延迟、消息乱序等情况出现。在微服务业务测试中,我们需要覆盖各类消息异常场景的测试。大致有如下的手段:

(1)基于混沌工程:混沌工程中有网络相关的异常模拟工具,可以模拟网络抖动、延迟、丢包等异常。这里可以参考本书的16.4节内容。

(2)基于时序建模:混沌工程的方式是采用的一种随机的方式来模拟各种时序上的异常,无法确保消息时序异常场景模拟的完备性,故而存在一定的局限性。时序建模测试是通过梳理出一个分布式系统的时序模型图,生成向量时钟图,并且基于向量比较和一定的策略生成时序测试用例,从而覆盖更完备的时序场景。这部分内容可以参考本书的20.4章节。

2.7 协议一致性测试

首先我们先拓展下协议的概念,除了微服务本身的服务协议文件外,如:ProtoBuf(PB)协议,一般微服务还会和DB有交互,DB的字段定义也算是一种协议。归纳起来有如下三类协议不一致的场景,如图13所示。

图13 协议不一致场景

2.7.1 PB之间字段定义的一致性

这里我们重点关注PB协议字段的名称、类型和取值范围,在多个不同PB间是否一致。

syntax = "proto3"; import "validate.proto"; message QueryReq { string rest_code = 1 [(validate.rules).string = {min_len: 4, max_len: 4}]; string order_id = 2 [(validate.rules).string = {min_len: 1, max_len: 64}]; int32 from_type = 3 [(validate.rules).int32 = {gt: 0, lt: 10}];// }

代码段 1 :Protobuf 示例

代码段1是基于proto3的并且包含字段校验定义的PB协议的示例,从这个示例里我们可以看到rest_code是字符串类型,并且长度是必须是4;order_id也是字符串类型,长度是1到64之间;from_type是一个int32类型的,取值范围是(0,10)。

通过搜集出上下游相关微服务的PB协议的定义,我们就可以比对它们之间相同字段的定义是否要满足一定的一致性条件。当不一致时,是否可能会有服务异常。

比如:上游服务A调用下游服务B,B接着调用服务C,A和B之间的PB协议中的某个字段name长度为20,而B和C之间的PB协议中相同的字段name的长度为10。那么就存在从A传入的name长度大于10时,传递到C的name可能会被截断,导致业务bug出现。

2.7.2 PB和DB之间字段定义的一致性

对于一些和DB有交互的微服务,在接收到请求消息后,经过一系列逻辑处理后,会在DB中存储一些相关信息。这些信息有些是直接来自于请求消息,需要我们关注的是DB的字段定义和相应的PB字段定义之间是否要满足一定的一致性要求。当不一致时,同样可能会有服务异常。

对于DB字段来说,我们会重点关注:名称、类型、字符集、长度、默认值等信息。

CREATE TABLE `t_restaurant` ( `Fid` bigint NOT NULL AUTO_INCREMENT COMMENT '自增主键', `Frest_code` varchar(4) NOT NULL COMMENT '餐厅代号', `Forder_id` varchar(64) NOT NULL COMMENT '订单ID', `Ffrom_type` smallint NOT NULL COMMENT '订单来源', `Fstate` smallint DEFAULT '0' COMMENT '逻辑删除 1-删除 0-未删除', `Fmemo` varchar(500) DEFAULT NULL COMMENT '备注', `Fcreate_time` datetime NOT NULL COMMENT '创建时间', `Fmodify_time` datetime NOT NULL COMMENT '修改时间', PRIMARY KEY (`Fid`), KEY `idx_modify_time` (`Fmodify_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='餐厅订单表';

代码段2:t_restaurant表的sql创建语句

代码段2是一个t_restaurant表的sql创建语句,从这个sql语句中,我们可以看到表的各个字段的定义。有了DB字段的定义和上节我们提到的PB的协议定义后,我们就可以对比他们之间的差异,找到一些不一致的点,发现一些潜在的问题。

(1)名称是否一致:建议规则:DB表字段的名称是以F开头的,去掉F后的名称则是PB协议中的字段名称。遵照这个规则,DB和PB间的字段就建立起了映射关系,可以非常方便我们编写脚本来自动做对比和监控,而无需人工做比对了,特别是当微服务和DB表数量比较大的时候,这个规则就变得尤其重要了,最好是在项目初期研发人员就按照这个规则来设计PB和DB表,将会极大的提升我们的比对效率和准确度。

(2)定义类型是否一致:需要我们梳理出PB中的字段定义类型和DB的字段定义类型的映射关系,读者可以查询PB官网的字段类型定义和DB(如MySQL)的字段类型定义,确保两者的映射关系是正确的。比如:PB中定义的是varchar,而DB中用的bigint类型,如果在DB操作时,未考虑到两者的差异,可能会引入bug。如:"10" 映射成bigint是10,而"10ab"映射成bigint时也是"10",就存在信息的丢失了。

(3)字段长度是否一致:这一点也需要我们特别的关注,如果DB字段的长度比PB的字段长度短,当数据超过DB字段长度时,在保存DB数据时,就存在数据被截断的情况,进而导致存储的数据有误的bug。

2.7.3 DB之间字段定义的一致性

这一项主要是确保整个系统的DB之间存储的相同的字段要做到统一一致,字段上主要关注名称、类型、字符集、长度、默认值等信息的一致性。

DB一般是为微服务使用的,当存在不一致时,就可能会导致一些风险。

比如(以下举例以MySQL DB为例):

(1)字符集不一致:表1用utf8mb4,表2用utf8(即utf8mb3),对于一些特殊的4字节的emoji表情字符,同一份数据在表1可以正常存储,在表2会被截断成3字节存储,如果微服务读取表1的数据,然后保存到表2时,就会出现数据不一致的情况。

(2)定义类型不一致:可能会遇到mysql隐式转换的问题,比如:一个表里保存的是int类型,另外一个表是varchar类型,当用int类型值来匹配varchar类型的数据时,mysql会将varchar转化成int类型进行匹配,导致匹配结果有误,如,第7行的'a5'被隐式转换成0,从而匹配成功。

mysql> SELECT 1 > '5a'; -> 0 mysql> SELECT 7 > '5a'; -> 1 mysql> SELECT 0 > 'a5'; -> 0 mysql> SELECT 0 = 'a5'; -> 1

代码段3:sql隐式转换举例

(3)字段长度不一致:会导致从一个表读取的数据,保存到另外一个表时可能会被截断的风险。

(4)默认值不一致:当相同的字段数据保存到不同的表中,对于有默认值的相同字段,要确保默认值的一致性。

2.8 分布式事务测试

事务的概念是指操作各种数据项的一个操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。在单机系统下可以通过DB来完成一次事务操作。在分布式系统下,服务之间需要通过网络远程协作完成的事务称之为分布式事务。

在分布式事务场景下,有多种解决方案,包括:2PC两阶段提交,TCC(try-confirm-cancel)模式,Saga模式以及最大努力等模式。其中两阶段提交存在单点故障及同步阻塞等问题,故不适合高并发的互联网业务。TCC模式,需要业务遵循TCC开发模式,导致开发成本较高,同时,对于业务流程长的场景,事务边界长,加锁时间长,TCC模式将会影响并发性能。所以对于业务流程长的微服务业务,Saga模式将更适用一些。

Saga 事务基本协议如下,参见图14:

每个 Saga 事务由一系列有序本地子事务(sub-transaction) T1,T2,…,Ti,…,Tn组成。

本地子事务操作本地存储,其事务性可以通过DB来实现,当本地子事务成功后,会发送通知下一个子事务执行。

图14 Saga模式举例

如果 T1 到 Tn 均成功提交,那么事务就可以顺利完成。否则,就要采取恢复策略,恢复策略分为向前恢复和向后恢复两种。

向前恢复的执行模式为,T1,T2,…,Ti(失败),Ti(重试)…,Ti+1,…,Tn。Ti失败时,会一直重试Ti直到成功,接着执行后续的事务,参加图15。

图15 Saga模式之向前恢复机制

向后恢复的执行模式为:T1,T2,…,Ti(失败),Ci(补偿),…,C2,C1。Ti失败时,会反向执行Ci及其之前的每个补偿动作。每个 Ti都有对应的幂等补偿动作C1,C2,…,Ci,…,Cn,补偿动作用于撤销 T1,T2,…,Ti,…,Tn造成的结果,参见图16。

图16 Saga模式之向后恢复机制

结合表2的第三方平台网上购物场景举例,我们可以看到本地子事务分为三类:

(1)可补偿性事务(Compensatable transactions):可以使用补偿事务回滚的事务。比如:网上购物时,客户下单后,一直未完成支付,那么下单服务对应的补偿事务将完成将订单关闭的动作。

(2)关键性事务(Pivot transactions):Saga执行过程的关键点。如果关键性事务成功,则Saga将一直运行到完成。比如:网上购物时,买家完成了支付,卖家发货,买家确认收货。买家确认收货是一个关键性事务,其之后的事务要保证成功,如:最终确保第三方支付平台将收款转到卖家。

(3)可重复性事务(Retriable transactions):在关键性事务之后的事务,可通过重复执行确保成功,这就要求服务的幂等性。如:表2的第6步遇到失败时,反复向卖家转账直到转账成功。

表2 分布式事务场景举例

步骤

微服务名称

微服务行为

事务类别

备注

1

下单服务

买家下单服务

可补偿性事务

补偿行为:关闭订单

2

账号服务

买卖家身份验证服务

-

只是做验证,无需补偿

3

支付服务

买家支付到第三方平台

可补偿性事务

补偿行为:对买家退款

4

发货服务

卖家发货

可补偿性事务

补偿行为:取消订单

5

收货服务

买家确认收货

关键性事务

买家确认收货后,需要确保剩下的事务操作成功

6

转账服务

第三方平台转账到卖家

重复性事务

可重复多次,确保转账成功

在理解了分布式事务Saga模式后,我们的测试思路和重点如下:

(1)首先构建业务的Saga模型,包含本地子事务的调用序列、每个子事务的类型(可补偿性事务/关键性事务/可重复性事务)。

(2)评估Saga模型的合理性,比如:调用序列、每个子事务类型是否正确等。

(3)验证向前恢复,对可重复性事务进行幂等验证。

(4)验证向后恢复,这点是针对可补偿性事务,按照调用事务链,从关键性事务点依次往前回退验证。

另外,关于补偿性事务和原事务要特别关注两个时序问题:

(1)原事务未执行,补偿事务执行了:比如原事务的请求因为网络丢包没被执行,导致整个事务回滚,原事务的补偿事务会被执行,这个时候要确保补偿事务做正确的校验并执行正确。

(2)补偿事务先于原事务执行:原事务的请求因为网络拥塞,请求超时,导致整个事务回滚,原事务的补偿事务被执行,接着原事务的请求又到达了,这时对原事务的请求是要禁止执行的。

3. 总结

本节介绍了云原生及微服务相关的基本概念。通过对比单体架构和微服务架构,我们了解到微服务架构的特点以及微服务架构的分布式特性带来的测试挑战。接着针对这些挑战,我们梳理出了微服务测试体系,覆盖了微服务特有的多种测试维度,包括:微服务生命期测试,调用链路异常测试、协议一致性测试以及分布式事务测试等,从而为微服务的产品质量提供更全面的保障。

4. 参考文献

[1] CNCF官网:/

[2] 克里斯·理查森(Chris Richardson):<<微服务架构设计模式>>

[3] 科妮莉亚·戴维斯(Cornelia Davis):<<云原生模式>>

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。原始发表:2025-02-09,如有侵权请联系 cloudcommunity@tencent 删除测试服务配置事务微服务

本文标签: 微服务测试