本文要点
- 如何在生产上线之前未测试微服务间的通信,那接下来恐怕就会有段不好过的日子了。
- 消费者驱动契约预先定义了交互,可以使用它实现微服务间的交互测试
- 消费者驱动契约可以用于验证你正在构建的服务消费者驱动契约可以让你构建出消费者需要的产品,而不是你认为他们需要的产品。
- Pact 框架是消费者驱动契约的一种实现。简单的 JSON 文件定义了两个微服务之间预期请求和响应的结构。
- Docker 可以使微服务摆脱平台依赖。你可以利用它在本地和持续集成环境中测试你的微服务。
一开始,有很多的巨型单体应用。这些巨型单体应用是大型应用,包含了针对于一个项目的所有业务逻辑。随着时间的推移,这些巨型单体应用往往会越来越大,越来越笨重,也就越来越难部署了。于是,诞生了新的模式:微服务。
大多数人都知道微服务是什么,以及关于可伸缩性和业务领域崩溃的问题。微服务的另一个主要部分是通信。如果使用巨型单体应用,你会有些访问数据源的方法调用,而现在就是通过网络向其他服务发 HTTP 请求了。
这种通信需要大量的管理。比如,在 Java 中,如果一个方法不存在,那么代码就无法编译。然而,如果一个 HTTP 端点不存在,那么你可能在应用部署时才会刚刚发现这一点。
对于改过并立即部署的微服务时,你如何使这种混乱变得有序?你如何确保服务继续按恰当的消费格式提供正确的数据给下流的服务?测试变成非常非常重要。
就像大多数测试形式一样,规模较小的待测对象更容易测试。微服务也不例外,但这么做也带来了一些有意思的挑战。例如,如果一个流程需要 15 个微服务,那么你怎么来测试呢?
你得让所有的应用都在你本地运行起来。当然,你可能有更多的微服务,这种方法是无法伸缩的。例如,Uber 有超过一千个微服务。你的苹果笔记本根本不可能处理这么多服务。
你可能有一个质保或测试环境,在上面运行所有的应用。这很不错,你可以看到使用着真实数据的应用,拥有所有活动的部件。这种方式最大的缺点是你必须得一口气发布整个应用。团队无法独立地部署这些服务,必须把所有东西当作一个整体来测。微服务的任何一点儿变更,都意味着你需要重新验证整个质保环境,以确保方方面面都仍然可以正常运转。
于是,就需要消费者驱动契约(Consumer Driven Contracts)了。
消费者驱动契约
消费者驱动契约不是什么新发明。它们已经存在很多年了。Ian Robinson 在 Martin Fowlers 的博客上发表了一篇非常精彩的文章,完善概括了消费者驱动契约。
那么,什么是消费者驱动契约(CDC)呢?
它是消费服务和提供服务间的契约,声明消费者想从提供服务那里以已定义的格式获得什么内容。
它们是消费和提供服务彼此如何交互的一种描述方式,但在这里,消费服务是通信的驱动方。
这种方式有几个关键的好处。第一,它们可以使服务独立地部署。你不再需要拥有一个具备所有服务的质保环境了,不再需要用它将所有服务作为一个整体来运行以去测试应用的运转了。你不需要做长长的、脆弱的、昂贵的流程测试以确保服务可以启动和通信。
另一个关键好处是,这种契约可以由一个服务独立地生成。假想一下,你需要一个用于提供服务新端点。你知道它大概应该是什么样的,以及新的消费者将需要的确切数据。
那么就可以为提供者团队生成消费者驱动契约让他们去实现了。你可以放心,只要提供团队遵循这个消费者驱动契约,那么所有东西都可以正常工作。这种方式非常适用于分布式团队。这份消费者驱动契约形成之后,消费团队和提供团队就可以同时去忙自己的代码了,这时,整个世界是分离开来的。
最后,消费者驱动契约更微妙的地方在于,它们能让你验证正在做的东西。从提供者的视角你不能 100% 地确保所提供的服务实际会满足消费者的需要。通过从消费者的视角出发,你可以定义应提供什么,以及应采用什么样的格式。
同时,消费服务使用任何数据都不应需要任何额外的逻辑或转换,因为契约已经从消费者的视角定义了格式。
消费者驱动契约不是银弹. 有许多东西消费者驱动契约都未涉及。首先,它们不是业务逻辑的测试。这些应由服务的单元测试来覆盖。
如果你打算改动一个端点潜在的业务逻辑,但不改变其返回的数据格式,那么就应该没问题,因为消费服务不必担心数据是如何创建或交付的。如果消费服务介意数据是如何形成的,那么就说明业务边界未得到适当地定义。然而,就所有事情一样,变更时团队间的沟通还是必要的。
消费者驱动契约也不是服务间的服务层协议(SLA)。它们不声明一项服务应有多久的有效期,每分钟要能处理多少请求。
还要注意的是,消费者驱动契约不能验证外部API 服务响应。例如,消费者驱动契约不能验证谷歌地图。如果外部来源决定改变他们的格式,那这是人家的权力。和第三方合作伙伴保持沟通总是件有益无害的事,需要时就做好迁移的准备。
Rightmove 是如何使用消费者驱动契约的
Rightmove 是英国最大的房地产门户网站,每天的请求数超过 5 千万。我们帮人们找到他们的梦想家园,无论它是古老的农舍,还是流行的伦敦公寓。在 Rightmove ,我们有许多已经集成和部署的微服务。这就产生了大量的挑战和协调,从而促使我们使用了消费者驱动契约。
在 Rightmove 内部使用了消费者驱动契约之后,我们提出了一个愿望清单,这样我们就可以更容易地分享我们出于任何原因所做的任何事情了。
消费者驱动契约愿望清单是:
- 描述提供者和消费者之间请求和响应的统一格式
- 能够简单制定契约的方式
- 保存已制定契约的方式
- 针对契约测试服务的方式,要在我们的持续集成 / 持续交付流程中以自动化的方式进行
- 本地执行这些测试的方式
1. 对于消费者驱动契约的格式
消费者驱动契约是一个概念,所以要用的话就需要一个已定义的、共同约定的格式。其本质,无非就是这样的一种陈述:“如果我发送这种请求,预期得到这种结果”。于是,轮到 Pact Foundation 出面了。他们创建了一种用于描述请求和响应的一致的 JSON 格式。在 Pact,契约被称为条约(pact),它们是简单的 JSON 文件,包含请求和预期的响应。如果你的服务发送一个 HTTP GET 请求到 /user/125sb45sd,预期得到 200 Ok 返回码和一个 JSON 报文,以及其他预期的报头。
条约最值得称道的地方就是它是由 JSON 来写的,这是种通用数据格式。几乎每种程序语言都有 JSON 解析器,所以围绕条约的生成有许多的类库。实际上,服务的消费和提供甚至不需要用同一种语言编写。
2. 消费者驱动契约的创建
Pact 基金会也有很多用许多不同语言实现的用于创建消费者驱动契约的类库。如果用 Java,你可以找到 Pact JVM library 。创建一个条约看起来非常像写一个 JUnit 测试。
(来自 – Pact JVM Consumer JUnit Library )
当这个“测试”运行时,它将生成一个 JSON 文件,也就是条约。这意味着,对于开发人员来说很容易就能理解将发生什么,然后去构建和扩展它们。你的条约是以代码的方式定义的,是可复写的。只要你像 JUnit 测试那么一运行,就将创建出条约文件,无论何时何地,比如在构建的时候。
3. 消费者驱动契约的保存
在 Rightmove,我们有我们自己内部构建的条约代理服务(尽管也有一个是由 Pact 基金会提供的),它把我们所有的条件都保存在一个集中的地方。然后就可以使用 RESTful API 进行查询了。我们在服务构建过程中根据服务名和版本号来储存所有的条约。
我们还有一个自主开发的已部署的版本(Deployed Versions)的服务。该已部署的版本服务让我们可以了解哪些服务的版本已经部署了,在什么时候部署的。搭配条约代理,让我们可以更清楚地了解变更,从而进行更好地测试。
4. 在持续集成 / 持续交付管道中运行测试
在我们的持续交付管道中,有一个测试关卡,我们会在此检查该服务是否已经符合消费者驱动契约。
我们的持续交付管道是一个非常标准的事务,我们从那里提交任务,其不仅会生成条约,还将生成可交付的产品工件。该产品工件将上传到我们的资源库,而同时条约会上传到条约代理。
在“提交”这一步时,我们还会生成一个应用程序的桩(如果它是提供者),它只包含控制器和一个模拟的服务层。这个“桩”代替消费者驱动契约测试中的产品工件,因为该工件不需要数据源或上游依赖,它们已经被模拟了。
如果消费者驱动契约测试这一步失败,那么整个管道也将失败。这意味着,潜在的产品工件不能继续往下走,必须得看看什么出错了。
所以,这些测试实际看起来是什么样的呢?
首先,我们必须把这个过程分解为两个不同的流程,提供者流程和消费者流程。测试运行依赖于待测服务是消费服务还是提供服务。应该注意的是,如果一个服务即是消费者又是提供者,我们就会运行两次。
最初,我们只测试提供者及其流程。然而,我们发现在他们各自的管道分别测试消费者流程和提供者流程好处更大。这确保了使用者不能改变想从 API 中得到的东西,而不需要提供者意识到这些变化,并为它们做好准备。虽然这需要团队间进行一些额外的协调,但是它能让我们在生产环境中回滚应用时更有信心,知道消费服务仍能正常工作,它仍可以被消费。
提供者测试流程
我们围绕一组具体的微服务展开思考,这样就能更容易理解这个流程了。在本例中,我们想测试一个名为 Location 的服务。它有两个消费者服务:Location-Frontend 和 Management。
下面是测试过程的样子,拆分为:
- 下载用于 Location 服务的最新的 Location-Frontend 和 Management 服务条约。
- 下载模拟的 Location 服务
- 启动这个模拟的 Location 服务
- Gradle 运行器读取这些条约,发送请求并检查响应
- 停掉那个模拟的 Location 服务
首先,我们下载了用于 Location 服务的 JSON 条约文件。条约代理很容易做到了这一点,它可以查找 Locations 消费者,以及用于这些消费者的最新条约。具体在本例中,即为 Management 服务和 Location-Frontend。
接下来,我们下载 Location 服务。然而,我们下载的并不是“真正的”Location 服务,也就是那个实际将被部署的服务。而是它的模拟版,里面已经清除了逻辑服务和数据层。它只有控制器和可启动的 web 服务。这样就消除了对上游服务和数据源的依赖。这个桩只是展示了 API 层和伪造的数据。记住,消费者驱动契约不是业务逻辑的测试。我们用 Spring 实现了这一点,替换了注入的依赖。其他依赖注入框架同样也能实现它。
为针对这个桩运行条约的内容,我们使用了一个为 Gradle 定制的运行器。这是我们内部用 Pact JVM Gradle 插件实现的。它其实起到了一个装具的作用,启动桩、阅读条约、发送请求,然后记录结果。
消费者条约流程
在下载桩工件和运行条约这一部分,消费者条约流程与提供者条约流程非常类似。然而,还有些关键的不同。
再强调一次,当你围绕一组具体的微服务展开思考时,就很容易理解它了。
例如,我们一起来考虑一下 Location-Frontend。Location-Frontend 是我们的针对前端的后端 (或 BFF) 服务,用于显示搜索结果。为做到它,它先通过 Location 服务查找一个 location (位置),然后把它传递给我们的和 Search(检索)服务。这表示 Location-Frontend 会从两个提供者处消费。
当消费者管理到达消费者驱动契约测试阶段时,它会从条约代理那里下载自己最新的条约。如果一个服务从多个提供者那里消费,将会从每个提供者那里下载条约。
让我们把这个过程分解为几步:
- 为 Location-Frontend 下载最新的条约,包括 Location 和 Search(提供者)
- 针对每个提供者:
- a. 下载和启动桩
- b. Gradle 运行器阅读 Location-Frontend 和提供者之间的条约,并发送请求 / 检查响应
- c. 关闭模拟的提供者
- 发布结果
就像你看到的,我们仍然下载了提供者桩,但是是针对于我们的消费者生成的单个条约运行它的,而不是用于这些提供者的每个消费者的多个条约。
它与提供管道的流程实际上是相同的,只是我们为单个指定消费者提供了各自的提供者。
5. 运行本地的测试
就像上面所说的,我们为实际的微服务创建了一个模拟的副本,这些桩处于数据层和任何额外的东西之上。它差不多就是一个有着控制器和伪造的静态数据的外壳。
使用微服务的一个问题就是得在本地将它们作为一个整体去运行全部所有。条约只在一定程度上解决了这一点,但它可以很好地测试你本地的应用条约,而不需要通过整个交付管道。
于是,Docker 来了。
Docker 让我们可以在任何平台上以同样的方式运行我们的桩应用,包括在管道中。有许多能让我们与 Docker 交互的类库,所以 Rightmove 创建了一个名为条约运行器(Pact Runner)的新项目,它是一个单独的 jar。
条约运行器做出来替代了我们的 Gradle 运行器。Gradle 运行器是我们做的,用于运行消费者和提供者流程。所以,它分为两个项目,每个针对其中一个流程。我们发现它很难维护,我们不可以用它来运行本地的条约测试。而条约运行器就是旨在解决这些问题的。它不是基于 Pact JVM Gradle 类库了,而是依赖于 Pact JVM Provider 来写的。这让我们有了更好的控制力,能让我们写一些轻便的东西,即能作为消费者运行器,也能作为提供者运行器。使用 Docker 能使其不依赖于平台,也改进了我们条约测试的速度,因为我们可以在本地缓存服务的图片,并在运行不同的测试时复用它们。
应该注意的是,条约运行器遵循上面所述的消费者和提供者流程,它只是此逻辑的实现。例如,依赖于测试流程,条约运行器将下载桩提供者,它们是 Docker 镜像的形式,将此镜像作为容器启动,针对这些容器运行条约,然后关掉它并发布结果。
条约运行器使团队可以运行将在交付管道内运行的同样的测试。
总结
虽然这个方式不够完美,但它使我们的团队可以独立地工作,使团队在部署自己的服务时有信心它们的变更不会破坏其他的服务。它还改进了团队间的交流,有助于在早期了解开发人员关于 API 设计的想法。未来的改进包括将条约用作自动化生成端点文档的来源,它将使 API 的版本管理更加轻松。
自在 Rightmove 实施消费者驱动契约以来,我们得到了很多收获。我们的团队能够独立部署服务,而不依赖于其他服务,同时有信心他们的服务能保持通信。我们回顾了几个迭代,从只测试提供者,到测试消费者和提供者,以及测试实时应用和回滚的应用。我们意识到需要一种简化本地测试的方式,从而引领着我们走向了 Docker 提供的平台无关的特性。
就如何针对你的项目完成此类测试, Pact 基金会有许多伟大的工具和文档,如果没有它,以上任何测试我们都做不到。非常感谢为此付出艰苦工作的每个人,是他们完成了所有的成果,它们都是免费、开源的。
关于作者
Harry Winser 是 Rightmove 搜索团队的技术经理。之前他在平台团队,在那里他从事许多有意思的内部项目,这些项目让开发人员可以持续地交付代码。他不工作的时候,通常会去改改小型项目、写写博客、弹弹吉他,或者驾他的小船在英国的运河上巡游。
查看英文原文: How to be Confident that Your Microservices Can Still Communicate in Production with Pact and Docker
评论