现代分布式事务的两种形式 - a16z


长期以来,事务数据库一直是应用程序设计中最关键的组成部分。为什么?因为稳定的数据库通常是混乱的分布式世界中正确性的最终实施点。没有他们,我们就会多付钱和少收钱。我们会失去试图从机场回家的乘客,我们会丢失购物车中的物品。我们的在线帐户会丢失、复制或损坏,并变得无法操作。 

事实上,事务数据库(通常称为 OLTP — 在线事务处理的缩写 — 数据库)一直是应用程序开发的核心,随着时间的推移,它消耗了越来越多的应用程序功能。然而,微服务和其他现代应用程序架构给应用程序设计带来了新的复杂性:开发人员需要跨不同服务管理数据并确保它们之间的一致性,这迫使他们在内部构建复杂的数据同步和处理机制。 

我们越来越意识到在传统模式之外需要交易保证。我们看到系统的出现将强大的事务保证扩展到数据库之外,扩展到分布式应用程序本身。 

在过去的几年里,我们一直在跟踪这些解决方案。通常,他们努力允许在大型分布式应用程序中进行状态事务管理,而不会产生扩展挑战,同时提供现代编程环境。 

我们发现这些解决方案大致分为两类:

  • 一类是工作流编排。这基本上可以保证即使遇到故障,代码块也能运行完成。因此,它可以用于确定性地管理分布式状态机而不会变得不稳定。
  • 第二类是数据库+工作流,它扩展了传统的OLTP数据库设计,允许为同一目的执行任意代码。 

这仍然是一个非常新的领域,围绕命名法、每个工具在实践中如何使用以及应该由谁使用它们存在很多混淆。为了帮助更好地理解,我们询问了来自领先工程组织的从业者关于他们的事务堆栈以及他们如何思考事务工作负载的三个关键概念:应用程序状态、业务逻辑和业务数据。 

不过,在检查这些新堆栈之前,这里有一个快速的半技术性题外话,以帮助理解我们是如何走到这一步的。

事务交易、担保和现代应用程序 
非常粗略的版本是这样的:有一组任务——交易——你要么想要全部完成,要么什么都不做。介于两者之间的任何事情(部分完成)都将以损坏状态结束。很难保证分布式系统中的任何事情,但数据库可以很好地处理事务。因此,在许多系统中处理保证的最简单方法是让大多数事物成为事务并让数据库处理它们。

现代应用程序是大型分布式系统,有很多用户在做很多事情。因此,即使保持应用程序状态一致(例如跟踪不同用户在结帐流程中的位置)也会变成分布式事务问题。在传统的单体架构中,使用 SQL 和 OLTP 数据库管理事务在一定程度上是有效的。但在微服务通过更高级别的 API(例如 REST 或 gRPC)进行交互的新的、复杂的世界中,事务需求在本质上已经变得分布式。 

然而,许多正在进行微服务之旅的公司并没有做太多工作来将强大的事务保证扩展到数据库之外。而且,在实践中,这几乎总是可以的。但是随着应用程序的扩展,数据的不一致性会增加,由此产生的错误和业务数据中未协调的错误也会增加。当然,这可能会带来很大的问题。这迫使应用程序开发人员处理大量的故障场景和冲突解决策略,并通过不同的架构模式提出自己的策略来确保状态一致性。

定义

业务数据(“数据”)是指传统上存储在 OLTP 数据库中以供持久化和处理的关键业务数据(例如,姓名、地址、信用评分等用户配置文件信息)。

应用状态是指系统当前的状态;应用程序状态由存储在数据存储系统中的值以及程序在有限状态机中执行的步骤决定(例如,订单状态,例如“已收到订单”、“已检查库存”、“已检查信用” ”、“已发货”、“已退货”)。

业务逻辑 是指程序中处理应用程序实际如何工作或做什么的部分,而不是执行细节(例如“如果 user_income > $100K & credit_score >650 ⇒ mortgage_approved = TRUE”)。

