Appearance
22技术新趋势,微服务下的测试框架分层实践
我们知道,没有微服务之前,我们的应用都是单体应用,但是单体应用有如下明显缺点。
- 部署成本高, 部署频率低
由于单体服务只能统一部署,假设单体服务有很多个模块,即使你只改动了其中一个模块,在部署时,你都必须全量部署。而当业务复杂时,部署动辄花费数十分钟,甚至数小时之久,在这段时间内,应用就无法正常对外提供服务,部署的成本非常高。
正因为部署的成本很高,所以单体服务的部署通常会积累一定的需求后,统一部署,把部署频率降低来减少服务不可用时间,这就导致了单体应用无法适应快速变化的外部环境,以及无法及时响应客户的需求。
- 改动影响大,风险高
在单体服务架构下,无论你是改动一行代码,还是改动多个模块的代码,都要经历重新编译、打包、测试和部署。这样一来,改动的影响就非常大,无论是开发人员还是测试人员,都疲于奔命。如果测试不充分,还会导致服务不可用,发布风险非常大。
- 技术债务多,扩展困难
单体应用由于所有的模块都在一块,会导致模块的边界比较模糊,依赖关系不清晰。并且随着时间的推移,这些相互依赖的地方,逻辑关系越来越难以理顺,逐渐就会变成技术债务。
单体应用所有模块和功能都耦合在一块,但是这些模块之间,需要的软硬件资源确不尽相同, 单体应用为了保证可用性,必须使得软硬件资源满足每一个模块的需求,这样不仅造成了资源的浪费,还导致了扩展的困难(无法按模块扩展)。
正是由于这些原因,微服务架构应运而生,而本讲我们就介绍微服务下的分层测试实践,下图是我们这一讲的知识脑图,可供你学习参考。
既然是微服务下的分层测试实践,我们就要先搞清楚,什么是微服务?
什么是微服务?
在《课前必读 1 | 敏捷,DevOps,微服务带给测试人员的挑战》这个章节里,我提到过微服务的定义:
微服务是一种开发软件的架构和组织方法,其中软件由通过明确定义的 API 进行通信的小型独立服务组成,这些服务由各个小型独立团队负责。微服务架构使应用程序更易扩展和更快地开发,从而加速创新并缩短新功能的上市时间。
微服务相对于单体应用来说,最大的不同是微服务将单体应用拆分,变成一个个的单独功能,每个功能都被称为一项微服务,每个微服务围绕具体的业务,能够单独部署、发布。各个微服务之间一般通过 RESTFUL 集成。
下面这个图展示了单体应用和微服务之间的区别:
微服务之 CAP 定理
微服务本质上是分布式服务。分布式服务遵循 CAP 定理,即在一个互相连接并共享数据的节点的分布式系统中,当涉及读写操作时,在一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中, 只能保证两者可用。
C(Consistency):一致性,即数据一致性。更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致,一致性分为强一致性和最终一致性。
A(Availability):可用性,即服务的高可用。某个服务瘫痪不影响整个分布式系统的正常运行。
P(Partition Tolerance):分区容错性,也叫作分区耐受性。分布式系统在遇到某节点或网络分区故障的时候,仍能够对外提供满足一致性和可用性的服务。
为什么 CAP 同时只能满足两个呢?因为在事务执行过程中,系统其实处于一个不一致的状态,不同的节点的数据并不完全一致。
CAP 理论有什么作用呢? 它指导了分布式服务下的系统设计。即在 P(分区容错性)发生的前提下(分区容错性无法避免),A(一致性)和 C(可用性)之间,能选择一个作为系统设计的目标。如果追求 A(一致性),则无法保证所有节点的 C(可用性);如果追求所有节点的 C(可用性),则无法做到 A(一致性)。
微服务解决了什么问题?
我们把单体应用拆解成微服务,会有什么收益呢? 举个例子来说,单体应用内部是由许多组件组成的,如果一个组件更改了,就不得不更新整个应用。 另外随着业务的发展,组件之间的依赖、复杂度不断增加。此时,一个组件出问题就可能导致整个应用不可用。
由此可见,微服务首先解决了组件间的互相依赖问题。 除此之外,微服务还有如下好处。
1. 独立部署,系统更加健壮
把单体应用拆分为微服务,首当其冲的好处就是独立部署。独立部署,一方面使得部署上线的时间可以缩短,另一方面,一个微服务的不可用, 不会影响到其他的微服务。
独立部署使得系统的健壮性变强。
2. 开发技术多元
使用微服务后,微服务之间的访问、调用,可以走统一的 RESTFUL HTTP 协议来通信。只要对外的接口不变,在微服务内部,技术团队可以采用自己最擅长的编程语言、数据库等。而这些是单体应用无法想象的。
在新技术出现时,微服务可以快速引入小部分升级;但单体应用做不到,技术更新的成本非常高。
因此微服务使得开发技术更加多元化。
3. 快速扩展并减少资源浪费
单体应用的扩展,其水平扩展能力非常有限,大部分的扩展依赖垂直扩展。但由于单体应用是一个整体,势必造成资源浪费。比如,有的模块是计算密集型需要更强的 CPU,有的模块是 I/O 密集型需要更大的内存支持,但是由于单体应用无法拆开部署,导致了硬件必须同时满足这两种需求,这就造成了资源的浪费。
而微服务的水平扩展非常容易,垂直扩展也可以拆分部署,减少了浪费。
4. 服务之间边界清晰
一般情况下,一个微服务负责提供一个模块功能。我们用订单信息的共享和调用来举例,在没有把这个功能变成一个微服务前,由于各个业务模块都会用到订单信息,那么就可能存在各个业务模块自己去写代码来访问订单信息数据库(通常是通过 DAO 层来拼接 SQL)的情况,这样就造成了代码的重复编写。而且,随着调用的模块增多,你很难知道到底有多少模块存在自己访问订单信息的情况。
而有了微服务后,订单信息的获取由订单微服务统一提供,这样业务边界就变得非常清晰。
微服务给测试带来的挑战及解决之道
我们都知道,在软件开发领域,"没有银弹"(No Silver Bullet)是一种普遍共识。 微服务解决了这么多的问题,也势必会带来新的问题。下面我们来看下,微服务带给测试的挑战有哪些。
1. 测试环境部署困难------服务容器化
想一下这个问题,你的单体应用被拆分成多个微服务,而你只负责其中一个微服务的测试,那么,你如何进行你的测试?
因为微服务独立部署、独立发布的特性,搭建起一套可用的测试环境变得困难起来。加上微服务导致的技术多元化,有时候需要测试人员为不同语言、不同数据库的服务去搭建各自的环境,从而导致测试成本增加。
于是,服务容器化 应运而生。通过Docker 容器及镜像,可以做到环境部署简单,可配置。不仅如此,使用 Docker 镜像部署,保证了开发和测试环境的一致性,避免了由于开发环境和测试环境不一致带来的各种问题。
2. 微服务下整合测试困难------契约测试
实施微服务后, 你负责测试的微服务可能被其他微服务依赖,而你也可能需要其他微服务提供数据,才能开展你的测试工作。
但是微服务通常伴随着团队划分,即负责不同微服务的人处于不同的团队。这样, 你们互相不了解对方的业务,在两方微服务需要进行整合,以及联调测试时,测试就变得非常困难。即使你们联调测试通过,在后续业务的开发中,可能各自又更改了微服务对外的接口,那么下次联调测试时,你们互相不知道对方对外服务的接口有改变。那么,测试又是一场噩梦。
于是,契约测试(Pact), 特别是消费者驱动的契约测试(Consumer-Driven Contracts)应运而生。契约测试是用来验证服务提供方(Provider)和服务消费者(Consumer)彼此之间契约是否(Pact)完备正确的测试活动。
由于有了契约测试,服务提供方改动契约而导致的测试失败能够立即被发现,这让联调测试不再是噩梦。假若有一天契约改变了,我们也可以通过更换契约文件来保证双方都得到通知,从而避免测试噩梦。
3. 非功能性测试容易被忽略------全链路压测
单个微服务下的功能测试跟单体应用下的功能测试并没有区别,但微服务整体对外提供服务后,测试人员很容易对非功能性的测试认识不足。
比如,单个微服务功能、性能都满足要求,但是多个微服务集成为系统整体向外提供服务时,由于可能的网络延迟带来额外的性能开销,可能会使得性能相对于单体应用有所下降。
另外,由于一条调用链路上的不同微服务能够承受的最大压力不一样,如果微服务没有"降级""限流"和"熔断"的能力,当某个微服务接收到的请求超出它能够处理的最大强度时,系统就有"雪崩"的可能。
那么如何保证跨服务调用的可靠性,以及整个系统集成后的性能不受影响呢?全链路压测近年来变得越来越重要,并逐渐演化成为系统性能保驾护航的重要途径。
要实施全链路压测,必须注意以下问题。
- 理顺全业务核心链路
既然是全链路,就需要联合业务相关方,理顺业务逻辑,最终生成从真实用户角度出发,如何使用系统业务的各个操作,并将它们组成一个个的测试链路。
在实施中,常常将核心业务和非核心业务进行拆分,从核心业务出发,逐渐扩展到非核心业务。
- 做好数据模型构建
实施全链路压测的一大难点便是测试数据模型的构造、数据流量的引入,以及数据的脱敏和隔离。
在实践中,数据流量的引入往往是从生产环境数据库中获取数据,经过数据的脱敏后进行使用。因为全链路压测一般直接使用生产环境测试,所以还需防止测试数据污染(通常会采用数据隔离,写影子库的方式来避免)。
- 做好系统容量规划
因为全链路压测常常在生产环境运行,而压测会产生大量的流量压力,所以在执行全链路压测之前,必须做好系统容量的规划工作,防止因为测试时忽然增大的流量压力,造成系统不堪重负甚至宕机。
在实施上,通常全链路压测会从单个接口、单个微服务的基准测试做起,并逐渐扩大到全部微服务。
全链路压测近年来逐渐演化成一个专门的测试领域,无论其工具选型、技术方案均与常规的测试有所不同,建议大家根据自身业务需求,找到合适自身业务的技术方案。
而除了性能问题需要关注外,微服务也要关注幂等测试。
以下单扣除金额为例,在复杂的生产环境里,可能发生某个微服务,或者某个接口忽然不可用,导致这一笔金额扣除的业务链没有全部执行完毕,业务的执行在中间的某个过程失败了(业务没走到最终态)。当微服务或者微服务接口恢复提供服务后,这些没走完的请求,应该能继续执行下去,并最终达到最终态。当业务达到最终态后,拿具备相同订单 id 的请求再次发送请求,系统将直接返回结果而不去执行。
4. 分库分表增大了测试难度------写反向查找函数
虽然单体应用也可以分库分表,但是微服务往往伴随着分库分表,因为每个微服务通常都有自己独立的数据库,那么分库就变成自然的一个操作,而随着业务发展,数据量累积到一定程度,也必然会分库、分表。
分库分表给测试带来的最大问题是测试数据的构造和获取变得复杂。例如在开始测试时创建了一个用户,这个用户根据规则(通常是根据数值取模)创建后,用户的各项信息被存储到了 user_1 这个表;等到用户下单时,系统要根据 user id 去查询当前用户的状态,那么就需要我们反向根据 user id 获取到这个用户所在的表后再进行操作。
分库分表的算法往往不同,测试人员需要根据分库分表算法写个反向查找函数,或者提供一个服务供其他测试人员调用,这对代码能力有一定要求。
关于如何分库分表,由于细节较多,解释起来所占篇幅较多,我不在此详述。可参考我公众号 iTesting 上发布的文章《分库分表小结 - 论QA的自我修养》。
5. 端到端测试变得困难------Mock 服务
由于微服务的复杂性,在测试阶段,测试环境可能无法拥有与线上系统一样完备的环境以供测试 。特别是当你的服务存在外部 service 依赖、第三方调用、通知的情况时,比如你的服务调用银行接口,在端到端测试时就会因为对方服务无法连通而失败,或者能够连通,但是调用需要收费。此时端到端测试变得困难甚至不可能。
微服务中往往需要大量的 Mock 来过滤掉与当前任务无关的请求。在测试环境进行端到端测试时,可以使用 Mock 服务过滤无关请求,将重点放在当前微服务本身上。
关于如何搭建 Mock Server,以及如何利用 Mock Server 进行测试,我将在下一讲《23 | 告别依赖,Mock Server 必杀技》中为大家详细讲解。
6. 微服务依赖导致上线、回滚困难------分析清楚依赖关系
在微服务实施后,由于相互之间存在依赖,上线和回滚要遵照一定的顺序,否则可能会引发系统崩溃。
例如,微服务 A 和微服务 B 均是准备上线的新服务。微服务 A 依赖微服务 B,当部署时,必须先部署 B,如果部署顺序错误,比如 A 先上线,就可能会发生,由于 A 找不到 B,而出现 Error 500、404 的情况。
回滚时也是如此,假设微服务 A 依赖微服务 B,而微服务 B 更新了自己的接口,则微服务 A 必须相应更新,而上线后发现微服务 B 存在严重 Bug 需要回滚。此时,如果 B 直接回滚,A 就会由于接口请求参数不对,导致调用 B 出错。
所以对于测试人员,一定要了解自己负责的微服务与其他微服务之间的依赖关系。
微服务下的测试框架分层实践
我在前面章节《03 | 告别三大误区,别让分层测试欺骗了你!》中讲过,测试金字塔模型虽然是软件测试中最经典的模型,但根据业务模型的不同,也有不同的变种。 例如,仅仅对于微服务本身,常见的就有如下两种分层模型:
那么这两种分层模型哪一种好呢?在我看来,采用什么类型的分层模型测试,与你的业务类型有着非常紧密的关系,下面我们详细分析一下。
1.纺锤模型
如果你的项目与第三方依赖比较多,或者你的项目本就是基于第三方提供的服务而建立的。那么你就应该使用纺锤模型。
在纺锤模型中,最底层 Implementation Detail是第三方服务的实现细节。针对这部分,其实不在我们的掌控范围内,我们在测试中可以直接忽略。
注意:这不意味着这部分没有人测试。 而是这部分的测试会被第三方自己测试掉。而他们测试所用的策略可能正是我们在 《03 | 告别三大误区,别让分层测试欺骗了你!》所学的经典------测试金字塔分层。
针对第二层 Integration Test,主要是测试我们的服务与第三方接口之间的连通性和正确性。这部分测试,在我们测试框架中应该属于 API 测试那一层。
而最上层 Integrated Test,这部分的测试其实是端到端测试。这部分测试的成功与否,不仅取决于本系统所属的前端和本系统的后端接口的联通和正确性,还取决于本系统的后端和第三方接口的连通性和正确性。当这部分端到端测试出现问题时,需要排查是自己后端的问题还是第三方接口的问题。
对于这部分端到端的测试,在我们的测试框架分层中,最好与第二层的 API 测试对应起来。比如,我们建立如下的文件结构:
java
|--e2e
|--test_deposit_e2e
|--API
|--test_deposit_api
在这个结构中,我们针对同一个测试用例,有两个维度的测试:
第一个维度,是 e2e 即端到端测试。例如,当这个测试 test_deposit_e2e 成功时,表明我们的系统和第三方系统都在正确运行。
第二个维度,是我们的后端与第三方的接口的测试。这个维度的测试,是与第一种维度的测试一一对应的 。比如,test_deposit_e2e 的执行,需要调用第三方接口 A,那么在 test_deposit_api 的测试里,我们就针对 A 这个接口进行测试。
当 test_deposit_api 这个测试成功而 test_deposit_e2e 这个测试失败时,我们知道问题一定出在我们的系统;反之,当 test_deposit_api 失败,我们就要分析,是 A 接口调用出错,还是我们的后端连通 A 接口后,自己内部的逻辑处理出错。
2.微服务金字塔模型
当我们的系统业务主要是自研且业务模型类似下图时,就合适使用微服务金字塔模型。
适用微服务金字图模型的业务图
我们对照微服务金字塔模型一一讲解:
针对这个业务模型的每一层,都要做Unit Test以确保业务功能本身不存在问题;
针对这个业务模型不同层次之间或者相同层次不同模块之间的调用,既要做集成测试 以验证模块集成后是否达成业务目标,也要做契约测试(对应 Component 层),以保障模块之间的调用和更改不会对业务目标产生影响;
针对前端页面层,需要从端到端以及探索测试层面来保障系统能够完成业务目标。
相应地,对应我们的测试框架分层来说,我们就需要进行如下分层:
java
|--e2e
|--test_deposit_e2e
|--API
|--test_deposit_api
|--test_deposit_api_step1
|--test_deposit_api_step2
此时,相较于纺锤模型来说,微服务测试金字塔模型对 API 层面的细节有了更仔细的检查。如果说在纺锤模型中,我们不必关心第三方服务接口内部细节;那么在微服务测试金字塔模型里, 我们需要对 API 层进行更加深入的检查,例如 test_deposit_api 这个接口对外提供 deposit 服务,但是它的内部,可能包括多个步骤,涉及多个微服务及其接口,因为它们都是我们自身的服务,故必须进行测试。
"纺锤模型" 和 "微服务金字塔模型"对测试框架的分层来说,主要是一个测试粒度的区别。
关于微服务模型,还有其他的分层实践,例如测试钻石型和全面测试型等。在我看来,只要是自研的服务和应用,都属于"微服务金字塔模型"这一个范畴,都应该进行全面、深入的测试,而不能只关注于集成测试。
小结
下面来总结下本讲所学习的内容。
本讲我从微服务的概念及 CAP 原理出发,详细讲解了微服务的应用带来了哪些好处,以及解决了哪些问题;然后引出了微服务带来的挑战及解决之道;最后我们讨论了在微服务模型下,测试框架分层应该要注意的事项和在不同分层模型下的测试侧重点。
随着微服务的流行,微服务及微服务下的测试,势必会常常出现在我们的日常工作和面试中,关于微服务的更多基础知识,我们有必要深入了解一下。
关注微信公众号 iTesting,回复"微服务",了解更多微服务有关知识。