单体架构如何拆分为微服务架构?迁移步骤与实践指南

Meta 描述: 本文围绕单体架构拆分、单体应用迁移到微服务架构、微服务拆分顺序、数据解耦和架构演进等问题,介绍从单体系统逐步迁移到微服务生态的实践方法。

随着单体系统规模不断膨胀,许多企业开始尝试将其拆分为微服务架构。这是一条值得探索的道路,但绝非易事。实践经验表明,推进单体架构拆分和微服务迁移时,最好先从一个简单服务入手,再逐步围绕关键业务垂直能力,以及那些需要频繁变更的功能构建服务。早期拆分出的服务不宜过小,并且应尽量减少对原有单体系统的依赖。更重要的是,迁移过程中的每一步,都应当是对整体架构的一次原子化改进。

将单体系统迁移到微服务生态,是一项艰巨的工程。踏上这段旅程的团队,通常怀抱着许多期望:扩大运营规模、加快变化速度、降低变更成本。他们希望在团队数量增加的同时,让不同团队能够并行、独立地交付价值;希望围绕核心业务能力快速试验,并更快将价值交付给用户;也希望摆脱修改既有单体系统时所伴随的高昂成本。

单体架构如何拆分为微服务架构?迁移步骤与实践指南

在将单体应用拆解为微服务生态的过程中,架构层面的核心挑战包括:何时解耦哪些功能,以及如何分阶段推进迁移。本文将分享一些实践方法,帮助交付团队,包括开发人员、架构师和技术经理,在拆分过程中做出更合理的判断。

为了说明这些方法,本文使用一个多层在线零售应用作为示例。该应用将用户界面层、业务逻辑层和数据层紧密耦合在一起。之所以选择这个案例,是因为它具备许多企业级单体应用的典型特征,同时其技术栈又足够现代化,仍然有条件进行渐进式拆分,而不必彻底重写或整体替换。

微服务架构的目标状态是什么

在开始单体架构拆分之前,团队中的每个人都需要对微服务生态形成共同理解。微服务生态是一个由多个服务组成的平台,其中每个服务都封装一种业务能力。业务能力代表企业在某个特定领域中,为实现自身目标和职责而执行的一组活动。

每个微服务都应对外暴露 API,开发者可以通过自助方式发现和使用这些 API。微服务拥有独立的生命周期,开发者可以独立地构建、测试和发布每个服务。微服务生态还会推动一种由长期自治团队组成的组织结构,每个团队负责一个或多个服务。

与人们对“微”这个词的直观理解不同,服务的大小并不是最关键的问题。服务规模可能因组织的运营成熟度而异。正如海外某位技术专家所说,微服务更多是一个标签,而不是对服务规模的严格描述。

单体架构如何拆分为微服务架构?迁移步骤与实践指南

图 1:服务封装了业务能力,并通过自助式 API 公开数据和功能。

单体应用迁移前需要明确的事项

在深入拆分方法之前,需要先认识到:将现有系统拆分为微服务,通常会带来较高的总体成本,并且往往需要多轮迭代。开发人员和架构师必须谨慎评估,拆分现有单体架构是否真的是正确方向,以及微服务本身是否应当成为最终目标。明确这一点之后,再进入具体的迁移实践。

在实际推进过程中,团队还需要把目标制定、客户反馈、需求评审、排期、开发、测试、发布和知识沉淀串联起来,否则微服务迁移很容易变成一组分散的技术任务。对于研发团队来说,可以借助 PingCode 这类智能化研发管理工具,将研发管理过程自动化、数据化和智能化,并通过需求、项目、测试、发布和 Wiki 等能力沉淀迁移过程中的经验。

从简单且相对独立的功能开始热身

迈向微服务架构,团队首先需要具备最低限度的运维就绪度。这包括:能够按需访问部署环境;能够构建新的持续交付流水线,用于独立构建、测试和部署可执行服务;并具备保护、调试和监控分布式架构的能力。

无论是构建全新的服务,还是从现有系统中拆分服务,足够的运维就绪度都是前提。关于这一点,海外技术社区中已有不少关于微服务前提条件的讨论。好消息是,支撑微服务架构运维的技术已经快速发展,包括服务网格、容器编排平台,以及持续交付系统的演进。服务网格提供了一个专门的基础设施层,用于运行快速、可靠且安全的微服务网络;容器编排平台提供了更高层次的部署基础设施抽象;持续交付系统也在不断演进,以支持容器化微服务的构建、测试和部署。

