当前位置: 首页 > 科技观察

微服务架构中数据一致性详解

时间:2023-03-20 11:02:57 科技观察

在微服务中,一个逻辑原子操作往往可以跨越多个微服务。即使是单体系统也可能使用多个数据库或消息传递解决方案。使用多个独立的数据存储解决方案,如果其中一个分布式流程参与者失败,我们将面临数据不一致的风险——例如向客户收取未下订单的费用或未通知客户订单已成功。在这篇文章中,我想分享一些我学到的使微服务之间的数据最终保持一致的技术。为什么实现这个目标如此具有挑战性?只要我们有多个地方存储数据(而不是在单个数据库中),一致性问题就无法自动解决,工程师在设计系统时需要注意一致性。目前,在我看来,业界还没有一个众所周知的解决方案来自动更新多个不同来源的数据——我们可能不应该等待很快得到一个。以自动且无障碍的方式解决此问题的一种尝试是实施XA协议的两阶段提交(2PC)模式。但在现代的大规模应用中(尤其是在云环境中),2PC似乎表现不佳。为了消除2PC的缺点,我们必须用ACID换成BASE,并根据需求不同地覆盖一致性问题。Saga模式处理跨多个微服务的一致性问题最著名的方法是Saga模式。您可以将Sagas视为多个事务的应用程序级分布式协调。根据您的用例和要求,您可以优化自己的Saga实现。相比之下,XA协议试图涵盖所有场景。Saga模式也不是新的。过去,它在ESB和SOA架构中广为人知并使用过。***,成功转型为微服务世界。跨越多个服务的每个原子业务操作可能包含技术级别的多个事务。Saga模式的关键思想是能够回滚其中一个单独的事务。众所周知,开箱即用的已提交的单个事务无法回滚。但这是通过引入补偿操作来实现的——通过引入“取??消”操作。除了取消之外,您还应该考虑使您的服务幂等,以便在失败的情况下可以重试或重新启动某些操作。故障应该被监控并且应该被主动响应。协调如果在处理过程中负责调用补偿操作的系统崩溃或重启怎么办?在这种情况下,用户可能会收到一条错误消息,并且应该触发补偿逻辑,或者-当处理异步用户请求时,应该恢复执行逻辑。为了找到崩溃的交易并恢复操作或应用补偿,我们需要协调来自多个服务的数据。对账是从事金融工作的工程师所熟悉的一种技术。您有没有想过银行如何确保您的汇款不丢失,或者两个不同银行之间如何汇款?快速的答案是和解。在会计中,对账是确保两组记录(通常是两个账户的余额)一致的过程。调节用于确保离开账户的资金与实际花费的资金相匹配。这是通过确保余额在特定会计期末匹配来实现的。-JeanScheid,“了解资产负债表账户对账”,BrightHub,2011年4月8日回到微服务,使用相同的原则,我们可以在某个动作触发器上协调来自多个服务的数据。当检测到故障时,可以按计划或由监控系统触发操作。最简单的方法是逐条记录比较。可以通过比较聚合值来优化此过程。在这种情况下,其中一个系统将成为每条记录的真实来源。事件簿想象一个多步骤交易。我如何确定哪些交易可能已经失败,哪些步骤在对账期间失败了?一种解决方案是检查每笔交易的状态。在某些情况下,此功能不可用(想象一下发送电子邮件或生成其他类型消息的无状态邮件服务)。在其他情况下,您可能希望立即了解事务状态,尤其是在具有许多步骤的复杂场景中。例如,用于预订航班、酒店和接送服务的多步订单。复杂的分布式进程在这些情况下,事件日志可以提供帮助。日志记录是一种简单但功能强大的技术。许多分布式系统依赖于日志。“预写日志记录”是数据库在内部实现事务行为或保持副本之间一致性的方式。同样的技术可以应用于微服务设计。在进行实际数据更改之前,该服务会写入一个日志条目,说明其进行更改的意图。事实上,事件日志可以是协调服务拥有的数据库中的一个表或一个集合。事件日志不仅可用于恢复交易,还可用于为系统用户、客户或支持团队提供可见性。但是,在简单的场景中,服务日志可能是多余的,状态端点或状态字段就足够了。编排(Orchestration)和编排(choreography)到目前为止,你可能认为sagas只是编排(orchestration)解决方案的一部分。但是sagas也可以用于编排,其中每个微服务只知道流程的一部分。Sagas包括处理分布式事务的正负流知识。在编排中,分布式事务中的每个参与者都具有这种知识。单个写入事件到目前为止描述的一致性解决方案并不容易。它们确实很复杂。但是有一种更简单的方法:一次修改一个数据源。我们可以将这两个步骤分开,而不是在一个过程中更改服务状态和发出事件。Changetofirst在主要业务操作中,我们修改自己的服务状态,同时一个单独的进程可靠地捕获变化并生成事件。这种技术称为更改数据捕获(CDC)。实现这种方法的一些技术是KafkaConnect或Debezium。使用Debezium和KafkaConnect更改数据捕获但是,有时不需要特定的框架。一些数据库提供了一种友好的方式来跟踪他们的操作日志,例如MongoDBOplog。如果您的数据库中没有这样的功能,您可以通过时间戳轮询更改,或使用最后处理的不可变记录ID查询更改。避免不一致的关键是使数据更改通知成为一个单独的过程。在这种情况下,数据库记录是唯一的真实来源。仅当更改首先被更改时才会捕获更改。没有特定工具的变更数据捕获变更数据捕获的最大缺点是业务逻辑的分离。变更捕获过程很可能位于您的代码库中,与变更逻辑本身分开——这很不方便。最著名的变更数据捕获应用程序是独立于域的变更复制,例如与数据仓库共享数据。对于域事件,***使用不同的机制,例如显式发送事件。大事记***让我们看看颠倒过来的单一事实来源。如果不是先写入数据库,而是触发一个事件并将其与您自己和其他服务共享。在这种情况下,事件成为事实的唯一来源。这将是一种事件溯源形式,我们自己的服务状态有效地成为一个读取模型,而每个事件都是一个写入模型。事件优先方法一方面,它是一种命令查询责任分离(CQRS)模式,我们将读取和写入模型分开,但CQRS本身并不关注解决方案中最重要的部分——使用多个服务来消费事件.相比之下,事件驱动架构关注于被多个系统消费的事件,但并不强调事件是数据更新的唯一原子部分。所以我想引入“事件优先”作为这种方法的名称:通过发出单个事件来更新微服务的内部状态——包括我们自己的和任何其他感兴趣的微服务。“事件优先”方法的挑战也是CQRS本身的挑战。想象一下,我们想在下订单之前检查商品的可用性。如果两个实例同时收到同一商品的订单怎么办?两者都将同时检查读取模型中的库存并发出订单事件。如果没有某种覆盖方案,我们可能会遇到麻烦。处理这些情况的一种常见方法是乐观并发:将读取模型版本放入事件中,如果读取模型已在消费者端更新,则在消费者端忽略它。另一种解决方案是使用悲观并发控制,例如在检查项目可用性时为项目创建锁。“事件优先”方法的另一个挑战是任何事件驱动架构的挑战——事件的顺序。多个并发消费者以错误的顺序处理事件会给我们带来另一种一致性问题,例如处理尚未创建的客户的订单。Kafka或AWSKinesis等数据流解决方案可以保证与单个实体相关的事件将按顺序处理(例如,仅在创建用户后才为客户创建订单)。例如,在Kafka中,您可以按用户ID对主题进行分区,以便与单个用户相关的所有事件都将由分配给该分区的单个消费者处理,从而使它们能够按顺序处理。相反,在MessageBrokers中,消息队列是有顺序的,但是多个并发的消费者按照给定的顺序进行消息处理(如果不是不可能的话)。在这种情况下,您可能会遇到并发问题。实际上,在需要线性化或具有许多数据约束(例如唯一性检查)的情况下,“事件优先”方法很难实施。但它在其他情况下真的很有用。然而,由于其异步性质,并发和竞争条件的挑战仍然需要解决。设计一致性有很多方法可以将一个系统拆分成多个服务。我们努力将单独的微服务与单独的域相匹配。但是域名的粒度有多细?有时很难将域与子域或聚合根区分开来。定义微服务拆分没有简单的规则。我建议务实并考虑设计解决方案的所有影响,而不是只关注领域驱动设计。其中一个影响是微服务隔离与事务边界的对齐。事务仅驻留在微服务中的系统不需要上述任何解决方案。我们在设计系统时必须考虑事务边界。在实践中,以这种方式设计整个系统可能很困难,但我认为我们应该以尽量减少数据一致性挑战为目标。接受不一致虽然匹配账户余额很重要,但在许多用例中,一致性并不那么重要。想象一下为分析或统计目的收集数据。即使我们从系统中随机丢失10%的数据,也很可能不会影响分析的商业价值。选择哪种解决方案来与事件共享数据数据的原子更新需要两个不同系统之间的共识,如果单个值是0或1,则需要达成一致。谈到微服务,归结为两个参与者之间的一致性问题,所有实用的解决方案都遵循一个经验法则:在给定的时刻,对于每条数据记录,你需要找到系统信任的数据源。事实来源可以是事件、数据库或其中一项服务。开发人员有责任在微服务系统中实现一致性。我的做法是:尝试设计一个不需要分布式共识的系统。不幸的是,这对于复杂系统来说几乎是不可能的。尝试通过一次修改一个数据源来减少不一致的数量。考虑一个事件驱动的架构。除了松散耦合之外,事件驱动架构的一个强大优势是通过将事件作为单一真实来源或由于数据捕获的变化而产生的事件来实现数据一致性的一种自然方式。更复杂的场景可能仍然需要服务之间的同步调用、故障处理和补偿。知道有时您可能需要事后和解。将您的服务功能设计为可逆的,决定如何处理故障情况并在设计阶段尽早实现一致性。