从单体到微服务的迁移:持久层迁移要点说明 - thorben


自从微服务变得流行以来,团队正试图将其单体划分为一组小型、独立且可高度扩展的微服务。从理论上讲,这通常看起来很容易。您只需要遵循领域驱动设计的关键原则,在您的应用程序中标识有界的上下文,并将每个上下文提取为微服务即可。
通常,实现很快变得比看起来复杂得多。总是有一些用例需要来自完全独立的服务的数据。并且一些写入操作需要在添加或更新多个服务中的信息时确保数据一致性。
 
保持服务独立
在设计微服务架构时,避免服务之间的依赖性很重要。这使您能够独立实施,发布,部署和扩展每个服务。它还降低了构建分布式整体的风险,在这种整体中,一项服务的错误或性能问题会串联到其他服务,并对整个系统产生负面影响。
在实现此目标时,如何管理数据起着重要作用。为了使服务彼此独立,在设计服务时应遵循3条基本规则:

  1. 每个服务必须具有自己的数据库,以使其独立于所有其他服务。我经常被问到是否可以将多个服务的数据库部署到同一数据库服务器上。我宁愿避免这种情况,因为它带来了数据库问题立即影响多个服务的风险。但这可能是您的第一个系统降低部署复杂性的一种选择。
  2. 没有微服务可以访问另一个服务的数据库。当然,这还包括外键引用,数据库视图和存储过程。直接访问另一个服务的任何数据存储区会引入非常强的依赖性。
  3. 每个服务都管理自己的本地事务,不参与任何分布式事务。

根据这些规则和设计软件的一些一般最佳实践,您可以开始重构数据模型。
 
重构数据模型
我总是建议着重于您的业务领域,而不是源代码的特定部分。它使识别应用程序中应独立的部分变得容易得多,并且消除了所有技术干扰。
而且,如果您正在处理庞大的代码库,则不需要并且可能不应该立即处理整个整体。通常最好采用增量方法,并专注于应用程序的某些部分。
  • 步骤1:确定独立模块并拆分您的业务代码

您可能已经听了一千遍了。在将您的整体拆分成微服务之前,您需要确定将成为独立服务的部分。识别这些部分的常见建议是在系统中查找有界的上下文。如果您做对了,应该一切都准备就绪,并且重构应该是快速而简单的。
好吧,这不是完全错误的。在找到微服务的正确范围之后,从整体中提取微服务会容易得多。但是仍然有很多工作要做,并且需要解决一些问题。但是,让我们一次迈出一步。
有几种方法可以识别和提取微服务。您可以找到许多讨论不同选择的文章和讲座。我更喜欢先将整体拆分成模块。这使您可以采用迭代方法,并且现在暂时忽略与远程服务的所有交互。这样,您可以快速找到开始提取微服务时将发生的所有技术问题,并帮助您识别隐藏的依赖项。
这样做时,您会发现一些跨越模块边界的查询和建模关联。其中一些暗示这些模块不应分开,而应成为一种服务。其他可以重构。
  • 步骤2:删除跨模块边界的查询和关联

这通常是重构中最难的部分。您应该首先查看实体之间的关联。对于引用另一个模块中实体的每个实体,您需要确定是否需要保留它或可以将其删除。以下是三个有助于您做出决定的问题:
  1. 您的表模型中是否有匹配的外键约束?如果是这样,将其删除会给您带来什么损失?
  2. 哪些查询使用该关联?您需要存储在关联记录中的详细信息吗?
  3. 是否有使用此关联的复杂业务逻辑?

用外键引用替换关联
通常,在任何复杂的查询或业务操作中都不会使用很多这样的关联。然后,您可以轻松地删除它们,并使用基本属性来存储对引用记录的键的引用。此信息足以查找另一个服务引用的所有记录,或为调用者提供所需的信息,以连接由不同服务管理的信息。

引入冗余
如果您只需要在参考记录中存储几条信息,则最好将这两个数据冗余地保留在两个服务中。那会从您的域模型中删除该关联,但不会删除您服务的依赖关系。您将需要定义哪个服务拥有该信息,并且需要将其复制到其他服务。重要的区别在于,您可以使用诸如“查看数据库”和“发件箱模式”之类的模式来健壮和异步地执行此操作。

合并相关模块或引入另一个模块
如果多个服务需要大量相同信息,则通常会遇到服务设计错误的症状。您的服务可能太小,或者您可能缺少服务。
解决此问题的最常见方法是将这些服务合并为一个。然后,该服务以与在整体中相同的方式在内部处理这些依赖关系,问题消失了。如果您没有充分的理由将服务分开,则建议您采用这种方法。
但是不时地,您将获得应该分开的服务。在这些情况下,通常最好将共享数据和对其进行操作的逻辑提取到单独的服务中。而不是拥有多个相互依赖的服务,而是拥有多个独立的服务以及一个相互依赖的服务。
这仍然不是理想的情况。您将要删除所有依赖项。但是在上述情况下,这是不可能的。因此,您选择了第二好的选择:您尝试将所有依赖项移至一项服务中,并异步复制所需的信息,以避免同步服务调用。这样,您可以降低复杂性并提高容错能力。

  • 步骤3:处理分布式写操作

在理想的情况下,您不需要处理任何影响多个服务的写入操作。这是因为在不使用分布式事务的情况下很难确保数据的一致性。
避免分布式写操作的一种选择是合并其中的所有服务。如果一组服务参与了多个分布式写操作,那么这通常是正确的方法。这清楚地表明这些服务并不像开始时那样独立。
如果您有充分的理由将服务分开,则可以使用SAGA模式。总体思路很简单。与斯堪的纳维亚的故事类似,您将操作分为多个步骤,这些步骤按定义的顺序进行。完成所有步骤后,您的SAGA及其建模的写入操作成功。如果出现问题,请对所有已执行的步骤执行相反的操作。这样,您将获得一个最终一致的系统。
在大多数情况下,实施和监视这样的SAGA比起初看起来要复杂得多。因此,我建议尝试避免使用它们,并在实现它们之前重新考虑服务设计。
如果您决定实施SAGA,建议对涉及3种以上服务或需要复杂决定才能触发后续步骤的所有复杂SAGA使用基于Orchestrator的SAGA。在这种模式下,协调器控制SAGA的流程。这使得跟踪和实施变得更加容易。
如果您想要一个松散耦合的SAGA,基于编排choreography的SAGA可能是一个不错的选择。与Orchestrator类似,每个服务都知道什么以及何时需要执行某些操作。没有管理流程的中央组件。这样可以提供很大的灵活性,但同时也使监视和管理复杂操作变得非常困难。
  • 步骤4:每个模块成为服务

最后一步很容易。您已经在步骤2和3中进行了艰苦的工作。现在,您只需要将模块重构为独立的服务即可。如果您在前面的步骤中没有错过任何内容,则只需要将每个模块的代码复制到另一个文件夹中,并添加一些构建和部署工具。
 
结论
在您确定系统中的有界上下文并使用它们来对您的业务代码进行模块化之后,仍然有很多工作要做。您需要对持久层应用相同的模块化。
当您拆分持久层时,您会发现跨越模块边界的关联和查询。然后,您需要确定是否可以:
  • 删除它们,
  • 更改服务设计以使它们保持在同一服务中,
  • 通过异步复制数据来替换它们。