分布式数据管理的挑战和解决方案

小窍门

此内容摘自电子书《适用于容器化 .NET 应用程序的 .NET 微服务体系结构》,可以在 .NET Docs 上获取,也可以下载免费的 PDF 以供离线阅读。

适用于容器化 .NET 应用程序的 .NET 微服务体系结构电子书封面缩略图。

挑战 #1:如何定义每个微服务的边界

定义微服务边界可能是任何人遇到的第一个挑战。 每个微服务都是您应用程序的一个独立组件,并且它应是自主的,具有其带来的所有优势和挑战。 但是,如何识别这些边界?

首先,需要专注于应用程序的逻辑域模型和相关数据。 尝试在同一应用程序中识别分离的数据岛和不同上下文。 每个上下文可以有不同的业务语言(不同的业务术语)。 上下文应独立定义和管理。 在这些不同上下文中使用的术语和实体听起来可能类似,但你可能会发现,在特定上下文中,具有一个上下文的业务概念用于另一个上下文中的不同用途,甚至可能具有不同的名称。 例如,用户可以在标识或成员身份上下文中称为用户、CRM 上下文中的客户、订单上下文中的购买者,等等。

确定多个应用程序上下文之间的边界,每个上下文具有不同域的方式正是如何标识每个业务微服务及其相关域模型和数据边界。 始终尝试最大程度地减少这些微服务之间的耦合。 本指南将更详细地介绍有关此标识和域模型设计的详细信息,请参阅后面的 每个微服务标识域模型边界

挑战 #2:如何创建从多个微服务检索数据的查询

第二个挑战是如何实现从多个微服务检索数据的查询,同时避免从远程客户端应用与微服务的聊天通信。 例如,移动应用中的单个屏幕需要显示篮子、目录和用户标识微服务拥有的用户信息。 另一个示例是涉及多个微服务中的多个表的复杂报表。 正确的解决方案取决于查询的复杂性。 但无论如何,如果要提高系统通信的效率,则需要一种方法来聚合信息。 最常见的解决方案如下。

API 网关。 对于拥有不同数据库的多个微服务的简单数据聚合,建议的方法是称为 API 网关的聚合微服务。 但是,需要注意实现此模式,因为它可能是系统中的一个阻塞点,并且可能会违反微服务自治原则。 为了缓解这种可能性,可以有多个细化 API 网关,每个网关都专注于系统的垂直“切片”或业务区域。 稍后的 API 网关部分 更详细地介绍了 API 网关模式。

GraphQL 联合身份验证 如果微服务已在使用 GraphQL,请考虑的一个选项是 GraphQL 联合身份验证。 联合允许您定义来自其他服务的“子图”(subgraphs),并将它们组合成充当独立架构的整体“超级图”(supergraph)。

具有查询/读取表的 CQRS。 另一种聚合来自多个微服务的数据的解决方案是 具体化视图模式。 在此方法中,会提前生成(在实际查询发生之前准备非规范化数据),这是一个只读表,其中包含多个微服务拥有的数据。 该表的格式适合客户端应用的需求。

考虑到移动应用屏幕等。 如果你有单个数据库,可以使用 SQL 查询来执行涉及多个表的复杂联接,从而获取该界面所需的数据。 但是,如果有多个数据库,并且每个数据库都由不同的微服务拥有,则无法查询这些数据库并创建 SQL 联接。 复杂的查询成为一项挑战。 可以使用 CQRS 方法满足要求, 可以在仅用于查询的不同数据库中创建非规范化表。 该表专为复杂查询所需的数据而设计,应用程序屏幕所需的字段与查询表中的列之间存在一对一关系。 它还可用于报告目的。

此方法不仅解决了原始问题(如何跨微服务查询和联接),而且与复杂联接相比,它还可显著提高性能,因为应用程序在查询表中已有应用程序所需的数据。 当然,将命令和查询责任分离(CQRS)与查询/读取表结合使用意味着需要额外的开发工作,你需要接受最终一致性。 尽管如此,在协作场景(或竞争场景,具体取决于观点)中对性能和高可伸缩性的要求,是将 CQRS 应用于多个数据库的恰当场合。

中央数据库中的“冷数据”。 对于可能不需要实时数据的复杂报表和查询,一种常见方法是将“热数据”(来自微服务的事务数据)导出为“冷数据”到仅用于报告的大型数据库。 该中央数据库系统可以是基于大数据的系统,例如 Hadoop;数据仓库,例如基于 Azure SQL 数据仓库的数据仓库;甚至只用于报表的单个 SQL 数据库(如果大小不会是问题)。

请记住,此集中式数据库仅用于不需要实时数据的查询和报表。 作为事实来源的原始更新和事务必须位于微服务数据中。 同步数据的方式是使用事件驱动的通信(在下一部分介绍)或使用其他数据库基础结构导入/导出工具。 如果使用事件驱动的通信,则集成过程将类似于如前面所述的 CQRS 查询表所述传播数据的方式。

