如何拆分微服务应用

本文翻译自:https://martinfowler.com/articles/break-monolith-into-microservices.html

什么时候解偶和,解什么

由于单一系统变得太大而无法应对,许多企业被迫将其分解为微服务架构风格。 这是一次有价值的旅程,但并不容易。 我们已经了解到,为了做到这一点,我们需要从简单的服务开始,然后提取基于垂直功能的服务,这些服务对业务很重要并且经常变化。 这些服务最初应该很大,并且最好不依赖于剩余的整体结构。 我们应确保每个迁移步骤都代表整体架构的原子改进。

将单片系统迁移到微服务生态系统是一个史诗般的旅程。踏上这一旅程的人们抱有诸如增加运营规模,加快变革步伐,避免高昂变革成本等愿望。他们希望增加团队数量,同时使他们能够并行地相互独立地交付价值。他们希望快速尝试其业务的核心功能并更快地实现价值。他们还希望避免与更改现有单片系统相关的高成本。

决定何时以及如何逐步迁移分离的能力是将整体分解为微服务生态系统的一些架构挑战。在这篇文章中,我分享了一些技术,可以指导交付团队 - 开发人员,架构师,技术经理 - 在整个过程中做出这些分解决策。

为了阐明这些技术,我使用了多层在线零售应用程序。该应用程序紧密结合面向用户,业务逻辑和数据层。我之所以选择这个例子,是因为它的架构具有许多企业运行的单片应用程序的特性,而且它的技术堆栈足够现代,可以证明分解是合理的,而不是完全重写和替换。

微服务生态系统目的地

在开始之前,每个人都对微服务生态系统有共同的理解是至关重要的。 微服务生态系统是一个服务平台,每个服务都封装了业务能力。 业务功能表示企业在特定域中执行的目标和职责。 每个微服务都公开了一个API,开发人员可以以自助方式发现和使用它。 微服务具有独立的生命周期。 开发人员可以独立构建,测试和发布每个微服务。 微服务生态系统强制执行自主长期团队的组织结构,每个团队负责一个或多个服务。 与微服务中的一般感知和“微观”相反,每项服务的规模最小,并且可能根据组织的运营成熟度而变化。 正如Martin Fowler所说,“微服务是标签,而不是描述”。

图1

旅程指南

在深入了解指南之前,重要的是要知道将现有系统分解为微服务会产生很高的总体成本,并且可能需要多次迭代。 开发人员和架构师必须仔细评估现有巨型组件的分解是否是正确的路径,以及微服务本身是否是正确的目的地。 搞清楚这些后,让我们开始指南。

简单和分离能力的热身

走上微服务这条路,需要最低级别的操作准备。它需要按需访问部署环境,构建新的连续交付管道以独立构建,测试和部署可执行服务,以及保护,调试和监视分布式体系结构的能力。

无论我们是在构建新的服务还是分解现有系统,都需要运营准备就绪。有关此操作准备情况的更多信息,请参阅Martin Fowler关于微服务先决条件的文章。好消息是,自Martin发表文章以来,运行微服务架构的技术发展迅速。这包括创建Service Mesh,一个专用的基础架构层,用于运行快速,可靠和安全的微服务网络,容器编排系统,以提供更高级别的部署基础架构抽象,以及持续交付系统(如GoCD)的演进,以构建,测试和部署微服务作为容器。

我的建议是开发人员和运营团队使用他们分解或构建新服务的第一和第二服务构建底层基础架构,持续交付管道和API管理系统。从与monolith完全分离的功能开始,它们不需要更改当前使用整体的许多面向客户端的应用程序,并且可能不需要数据存储。交付团队正在优化的目的是验证他们的交付方法,提高团队成员的技能,并构建提供可自行部署的安全服务所需的最低基础架构,以暴露自助式API。例如,对于在线零售应用程序,第一个服务可以是monolith可以调用以验证最终用户的“最终用户身份验证”服务,第二个服务可以是“客户配置文件”服务,提供外观服务更好地了解新客户。

首先,我建议解耦简单的边缘服务。 接下来,我们采用不同的方法将解藕深深嵌入单片系统的功能。 我建议首先进行边缘服务,因为在旅程开始时,交付团队面临的最大风险是无法正确操作微服务。 因此,使用边缘服务来实践他们所需的操作先决条件是很好的。 一旦他们解决了这个问题,他们就可以解决分裂巨石的关键问题。

最大限度地减少对整体应用的依赖

