跨微服务的数据一致性

34

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

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

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

这是可行的,但它需要在各个服务之间进行大量仔细协调的编程工作。

AKKA或其他框架能解决这个用例吗?如何解决?

编辑1:
为了更清晰明了,添加下面的图表。
基本上,我想了解当前是否有可用的框架可以解决这个数据一致性问题。

对于队列,我可以使用任何AMQP软件,例如RabbitMQ或Qpid等。
至于数据一致性框架,我不确定目前是否可以使用Akka或其他软件来解决。或者这种场景非常罕见,是一种反模式,根本不需要任何框架吗?
enter image description here

7个回答

13
微服务架构试图让组织拥有小团队独立开发和运行服务的能力。具体请参见read。最难的部分在于以有用的方式定义服务边界。当你发现应用程序的拆分方式导致需求经常影响多个服务时,这会提示您重新考虑服务边界。当你感觉需要在服务之间共享实体时,也是如此。
因此,一般建议尽可能避免这种情况。然而,可能有些情况下你无法避免它。由于良好的架构通常涉及做出正确的权衡,在这里提供一些想法。
考虑使用服务接口(API)而不是直接的数据库依赖来表达依赖关系。这将允许每个服务团队根据需要更改其内部数据架构,并且只需要在依赖项方面担心接口设计。这非常有帮助,因为添加其他API并逐渐淘汰旧API比同时更改DB设计以及所有相关微服务要容易得多。换句话说,只要支持旧API,您仍然能够独立部署新的微服务版本。这是亚马逊CTO推荐的方法,他是微服务方法的先锋。这里建议阅读他在2006年的interview
当你真的无法避免使用相同的数据库并且你正在以多个团队/服务需要相同实体的方式划分你的服务边界时,你引入了两个依赖于微服务团队和负责数据架构的团队之间的依赖关系: a)数据格式,b)实际数据。这不是不可能解决的,但组织上需要一些额外的开销。如果引入太多这样的依赖性,您的组织在开发中很可能会变得瘫痪和缓慢。

a) 数据方案的依赖性。实体数据格式不能在不需要更改微服务的情况下进行修改。为了解耦,您需要严格版本化实体数据方案,并在数据库中支持微服务当前正在使用的所有数据版本。这将允许微服务团队自行决定何时更新其服务以支持新版本的数据方案。虽然这在所有用例中都不可行,但它适用于许多用例。

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

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

图表看起来不错,只需明确一下通常是从服务向队列进行“拉取”,而不是“推送”。 Akka 将帮助解决其他问题,例如构建相当具有弹性的消息传递系统并简化分布式服务的部署(仅限基于 JVM),但它无法解决应用程序架构设计的根本问题,例如决定在哪里绘制服务边界。这只能通过研究您的领域和应用要求来回答。我建议您尝试了解一些大公司在其架构中所做的事情。 - Oswin Noetzelmann

8
理论限制

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

在出现分区的情况下,只剩下两个选择: 一致性或可用性。当选择一致性而非可用性时,如果由于 网络分区导致特定信息无法保证最新,则系统将返回错误或超时。

因此,通过“要求”在多个服务之间保持某些实体的一致性,会增加处理超时问题的概率。

Akka分布式数据

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

所有数据条目都通过直接复制和基于流言蜚语的传播传播到集群中的所有节点或具有某个角色的节点。您可以对读取和写入的一致性级别进行细粒度控制。


感谢提到Akka分布式数据。它是否可以按照我在上面的图表中展示的方式工作?您能否指出这样的资源给我吗? 或者,如果您知道其他可以实现此功能的框架,请告诉我。 - Santanu Dey
1
值得注意的是,Akka集群最适合单个服务,而不是多个服务。试图在一个Akka集群中运行多个服务更像是分布式单体结构(也就是两者都不好的情况)。 - Levi Ramsey

4
同样的问题也困扰着我们。我们的数据存储在不同的微服务中,有些情况下一个服务需要知道另一个微服务中是否存在一个特定实体。我们不希望服务之间相互调用来完成请求,因为这会增加响应时间并增加宕机的可能性。此外,这还会增加耦合深度的风险。客户端也不应该决定业务逻辑和数据验证/一致性。我们也不希望像“Saga Controllers”这样的中央服务提供服务之间的一致性。
因此,我们使用Kafka消息总线来通知观察到状态变化的服务。即使在错误条件下,我们也非常努力地不漏掉或忽略任何消息,并使用Martin Fowler的“宽容读者”模式尽可能地解耦。但是有时候服务会发生变化,变化后它们可能需要其他服务的信息,但这些信息以前曾经通过总线发送过,但现在已经不存在了(即使Kafka也不能永久存储)。
我们目前的决策是将每个服务拆分成一个纯粹和解耦的Web服务(RESTful),执行实际工作,和一个单独的连接器服务,监听总线并可以调用其他服务。这个连接器在后台运行,并且仅由总线消息触发。然后它将尝试通过REST调用向主服务添加数据。如果服务响应带有一致性错误,连接器将尝试通过从上游服务中获取所需数据并根据需要进行注入来修复这个问题(我们无法承担“同步”数据的批处理作业,因此我们只获取我们需要的数据)。如果有更好的想法,我们总是乐意接受,但是“拉取”或“仅更改数据模型”不符合我们的可行性考虑...