出于本次讨论的目的,区分应用程序状态和业务数据很重要。例如,知道客户已输入他们的信用卡但尚未结帐是应用程序状态。信用卡数据和申请购物车中的项目是业务数据。 

在典型的流程中,请求来自前端,经过身份验证,然后通过 API 网关或 GraphQL 路由到相关端点。 

现在,单个 API 端点必须编排数十或数百个微服务,以将业务交易交付给最终客户。这是开发人员通常将所有内容都集中到业务逻辑块中的地方,然后使用队列、缓存和手动编码重试机制的组合将数据获取到数据库——希望作为完整事务提交。

随着应用程序规模的增加,管理队列和缓存的复杂性以及出现问题时协调逻辑中的尖锐边缘数量也会增加。

以工作流为中心和以数据库为中心的事务堆栈的兴起
好的,所以事务交易很重要。数据库上的 LAMP 不足以扩展。队列和重试逻辑的巨大毛球太脆弱了。为了解决这个问题,在过去的几年中,我们看到了新的解决方案的出现,这些解决方案使事务逻辑恢复了理智。它们可以大致分为以工作流为中心的方法或以数据库为中心的方法。

迄今为止,工作流引擎主要处理应用程序状态而不是业务数据,并且在与传统数据库集成时通常需要一些复杂性。以数据库为中心的方法在业务数据旁边添加了应用程序逻辑,但还没有工作流引擎相同的代码执行复杂性。 

详细介绍以工作流为中心的方法 
工作流只是基于事件或计时器执行的代码块,它们会演化应用程序状态机。事务性工作流以强有力的保证确保代码执行,防止应用程序中的部分或意外状态。开发人员编写逻辑,工作流引擎处理事务、变更和幂等性。不同的工作流引擎在向开发人员公开多少交易细节方面做出不同的权衡。 

工作流引擎获得牵引力有两种粗略的方法。其中之一(以 Temporal.io 为代表),开发人员使用标准后端编程语言(例如 Go 或 Java)编写代码,系统将确保代码运行完成,即使在出现故障时也是如此。
在此模型中,即使代码正在等待阻塞调用完成(例如读取或写入),程序调用堆栈也会得到维护。
为此,修改了语言运行时以防止在故障期间执行部分代码。

这种方法的好处是开发人员可以使用熟悉的语言编写,并使用维护的调用堆栈轻松调试。我们发现这种方法在处理大型复杂应用程序的后端团队中最为流行。 

缺点是它通常需要大量的集成工作和包装器代码才能向应用程序开发人员公开有用且安全的接口。另一个缺点是它依赖于自定义执行层而不是裸语言,并且在某些边缘情况下,执行将不同于本地语言运行时。因此,尽管开发人员可以使用他们熟悉的语言,但他们仍然需要了解底层系统的工作原理。  

另一种更受应用程序开发人员(尤其是 Typescript/Javascript)欢迎的方法是让工作流引擎充当异步功能(例如 Inngest、Defer 和 Trigger)的编排器。在这个模型中,第三方事件或功能被定向到工作流引擎,然后工作流引擎将调度由应用程序程序员注册的逻辑,一旦需要阻塞另一个异步函数,他们必须交还控制权。好处是这是一种集成到程序中的轻量级方法。它还在代码上施加了足够的结构,使处理它的团队可以更容易地理解它。但是,如果没有工具支持,这种方法可能更难调试,因此调试往往是特定于平台的。

工作流引擎特别强大,因为它们允许现有应用程序逐步采用。它们可以零星地应用于某些工作流程,占用空间最小。也就是说,工作流引擎的两个最大缺点是它们没有扩展到数据库中。因此,应用程序状态和业务数据之间没有单一的、可查询的真实来源。此外,事务语义通常不同于数据库语义,需要应用程序开发人员处理边缘条件。 

