将单体式应用重构为微服务

本参考指南是由四个部分组成的系列教程中的第二篇,该系列教程介绍了如何设计、构建和部署微服务。本系列文章介绍了微服务架构的各种元素。该系列介绍了微服务架构模式的优缺点及其应用方式。

  1. 微服务简介
  2. 将单体式应用重构为微服务(本文档)
  3. 微服务设置中的服务间通信
  4. 微服务应用中的分布式跟踪

本系列文章面向设计和实施迁移以将单体式应用重构为微服务应用的应用开发者和架构师。

将单体式应用转换为微服务的过程是应用现代化改造的一种形式。为了完成应用现代化改造,我们建议您不要同时重构所有代码。相反,我们建议您逐步重构单体式应用。逐步重构应用时,您需要逐步构建由微服务组成的新应用,并运行该应用与单体式应用。此方法也称为 Strangler Fig 模式。随着时间的推移,单体式应用实现的功能量会逐渐减少,直到它完全消失或成为另一个微服务。

如需分离功能与单体式应用,您必须仔细提取功能的数据、逻辑和面向用户的组件,并将其重定向到新服务。在进入解决方案空间之前,您必须充分了解问题空间。

了解问题空间后,您就可以了解提供适当隔离级别的网域的自然边界。我们建议您充分了解网域,然后再创建较大的服务,而不是创建较小的服务。

定义服务边界是一个迭代过程。由于此过程具有非常大的工作量,因此您需要持续评估分离所带来的好处和费用。以下因素可帮助您评估如何分离单体式应用:

  • 避免一次重构所有内容。如需确定服务分离的优先级,请评估费用与优势。
  • 微服务架构中的服务是围绕业务问题组织的,而不是技术问题。
  • 在逐步迁移服务时,请配置服务和单体式应用之间的通信以经过明确定义的 API 合约。
  • 微服务需要更多的自动化功能:因此请提前考虑持续集成 (CI)持续部署 (CD)、集中日志记录和监控。

以下部分讨论了分离服务并逐步迁移单体式应用的各种策略。

按网域驱动的设计分离

微服务应围绕业务功能(而非数据访问或消息功能等水平层)而设计。微服务还应具有松散耦合和高功能凝聚度。如果您可以更改一项服务,而无需更改其他服务,则微服务是松散耦合的。如果微服务具有单一且明确定义的用途(例如管理用户账号或处理付款),则微服务具有凝聚力。

网域驱动型设计 (DDD) 需要充分了解为其编写应用的网域。创建应用所需的网域知识由网域人员掌握,即网域专家。

您可以将 DDD 方法追溯应用于现有应用,如下所示:

  1. 识别无所不在的语言,即在所有利益相关方之间共享的通用词汇。作为开发者,请务必在代码中使用非技术人员可以理解的术语。您的代码尝试实现的目标应反映您的公司流程。
  2. 确定单体式应用中的相关模块,然后将通用词汇表应用于这些模块。
  3. 定义有界限上下文,在此上下文中,您将显式边界应用于具有明确定义的责任的已识别模块。您确定的有界限上下文将被重构为更小的微服务的候选定位设置。

下图展示了如何将有界限上下文应用于现有电子商务应用:

有界限上下文会应用于应用。

图 1. 应用功能分为迁移到服务的有界限上下文。

在图 1 中,电子商务应用的功能分为有界限上下文并迁移到服务,如下所示:

  • 订单管理和履单功能分为以下几类:
    • 订单管理功能将迁移到订单服务。
    • 物流交付管理功能迁移到了交付服务。
    • 产品目录功能将迁移到产品目录服务。
  • 会计功能被绑定到某一单一类别:
    • 消费者、卖家和第三方功能绑定在一起并迁移到账号服务。

迁移的服务优先级

解耦服务的理想起点是确定单体式应用中的松散耦合模块。您可以选择松散耦合模块作为第一批要转换为微服务的模块。如需完成每个模块的依赖项分析,请注意以下事项:

  • 依赖项的类型 - 来自数据或其他模块的依赖项。
  • 依赖项的规模:已识别模块中的更改如何影响其他模块。

