独立开发和部署微服务之间兼容性问题的必要条件
发表时间:2023-08-29 06:00:13
文章来源:炫佑科技
浏览次数:228
菏泽炫佑科技
独立开发和部署微服务之间兼容性问题的必要条件
作者 | 耆那教
译者|
规划| 丁小云
独立开发和部署单个微服务的能力是微服务策略成功的*关键指标。 然而,大多数团队在部署微服务之前都必须经过广泛的集成测试。 这是因为集成测试已成为识别微服务之间的兼容性问题所必需的,因为单元和组件或 API 测试不涵盖微服务之间的交互。
首先,集成测试是一种发现兼容性问题的后期反馈机制。 随着时间的推移,解决这些问题的成本会成倍增加(如上图底部的热图所示)。
此外,这可能会导致客户端和服务器团队进行大量返工,严重影响功能交付的可预测性,因为团队必须兼顾常规功能开发和集成错误修复。
集成环境可能非常脆弱。 由于两个组件或服务之间的兼容性问题,即使是单个中断的交互也可能导致整个环境受到损害,这意味着甚至无法测试其他不相关的功能和微服务。
这会阻碍生产交付,甚至修复关键问题,并使整个交付过程陷入停滞,即我们所说的“集成地狱”。
1 集成测试 - 了解野兽
在结束集成测试之前,让我们先了解一下它到底是什么。 这个术语经常被不恰当地使用。
测试应用程序不仅仅是测试每个函数、类或组件的逻辑。 应用程序的功能是这些单独的逻辑块与其对应部分交互的结果。 如果两个组件之间的服务边界或 API 不明确,则可能会导致通常所说的集成问题。 例如,如果函数 A 调用函数 B 仅使用一个参数,而函数 B 需要两个必需参数,则这两个函数之间存在集成或兼容性问题。 这种快速反馈有助于我们及早纠正问题并立即解决问题。
然而,当我们识别微服务级别的兼容性问题(HTTP、消息传递或事件级别的服务边界)时,单元、组件或 API 测试都无法立即识别任何偏离或违反服务边界的情况。 必须使用所有实际的相应服务来测试微服务,以验证不存在中断的交互。 这些被广泛地(有些不正确地)归类为集成测试。
术语集成测试用于描述多种类型的检查:
两个或多个组件之间的兼容性;
工作流程测试 - 涉及交互式编排的整个功能;
与其他依赖项(例如存储、消息传递基础设施等)的交互;
除了生产基础设施的端到端测试之外,还有更多。
需要明确的是,当我们说“集成测试”的结束时,我们谈论的是消除对“集成测试”的依赖,而不是作为识别微服务之间兼容性问题的唯一方法。 但其他事情,例如工作流程测试,可能仍然是必要的。
2 确定拐点 - 知道从哪里开始
当所有代码都属于一个主体时,方法签名可以作为服务边界的 API 规范。 我们可以通过编译时检查等机制强制执行方法签名检查,从而向开发人员提供早期反馈。
然而,当服务的组件被拆分为多个微服务并且服务边界变成接口(例如 HTTP REST API)时,这种早期反馈就会丢失。 以前记录为方法签名的 API 规范现在需要明确记录,描述调用它们的正确方法。 如果 API 文档不可机器解析,它也会导致团队之间的沟通混乱。
如果没有详细记录的服务边界:
客户端只能使用近似模拟的服务器来构建,并且手动模拟和存根技术通常会导致过时的存根无法真正代表服务器。
对于服务器来说,不可能冒充客户端。
这意味着我们必须采用缓慢的串行开发风格,必须等待一个组件完成构建才能开始开发另一个组件。 如果您需要快速发布功能,这不是一个有效的方法。
转向微服务后,我们失去了两个关键能力:
明确表示两个组件之间的服务边界的 API 规范;
执行描述服务边界的 API 规范。
我们需要另一种方法来弥补这两方面的不足。
3 API规范
如果要恢复以清晰且机器可解析的方式表示 API 签名的能力,那么采用 API 规范标准(例如 或 )至关重要。 虽然这增加了开发人员创建和维护这些规范的工作量,但好处大于成本。
即便如此,API 规范,顾名思义,只是帮助描述 API 签名。 如何强制执行这些规则以便在开发过程中获得早期反馈? 这部分仍然缺失。
4 代码/文档生成 - 无效且不可持续
我们可以说我们可以通过代码生成技术来生成和维护 API 规范。 从表面上看,如果代码是根据规范生成的,那么就没有偏离规范的情况。
然而,这里有一些困难:
持续开发 - 大多数代码生成工具/技术都会生成服务器端和客户端代码的脚手架,并要求我们在此脚手架/模板中填写业务逻辑。 问题在于,当规范发生变化时,我们通常需要重新生成脚手架,从旧版本的代码中提取业务逻辑,然后再次粘贴到新的脚手架中,这增加了出现人为错误的可能性。
数据类型不匹配 - 代码生成工具/技术必须支持每种编程语言。 在多语言环境中,生成的脚手架在不同编程语言之间可能不具有一致的数据类型(或其他内容)。 如果我们为编程语言生成文档(根据服务器端代码生成 API 规范),然后使用生成的规范进一步生成客户端代码的脚手架独立开发和部署微服务之间兼容性问题的必要条件,这种情况会进一步恶化。
数据类型不匹配 - 代码生成工具/技术必须支持每种编程语言。 在多语言环境中,生成的脚手架在不同编程语言之间可能不具有一致的数据类型(或其他内容)。 如果我们为编程语言生成文档(根据服务器端代码生成 API 规范),然后使用生成的规范进一步生成客户端代码的脚手架,这种情况会进一步恶化。
一般来说,代码生成和文档生成只能满足有限的场景。 虽然它们*初可能为团队提供一种通过生成代码来构建应用程序的快速简便的方法,但该技术的持续成本可能会让团队不知所措。
因此,我们需要另一种方式来执行 API 规范。
5 合约驱动开发 - API 规范作为可执行合约
方法签名可以由编译器强制执行,当开发人员偏离方法签名时向他们提供早期反馈。 那么API能否达到类似的效果呢?
合同测试就是实现这一效果的尝试。 Pact.io 的文档说:
契约测试是一种通过单独检查每个应用程序来测试集成点的技术,以确保它们发送或接收的消息符合“契约”中记录的内容。
但需要注意的是,合约测试本身还包括几种方法,比如客户端驱动合约测试(Pact.io)、服务端驱动合约测试(云合约中的生产者合约测试方法)、双向合约测试(.io) 等等。 在大多数这些测试方法中,API 合约是独立于 API 规范的单独文档。 例如,在 Pact.io 中,JSON 是 API 合约。 Cloud 还有一个用于定义合约的 DSL。 与其维护两个不同的工件(这可能会不同步),不如利用 API 规范本身作为 API 合约,在开发人员偏离 API 规范并给客户带来问题时为他们提供早期反馈,这不是更好吗?
这就是它的作用。 它是一个基于合约驱动开发的开源工具。 它将客户端和服务器之间的交互划分为独立可验证的单元。 考虑两个微服务之间的以下交互,目前仅在更大级别的测试环境中进行验证。
ServiceA <-> ServiceB
CDD 可以将这种交互分解为连续的组件:
ServiceA <-> Contract as Stub {API spec of ServiceB}
Contract as Test {API spec of ServiceB} <-> ServiceB
现在让我们仔细看看。
左图: => as Stub 我们为客户端()模拟服务器(),这样客户端应用程序的开发就可以独立于服务器来完成。 由于Stub(智能Mock)是基于双方约定的API规范,所以它确实可以作为服务端()的Mock,当客户端()调用API出现偏差时,它会给出反馈/抛出错误来自 API 规范。
右边:as Test => 为服务端()模拟客户端(),验证响应是否符合双方约定的API规范。 as Test 将在服务器端()应用程序开发人员偏离规范时立即提供反馈。
现在我们可以让客户端()和服务器()应用程序在组件级别上符合 API 规范,同时独立构建,无需将它们部署在一起来测试它们的交互。 这样我们就不再需要依赖集成测试来识别兼容性问题。
这就是 API 规范被用作可执行合约的方式。
6 合约即代码
这里的关键是API规范本身,它允许API提供者和消费者分离并独立驱动各自组件的开发和部署,同时保持所有组件的一致性。
为了成功地进行契约驱动的开发,我们需要采用 API 优先的方法,其中 API 提供者和消费者需要首先合作设计和记录 API 规范。 这意味着他们需要使用现代可视化编辑器之一,例如 、 、 等来编写 API 规范软件开发,在开始独立构建各自的部分之前专注于 API 设计,并确保所有利益相关者保持同步。
习惯于根据代码生成 API 规范的团队可能会对这种先编写 API 规范的反向过程感到不舒服。 CDD 需要类似于测试驱动开发的思维方式转变。 在进行测试驱动开发时,我们需要先通过编写测试来指导/驱动代码设计。 同样,在CDD中,我们需要先手动编写API规范,然后使用诸如将其转换为可执行的合约测试等工具。
我发现,使用基于代码生成 API 规范的方法,API 设计处于次要地位,并且更多地成为事后的想法,或者客户端或服务器端的偏见。 另外,由于发布时间的压力,当采用API规范优先的方式时,我们可以并行地独立开发客户端和服务端组件,而这是基于代码生成API规范所无法实现的(客户端必须等待服务器端)代码来完成并生成规格)。
一旦就公共 API 规范达成共识,拥有这些 API 规范的单一事实来源就变得很重要。 如果这些规范有多个副本,则会导致客户端和服务器团队在实现上出现分歧。
CDD 建立在三个基本支柱之上。 “合同作为存根”和“合同作为测试”使客户端和服务器团队保持一致,但将所有内容结合在一起的粘合剂是第三个支柱 - “中央合同存储库”。
API 规范是机器可解析的代码,那么还有什么地方比版本控制系统更好地存储它们呢? 将它们存储在版本控制系统(例如 Git)中,使我们可以通过添加拉取/合并请求过程来为其构建过程添加一些严格性。 理想情况下,拉取/合并请求应包括以下步骤:
语法检查以确保一致性;
向后兼容性检查以查看是否有任何重大更改;
*终审查并合并。
强烈建议将规格存储在同一个中心位置,这适合大多数情况(甚至是大型企业)。 除非绝对必要,否则不建议跨多个存储库存储规范。
一旦规范存储在中央存储库中,它们可以是:
客户端和服务器团队使用它进行独立开发;
发布到API网关。
7 集成测试结束
我们已经不再需要进行集成测试来识别应用程序兼容性问题,那么系统测试和工作流程测试呢?
CDD 为更大规模的测试环境铺平了道路,因为所有兼容性问题都是在开发早期(在本地和 CI 等环境中)发现的,修复这些问题的成本要低得多。 我们可以通过系统测试和工作流程测试,在稳定的更大级别的环境中验证复杂的编排问题。 此外,由于我们不再需要集成测试来识别兼容性问题,因此测试套件在更大环境中的整体运行时间也减少了。
原文链接: