34

虽然每个微服务通常都有自己的数据 - 某些实体需要在多个服务之间保持一致。

对于微服务架构等高度分布式环境中的这种数据一致性要求,设计上有哪些选择?当然,我不想要共享数据库架构,其中单个数据库管理所有服务的状态。这违反了隔离和无共享原则。

我确实理解,微服务可以在创建、更新或删除实体时发布事件。对此事件感兴趣的所有其他微服务可以相应地更新其各自数据库中的链接实体。

这是可行的,但是它会导致跨服务进行大量仔细和协调的编程工作。

Akka 或任何其他框架可以解决这个用例吗?如何?

EDIT1:
为清楚起见,添加下图。
基本上,我试图了解,如果今天有可用的框架可以解决这个数据一致性问题。

对于队列,我可以使用任何 AMQP 软件,例如 RabbitMQ 或 Qpid 等。对于数据一致性框架,我不确定目前 Akka 或任何其他软件是否可以提供帮助。还是这种情况如此罕见,并且是一种不需要任何框架的反模式?
在此处输入图像描述

4

7 回答 7

13

Microservices架构风格试图让组织拥有独立于开发和运行时的小型团队自己的服务。看这个。最难的部分是以有用的方式定义服务边界。当您发现拆分应用程序的方式导致需求频繁影响多个服务时,这会告诉您重新考虑服务边界。当您强烈需要在服务之间共享实体时也是如此。

所以一般的建议是尽量避免这种情况。但是,在某些情况下,您可能无法避免这种情况。由于一个好的架构通常是关于做出正确的权衡,这里有一些想法。

  1. 考虑使用服务接口(API)而不是直接的数据库依赖来表达依赖。这将允许每个服务团队根据需要更改其内部数据模式,并且只在涉及依赖关系时担心接口设计。这很有帮助,因为它更容易添加额外的 API 并慢慢弃用旧 API,而不是更改数据库设计以及所有相关的微服务(可能同时)。换句话说,只要仍然支持旧的 API,您仍然可以独立部署新的微服务版本。这是 Amazon CTO 推荐的方法,他开创了许多微服务方法。这是 2006 年对他的一次采访的推荐读物。

  2. 每当您真的无法避免使用相同的数据库并且您以多个团队/服务需要相同实体的方式分割服务边界时,您会在微服务团队和负责数据方案的团队之间引入两个依赖关系: ) 数据格式,b) 实际数据。这不是不可能解决的,但只有在组织方面有一些开销。如果您引入太多此类依赖项,您的组织可能会瘫痪并减慢开发速度。

a) 对数据方案的依赖。如果不更改微服务,就无法修改实体数据格式。为了解耦,您必须严格对实体数据方案进行版本控制,并且在数据库中支持微服务当前使用的所有数据版本。这将允许微服务团队自行决定何时更新他们的服务以支持新版本的数据方案。这并非适用于所有用例,但适用于许多用例。

b) 依赖于实际收集的数据。已收集且属于微服务已知版本的数据可以使用,但是当您有一些服务生成更新版本的数据并且另一个服务依赖于它时会出现问题 - 但尚未升级为能够阅读最新版本。这个问题很难解决,并且在许多情况下表明您没有正确选择服务边界。通常,您别无选择,只能在升级数据库中的数据的同时推出所有依赖于数据的服务。一种更古怪的方法是同时写入不同版本的数据(这主要在数据不可变时起作用)。

为了解决 a) 和 b) 在某些其他情况下的依赖关系,可以通过hidden data duplication和来减少eventual consistency。这意味着每个服务都存储自己的数据版本,并且仅在该服务的需求发生变化时才对其进行修改。服务可以通过监听公共数据流来做到这一点。在这种情况下,您将使用基于事件的体系结构,您可以在其中定义一组公共事件,这些事件可以排队并由来自不同服务的侦听器使用,这些服务将处理事件并存储与其相关的任何数据(可能会造成数据重复)。现在,一些其他事件可能表明必须更新内部存储的数据,并且每个服务都有责任使用自己的数据副本进行更新。维护这样一个公共事件队列的技术是Kafka.

于 2017-05-14T01:58:08.253 回答
8

理论限制

要记住的一个重要警告是CAP 定理

如果存在分区,则剩下两个选项:一致性或可用性。When choosing consistency over availability, the system will return an error or a time-out if particular information cannot be guaranteed to be up to date due to network partitioning.

因此,通过“要求”某些实体在多个服务中保持一致,您增加了必须处理超时问题的可能性。

Akka 分布式数据

Akka 有一个分布式数据模块来在集群内共享信息:

所有数据条目通过直接复制和基于八卦的传播传播到集群中的所有节点或具有特定角色的节点。您可以细粒度地控制读取和写入的一致性级别。

于 2017-05-13T09:12:15.557 回答
4

这里同样的问题。我们在不同的微服务中拥有数据,并且在某些情况下,一个服务需要知道另一个微服务中是否存在特定实体。我们不希望服务相互调用以完成请求,因为这会增加响应时间并增加停机时间。它还增加了耦合深度的噩梦。客户也不应该决定业务逻辑和数据验证/一致性。我们也不希望像“Saga Controllers”这样的中央服务提供服务之间的一致性。

因此,我们使用 Kafka 消息总线来通知观察服务“上游”服务的状态变化。即使在错误条件下,我们也努力不遗漏或忽略任何消息,并且我们使用 Martin Fowler 的“宽容阅读器”模式尽可能松散地耦合。仍然有时服务会发生变化,并且在更改之后他们可能需要来自其他服务的信息,这些信息之前可能已经在总线上发出,但现在它们已经消失了(即使 Kafka 也无法永久存储)。