但是,如果应用程序设计涉及不断聚合来自多个微服务的信息进行复杂查询,则可能是设计不佳的症状,-a 微服务应尽可能与其他微服务隔离。 (这不包括应始终使用冷数据中心数据库的报表/分析。通常遇到此问题可能是合并微服务的原因。 需要平衡每个微服务的演变和部署的自主性,以及强大的依赖项、凝聚力和数据聚合。

挑战 #3:如何跨多个微服务实现一致性

如前所述,每个微服务拥有的数据是该微服务的专用数据,只能使用微服务 API 进行访问。 因此,提出的挑战是如何实现端到端业务流程,同时保持多个微服务的一致性。

若要分析此问题,让我们从 eShopOnContainers 参考应用程序查看一个示例。 目录微服务维护有关所有产品的信息,包括产品价格。 购物篮微服务管理用户添加到购物篮的产品项的临时数据,其中包括添加到购物篮时商品的价格。 当产品的价格在目录中更新时,该价格也应更新到含有该产品的活跃购物篮中,并且系统可能应该警告用户,自从他们将商品添加到购物篮之后,商品价格已发生变化。

在此应用程序的假设整体版本中,当产品表中的价格变化时,目录子系统只需使用 ACID 事务来更新购物篮表中的当前价格。

但是,在基于微服务的应用程序中,Product 和 Basket 表由各自的微服务拥有。 任何微服务都不应在其自己的事务中包含另一个微服务拥有的表/存储,即使在直接查询中也是如此,如图 4-9 所示。

显示微服务数据库数据无法共享的图。

图 4-9. 微服务不能直接访问另一个微服务中的表

目录微服务不应直接更新购物篮表,因为 Basket 表归购物篮微服务所有。 若要更新购物篮微服务,目录微服务可能应当基于异步通信(例如集成事件,即消息和基于事件的通信)使用最终一致性。 这就是 eShopOnContainers 引用应用程序在跨微服务中实现这种类型的一致性的方法。

CAP 定理所述,需要在可用性与 ACID 强一致性之间进行选择。 大多数基于微服务的方案需要可用性和高可伸缩性,而不是强一致性。 任务关键型应用程序必须保持正常运行,开发人员可以使用处理弱或最终一致性的技术来解决高度一致性问题。 这是大多数基于微服务的体系结构采用的方法。

此外,ACID 或两阶段提交事务不仅违背微服务原则,并且大多数 NoSQL 数据库(如 Azure Cosmos DB、MongoDB 等)不支持常见于分布式数据库场景的两阶段提交事务。 但是,跨服务和数据库保持数据一致性至关重要。 此挑战还与当某些数据需要冗余时如何跨多个微服务传播更改的问题有关,例如,当需要在目录微服务和购物篮微服务中具有产品的名称或说明时。

解决此问题的一个很好的解决方案是使用通过事件驱动通信和发布和订阅系统阐明的微服务之间的最终一致性。 本指南后面的 异步事件驱动通信 部分介绍了这些主题。

挑战 #4:如何跨微服务边界设计通信

跨微服务边界进行通信是一个真正的挑战。 在此上下文中,通信不是指应使用哪种协议(HTTP 和 REST、AMQP、消息传递等)。 相反,它解决了应使用哪种通信样式,尤其是微服务的耦合方式。 根据耦合级别,当发生故障时,该故障对系统的影响将有很大差异。

在分布式系统中(如基于微服务的应用程序),由于众多软件组件在多个服务器或节点之间的流动和分布,组件最终会出故障。 将发生部分故障甚至更大的中断,因此需要设计微服务及其之间的通信,并考虑此类分布式系统中的常见风险。

一种常用的方法是实现基于 HTTP(REST)的微服务,因为其简单性。 基于 HTTP 的方法是完全可以接受的;此处的问题与如何使用它相关。 如果使用 HTTP 请求和响应只是为了从客户端应用程序或 API 网关与微服务交互,则情况很好。 但是,如果跨微服务创建长时间的同步 HTTP 调用链,则跨其边界进行通信,就好像微服务是整体应用程序中的对象一样,应用程序最终会遇到问题。

例如,假设客户端应用程序对单个微服务(如 Ordering 微服务)进行 HTTP API 调用。 如果订购微服务又在同一请求/响应周期中使用 HTTP 调用其他微服务,则创建 HTTP 调用链。 最初听起来可能很合理。 但是,在走下此路径时,需要考虑一些要点:

  • 阻止和低降低。 由于 HTTP 的同步性质,在完成所有内部 HTTP 调用之前,原始请求不会获得响应。 想象一下,如果这些调用的数量显著增加,同时一个对微服务的中间HTTP调用被阻塞。 结果是性能受到影响,随着其他 HTTP 请求的增加,整体可伸缩性将受到指数级影响。

  • 将微服务与 HTTP 耦合。 业务微服务不应与其他业务微服务结合使用。 理想情况下,它们不应“了解”其他微服务的存在。 如果应用程序依赖于耦合微服务,如示例中所示,实现每个微服务的自治几乎是不可能的。

  • 任一微服务中的失败。 如果实现了由 HTTP 调用链接的微服务链,当任何微服务发生故障(最终会失败)时,整个微服务链将失败。 应将基于微服务的系统设计为在部分故障期间尽可能正常地工作。 即使实现使用指数退避或断路器机制重试的客户端逻辑,HTTP 调用链越复杂,基于 HTTP 实现故障策略就越复杂。

事实上,如果内部微服务通过创建 HTTP 请求链进行通信,则可以认为你有一个整体式应用程序,但基于进程之间的 HTTP 而不是进程内部通信机制。

因此,为了强制实施微服务自治并具有更好的复原能力,应最大程度地减少在微服务之间使用请求/响应通信链。 建议仅使用异步交互进行微服务间通信,无论是使用异步消息和基于事件的通信,还是使用独立于原始 HTTP 请求/响应周期的 (异步) HTTP 轮询。

异步通信的使用稍后将在本指南的异步微服务集成强化微服务的自治性基于消息的异步通信部分详细介绍。

其他资源