详细介绍以数据库为中心的方法 
以数据库为中心的方法从数据库开始,但将其扩展为支持任意代码执行,以允许工作流和数据管理。他们通过将控制权交给程序员来实现这一点,这样他们就可以对常规代码块的突变、事务和幂等性做出明确的决定——本质上是通过直接公开 OLTP 语义。程序员负责将业务逻辑和业务数据与应用程序状态分开。 

实际上,纯数据库视图是应用程序状态始终可以从业务数据中派生。这通常是通过将应用程序状态存储为一组修改数据库中业务数据的事务来完成的。最简单的做法是将其视为可以执行代码块的数据库,并具有与上述工作流系统相同的强大保证。 

在内部,我们将此称为应用程序逻辑事务平台 (ALTP)方法,因为它最终将 OLTP 事务扩展到应用程序中。但 ALTP 的真正特点在于,对于全新的应用程序,它可以完全避免应用程序开发人员直接管理后端基础设施的需要。  

从 ALTP 的角度来看,最常用的方法从 Firebase 开始,它提供全方位服务的“后端体验”,包括身份验证、数据存储、数据库等。Firebase 和 Supabase 等较新的进入者仍然是新项目非常受欢迎的平台。虽然他们倾向于忠于他们的 OLTP 根源——因此不支持事务性后端功能的任意代码执行——Supabase 已经开始增加对工作流的支持。

然而,像 Convex 这样的下一代 ALTP 产品确实允许将任意代码作为事务与数据库一起执行。这些产品允许使用普通语言(例如 Javascript/Typescript)编写完全事务兼容的代码,其中单个代码块可以读取、写入和更改数据——应用程序状态和业务数据。从某种意义上说,它为开发人员提供了一个单一的可查询的真实来源,并提供了像订阅这样的工作流原语。 

ALTP 解决了工作流引擎与数据库分离的问题,但因此需要用户依赖他们的数据库产品而不是标准的 OLTP 才能获得好处。因此,我们主要看到团队将 ALTP 用于新开发的应用程序,而不是将其集成到现有的复杂后端中。

总结
很明显,以工作流为中心的方法和以数据库为中心的方法正在发生冲突。这样做的主要原因是,虽然应用程序状态和数据库状态在逻辑上是不同的,但它们相互依赖,并且不涵盖两者的系统很难正确处理和调试。  

例如,考虑使用一个工作流引擎来跟踪用户结帐过程的状态机,并且该用户正在将商品添加到购物车。通常,工作流引擎确保代码步骤即使在发生故障时也能运行。但是,在某些情况下,引擎可能需要在失败期间重新运行给定的步骤,因为它不能完全确定该步骤是否已完全完成。如果该步骤涉及将业务数据写入传统数据库(在本例中为购物车中的商品),而数据库不知道重复重试,它最终会出现重复条目​​。 

有两种方法可以解决这个问题。一种方法是将问题推给应用程序开发人员,应用程序开发人员将使用工作流系统提供的随机数来确保只写入一个项目。但这假设开发人员了解幂等性,众所周知,幂等性很难做到正确,并且这消除了拥有工作流系统的许多魔力。另一种方法是将工作流引擎绑定到了解工作流事务语义的数据库。这还没有完全发生,但不难相信它会发生。 

另一方面,以数据库为中心的方法意识到通用工作流对应用程序开发人员非常有用。因此,我们开始看到数据库(如 Convex)——支持查询、突变、索引等传统数据库功能——实现调度和订阅等功能。这些允许它们用作工作流引擎。也就是说,它们允许执行具有强保证的任意代码块。 

正如 Ian Livingstone(他对这篇文章提供了反馈)所说,“这是经典的‘你是将应用程序逻辑引入数据库,还是将数据库引入应用程序逻辑?’ 再次发挥作用……这次是通过打破单体来实现的。” 几十年来一直存在这种二分法,很明显这两种模式都将在短期内持续存在。从长远来看,情况是否会一直如此还不太清楚。