1
我认为您可以从两个角度解决这个问题,即服务协作和数据建模:
服务协作
在这里,您可以选择服务编排和服务编舞之间。您已经提到了服务之间的消息或事件交换。这将是编舞方法,正如您所说,可能有效,但涉及到在每个服务中编写处理消息部分的代码。不过,我相信有相关的库。或者您可以选择服务编排,在其中引入一个新的组合服务 - 编排器,它可以负责管理服务之间的数据更新。由于数据一致性管理现在被提取到一个单独的组件中,这将允许您在不触及下游服务的情况下在最终一致性和强一致性之间切换。
数据建模
您还可以选择重新设计参与微服务后面的数据模型,并将需要在多个服务之间保持一致的实体提取到由专用关系微服务管理的关系中。这样的微服务与编排器有些相似,但耦合度较低,因为可以以通用方式对关系进行建模。

1
我认为这里有两个主要的力量:
- 解耦 - 这就是你首先使用微服务并希望采用共享无事务处理的数据持久化方法的原因。 - 一致性要求 - 如果我理解正确,您已经接受了最终一致性。
对我来说,图表非常有意义,但我不知道有哪些框架可以直接完成它,可能是由于涉及到许多特定用例的权衡。我会按照以下方式解决问题:
上游服务将事件发布到消息总线上,如您所示。出于序列化的目的,我会仔细选择不会过度耦合生产者和消费者的线路格式。我所知道的是 protobuf 和 avro。如果上游需要添加新字段且下游不关心,则可以在不必更改下游的情况下演化您的事件模型,并且如果下游需要,则可以进行滚动升级。
下游服务订阅事件——消息总线必须提供容错能力。我们正在使用 kafka 进行此操作,但由于您选择了 AMQP,我假设它提供了您需要的功能。

如果出现网络故障(例如下游消费者无法连接到经纪人),如果您更注重(最终)一致性而不是可用性,您可以选择拒绝提供依赖于已知可能比某个预配置阈值更陈旧的数据的请求。


0

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

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

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

这种方法可能会影响性能,但至少您可以选择要交换的内容和位置。降低延迟和增加吞吐量的可能方法包括:

  • 扩展数据提供者服务,使其能够处理更多的请求/秒并降低延迟
  • 使用具有短过期时间的本地缓存。这将引入最终一致性,但确实有助于提高性能。
  • 使用分布式缓存并直接面对缓存一致性问题

根据我在微服务领域所看到的,我不同意你的观点:“数据重复->失败。”通常情况下,你会尽可能避免重复 - 但我不认为这是一个失败。 - Santanu Dey
我已经添加了一个图表以增加清晰度。您知道Akka或其他框架是否有助于此用例吗?感谢指引。 - Santanu Dey
3
框架并不能真正帮助你,可以参考@Oswin Noetzelmann的优秀答案——这一切都与服务边界的设计有关,要使用拉取而不是推送。数据建模在第一次迭代中很难做到完美,因此Fowler建议先构建单体应用,然后再拆分:https://martinfowler.com/bliki/MonolithFirst.html 建议阅读他的其他文章。 - Sergey Alaev

0

管理模块之间的数据访问

什么是模块?

模块是一种具有自己功能的软件。一个模块可以作为整体一起部署,也可以分离成微服务独立部署。在定义模块时,应该谨慎考虑,因为管理模块之间的数据访问变得更加困难。因此,需要在特定领域拥有丰富的经验才能做出正确决策。最好将“实际上的两个模块”合并成一个而非将“单一的模块”拆分成两个,因为如果你在不应该分离模块的情况下将其分离成两个,这些模块之间将会有大量的数据访问,尤其是存在事务逻辑时,这可能会很难以管理。但有时候,当规模变得庞大时,建立模块是必要的。以下是我用来决定必须选择哪种策略的决策树:

读取数据的决策树

如果存在两个服务,A 依赖于 B...

  • 如果它们在同一个模块中...
    • 当 A 需要进行简单数据读取时: A 应该使用由直接数据库读取实现的 B 接口。
    • 当 A 需要进行复杂数据读取1 时: 应使用直接数据库表联接进行读取。
  • 如果它们在不同的模块中...
    • 当 A 需要进行简单数据读取时...
      • 当它们部署为单体应用时: A 应使用由直接数据库读取实现的 B 接口。
      • 当它们部署为微服务时: A 应使用由 HTTP 客户端实现的 B 接口。
    • 当 A 需要进行复杂数据读取时...
      • 当它们部署为单体应用时: A 应通过从内存事件总线中消费来自 B 的数据并将其复制到针对其用例进行优化的不同格式中。
      • 当它们部署为微服务时: A 应使用事件总线消费者将来自 B 的数据复制到针对其用例进行优化的不同格式中。

数据写入的决策树

如果有两个服务 A 和 B,其中 A 依赖于 B...

  • 如果它们被部署为单体应用:B 的接口应该使用直接数据库写入来实现。
  • 如果它们被部署为微服务...(可能需要在服务之间进行分布式事务管理)
    • A 需要进行简单数据写入:A 应该使用 B 的接口,该接口使用 HttpClient 实现。
    • A 需要进行复杂数据写入2:A 应该使用 B 的接口,该接口使用事件总线生产者实现。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接