迁移包含大量数据依赖项的模块通常是一项复杂的任务。如果您先迁移特征,稍后再迁移相关数据,可能会临时从多个数据库中读取数据以及向其写入数据。因此,您必须考虑数据完整性和同步挑战。

我们建议您提取与单体式应用的其余部分相比,资源要求不同的模块。例如,如果模块具有内存数据库,您可以将其转换为服务,然后可以将该服务部署在具有更高内存的主机上。将具有特定资源要求的模块转换为服务时,可以使应用更容易扩缩。

从运营的角度来看,将模块重构为自己的服务也意味着在调整现有的团队结构。明确问责制的最佳方法是为拥有整个服务的小型团队提供支持。

可影响迁移服务优先级的其他因素包括业务重要性、全面的测试覆盖范围、应用的安全状况和组织购买。根据您的评估,您可以按照本系列第一个文档中的说明对服务进行排名,即从重构中受益

从单体式应用中提取服务

确定理想的候选服务后,您必须确定微服务和单体式应用模块共存的方法。管理这种共存的一种方法是引入进程间通信 (IPC) 适配器,适配器可以帮助模块协同工作。随着时间的推移,微服务承担了负载并消除了单体式应用组件。这一增量过程可以降低从单体式应用迁移到新的微服务的风险,因为您可以逐步检测错误或性能问题。

下图显示了如何实现 IPC 方法:

实现了 IPC 方法以帮助模块协同工作。

图 2:IPC 适配器可协调单体式应用与微服务模块之间的通信。

在图 2 中,模块 Z 是您希望从单体式应用中提取的服务候选对象。模块 X 和 Y 依赖于模块 Z。微服务模块 X 和 Y 使用单体式应用中的 IPC 适配器,通过 REST API 与模块 Z 进行通信。

本系列的下一个文档是微服务设置中的服务间通信,介绍了 Strangler Fig 模式以及如何解构单体式应用中的服务。

管理单体式数据库

通常,单体式应用具有自己的单体式数据库。微服务架构的一个原则是每个微服务都有一个数据库。因此,在将单体式应用现代化改造为微服务时,您必须根据所识别的服务边界拆分单体式数据库。

要确定拆分单体式数据库的位置,请首先分析数据库映射。在服务提取分析过程中,您收集了需要创建的微服务的一些数据洞见。您可以使用相同的方法分析数据库使用情况,并将表或其他数据库对象映射到新的微服务。Schema DatapreperSchemaSpyERBuilder 等工具可帮助您执行此类分析。映射表和其他对象可以帮助您了解跨越潜在微服务边界的数据库对象之间的耦合。

但是,拆分单体式数据库非常复杂,因为数据库对象之间可能没有明确分离。您还需要考虑其他问题,例如数据同步、事务完整性、联接和延迟时间。下一部分将介绍拆分单体式数据库时可帮助您响应这些问题的模式。

参考表

在单体式应用中,模块通常通过 SQL 联接到其他模块的表访问不同模块所需的数据。下图使用上一个电子商务应用示例来展示此 SQL 联接访问过程:

一个模块使用 SQL 联接从另一个模块访问数据。

图 3:模块将数据联接到其他模块的表。

在图 3 中,为了获取产品信息,订单模块使用 product_id 外键将订单联接到商品表。

但是,如果您将模块解构为单独的服务,我们建议您不要让订单服务直接调用产品服务的数据库来运行联接操作。以下各部分介绍了您可以考虑隔离数据库对象的选项。

通过 API 共享数据

将核心功能或模块分离为微服务时,通常使用 API 共享和公开数据。引用的服务以调用服务所需的 API 的形式公开了数据,如下图所示:

数据通过 API 公开。

图 4.服务使用 API 调用从其他服务获取数据。