作为一项基本原则,交付团队需要最大限度地减少新组建的微服务与整体结构的依赖关系。 微服务的一个主要好处是拥有快速独立的发布周期。 如果依赖于整体 - 数据,逻辑,API - 将服务耦合到整体的发布周期,请禁止这种好处。 远离整体的主要动机通常是锁定其中的功能的高成本和缓慢的变化速度,因此我们希望通过消除对整体结构的依赖性来逐步向这些核心功能分离。 如果团队遵循本指南并将功能构建到他们自己的服务中,他们发现的是相反的依赖性,从整体到服务。 这是一个理想的依赖方向,因为它不会减慢新服务的变化速度。

下一步指南提供了其他方法来确定开发人员分离服务的顺序。 这意味着他们可能无法始终避免依赖回到整体。 如果新服务最终回调整体,我建议从整体中公开一个新的API,并通过新服务中的anti-corruption层访问API,以确保整体概念不会 泄漏。 努力定义反映明确定义的域概念和结构的API,即使monolith的内部实现可能不同。 在这种不幸的情况下,交付团队将承担改变整体,测试和发布新服务以及整体版本的成本和难度。

首先将不需要依赖关系的服务解耦回整体块,并尽量减少对整体块的更改:

尽早拆分粘性功能

我假设在这一点上,交付团队很乐意构建微服务并准备好攻击棘手的问题。 然而,他们可能会发现无法做到在不依赖整体的情况下解耦出下一个服务的能力。 造成这种情况的根本原因通常是整体结构中的功能泄漏,没有明确定义为域概念,其中许多整体功能依赖于它。 为了能够进步,开发人员需要识别粘性功能,将其解构为定义良好的域概念–物化,然后将这些域概念重新划分为单独的服务。

例如,在基于整体应用的web中,“(web)会话”的概念是最常见的耦合因素之一。在在线零售示例中,会话通常是许多属性的桶,范围从跨越不同域边界的用户偏好(例如运送和支付偏好)到用户意图和诸如最近访问的页面,点击的产品和愿望列表之类的交互。除非我们解决当前的“会话”概念的解耦,解构和统一,否则我们将努力解耦许多未来的能力,因为它们将通过泄漏的会话概念与整体纠缠在一起。我也不鼓励在整体结构之外创建一个“会话”服务,因为它只会导致目前整体流程中存在的类似紧密耦合,更糟糕的是,在流程外和整个网络中。

开发人员可以从粘性功能中逐步提取微服务,即时服务。例如,首先重构“客户愿望清单”并将其提取到新服务中,然后将“客户支付首选项”重构为另一个微服务并重复。

确定最具耦合性的概念,并将其解耦,解构并具体化为具体的域服务:

使用依赖关系和结构代码分析工具(如Structure101)来识别整体中的最大耦合和约束因子功能。

垂直去耦并尽早释放数据

从整体解耦功能的主要驱动力是能够独立发布它们。 第一个原则应该指导开发人员围绕如何执行解耦的每个决策。 单片系统通常由紧密集成的层或甚至多个系统组成,这些系统需要一起发布并具有脆弱的相互依赖性。 例如,在在线零售系统中,由一个或多个面向在线购物应用程序的客户组成的整体结构,后端系统通过集中集成的数据存储实现许多业务功能以保持状态。

大多数解耦尝试都是从提取面向用户的组件和一些前端服务开始,为现代UI提供给开发人员友好的API,而数据仍然锁定在一个架构和存储系统中。 虽然这种方法可以获得一些快速的成功,例如更频繁地更改UI,但在核心功能方面,交付团队只能像最慢的部分(整体和整体应用的数据存储)一样缓慢地移动。 简而言之,在不解耦数据的情况下,架构不是微服务。 将所有数据保存在同一数据存储中与微服务的分散数据管理特性相反。

该策略是垂直移出功能,将核心功能与其数据分离,并将所有前端应用程序重定向到新的API。

将数据与服务一起解偶的主要障碍是在集中共享数据中心写入和读取数据。 交付团队需要结合适合其环境的数据迁移策略,具体取决于他们是否能够同时重定向和迁移所有数据读/写器。 Stripe的四阶段数据迁移策略适用于需要逐步迁移通过数据库集成的应用程序的许多环境,而所有正在变更的系统都需要连续运行。

将其数据与服务分离到新的微服务,从而暴露新接口,修改消费者并将消费者重定向到新API:

避免只去耦外观的反模式或只解耦后端服务或永远不解耦数据。