我们现在决定将每个服务拆分为一个纯粹的、解耦的 Web 服务 (RESTful) 来完成实际工作,以及一个单独的连接器服务,它监听总线并可能调用其他服务。此连接器在后台运行。它仅由总线消息触发。然后它将尝试通过 REST 调用将数据添加到主服务。如果服务以一致性错误响应,连接器将尝试通过从上游服务获取所需数据并根据需要注入来修复此错误。(我们负担不起批处理作业来“同步”数据块,所以我们只是获取我们需要的东西)。如果有更好的想法,我们总是开放的,但“拉”或“只是改变数据模型”不是我们认为可行的……

于 2019-09-22T19:07:53.067 回答
1

我认为你可以从两个角度来解决这个问题,服务协作和数据建模:

服务协作

在这里,您可以在服务编排和服务编排之间进行选择。您已经提到了服务之间的消息或事件交换。这将是一种编排方法,正如您所说的那样可能有效,但涉及在处理消息传递部分的每个服务中编写代码。我敢肯定有图书馆。或者您可以选择服务编排,在其中引入新的复合服务 - 编排器,它可以负责管理服务之间的数据更新。因为数据一致性管理现在被提取到一个单独的组件中,这将允许您在最终一致性和强一致性之间切换,而无需触及下游服务。

数据建模

您还可以选择重新设计参与微服务背后的数据模型,并将需要在多个服务之间保持一致的实体提取到由专用关系微服务管理的关系中。这样的微服务与编排器有些相似,但耦合会减少,因为可以以通用方式对关系进行建模。

于 2017-05-14T04:11:25.810 回答
1

我认为这里有两个主要力量在起作用:

  • 解耦——这就是为什么你首先拥有微服务并想要一种无共享的数据持久性方法
  • 一致性要求 - 如果我理解正确,你已经可以接受最终的一致性

该图对我来说非常有意义,但我不知道有什么框架可以开箱即用,这可能是由于涉及到许多特定于用例的权衡。我将按如下方式解决问题:

如您所示,上游服务将事件发送到消息总线。出于序列化的目的,我会仔细选择不会过多耦合生产者和消费者的有线格式。我知道的是protobuf和avro。如果它不关心新添加的字段,您可以在上游发展您的事件模型而无需更改下游,并且可以在它关心的情况下进行滚动升级。

下游服务订阅事件 - 消息总线必须提供容错。我们为此使用 kafka,但由于您选择了 AMQP,我假设它可以满足您的需求。

在网络故障的情况下(例如下游消费者无法连接到代理),如果您更喜欢(最终)一致性而不是可用性,您可以选择拒绝服务依赖于您知道可能比某些预配置阈值更陈旧的数据的请求。

于 2017-06-25T15:51:25.743 回答
0

“相应地更新各自数据库中的链接实体”->数据重复->失败。

使用事件更新其他数据库与缓存相同,这会带来缓存一致性问题,这是您在问题中出现的问题。

尽可能保持本地数据库分离,并使用拉取语义而不是推送,即在需要一些数据时进行 RPC 调用,并准备好优雅地处理可能出现的错误,如超时、丢失数据或服务不可用。Akka 或 Finagle 提供了足够的工具来做到这一点。

这种方法可能会损害性能,但至少您可以选择交易的内容和地点。减少延迟和增加吞吐量的可能方法是:

  • 扩展数据提供者服务,以便他们能够以更低的延迟处理更多的请求/秒
  • 使用过期时间短的本地缓存。这将引入最终的一致性,但确实有助于提高性能。
  • 使用分布式缓存,直接面对缓存一致性问题
于 2017-05-13T09:17:56.830 回答
0

管理模块之间的数据访问

什么是模块?

模块是一个自身具有功能的软件。一个模块可以与其他模块一起部署为一个整体,也可以单独部署为一个微服务。定义模块时应该小心,因为管理模块之间的数据访问变得更加困难。因此,它需要在特定领域的大量经验来决定。最好犯下将“实际的两个模块”合并为一个的错误,而不是将“单个模块”分成两个。因为如果你在不应该的时候将一个模块分成两个,那么这些模块之间就会有大量的数据访问,这可能很难管理,尤其是在有事务逻辑的情况下。但有时当事情开始变大时,有必要特别制作模块。

数据读取的决策树

如果有两个服务 A 依赖于 B...

  • 他们在同一个模块中......
    • A 需要简单的数据读取:A 应该使用 B 的接口,该接口是通过直接数据库读取实现的。
    • 和 A 需要读取复杂数据1:应该使用直接数据库表连接进行读取。
  • 他们在不同的模块中......
    • 和 A 需要简单的数据读取...
      • 并且它们被部署为单体:A 应该使用 B 的接口,该接口是通过直接数据库读取实现的。
      • 并且它们被部署为微服务:A 应该使用 B 的接口,该接口是通过 http 客户端实现的。
    • 和 A 需要读取复杂的数据...
      • 并且它们被部署为整体:A 应该通过从内存中的事件总线消费,以针对其用例优化的不同格式从 B 复制数据。
      • 并且它们被部署为微服务:A 应该使用事件总线消费者以针对其用例优化的不同格式从 B 复制数据。

数据写入的决策树

如果有两个服务 A 依赖于 B...

  • 并且它们被部署为一个整体:B的接口应该通过直接数据库写入来实现。
  • 并且它们被部署为微服务......(可能需要服务之间的分布式事务管理)
    • A 需要简单的数据写入:A 应该使用 B 的接口,该接口是用 HttpClient 实现的。
    • 和 A 需要复杂的数据写入2:A 应该使用 B 的接口,该接口是用事件总线生产器实现的。

复杂数据读取1:批处理、加入后的排序/过滤、事务管理等 复杂数据写入2:IO 密集型、CPU 密集型、网络密集型

于 2022-02-07T14:57:33.830 回答