在图 4 中,订单模块使用 API 调用从产品模块获取数据。由于存在额外的网络和数据库调用,此实现存在明显的性能问题。但是,当数据大小受限时,通过 API 共享数据的效果很好。此外,如果被调用服务返回具有已知变化率的数据,您可以在调用者上实现本地 TTL 缓存,以减少对被调用服务的网络请求。

复制数据

在两个单独的微服务之间共享数据的另一种方法是复制依赖服务数据库中的数据。数据复制是只读的,可以随时重新构建。这种模式使服务更加统一。下图展示了数据复制在两个微服务之间的工作原理:

数据在微服务之间复制。

图 5.在依赖的服务数据库中,服务中的数据会被复制。

在图 5 中,产品服务数据库被复制到订单服务数据库。此实现允许订单服务获取商品数据,而无需重复调用商品服务。

如需构建数据复制,您可以使用具体化视图、更改数据捕获 (CDC) 和事件通知等技术。复制的数据最终是一致的,但复制数据可能会有延迟,因此存在传送过时数据的风险。

将静态数据作为配置

静态数据(例如国家/地区代码和支持的货币)的更改速度很慢。您可以将此类静态数据作为微服务中的配置进行注入。现代微服务和云框架提供了利用配置服务器、键值对存储区和保险柜管理此类配置数据的功能。 您可以通过声明方式添加这些功能。

共享的可变数据

单体式应用具有一种称为共享可变状态的通用模式。在共享可变状态配置中,多个模块会使用单个表,如下图所示:

共享可变状态配置使单个表可用于多个模块。

图 6. 多个模块使用一个表。

在图 6 中,电子商务应用的订单、付款和配送功能使用相同的 ShoppingStatus 表来维护客户在整个购物历程中的订单状态。

如需迁移共享的可变状态单体式应用,您可以开发一个单独的 ShoppingStatus 微服务来管理 ShoppingStatus 数据库表。此微服务公开了用于管理客户的购物状态的 API,如下图所示:

API 向其他服务公开。

图 7.微服务将 API 公开给多个其他服务。

在图 7 中,付款微服务、订单微服务和配送微服务使用的是 ShoppingStatus 微服务 API。如果数据库表与其中一项服务密切相关,我们建议您将数据迁移到该服务。然后,您可以通过 API 公开数据以供其他服务使用。此实现可帮助您确保不会有太多相互频繁调用的精细服务。如果您错误地拆分服务,则需要重新访问定义服务边界。

分布式事务

将服务与单体式应用隔离后,原始单体式系统中的本地事务可能会分布在多项服务之间。跨多个服务的事务被视为分布式事务。在单体式应用中,数据库系统会确保事务是原子性的。如需在基于微服务的系统中处理各种服务之间的事务,您需要创建全局事务协调器。事务协调器会处理回滚、补偿性操作以及本系列的下一个文档(微服务设置中的服务间通信)中所述的其他事务。

数据一致性

分布式事务带来了保持跨服务的数据一致性的挑战。所有更新都必须以原子方式完成。在单体式应用中,事务属性可保证查询根据其隔离级别返回一致的数据库视图。

相比之下,考虑基于微服务的架构中的多步骤事务。如有任何一项服务事务失败,必须通过回滚在其他服务中成功完成的步骤来协调数据。否则,应用数据的全局视图在服务之间会不一致。

确定实现最终一致性的步骤何时失败,可能会很困难。例如,某个步骤可能不会立即失败,但可能会阻止或超时。因此,您可能需要实现某种超时机制。如果复制的数据在调用的服务访问数据时已过时,则服务之间缓存或复制数据可以减少网络延迟,这样也可能会导致数据不一致。

本系列的下一个文档是微服务设置中的服务间通信,提供了一个处理微服务间分布式事务的模式示例。

设计服务间通信

在单体式应用中,组件(或应用模块)通过函数调用直接相互调用。相比之下,基于微服务的应用包含通过网络相互交互的多个服务。

在设计服务间通信时,请先考虑服务之间期望的交互方式。服务交互可以是以下项之一:

  • 一对一交互:每个客户端请求仅由一项服务处理。
  • 一对多交互:每个请求由多项服务处理。

