拆分单体应用为服务的难点
从表面上看,通过定义与业务能力或子域相对应的服务来创建微服务架构的策略看起来很简单。但是,你可能会遇到几个障碍:
网络延迟。
同步进程间通信导致可用性降低。
在服务之间维持数据一致性。
获取一致的数据视图。
上帝类阻碍了拆分。
让我们来看看每个问题,先从网络延迟开始。
网络延迟
网络延迟是分布式系统中一直存在的问题。你可能会发现,对服务的特定分解会导致两个服务之间的大量往返调用。有时,你可以通过实施批处理 API 在一次往返中获取多个对象,从而将延迟减少到可接受的数量。但在其他情况下,解决方案是把多个相关的服务组合在一起,用编程语言的函数调用替换昂贵的进程间通信。
同步进程间通信导致可用性降低
另一个需要考虑的问题是如何处理进程间通信而不降低系统的可用性。例如,实现 createOrder()操作最常见的方式是让 Order Service 使用 REST 同步调用其他服务。这样做的弊端是 REST 这样的协议会降低 Order Service 的可用性。如果任何一个被调用的服务处在不可用的状态,那么订单就无法创建了。有时候这可能是一个不得已的折中,但是在第 3 章中学习异步消息之后,你就会发现其实有更好的办法来消除这类同步调用产生的紧耦合并提升可用性。
在服务之间维持数据一致性
另一个挑战是如何在某些系统操作需要更新多个服务中的数据时,仍旧维护服务之间的数据一致性。例如,当餐馆接受订单时,必须在 Kitchen Service 和 Delivery Service 中同时进行更新。Kitchen Service 会更改 Ticket 的状态。Delivery Service 安排订单的交付。这些更新都必须以原子化的方式完成。
传统的解决方案是使用基于两阶段提交(two phase commit)的分布式事务管理机制。但正如你将在第 4 章中看到的那样,对于现今的应用程序而言,这不是一个好的选择,你必须使用一种非常不同的方法来处理事务管理,这就是 Saga。Saga 是一系列使用消息协作的本地事务。Saga 比传统的 ACID 事务更复杂,但它们在许多情况下都能工作得很好。Saga 的一个限制是它们最终是一致的。如果你需要以原子方式更新某些数据,那么它必须位于单个服务中,这可能是分解的障碍。
获取一致的数据视图
分解的另一个障碍是无法跨多个数据库获得真正一致的数据视图。在单体应用程序中,ACID 事务的属性保证查询将返回数据库的一致视图。相反,在微服务架构中,即使每个服务的数据库是一致的,你也无法获得全局一致的数据视图。如果你需要一些数据的一致视图,那么它必须驻留在单个服务中,这也是服务分解所面临的问题。幸运的是,在实践中这很少带来真正的问题。
上帝类阻碍了拆分
分解的另一个障碍是存在所谓的上帝类。上帝类是在整个应用程序中使用的全局类。上帝类通常为应用程序的许多不同方面实现业务逻辑。它有大量字段映射到具有许多列的数据库表。大多数应用程序至少有一个这样的上帝类,每个类代表一个对领域至关重要的概念:银行账户、电子商务订单、保险政策,等等。因为上帝类将应用程序的许多不同方面的状态和行为捆绑在一起,所以将使用它的任何业务逻辑拆分为服务往往都是一个不可逾越的障碍。
Order 类是 FTGO 应用程序中上帝类的一个很好的例子。这并不奇怪:毕竟 FTGO 的目的是向客户提供食品订单。系统的大多数部分都涉及订单。如果 FTGO 应用程序具有单个领域模型,则 Order 类将是一个非常大的类。它将具有与应用程序的许多不同部分相对应的状态和行为。图 6 显示了使用传统建模技术创建的 Order 类的结构。
图 6 Order 这个上帝类承载了太多的职责
如你所见,Order 类具有与订单处理、餐馆订单管理、送餐和付款相对应的字段及方法。由于一个模型必须描述来自应用程序的不同部分的状态转换,因此该类还具有复杂的状态模型。在目前情况下,这个类的存在使得将代码分割成服务变得极其困难。
一种解决方案是将 Order 类打包到库中并创建一个中央 Order 数据库。处理订单的所有服务都使用此库并访问访问数据库。这种方法的问题在于它违反了微服务架构的一个关键原则,并导致我们特别不愿意看到的紧耦合。例如,对 Order 模式的任何更改都要求其他开发团队同步更新和重新编译他们的代码。
另一种解决方案是将 Order 数据库封装在 Order Service 中,该服务由其他服务调用以检索和更新订单。该设计的问题在于这样的一个 Order Service 将成为一个纯数据服务,成为包含很少或没有业务逻辑的贫血领域模型(anemic domain model)。这两种解决方案都没有吸引力,但幸运的是,DDD 提供了一个好的解决方案。
更好的方法是应用 DDD 并将每个服务视为具有自己的领域模型的单独子域。这意味着 FTGO 应用程序中与订单有关的每个服务都有自己的领域模型及其对应的 Order 类的版本。Delivery Service 是多领域模型的一个很好的例子。如图 7 所示为 Order,它非常简单:取餐地址、取餐时间、送餐地址和送餐时间。此外,DeliveryService 使用更合适的 Delivery 名称,而不是称之为 Order。
图 7 Delivery Service 的领域模型
Kitchen Service 有一个更简单的订单视图。它的 Order 版本就是一个 Ticket(后厨工单)。如图 8 所示,Ticket 只包含 status、requestedDeliveryTime、prepareByTime 以及告诉餐馆准备的订单项列表。它不关心消费者、付款、交付等这些与它无关的事情。
图 8 Kitchen Service 的领域模型
Order Service 具有最复杂的订单视图,如图 9 所示。即使它有相当多的字段和方法,它仍然比原始版本的那个 Order 上帝类简单得多。
图 9 Order Service 的领域模型
每个领域模型中的 Order 类表示同一 Order 业务实体的不同方面。FTGO 应用程序必须维持不同服务中这些不同对象之间的一致性。例如,一旦 OrderService 授权消费者的信用卡,它必须触发在 Kitchen Service 中创建 Ticket。同样,如果 Kitchen Service 拒绝订单,则必须在 Order Service 中取消订单,并且为客户退款。在第 4 章中,我们将学习如何使用前面提到的事件驱动机制 Saga 来维护服务之间的一致性。
除了造成一些技术挑战以外,拥有多个领域模型还会影响用户体验。应用程序必须在用户体验(即其自己的领域模型)与每个服务的领域模型之间进行转换。例如,在 FTGO 应用程序中,向消费者显示的 Order 状态来自存储在多个服务中的 Order 信息。这种转换通常由 API Gateway 处理,将在第 8 章中讨论。尽管存在这些挑战,但在定义微服务架构时,必须识别并消除上帝类。
我们现在来看看如何定义服务 API。
定义服务 API
到目前为止,我们有一个系统操作列表和一个潜在服务列表。下一步是定义每个服务的 API:也就是服务的操作和事件。存在服务 API 操作有以下两个原因:首先,某些操作对应于系统操作。它们由外部客户端调用,也可能由其他服务调用。另次,存在一些其他操作用以支持服务之间的协作。这些操作仅由其他服务调用。
服务通过对外发布事件,使其能够与其他服务协作。第 4 章将描述如何使用事件来实现 Saga,这些 Saga 可以维护服务之间的数据一致性。第 7 章将讨论如何使用事件来更新 CQRS 视图,这些视图支持有效的查询。应用程序还可以使用事件来通知外部客户端。例如,可以使用 WebSockets 将事件传递给浏览器。
定义服务 API 的起点是将每个系统操作映射到服务。之后确定服务是否需要与其他服务协作以实现系统操作。如果需要协作,我们将确定其他服务必须提供哪些 API 才能支持协作。首先来看一下如何将系统操作分配给服务。
把系统操作分配给服务
第一步是确定哪个服务是请求的初始入口点。许多系统操作可以清晰地映射到服务,但有时映射会不太明显。例如,考虑使用 noteUpdatedLocation()操作来更新送餐员的位置。一方面,因为它与送餐员有关,所以应该将此操作分配给 Courier Service。另一方面,它是需要送餐地点的 DeliveryService。在这种情况下,将操作分配给需要操作所提供信息的服务是更好的选择。在其他情况下,将操作分配给具有处理它所需信息的服务可能是有意义的。表 4 显示了 FTGO 应用程序中的哪些服务负责哪些操作。
表 4 FTGO 应用程序的系统操作映射到具体的服务
把操作分配给服务后,下一步是确定在处理每一个系统操作时,服务之间如何交互。
确定支持服务协作所需要的 API
某些系统操作完全由单个服务处理。例如,在 FTGO 应用程序中,Consumer Service 完全独立地处理 createConsumer()操作。但是其他系统操作跨越多个服务。处理这些请求之一所需的数据可能分散在多个服务周围。例如,为了实现 createOrder()操作,Order Service 必须调用以下服务以验证其前置条件并使后置条件成立:
Consumer Service:验证消费者是否可以下订单并获取其付款信息。
Restaurant Service:验证订单行项目,验证送货地址和时间是否在餐厅的服务区域内,验证订单最低要求,并获得订单行项目的价格。
Kitchen Service:创建 Ticket(后厨工单)。
Accounting Service:授权消费者的信用卡。
同样,为了实现 acceptOrder()系统操作,Kitchen Service 必须调用 Delivery Service 来安排送餐员交付订单。表 2-3 显示了服务、修订后的 API 及协作者。为了完整定义服务 API,你需要分析每个系统操作并确定所需的协作。
总结
微服务中的服务是根据业务需求进行组织的,按照业务能力或者子域,而不是技术上的考量。
有两种分解模式:
按业务能力分解,其起源于业务架构。
基于领域驱动设计的概念,通过子域进行分解。
可以通过应用 DDD 并为每个服务定义单独的领域模型来消除上帝类,正是上帝类引起了阻碍分解的交织依赖项。
本文转载自技术锁话公众号。
原文链接:https://mp.weixin.qq.com/s/2qrEH2_ZwE0HD1M5bQ4i5A
评论