单体系统如何拆分为微服务
当单体系统越来越大,并难于维护时,很多企业开始有意把单体系统拆分为微服务风格架构。这么做很有意义,但不容易。要做好这件事情我们必须学习,我们从一个简单的服务开始,另一方面拉出以垂直功能为基础的服务,这些功能对业务来说很重要并且经常变更。这些服务首先要很大并且最好不要依赖剩余的单体系统。我们应该确保每一步迁移对于整体架构而已是一个原子改进。
迁移巨型单体系统到微服务生态系统是一个史诗性任务。从事这项任务的人拥有增加经营规模、促进变化、避免变更带来的高开销的意愿。他们想要增加他们的团队规模同时让团队以并行的方式传输价值并彼此独立。他们想要快速实验他们的业务核心功能并且更快的传递价值。他们同时要避免关于变更现存单体系统高昂的开销。
决定解耦何种功能、何时、如何逐步迁移以分解单体系统到微服务生态是架构的挑战。在这篇文章里,我分享一些技术以引导交付团队(开发者、架构师、技术经理)使用这些技术在过程中做出拆分决定。
微服务生态系统目标
在开始之前,大家对微服务生态系统达成共识是关键。微服务生态系统是一个服务平台,每一个服务封装一个业务功能。一个业务功能代表业务在特殊领域可以做什么(实现目标和责任)。每个微服务暴露一个 API,以便开发者发现并用于自托管模式。微服务拥有独立的生命周期。开发者可以独立开发、构建、测试和发布。微服务生态系统实施长期自治的团队组织架构,每个团队负责一个或多个服务。与大多数看法相反,微服务中的“微”和服务大小几乎没有关系,而依赖于运营成熟的组织架构而改变(运营成熟的组织架构决定微服务)。
过程介绍
在深入介绍之前,了解分解现有系统到微服务会产生很高的总体成本(并且可能产生很多迭代)是很重要的。对于开发者和架构师来说密切的评估是否分解现有系统是正确的道路、微服务是否是正确的目的地。
通过一个简单的解耦功能热身
开始微服务需要一个最低层次的程序准备。它需要访问部署环境,构建新的类型的 CD 管道以独立构建、测试、部署执行服务,以及安全性能力,调试和监控一个分布式架构。准备程序就绪成熟化是必须的,无论我们是否构建绿地服务或者分解已有系统。
我的建议是开发和运维团队构建底层基础设施、持续交付管道和 API 管理系统,并分解或构建第一个和第二个服务。从分解一个单体系统的功能开始,这个功能相对目前的单体系统来说不需要变更很多客户端接口应用程序,也不需要数据存储。交付团队的优化点是验证他们的交付渠道,升级团队的技能,构建最小基础设施以交付部署安全服务以暴露自托管服务。做为示例,一个在线零售应用程序,第一个服务是“终端用户认证”服务,单体服务请求以认证终端用户,第二个服务是“客户档案”服务,一个外观服务为客户提供更好的客户视图。
首先我推荐解耦简单的边缘服务。下一步我们采用不同的方式解耦深度嵌入的单体系统。我建议先做边缘服务是因为在开始之初,交付团队最大的风险是合理的运维微服务。所以使用边缘服务体验运维很有好处。一旦他们定位到问题,他们就可以定位到分离单体服务的关键问题。
最小化对单体的依赖
交付团队的基本原则是最小化新的微服务对单体系统的依赖。微服务的主要好处是快速独立的发布闭体系。依赖单体系统的数据、逻辑、API,耦合服务到单体系统的发布体系,禁止使用单体系统的好处。通常从单体系统架构跑路的主要动机是由于高昂的代价和封装在单体系统中功能的缓慢变化,所以我们要缓缓的朝单体系统解耦核心功能的方向移动。如果团队按照这篇文章的指导来为他们的微服务增加功能,那么他们会发现从单体系统到服务的替换、依赖的反转。这是理想的依赖方向,因为它不会放慢变更服务的步伐。
考虑一个在线零售系统,“购买”和“促销活动”是核心功能。在结账过程中,“购买”使用“促销活动”给顾客提供最好的促销活动。如果要决定下一步解耦这两个功能里的哪一个,我建议先开始解耦“促销活动”然后才是“购买”。因为在这个顺序下我们减少里对单体系统的依赖。在这个顺序下,“购买”继续锁在单体系统中,依赖外部的新的“促销活动”微服务。
下一步本文将使用其它方式决定解耦服务。这意味着这些服务不是总是能避开对单体系统的依赖。如果一个新的服务最终回调到单体服务,我建议从单体系统中暴露一个新的 API,新的服务通过反腐层访问 API 以确保单体系统的概念不泄漏。力争定义的 API 对于领域概念和结构有良好的反映,即便单体系统的内部实现不是那样的。在这个不幸的案例中,交付团队将承担改变单体系统的开销和困难,即测试、发布新的服务和单体系统发布耦合。
尽早分离黏性功能
假设交付团队已经开始构建微服务并且准备进攻黏性问题。然而他们可能会发现他们能力有限,使下一个解耦的功能不依赖于单体系统。根本原因是通常是单体系统功能泄漏,定义的领域概念不好,有很多单体系统功能依赖于它。为了能处理这个问题,开发者需要辨别黏性功能,把它解构为定义良好的领域模型然后把这些领域概念实现到隔离的服务中。
例如 Web 单体系统,“session” 是最为常见的耦合因素之一。在在线零售示例中,session 通常是很多特性的封装,从用户的偏好(不同的领域边界,比如:配送和支付偏好)到用户的意图和交互(比如:最近访问的页面、点击的产品和购买清单)。若非我们处理解耦、解构和具体化当前 session 的概念,我们将陷入解耦功能(这些功能通过泄漏的 session 概念缠住单体系统)的竞争中。同时我也不鼓励在单体系统外创建 session 服务,因为它会导致和单体系统进程中类似的紧耦合,更糟糕的是,在进程外和跨网络。
开发者可以逐步从黏性功能中抽取微服务,每次一个服务。例如,先重构“顾客愿望清单”并抽取到一个新的服务中,然后重构“顾客支付偏好”到另一个服务中。
垂直解耦,尽早释放数据
从单体系统中解耦功能的主要驱动是可以独立发布它们。这是开发者在解耦过程中做每一个决定的首要原则。一个单体系统通常由紧密集成层,甚至几个系统组成(需要发布在一起并且有脆弱的相互依赖关系)。例如,在一个在线零售系统中,单体系统由一个或几个面向顾客的在线购物应用程序组成,一个后端系统实现很多业务功能(包含一个集中的数据存储)。
大多数解耦尝试从抽取面向用户组件、几个外观服务为 UI 提供友好的开发 API开始,同时数据仍然锁在同一个 schema 中。虽然这种方式在一些方面立竿见影,比如更加频繁的变更 UI,当涉及到核心功能时,交付团队只能按照最慢的部分步伐,单体系统和它的巨大数据存储。简单的说,不解耦数据,架构就不是微服务。所有数据在同一个存储中与微服务去中心化数据管理的特征背道而驰。
策略是垂直移除功能,解耦核心功能和它的数据,并重定向所有前端应用程序到新的 API。
有多个应用程序从中心共享数据读写是服务解耦数据的主要障碍。交付团队需要纳入一个数据迁移策略,这个策略适配他们的环境依赖,无论他们是否同时重定向和迁移所有数据读写者。四段数据迁移策略是其中一种适应很多环境(需要逐步迁移集成数据库的应用程序,同时所有系统在变更下需要继续运行)的策略。
解耦对业务重要和频繁变更的部分
从单体系统中解耦功能不容易。在在线零售应用程序中,抽取一个功能需要仔细抽取功能的数据、逻辑、面向用户的组件然后重定向它们到新的服务。因为这是一堆重要的工作,开发者需要持续评估解耦得到的好处,比如:跑的更快或者增加规模。例如,如果交付团队的目标是加速修改已经锁在单体系统中的功能,那么他们必须确定修改最多的功能。解耦代码中持续经受修改的部分(这部分代码持续得到开发者的关注,并最大限度限制了开发者快速交付成功)。交付团队可以分析代码提交模型找出历史上变化最大的内容,并将其与产品路线图和产品组合进行叠加,以了解在不久的将来会受到关注的最需要的功能。他们需要和业务、产品经理沟通以了解对他们来说重要的功能差异。
例如在一个在线零售系统中,“顾客个性化”是一个功能,该功能要进行大量的实验以为顾客提供最好的体验,并且也是一个好的解耦候选项。它是一个对业务很重要的功能,用户体验,并且频繁被修改。
解耦功能,不是代码
无论何时,开发者们要从一个现存系统中抽出一个服务,他们有两种方式:抽取代码或者重写功能。
通常情况下,服务抽取或者单体系统解构默认假设为重用已有的实现,原样抽取到一个分离的服务中。部分原因是我们对我们设计、编写的代码有一个认知偏见。建筑(没错,这里就是建筑,这里借助 IKEA Effect 理论)过程让我们对它产生热爱,无论这个过程多么痛苦,结果多么不完美。不幸的是这种偏见将阻碍单体系统解构的努力。它引发开发者们和更多的重要技术管理者不理会高开销和低价值的抽取和重用代码。
交付团队可以选择重写功能然后让老代码淘汰。重写给了他们机会重新访问业务功能,和业务开始一个新的谈话,简化遗留的过程和挑战随着时间推移建在系统中老的假设和限制。它同样提供了一个刷新技术的机会,使用最合适的一门编程语言和技术栈实现一个新的服务。
例如在零售系统中,“定价和促销活动”功能是一段逻辑复杂的代码。它启用动态配置和应用程序定价、促销活动规则,提供折扣(在各种参数的基础上,比如:客户行为、忠诚度、产品包等)。
这个功能可以说是一个很好的重用和抽取的候选项。相反,“顾客文档”是一个简单的 CRUD 功能,通常由样板代码组成(序列化、处理存储和配置),因此,它是重写和淘汰代码的候选项。
在我看来,在大多数解构场景中,团队最好重写功能到一个新的服务中,并且淘汰老的代码。这里考虑高开销和低价值的重用,因为以下几个原因:
- 有大量的模版代码要处理环境依赖,比如在运行时访问应用程序配置、访问数据存储、缓存并且构建于老的框架。大多数模版代码需要重写。新的基础设施要托管一个微服务和几十年应用程序运行时有很大的不同,并且需要不同种类的模版代码。
- 很有可能存在的功能不是构建于清晰的领域概念。导致传输或者存储数据结构不能反映新的领域模型和需要忍受一个大的重组。
- 一个长时间存在的遗留代码经历过很多迭代,导致很高的代码毒性级别和重用价值低。
除非能力是相关的,与清晰的领域概念保持一致并且具有很高的知识产权,否则我强烈建议重写和淘汰旧代码。
先微服务,然后再划分的更小
在遗留单体系统中寻找领域边界既是艺术也是科学。通常应用领域驱动设计技术查找边界上下文定义微服务边界是一个好的开始。我承认,我经常看到从巨大的单体系统到真正的小服务的过度修正,真正的小服务的设计是由于受到存在规范化的数据视图的鼓励和驱动。这种方式确认服务边界导致寒武纪爆发大量的贫血服务(CRUD 资源)。对于微服务架构新手来说,这会创建一个高摩擦环境,最终无法通过独立发布和执行服务的测试。它创建了一个难于调试的分布式系统,一个分布式系统打碎了事务边界,因此难以保持一致性,对于组织的运营成熟度而言过于复杂的系统。虽然有一些如何“微”微服务的启发式:团队大小、重写服务的时间、要封装多少行为等等。我的建议是大小依赖于有多少服务交付,多少服务运维团队可以独立发布、监控和操作。从围绕逻辑领域概念的大型服务开始,并在团队准备就绪时将服务分解为多个服务。
例如,在解耦零售系统的过程中,开发者可能开始于服务“购买”,这个服务封装了“购物袋”,功能是购物和购物袋(也就是买单)。随着他们组建更小团队和发布更多服务的能力的增长,他们可以将购物袋与结帐分离成单独的服务。
以原子进化步骤迁移
通过将一个遗留的单体系统解耦成设计精美的微服务而让它消失的想法在某种程度上是一个神话,可以说是不可取的。任何经验丰富的工程师都可以分享遗留迁移和现代化尝试的故事,这些尝试是在对完全完成过于乐观的情况下计划和启动的,但充其量在足够好的时间点被放弃。由于宏观条件发生变化,此类努力的长期计划被放弃:该计划的资金用完,组织将重点转向其他事物或支持它的领导层离开。所以这个现实应该被设计成团队如何处理单体应用到微服务之旅。我称这种方法为“架构演化的原子步骤中的迁移”,其中迁移的每一步都应该使架构更接近其目标状态。每个进化单元可能是一小步或一大步,但都是原子的,要么完成,要么恢复。这一点特别重要,因为我们正在采用迭代和增量方法来改进整体架构和解耦服务。每个增量都必须让我们在架构目标方面处于更好的位置。使用进化架构适应度函数比喻,迁移的每个原子步骤之后的架构适应度函数应该产生更接近架构目标的价值。
让我通过一个例子图解这个观点。假设微服务架构目的是增加开发者修改整个系统的速度交付价值。团队决定解耦用户认证到一个隔离的服务中,以 OAuth 2.0 协议为基础。这个服务想要同时替换已有客户端应用程序认证终端用户,而且新的架构微服务验证终端用户。让我们将这个进化过程中的增量称为“Auth 服务介绍”。一个方法介绍新的服务是先通过以下步骤:
(1)构建 Auth 服务,实现 OAuth 2.0 协议。
(2)在单体系统中添加一个新的认证路径并调用 Auth 服务认证终端用户。
如果团队在这里停下来并转向构建一些其他服务或功能,他们会使整体架构处于熵增加的状态。在这种状态下,有两种验证用户的方法,新的 OAuth 2.0 基本路径和旧客户端的基于密码/会话的路径。在这一点上,团队实际上离他们更快地做出改变的总体目标更远了。单体代码的任何新开发人员都需要处理两条代码路径,增加理解代码的认知负担,以及更慢的更改和测试过程。
团队可以包含以下步骤到我们的原子进化单元中:
(1)通过 OAuth 2.0 替换老客户端的密码/session
(2)从单体系统中淘汰老的认证代码
这时候我们可以说团队已经接近目标架构了。
单体系统原子单元解构包括:
- 解耦新服务
- 重定向消费者到新的服务
- 从单体系统中淘汰来的代码
反模式:解耦新服务用于新的消费者并且从不淘汰老的服务。
我经常发现团队终止从单体系统中迁移功能,新的功能开发出来以后马上就宣布胜利了,也不淘汰老的代码路径,正如上面描述的反模式。主要原因是:(a)聚焦于引入新功能的短期利益(b)淘汰老的代码会和构建新功能形成竞争优先级。为了做正确的事,我们应该尽可能的做原子步骤。