另外,请考虑互动是同步还是异步:

  • 同步:客户端希望服务及时响应,可能会在等待期间阻塞。
  • 异步:客户端在等待响应时不会阻塞。响应(如果有)不一定立即发送。

下表显示了互动样式的组合:

一对一 一对多
同步 请求和响应:向服务发送请求并等待响应。
异步 通知:向服务发送请求,但未期望或发送任何答复。 发布和订阅:客户端发布须知消息,且零或多个感兴趣的服务会使用该消息。
请求和异步响应:向服务发送请求,该服务会异步回复。客户端不会屏蔽。 发布和异步响应:客户端发布请求,并等待来自相关服务的响应。

每种服务通常使用这些互动样式的组合。

实现服务间通信

如需实现服务间通信,您可以从不同的 IPC 技术中进行选择。例如,服务可以使用基于同步请求的通信机制,例如基于 HTTP 的 REST、gRPC 或 Thrift。或者,服务可以使用基于消息的异步通信机制,例如 AMQP 或 STOMP。此外,您还可以从各种不同的消息格式中进行选择。例如,服务可以使用人类可读、基于文本的格式,例如 JSON 或 XML。此外,服务可以使用二进制格式,例如 Avro 或协议缓冲区。

将服务配置为直接调用其他服务会导致服务之间存在高耦合性。相反,我们建议使用消息功能或基于事件的通信:

  • 消息功能:实现消息功能时,您不再需要直接相互调用服务。相反,所有服务都知道消息代理,并且会将消息推送到该代理。消息代理将这些消息保存在消息队列中。其他服务可以订阅它们关注的消息。
  • 基于事件的通信:当您实现事件驱动的处理时,服务之间的通信通过各服务产生的事件进行。各项服务会将其事件写入消息代理。服务可以监听感兴趣的事件。这种模式使服务松散耦合,因为事件不包含载荷。

在微服务应用中,我们建议使用异步服务间通信而不是同步通信。请求响应是一种易于理解的架构模式,因此设计同步 API 可能比设计异步系统更自然。服务之间的异步通信可以使用消息功能或事件驱动的通信来实现。使用异步通信具有以下优势:

  • 松散耦合:异步模型将请求-响应交互拆分为两个单独的消息,一个用于请求,另一个用于响应。服务的消费者启动请求消息并等待响应,服务提供商等待其响应消息以及响应消息。此设置意味着调用者不必等待响应消息。
  • 故障隔离:即使下游消费者发生故障,发送者仍然可以继续发送消息。消费者会在恢复时提取积压输入量。此功能在微服务架构中尤其有用,因为每项服务都有自己的生命周期。但是,同步 API 要求下游服务可用或操作失败。
  • 响应速度:如果上游服务不等待下游服务,则可以更快地回复。如果存在服务依赖项(服务 A 调用 B,B 调用 C 等)链,等待同步调用可能会增加不可接受的延迟时间量。
  • 流控制:消息队列充当缓冲区,以便接收者可以按自己的速率处理消息。

但是,有效使用异步消息功能存在一些挑战:

  • 延迟时间:如果消息代理成为瓶颈,端到端延迟时间可能会变高。
  • 开发和测试开销:根据消息功能或事件基础架构的选择,可能存在重复消息,这使得操作很难幂等。使用异步消息功能可能难以实现和测试请求-响应语义。您需要一种方法来关联请求和响应消息。
  • 吞吐量:使用中央队列或一些其他机制的异步消息处理可能会成为系统中的瓶颈。后端系统(例如队列和下游消费者)应进行扩缩以匹配系统的吞吐量要求。
  • 错误处理处理:在异步系统中,调用者不知道请求是成功还是失败,因此需要在频段外处理错误处理。这种类型的系统可能难以实现重试或指数退避算法等逻辑。如果有多个必须全部成功或失败的链式异步调用,则错误处理会更加复杂。

本系列中的下一个文档(即微服务设置中的服务间通信)提供了一个参考实现,该实现可以解决上述列表中提到的一些挑战。

后续步骤