解藕重要业务功能和经常变化的部分

解耦整体应用很难。我听说Neal Ford使用了仔细的器官手术的类比。在在线零售应用程序中,提取功能包括仔细提取功能的数据,逻辑,面向用户的组件并将其重定向到新服务。因为这是一项非常重要的工作,开发人员需要不断评估与他们获得的好处脱钩的成本,例如:走得更快或规模越来越大。例如,如果交付团队的目标是加速对整体中锁定的现有功能的修改,那么他们必须确定最需要修改的功能。将不断变化的部分代码分离,并从开发人员那里获得很多爱,并且最大限度地限制它们以快速提供价值。交付团队可以分析代码提交模式,找出历史上最变化的内容,并将其与产品路线图和产品组合叠加在一起,以了解最近将获得关注的最佳功能。他们需要与业务和产品经理交谈,以了解对他们而言真正重要的差异化功能。

例如,在在线零售系统中,“客户个性化”是一项经过大量实验的功能,可以为客户提供最佳体验,并且是解耦的理想选择。 这是一项对业务很重要,客户体验和经常修改的功能。

识别并解耦最重要的能力:为业务和客户创造最大价值,同时定期更改。

使用CodeScene社交代码分析工具来查找最活跃的组件。 如果构建系统碰巧触摸或在每次提交时自动生成代码,请务必过滤来自噪声的信号。 将经常更改的代码与产品路线图即将发生的更改重叠,并找到要解耦的交叉点。

解耦功能而不是代码

每当开发人员想要从现有系统中提取服务时,他们就有两种方法:提取代码或重写功能。

通常默认情况下,服务提取或整体分解被设想为按原样重用现有实现并将其提取到单独服务中的情况。 部分原因是我们对我们设计和编写的代码存在认知偏差。 建筑劳动,无论过程多么痛苦或结果不完美,都会让我们对它产生爱。 这实际上被称为宜家效应。 不幸的是,这种偏见将使整体分解努力回归起点没什么价值。 因为它使开发人员和更重要的技术经理忽视了提取和重用代码的高成本和低价值。

或者,交付团队可以选择重写功能并重新使用旧代码。 重写使他们有机会重新审视业务能力,启动与业务的对话以简化遗留流程,并挑战随着时间推移构建到系统中的旧假设和约束。 它还提供了技术更新的机会,使用最适合该特定服务的编程语言和技术堆栈来实现新服务。

例如,在零售系统中,“定价和促销”功能是一种智能复杂的代码。 它支持动态配置和应用定价和促销规则,根据客户行为,忠诚度,产品包等各种参数提供折扣和优惠。

这种能力可以说是重用和提取的良好候选者。 相比之下,“客户资料”是一种简单的CRUD功能,主要由用于序列化,处理存储和配置的样板代码组成,因此,它是重写和退出的良好候选者。

根据我的经验,在大多数分解场景中,团队最好将该功能重写为新服务并退出旧代码。由于以下原因,这考虑了重用的高成本和低价值:

  1. 有大量的样板代码可以处理环境依赖性,例如在运行时访问应用程序配置,访问数据存储,缓存,以及使用旧框架构建。大多数样板代码都需要重写。托管微服务的新基础架构与几十年前的应用程序运行时非常不同,需要一种非常不同的样板代码。
  2. 现有功能很可能不是围绕明确的域概念构建的。这导致传输或存储不反映新域模型的数据结构,并且需要进行大的重组。
  3. 经过多次更改迭代的长期遗留代码可能具有较高的代码毒性级别和较低的重用价值。

除非与功能相关并与明确的领域概念保持一致并具有高知识产权,否则我强烈建议重写和退出旧代码。

重复使用和提取具有低毒性的高价值代码,重写和退出具有高毒性的低价值代码:

使用代码毒性分析工具(如CheckStyle)来围绕重写和重用做出决策。

首先是宏观(Macro),然后是微观(Micro)

在传统整体应用中寻找领域边界既是艺术又是科学。作为一般规则,应用域驱动设计技术来查找定义微服务边界的有界上下文是一个很好的起点。我承认,我经常看到从大型整体到真正小型服务的过度修正,实际上是小型服务,其设计受到现有规范化数据视图的启发和驱动。这种识别服务边界的方法几乎总是导致CRUD资源的大量贫血服务的寒武纪爆发。对于许多微服务架构的新手来说,这会产生一个高摩擦环境,最终无法通过独立发布和服务执行的测试。它创建了一个难以调试的分布式系统,一个跨越事务边界的分布式系统,因此难以保持一致,这个系统对于组织的运营成熟度而言过于复杂。虽然有一些关于微观应该如何成为微服务的启发式:团队的规模,重写服务的时间,必须封装的行为等等。我的建议是,规模取决于交付的服务数量和运营团队可以独立发布,监控和运营。从围绕逻辑域概念的大型服务开始,并在团队准备就绪时将服务分解为多个服务。

