Martin Fowler推荐的一篇微服务落地文章,很有参考价值
随着单体系统变得越来越臃肿,很多公司开始想要将它分解成微服务。这事很有价值,但并不是一件容易的事。我们需要从一个简单的服务开始,然后基于垂直扩展能力分解出更多的服务,这些服务对业务来说至关重要,而且经常发生变更。在一开始,解耦出来的服务不能太小,并且最好不依赖单体的其他部分。我们应该确保每一步迁移都是对整体架构的原子性改进。
将单体系统改造成微服务生态系统是一个史诗般的旅程。步入这段旅程的人无不怀揣愿景,期待着能够借此增加经营规模,加快交付步伐,并降低交付所带来的成本。他们希望发展出更多的团队,每个团队都能够独立交付价值。他们希望能够快速地试验他们的核心业务能力,并更快地交付价值。他们也希望能够降低变更现有单体系统所带来的成本。
在将单体系统分解为微服务生态系统时,我们要面临的主要挑战是决定什么时候以及如何进行增量迁移。在这篇文章中,我分享了一些技巧,可以指导交付团队(包括开发人员、架构师和技术经理)在这个旅程中做出决策。
为了更好地说明这些技巧,我将以一个多层架构的在线零售应用程序作为示例。这个应用程序的用户层、业务逻辑层和数据层紧紧地耦合在一起。我之所以选择这个例子,是因为它的架构具有单体系统的特点,并且它的技术栈足够与时俱进,足以证明我们应该进行分解,而不是重写或用其他技术替代。
在开始之前,需要让每个人都了解什么是微服务生态系统。微服务生态系统是由很多服务组成的平台,每一个服务都封装了一种业务功能。业务功能代表了某个企业在特定领域为实现其目标和责任所做的事情。每个微服务都公开了一组 API,开发人员可以发现和使用这些 API。
微服务具有独立的生命周期,开发人员可以独立构建、测试和发布微服务。微服务生态系统对应的组织结构由具备自治能力的团队组成,每个团队负责一个或多个服务。
然而,我们普遍认为,从字面上看,微服务应该尽可能“微”,但实际上,服务的大小是最不重要的一个因素,并且它可能因组织的运营成熟度不同而有所差异。正如 Martin Fowler 所说的那样,“微服务是一种标签,而不是描述”。
图 1 服务封装了业务功能,通过自助服务 API 暴露数据和功能
在深入了解这个指南之前,你需要先知道,将现有系统分解为微服务会带来很高的总体成本,并且可能需要很多次迭代。开发人员和架构师需要仔细评估分解现有单体是不是一个正确的决定,以及微服务架构是否适用于当前系统。在想清楚这些问题后,就可以进入这个指南了。
在开始微服务旅程之前,需要先做好最基础的运维准备。它需要访问部署环境的能力,能够通过持续交付管道来构建、测试和部署微服务,以及保护、调试和监控分布式系统。
无论我们是从头开始构建服务还是分解现有系统,运维成就绪熟度都是必需的。有关运维就绪更多的信息,请参阅 Martin Fowler 撰写的有关微服务先决条件的文章(这里就不多介绍了)。
自从 Martin 撰文以来,微服务架构的运维技术得到迅猛的发展,包括服务网格(一种专用的基础设施层,用以运行高速、可靠和安全的微服务网络)、容器编排系统(提供更高级别的部署基础设施抽象),以及用于将微服务作为容器来构建、测试和部署的持续交付系统(如 GoCD)。
我的建议是,开发人员和运营团队先为头一两个分解出来或全新开发的微服务构建底层的基础设施、持续交付管道和 API 管理系统。从分解耦合性较低的服务开始,不需要修改正在使用单体的客户端,并且可能连数据存储都不需要。
交付团队在这个时候需要进一步优化和验证他们的交付方式、提升团队成员技能,并构建出能够提供独立部署服务能力的最小化基础设施。例如,对于在线零售应用程序,第一个服务可以是“用户认证”,单体系统通过这个服务来验证用户;第二个服务可以是“客户资料”,一个为客户端应用程序提供用户视图的门面服务。
首先,我建议先解耦简单的边缘服务,然后再采用其他方式深入分解单体系统的其他功能。我之所以建议先分解边缘服务,是因为在这趟旅程开始时,交付团队面临的最大风险是缺乏正确运营微服务的能力。因此,通过分解边缘服务来训练他们的运营能力是个好主意。一旦解决了这个问题,他们就可以解决其他更大的问题。
图 2 通过分解一个简单功能来预热,建立我们的运营就绪状态
交付团队需要最小化新微服务对单体的依赖。微服务的一个主要优点是快速和独立的发布周期。如果依赖了单体——数据、逻辑和 API——那么微服务就会与单体的发布周期相耦合,让微服务的优势不复存在。
通常,分解单体的主要动机是摆脱缓慢的变更速度和高昂的变更成本,所以我们希望逐步朝着通过消除对单体的依赖来解耦核心功能的方向前进。
如果交付团队在解耦服务时遵循这个指导原则,他们会发现,依赖关系应该是反过来的,即让单体依赖微服务。这是一种理想的依赖关系,因为它不会减慢新服务的变更速度。
在这个在线零售系统中,“购买”和“促销”是核心功能。 在结帐过程中,“购买”需要使用“促销”,为顾客提供符合他们资格的最佳促销方案,并给予他们购买的商品。
在决定先解耦哪一个功能时,我建议先解耦“促销”,然后是“购买”。因为按照这个顺序,可以降低对单体的依赖,“购买”继续呆在单体中,让它依赖新的“促销”服务。
接下来的这个准则为交付团队提供了其他决定解耦服务顺序的方法。这意味着他们可能做不到完全不依赖单体。如果一个新服务最终会依赖单体,我建议在单体上暴露一个新的 API,并通过新服务中的反腐败(anti-corruption)层来访问这个 API。
努力尝试定义出能够反映领域概念和结构的 API,即使单体的内部实现可能使用了其他方式。如果发生了这种情况,交付团队需要承担变更单体、测试和发布新服务的成本和难度。
图 3 首先解耦不需要依赖单体的服务,并尽量减少对单体的更改
我假设到了这个时候,交付团队已经为解决粘滞性问题做好了准备。不过,他们可能会发现,再解耦出下一个不依赖单体的服务有点困难。造成这种情况的根本原因往往是没有定义好领域概念,单体中的很多功能都依赖于其中的某个功能。为了能够继续,开发人员需要识别出粘滞性功能,将其解构为定义明确的领域概念,然后让不同的服务来实现这些概念。
例如,在基于 Web 的单体中,“会话”是最常见的耦合因素之一。在我们的例子中,会话通常用于保存各种属性,如用户偏好设置(诸如送货和支付偏好)、用户的意图和交互细节(如最近访问的页面、点击的产品和愿望清单)。
除非我们能够解构“会话”这个概念,否则我们将很难解耦其他的功能,因为它们都通过会话与单体缠绕在一起。我也不鼓励在单体之外再创建一个“会话”服务,因为它只会导致类似的紧密耦合,更糟糕的是,还有可能让这种耦合散播到整个网络。
开发人员可以逐步从粘性功能中提取微服务,即一次提取一个服务。例如,首先重构“客户愿望清单”,并将其提取到新服务中,然后将“客户付款首选项”重构为另一个微服务,并重复这个过程。
图 4 找出耦合的功能,并将其解耦、解构、转化为具体的领域服务
你可以使用依赖性和结构化代码分析工具(如 Structure101)来确定单体中最具耦合性和约束性的因素。
解耦功能的主要目的是能够进行单独发布,开发者在做出解耦决策时应该遵循这一原则。单体系统通常由多个紧密集成的层或多个系统组成,这些系统需要一起发布,并具有脆弱的相互依赖。
例如,在一个在线零售系统中,单体由一个或多个面向客户的在线购物应用程序组成,后端系统借助能够保存状态的集中式存储系统来实现各种业务功能。
大多数解耦都是从提取用户组件和门面服务开始,以便为用户界面开发人员提供友好的 API,而数据仍然保存在同一个模式和存储系统中。虽然这种方法有一定的作用,例如可以更频繁地更改用户界面,但在核心功能方面,交付团队的交付速度受限于最慢的部分,也就是单体和单体数据存储系统。简而言之,在不分离数据的情况下,就算不上是微服务架构。在同一个数据存储中保存所有的数据与微服务的去中心化数据管理特征背道而驰。
最好的办法是进行垂直的功能解耦,同时将核心功能和数据分离出来,并将前端应用程序重定向到新的 API 上。
多个应用程序在集中式共享数据存储中写入和读取数据是阻碍数据与服务一起解耦的主要障碍。交付团队需要采取适当的数据迁移策略,具体取决于他们是否能够同时重定向和迁移所有数据读写者。 如果要进行逐步迁移,同时不影响系统的运行,可以参考 Stripe 的四阶段数据迁移策略。
图 5 将数据分离到微服务,暴露新接口,把消费者重定向到新 API
注意要避免只解耦门面,或只解耦后端服务,而不解耦数据。
单体中解耦出特定功能不是件容易的事。在在线零售应用程序中,提取功能涉及到数据、逻辑、面向用户的组件,并将它们重定向到新服务。这里涉及到大量的工作,因此开发人员需要不断评估解耦的成本与解耦可能带来的好处。例如,如果交付团队的目标是为了加快修改单体中已有功能的速度,那么他们必须先找出哪些功能是最经常发生变更的。
将代码中不断变化的部分分解开来,这部分代码占用开发人员太多的精力,并限制了他们快速交付价值的能力。交付团队可以通过分析代码提交模式来找出经常发生变更的代码,并将其与产品路线图和产品组合重叠起来,以便了解在不久的将来哪些是最受关注的功能。他们需要与业务和产品经理交流,了解哪些才是他们真正关心的功能。
例如,在一个在线零售系统中,“客户个性化”功能可以为客户提供最佳体验,并且经过大量的实验验证,所以可以作为解耦的入手点。这是一项重要的业务功能,而且经常发生变更。
图 6 识别和分离最重要的功能:为业务和客户创造最大价值,同时定期进行变更。
你可以使用社交代码分析工具(如 CodeScene)来查找最活跃的组件,将经常发生变更的代码与产品路线图叠加在一起,找到需要解耦的部分。
开发人员无论何时想要从现有系统中提取服务,都有两种方法可以达到目的:提取代码或重写功能。
通常情况下,服务提取或单体分解就是重用现有的代码,并将其提取到单独的服务中。我们对自己做出的设计和编写的代码存在认知偏见。当我们为一件事情付出努力,无论过程多么痛苦,结果有多么不完美,我们都会对它产生偏爱。这也就是所谓的宜家效应。不幸的是,这种偏见会让分解单体的的努力化为泡影。它让开发人员和技术管理人员忽略了提取重用代码的高成本和低价值。
或者,交付团队可以选择重写功能并弃用旧代码。重写功能让他们有机会重新审视业务逻辑,通过与业务的对话来简化流程,并打破长久以来构建系统所形成的假设和约束。它还为技术更新提供了机会,通过最适合该特定服务的编程语言和技术栈来实现新服务。
例如,在零售系统中,“定价和促销”功能相关代码比较复杂。它可以动态配置,并应用定价和促销规则,根据各种参数提供折扣和优惠,如客户行为、忠诚度、产品捆绑等。
可以说,这些功能是理想的重用和提取对象。相比之下,“客户资料”是一个简单的 CRUD 功能,主要由序列化、存储处理和配置等样本代码组成,因此它适合被重写和弃用。
从我的经验来看,在大多数解耦方案中,交付团队最好是重写功能并弃用旧代码,主要是考虑到重用的高成本和低价值:
有大量的样板代码可用于处理环境依赖问题,比如在运行时访问应用程序配置、访问数据存储、缓存等。大部分样板代码需要被重写。运行微服务的新基础设施与几十年前的应用程序运行时有很大的不同,而且需要的样板代码也不一样。
现有功能很可能不是围绕清晰的领域概念而构建的,数据结构可能无法反映出新的领域模型,需要进行重大的重构。
经历了多次迭代的遗留代码可能具有较高的代码毒性和较低的重用价值。
除非功能与领域概念具保持一致,并且具有高度的知识产权,否则我强烈建议重写和弃用旧代码。
你可以使用代码分析工具(如 CheckStyle)来帮助做出重写还是重用的决定。
在传统单体中识别领域边界既是一门艺术又是一门科学。在使用领域驱动设计技术来寻找有边界的上下文时,首先要定义好微服务的边界。我经常看到人们在将单体解耦成非常小型的服务时发力过猛,这些小型服务的设计受到了现有数据规范化观点的影响。这种识别服务边界的方法将会导致出现大量针对 CRUD 资源的弱服务。
对于大部分微服务架构新手来说,这种方式创建了一个高度摩擦的环境,最终导致服务无法通过测试和发布。它创建了一个难以调试的跨越事务边界的分布式系统,这对于组织的运维成熟度来说太复杂了。
不过,还是存在一些关于微服务应该“微”到何种程度的探索:团队的规模、重写服务的时间、需要封装多少行为,等等。我的建议是,服务规模取决于交付和运维团队可以独立发布、监控和操作多少个服务。先从较大型的服务开始,并在团队运营准备就绪时将服务进一步分解为多个小型的服务。
例如,在解耦零售系统的过程中,开发者可以从“购买”服务开始,因为该服务封装了“购物袋”和“结账”相关的内容。随着团队发布的服务数量越来越多,他们可以将“购物袋”和“结账”解耦为单独的服务。
图 8 基于丰富的领域概念去解耦宏观服务,如果准备就绪,就将服务细分为更小的领域概念
提示:使用 Richardson 成熟度模型 L3 和超链接可以在不影响调用者的情况下实现服务分离。
试图通过解耦微服务让遗留单体像尘埃一样消失在空气中,这种想法更像是一种神话,是不可取的。任何经验丰富的工程师都参与过遗留系统的迁移,他们在一开始总是过度乐观,但最终都会在某个合适的时间点放弃。由于宏观条件的变化,这种尝试终会被放弃:资金耗尽、组织将重点转移到其他领域或领导层的离去。
所以,对于交付团队来说,从单体到微服务的演化是一个需要仔细规划的旅程。我将这种方法称为“基于原子步骤的架构演进”,迁移的每一步都应使架构更接近其目标。每个进化单位可能是一个小步骤或一个大的飞跃,但都是原子的,要么完成要么回退。
这个非常重要,因为我们正在采取迭代和渐进的方法来改进整体架构和解耦服务。就架构目标而言,每个增量都必须让我们更进一步。我们可以使用演化架构适应度函数作为衡量指标,经过每一个原子步骤的迁移之后,架构适应度函数对应的值应该更接近架构目标。
我举一个例子来说明这一点。微服务架构的目标是提高开发人员交付整个系统的速度,以便交付更大的价值。我们假设交付团队决定将用户身份验证解耦成基于 OAuth 2.0 协议的独立服务。这个服务旨在替代现有客户端的用户身份验证机制。我们将这种增量迁移称为“Auth 服务引入”。引入这个新服务可以这么做:
(1)构建 Auth 服务,实现 OAuth 2.0 协议。
(2)在单体后端添加一个新的认证方式,用以调用 Auth 服务对用户进行认证。
如果交付团队在这个时候转去开发其他的一些服务或功能,就会使整体架构处于熵增状态。这个时候存在两种用户认证方式,即 OAuth 2.0 方式和基于密码 / 会话的方式。在这一点上,可以说交付团队实际上更加远离了总体目标。新来的开发人员需要处理两种认证方式,增加了代码阅读负担,让变更和测试的流程变得更慢。
相反,交付团队可以在原子演化单元中加入以下步骤:
(3)将基于密码 / 会话的身份认证替换为 OAuth 2.0。
(4)从单体中移除旧的认证代码。
在这一点上,我们可以说,交付团队更加接近目标架构。
图 9 通过架构演进的原子步骤将整体架构演化为微服务,完成每个步骤之后,整体架构都朝向目标方向改进,即使中间变更可能会使其偏离目标,但最终会朝着最终目标前进
分解单体的原子单位包括:
解耦新服务
将所有消费者重定向到新服务
淘汰单体中的旧代码
反模式:解耦新服务用于新消费者,永不淘汰旧代码。
我经常发现有些交付团队在完成了一个功能的迁移之后就立即宣告胜利,而不淘汰旧代码,这就是上述的反模式。其主要原因是(a)太过关注引入新功能带来的短期收益,(b)相比开发新功能的优先级,淘汰旧代码需要一定的工作量,优先级就被降低了。
我们通过这种迁移方式分阶段地完成我们的旅程。我们可以停下来休息,复原,并最终走完整个旅程,杀死单体。
原文链接:https://martinfowler.com/articles/break-monolith-into-microservices.html
我大概在 2006 年开始参与架构设计,原以为学习架构就像学习编程语言一样,先了解基本的语法,再研究细节和原理,然后实践一下就能够快速掌握。 但真正深入后才发现,架构设计的难度和复杂度要高很多。
从最早开始接触架构设计,到自我感觉彻底掌握架构设计的精髓,我至少花费了 8 年的时间。 我曾经以为是自己天资愚笨才会这样,后来我带了团队,看到几乎每个程序员在尝试架构设计的时候,都面临着我曾经遇到过的各种困惑和瓶颈。
今天,我想把我过去所有的经验都分享在这里,希望能帮你快速成为一名架构师。
关注公众号:拾黑(shiheibook)了解更多
[广告]赞助链接:
四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/
随时掌握互联网精彩
- 1 共绘亚太下一个“黄金三十年” 7995049
- 2 山里藏价值6000亿元黄金?村民发声 7907858
- 3 微信或史诗级“瘦身” 内存有救了 7820075
- 4 中国主张成为G20峰会的一抹亮色 7720422
- 5 朝鲜将军队提升至战斗准备状态 7606375
- 6 男子抱3个小孩跳海?官方通报 7546960
- 7 苏醒模仿向佐 7419931
- 8 带96岁母亲酒店养老遭拉黑 男子发声 7333500
- 9 男生解锁“滑步下泰山”技能 7216569
- 10 千年古镇“因网而变、因数而兴” 7126810