我的建议是,开发团队和运维团队应当先搭建底层基础设施、持续交付流水线和 API 管理系统,然后再基于这些能力拆分或构建新服务。第一批服务应当选择与单体架构相对解耦的功能。这些功能最好不需要修改大量当前依赖单体系统的面向客户端应用,也尽可能不需要独立的数据存储。

在这一阶段,交付团队最需要优化的不是业务边界本身,而是验证交付方法、提升团队技能,并构建交付独立服务所需的最低基础设施。目标是能够交付可独立部署、安全可靠,并通过自助式 API 对外提供能力的服务。

以在线零售应用为例,第一个服务可以是“终端用户身份认证”服务,由单体系统调用,用来验证终端用户身份。第二个服务可以是“客户档案”服务,它可以作为一个外观服务,为新的客户端应用提供更清晰的客户视图。

我建议一开始先解耦简单的边缘服务。之后,再采用不同策略,去解耦深嵌在单体系统内部的功能。之所以先处理边缘服务,是因为在初期阶段,交付团队面临的最大风险通常不是领域划分,而是无法正确运维微服务。用边缘服务来演练微服务所需的运维前提条件,是一种低风险且有效的方式。只有当这些问题得到解决后,团队才适合进入拆分单体系统核心能力的阶段。

单体架构如何拆分为微服务架构?迁移步骤与实践指南

图 2:通过一项变化范围较小的简单能力进行热身,以提升我们的作战准备水平

微服务拆分应尽量减少对单体系统的依赖

作为一项基本原则,交付团队应尽可能减少新创建的微服务对单体系统的依赖。微服务的重要优势之一,是能够快速且独立地发布。如果微服务依赖单体系统中的数据、逻辑或 API,那么微服务的发布周期就会与单体系统耦合在一起,从而削弱这一优势。

很多组织之所以希望摆脱单体架构,正是因为其核心功能变更成本高、变化速度慢。因此,迁移过程应当逐步朝着解耦这些核心功能的方向前进,也就是逐渐移除对单体系统的依赖。如果团队在构建服务能力时始终遵循这一原则,最终会发现依赖方向发生了反转:由单体系统依赖新服务,而不是新服务依赖单体系统。这是一种更理想的依赖方向,因为它不会拖慢新服务的变化速度。

以在线零售系统为例,“购买”和“促销”都是核心功能。“购买”功能在结账过程中会使用“促销”能力,根据顾客购买的商品,为其匹配最合适的优惠。如果我们需要在这两个功能之间选择下一个要拆分的对象,我建议先拆分“促销”,再拆分“购买”。按照这个顺序,“购买”功能仍保留在单体系统中,并依赖新的“促销”微服务,从而减少新服务对单体系统的依赖。

后续方法还会提供其他判断服务拆分顺序的依据。这也意味着,团队并不总能完全避免新服务对单体系统的依赖。如果新服务确实需要调用单体系统,我建议从单体系统中暴露一个新的 API,并在新服务中通过防腐层访问该 API,以避免单体系统内部的概念泄露到新服务中。即便单体系统的内部实现并不理想,团队也应努力让这个 API 体现清晰的领域概念和结构。

在这种情况下,交付团队需要承担额外成本:不仅要修改单体系统,还要测试和发布新服务,并可能需要同时发布单体系统。

单体架构如何拆分为微服务架构?迁移步骤与实践指南

图 3:首先将不需要依赖单体架构的服务解耦,并尽量减少对单体架构的更改。

尽早分离高黏性能力,降低系统耦合

假设此时交付团队已经能够熟练构建微服务,并准备开始解决更棘手的问题。然而,他们可能会发现,在不依赖单体系统的情况下,能够拆分出的功能非常有限。造成这种情况的根本原因,往往是单体系统中存在某些定义不清的能力。这些能力没有被清晰建模为领域概念,却被大量其他功能依赖,因而形成强耦合。

为了继续推进单体架构拆分,开发人员需要识别这些“高黏性”能力,将它们拆解为定义清晰的领域概念,再把这些领域概念具体化为独立服务。

例如,在基于 Web 的单体架构中,“Web 会话”就是最常见的耦合因素之一。在在线零售系统中,会话通常包含许多属性,例如跨领域的用户偏好,包括配送偏好和支付偏好;也可能包含用户意图和交互信息,例如最近访问的页面、点击过的产品和愿望清单。