例如,在零售系统脱钩的旅程中,开发商可以从一个服务“购买”开始,该服务包含“购物袋”的内容以及购买购物袋的能力,即“结账”。 随着他们组建小型团队和释放大量服务的能力不断增强,他们可以将“购物袋”与“结账”分离为单独的服务。

围绕丰富域概念去耦宏服务,并在准备就绪时,将服务分解为较小的域概念

使用Richardson成熟度模型L3,以便在不影响调用者的情况下实现服务的未来解耦,即调用者发现如何结账但不知道具体实现。

以原子演化步骤迁移

通过将传统整体应用像在空气中擦除那样拆分成精美设计的微服务在某种程度上是一种神话,并且可以说是不可取的。任何经验丰富的工程师都可以分享遗留迁移和现代化尝试的故事,这些故事是在总体完成过度乐观的情况下计划和启动的,并且最好在足够好的时间点放弃。这种努力的长期计划因宏观条件的变化而被放弃:该计划耗尽资金,该组织将其重点转向其他方面或支持它的领导层离开。所以这个现实应该根据团队如何接近整体到微服务的旅程来设计。我将这种方法称为“在体系结构演化的原子步骤中迁移”,其中迁移的每一步都应该使体系结构更接近其目标状态。每个进化单位可能是一个小步骤或一个大的飞跃,但是是原子的,要么完成要么还原。这一点非常重要,因为我们采用迭代和增量方法来改进整体架构和解耦服务。每个增量必须使我们在架构目标方面处于更好的位置。使用进化架构适应度函数隐喻,迁移的每个原子步骤之后的架构适应度函数应该为架构的目标生成更接近的值。

让我用一个例子说明这一点。 想象一下,微服务架构的目标是提高开发人员修改整个系统以提供价值的速度。 团队决定将最终用户身份验证分离为基于OAuth 2.0协议的单独服务。 此服务旨在替换现有(旧体系结构)客户端应用程序对最终用户进行身份验证的方式,以及新的体系结构微服务验证最终用户。 让我们在演变中称之为“Auth服务介绍”。 引入新服务的一种方法是首先完成以下步骤:

(1)构建Auth服务,实现OAuth 2.0协议。

(2)在整体应用后端添加一个新的认证路径,以调用Auth服务,以便对代表其处理请求的最终用户进行身份验证。

如果团队停在这里并转向构建其他服务或功能,他们会使整体架构处于增加的熵状态。 在此状态下,有两种方法可以对用户进行身份验证,即新的OAuth 2.0基本路径和旧客户端的基于密码/会话的路径。 此时,团队实际上远离了他们更快地进行更改的总体目标。 整体代码的任何新开发人员都需要处理两个代码路径,增加了解代码的认知负担,以及更改和测试代码的过程。

相反,团队可以在我们的原子演化单元中包含以下步骤:

(3)用OAuth 2.0路径替换旧客户端的密码/基于会话的身份验证

(4)从整体中删除旧的认证代码路径

在这一点上,我们可以争辩说团队已经接近目标架构。

利用体系结构演化的原子步骤将体系结构发展为微服务,在每个步骤之后,整体体系结构朝着其目标进行改进,即使中间代码更改可能会使其远离其适应目标

整体分解的原子单位包括:

  1. 解耦新服务
  2. 将所有消费者重定向到新服务
  3. 退出整体应用中的旧代码路径。

反模式(不可取):将新服务解耦,用于新消费者,永不退休。

我经常发现团队结束了一个功能的迁移,并且只要构建了新的功能而没有退出旧的代码路径(上面描述的反模式),就可以获得胜利。 主要原因是
(a)关注引入新功能的短期利益和(b)在面对构建新功能的竞争优先级的同时退出旧实施所需的总工作量。
为了做正确的事情,我们需要努力使原子步骤尽可能小。

通过这种方式迁移,我们可以打破短途旅行的旅程。 我们可以安全地停止,复兴并在这漫长的旅程中幸存下来,杀死整体应用。