如果不解决当前“会话”概念的解耦、解构和重构问题,后续许多功能都很难被顺利拆分,因为它们会通过这个有缺陷的会话概念继续与单体系统纠缠在一起。我也不建议在单体系统之外创建一个笼统的“会话服务”。这样做只会把单体系统内部已有的紧耦合复制到系统外部,而且情况可能更糟,因为这种耦合会发生在进程之外,并跨越网络边界。

开发人员可以从高黏性能力中逐步提取微服务,一次提取一个。例如,先重构“客户愿望清单”,并将其提取为一个新服务;随后再将“客户支付偏好”重构为另一个微服务,如此逐步推进。

单体架构如何拆分为微服务架构?迁移步骤与实践指南

图 4:识别耦合度最高的概念,并将其解耦、解构和具体化为具体的领域服务


使用Structure101
等依赖关系和结构代码分析工具, 识别单体架构中最主要的耦合和约束因素功能。

按垂直切片解耦,并尽早释放数据

拆分单体架构中各项能力的主要动因,是让这些能力能够独立发布。这个首要原则应当指导开发人员在解耦过程中做出的每一个决策。

单体系统通常由紧密集成的多个层组成,甚至可能包含多个必须同时发布的系统。这些系统之间存在脆弱的相互依赖关系。以在线零售系统为例,单体架构可能包含一个或多个面向客户的在线购物应用,也可能包含一个实现大量业务功能的后端系统,而后端系统又连接着一个集中式数据存储,用来保存系统状态。

大多数拆分尝试会从提取面向用户的组件和若干外观服务开始,为现代化用户界面提供更加开发者友好的 API,但数据仍然被锁定在单一模式和单一存储系统中。虽然这种方法可以带来一些短期收益,例如让用户界面更容易频繁变更,但对于核心功能而言,交付团队的推进速度仍然受限于最慢的部分,也就是单体系统及其单体数据存储。

简而言之,如果没有完成数据解耦,架构就还不能称为真正的微服务架构。将所有数据保存在同一个数据存储中,违背了微服务“去中心化数据管理”的核心特征。

更合适的策略是按垂直切片将能力迁移出来:将核心功能连同其数据一起解耦,并将所有前端应用重定向到新的 API。

多个应用同时读写集中共享数据,是实现数据与服务解耦的主要障碍。交付团队需要根据自身环境制定合适的迁移策略,尤其要考虑是否能够同时重定向和迁移所有数据读写方。海外某些公司提出的分阶段数据迁移策略,适用于许多需要逐步迁移数据库集成应用的场景,同时还能确保所有处于变更中的系统持续运行。

单体架构如何拆分为微服务架构?迁移步骤与实践指南

图 5:将功能与其数据解耦到微服务,该微服务公开一个新的接口,修改并重定向消费者到新的 API

避免只解耦外观、只解耦后端服务而不解耦数据的反模式。

优先解耦业务关键且变化频繁的能力

将功能从单体架构中解耦并不容易。海外某位架构师曾将其比作精细的器官手术。在在线零售应用中,提取一项能力,需要小心地分离它的数据、逻辑和面向用户的组件,并将它们重定向到新的服务中。

正因为这项工作成本很高,开发人员需要持续评估解耦的成本与收益,例如它能否提高开发速度,或能否提升扩展能力。举例来说,如果交付团队的目标是加速现有功能的变更,那么他们就必须找出单体系统中修改频率最高的能力,并优先将其迁出。

应优先解耦那些持续变化、备受开发人员关注,并且严重限制团队快速交付价值的代码区域。交付团队可以分析代码提交模式,找出历史上变化最频繁的部分,再将这些信息与产品路线图和产品组合进行对照,从而判断哪些能力最受重视、近期会获得更多关注。团队还需要与业务负责人和产品经理沟通,了解哪些能力才是真正重要的差异化能力。

例如,在在线零售系统中,“客户个性化”就是非常适合拆分的能力。为了给客户提供最佳体验,这项能力需要大量实验和快速迭代。它既对业务和客户体验至关重要,又需要频繁更新,因此非常适合作为微服务化的优先候选对象。

单体架构如何拆分为微服务架构?迁移步骤与实践指南

图 6:识别并分离最重要的能力:为企业和客户创造最大价值,同时定期变化。

使用
CodeScene 等
社交代码分析工具来查找最活跃的组件。如果构建系统每次提交都会修改或自动生成代码,请务必从噪声中过滤掉有效信息。将频繁更改的代码与产品路线图的后续变更进行对比,找到可以解耦的交集。

解耦业务能力,而不是简单搬迁代码

当开发人员想从现有系统中提取服务时,通常有两种选择:提取现有代码,或者重写相应能力。

很多时候,人们默认把服务提取或单体拆分理解为:直接复用现有实现,并将其搬迁到一个独立服务中。这部分源于我们对自己设计和编写的代码存在认知偏差。无论构建过程多么痛苦,结果多么不完美,只要是自己亲手构建出来的东西,我们往往都会更容易产生偏爱。这种现象可以称为“自我建构偏差”。

不幸的是,这种偏差会阻碍单体架构的拆分进程。它会让开发人员,尤其是技术经理,低估提取和复用旧代码的成本,同时高估其价值。

另一种选择是重写该能力,并逐步弃用旧代码。重写过程给团队提供了一个重新审视业务能力的机会:他们可以与业务部门对话,简化遗留流程,挑战系统中长期存在的旧假设和旧限制。同时,这也为技术更新提供了契机,让团队能够使用最适合该服务的编程语言和技术栈来实现新服务。

例如,在零售系统中,“定价和促销”通常是一段逻辑非常复杂的代码。它能够动态配置和应用定价、促销规则,并根据客户行为、忠诚度、产品组合等参数提供折扣和优惠。这类能力通常沉淀了大量业务知识,因此可能更适合复用和提取。

相比之下,“客户档案”往往只是一个简单的 CRUD 能力,主要由序列化、存储处理和配置等样板代码构成。因此,它更适合重写,并逐步弃用旧实现。

根据我的经验,在大多数单体应用迁移场景中,团队最好将能力重写为一个新服务,并弃用旧代码。这是因为复用旧代码往往成本高、价值低,原因包括:

  1. 现有代码中通常存在大量处理环境依赖的样板代码,例如运行时读取应用配置、访问数据存储、使用缓存等。这些代码往往基于旧框架构建,而托管微服务的新基础设施与沿用了多年的应用运行时环境完全不同,因此大部分样板代码都需要重写。
  2. 现有功能很可能并不是围绕清晰的领域概念构建的。这会导致传输或存储的数据结构无法反映新的领域模型,从而需要进行大规模重构。
  3. 长期存在的遗留代码经历了多轮迭代修改,可能已经积累了较高的复杂度和技术债,复用价值并不高。

除非某项功能本身具有高度相关性,符合清晰的领域概念,并沉淀了较高的知识产权价值,否则我通常强烈建议重写新服务,并逐步弃用旧代码。

单体架构如何拆分为微服务架构?迁移步骤与实践指南

图 7:重用并提取高价值、低毒性的代码,重写并弃用低价值、高毒性的代码

使用CheckStyle等代码毒性分析工具来决定是重写还是重用。

微服务拆分粒度:先宏观,后微观

在传统单体架构中寻找领域边界,既是一门艺术,也是一门科学。通常而言,使用领域驱动设计,也就是 DDD,来识别定义微服务边界的限界上下文,是一个不错的起点。

但我也经常看到另一种过度修正:团队从一个大型单体架构,直接转向大量极小服务,而这些小服务的设计灵感和驱动力主要来自现有的规范化数据视图。这样识别服务边界,几乎总会导致大量贫血的 CRUD 服务像寒武纪生命大爆发一样涌现。

对于许多刚接触微服务架构的团队来说,这会制造一个高摩擦的环境,最终导致服务无法真正独立发布和执行。它还会带来一个难以调试的分布式系统:这个系统在事务边界处容易失败,难以保持一致性,并且复杂度超出了组织当前的运营成熟度。

关于微服务到底应该“微”到什么程度,业界确实有一些指导原则,例如团队规模、重写服务所需时间、服务需要封装的行为量等。但我的建议是:服务规模应取决于交付团队和运维团队能够独立发布、监控和运维多少服务。

可以先围绕一个逻辑领域概念构建较大的服务,等团队的运维能力成熟后,再将其拆分为多个更小的子服务。

例如,在零售系统的拆分过程中,开发人员可以先开发一个名为“购买”的服务。这个服务既包含“购物袋”的内容,也包含完成购买的“结账”能力。随着团队逐渐具备组建更小团队和发布更多服务的能力,他们再将“购物袋”和“结账”拆分为独立服务。

单体架构如何拆分为微服务架构?迁移步骤与实践指南

图 8:围绕丰富的领域概念解耦宏服务,并在准备就绪后,将服务分解为更小的领域概念。

使用Richardson 成熟度模型 L3 和超链接,可以在不影响呼叫者的情况下实现服务的将来解耦,即呼叫者可以自行发现如何结账,而无需事先了解。

以原子化演进步骤完成微服务迁移

认为只要把传统单体架构拆成设计精美的微服务,它就会彻底消失,这在某种程度上是一种迷思,也未必值得追求。任何经验丰富的工程师都能讲出一些遗留架构迁移和现代化改造的故事:项目启动时规划乐观,大家以为一定能彻底完成,但最后往往只是在某个合适的时机停了下来。

这类长期计划之所以会被放弃,通常是因为宏观环境发生了变化:项目资金耗尽、组织重心转移,或支持项目的领导层离职。因此,团队在设计从单体架构迁移到微服务架构的路径时,必须考虑这种现实。

我把这种方法称为“以原子化步骤推进架构演进”。其中,迁移的每一步都应让架构更接近目标状态。每个演进单元可以很小,也可以较大,但必须是原子性的:要么完成,要么回滚。

这一点尤其重要,因为我们通常采用迭代和增量的方式来改进整体架构、解耦服务。每一次增量都必须让我们在架构目标上取得进展。借用演进式架构中“适应度函数”的比喻,每一次原子化迁移之后,架构适应度函数都应产生一个更接近目标架构的值。

让我们用一个例子说明。假设微服务架构的目标,是提高开发人员修改整个系统并交付价值的速度。团队决定将终端用户身份认证拆分为一个基于 OAuth 2.0 协议的独立服务。该服务既用于替换现有旧客户端应用的用户认证方式,也用于新的微服务架构中的终端用户认证。我们将这个演进阶段称为“引入身份认证服务”。

引入新服务的一种方法,是先完成以下步骤:

  1. 构建身份认证服务,并实现 OAuth 2.0 协议。
  2. 在单体后端中增加一条新的认证路径,调用身份认证服务来验证代表其处理请求的终端用户身份。

如果团队到这里就停下来,转而开发其他服务或功能,那么整体架构的熵值就会增加。在这种状态下,系统中同时存在两种用户认证方式:一种是新的、基于 OAuth 2.0 的路径;另一种是旧客户端基于密码和会话的路径。

此时,团队实际上离“加快变更速度”这个总体目标更远了。任何新加入单体架构的开发人员都必须处理两条代码路径,理解代码的认知负担会增加,变更和测试过程也会变得更慢。

相反,团队应当把以下步骤纳入同一个演进单元中:

  1. 将旧客户端基于密码和会话的认证方式替换为 OAuth 2.0 路径。
  2. 从单体架构中移除旧的认证代码路径。

完成这些步骤后,团队才可以认为自己更接近目标架构。

单体架构如何拆分为微服务架构?迁移步骤与实践指南

图 9:通过原子级的架构演化步骤,将架构逐步演化为微服务架构。每一步演化后,即使中间的代码变更可能会使其偏离目标,整体架构也会朝着目标方向改进。

整体拆分的原子化单元通常包括:

  1. 拆分出新服务。
  2. 将所有消费者重定向到新服务。
  3. 弃用单体架构中的旧代码路径。

需要避免的反模式是:只为新用户或新场景拆出新服务,却永远不淘汰旧实现。

我经常看到一些团队在将某项能力从单体架构中迁出后,只要新功能构建完成,就宣布大功告成,却没有弃用旧代码路径。这正是上面提到的反模式。造成这种情况的主要原因通常有两个:第一,团队过于关注引入新功能带来的短期收益;第二,弃用旧实现需要投入大量精力,而且还要与构建新功能的其他优先级竞争资源。

为了确保迁移方向正确,我们需要尽可能将每个步骤拆到足够小,并确保每一步都能让架构向目标状态前进。对于涉及多个团队、多个阶段和多类事项的迁移任务,也可以借助 Worktile 这类通用项目协作系统,统一管理任务、项目、文档、日程、甘特图和审批流程,降低协作成本。

采用这种方式迁移,就可以将一段漫长旅程拆分为一段段较短的行程。团队可以在合适的节点安全停下,恢复体力,然后继续前进。最终,他们才能真正完成这段漫长旅程,搬开那块挡在前方的巨石。

文章包含AI辅助创作,作者:liu,如若转载,请注明出处:https://docs.pingcode.com/baike/5243361

(0)
liuliu
免费注册
电话联系

4008001024

微信咨询
微信咨询
返回顶部