0%

本地事务表 (An Acid Alternative)

本文翻译自Base: An Acid Alternative

在分区数据库中,用一致性换取可用性可以显著提高可伸缩性。

Web应用程序在过去十年中越来越流行。无论您是为终端用户还是为应用程序开发人员(服务)构建应用程序,您最希望的是您的应用程序将获得广泛采用,并且随着广泛采用,将带来事务性增长。如果应用程序依赖持久性,那么数据存储可能会成为瓶颈。

有两种策略可以扩展任何应用程序。第一个,也是迄今为止最简单的,是垂直扩展:将应用程序移动到更大的计算机上。垂直缩放对数据的效果相当好,但有几个限制。最明显的限制是超出了最大系统的容量。垂直扩展也很昂贵,因为增加事务处理能力通常需要购买下一个更大的系统。垂直扩展通常会造成供应商锁定,进一步增加成本。

水平缩放提供了更大的灵活性,但也相当复杂。水平数据缩放可以沿两个方向执行。功能扩展涉及按功能对数据进行分组,并将功能组分布在数据库中。将功能区域内的数据跨多个数据库拆分,或进行切分,即第二种策略。下图解释了水平数据缩放策略。

图1

如图1所示,两种水平缩放方法都可以同时应用。用户、产品和事务可以位于单独的数据库中。此外,每个功能区域可以在多个数据库中拆分,以获得事务处理能力。如图所示,功能区域可以相互独立地缩放。

功能分区

功能分区对于实现高度的可伸缩性很重要。任何好的数据库体系结构都会将模式分解为按功能分组的表。用户、产品、交易和沟通都是功能领域的例子。利用外键等数据库概念是维护这些功能领域一致性的常用方法。

依靠数据库约束来确保功能组之间的一致性,可以将模式与数据库部署策略耦合起来。为了应用约束,这些表必须驻留在单个数据库服务器上,从而防止随着事务速率的增长而进行水平扩展。在许多情况下,最简单的扩展机会是将数据的功能组移动到离散的数据库服务器上。
可以扩展到非常高的事务量的模式将在不同的数据库服务器上放置功能不同的数据。这需要将数据约束从数据库中移出,并转移到应用程序中。本文还介绍了本文后面将讨论的几个挑战。

CAP理论

一致性: 客户端感知到一组操作同时发生。
有效性:每个操作都必须以预期的响应结束。
分区容错性:即使单个组件不可用,操作也将完成。

具体来说,Web应用程序在任何数据库设计中最多只能支持其中两个属性。显然,任何水平扩展策略都是基于数据分区的;因此,设计师不得不在一致性和可用性之间做出决定。

ACID

ACID数据库事务大大简化了应用程序开发人员的工作。正如首字母缩略词所示,ACID交易提供以下保证:
原子性: 事务中的所有操作都将完成,或者不完成。
一致性: 当事务开始和结束时,数据库将处于一致状态。
隔离性: 事务的行为就像它是对数据库执行的唯一操作一样。
持久性: 交易完成后,该操作将不会被撤销。

数据库供应商很久以前就认识到需要对数据库进行分区,并引入了一种称为2PC(两阶段提交)的技术,用于跨多个数据库实例提供ACID保证。该协议分为两个阶段:

  • 首先,事务协调器要求涉及的每个数据库预提交操作,并指示是否可以提交。如果所有数据库都同意可以继续提交,则第2阶段开始。
    事务协调器要求每个数据库提交数据。

  • 如果任何数据库否决了提交,那么所有数据库都会被要求回滚其事务部分。缺点是什么?我们正在跨分区实现一致性。如果Brewer是正确的,那么我们必须影响可用性,但这怎么可能呢?

任何系统的可用性都是运行所需组件可用性的产物。声明的最后一部分是最重要的。系统可能使用但不需要的组件不会降低系统可用性。在2PC提交中涉及两个数据库的事务将具有每个数据库可用性的乘积的可用性。例如,如果我们假设每个数据库有99.9%的可用性,那么事务的可用性将变为99.8%,或者每月额外停机43分钟。

ACID另外选择

如果ACID为分区数据库提供了一致性选择,那么如何实现可用性呢?一个答案是BASE(基本可用,软状态,最终一致)。

BASE与ACID截然相反。如果ACID是悲观的,并在每个操作结束时强制保持一致性,那么BASE是乐观的,并接受数据库一致性将处于不断变化的状态。虽然这听起来不可能解决,但实际上它是相当容易管理的,并且会导致使用ACID无法获得的可伸缩性级别。

BASE的可用性是通过支持部分故障而不是全部系统故障来实现的。下面是一个简单的例子:如果用户被划分到五个数据库服务器上,那么BASE方式鼓励以这样一种方式精心设计操作,即用户数据库故障只影响该特定主机上20%的用户。这不需要魔法,但这确实会提高系统的可用性。

那么,既然您已经将数据分解为功能组,并在多个数据库中划分了最繁忙的组,那么如何将BASE整合到应用程序中呢?与通常应用于ACID相比,BASE需要对逻辑事务中的操作进行更深入的分析。你应该找什么?以下几节提供了一些指导。

一致性模式

按照Brewer的推测,如果BASE允许分区数据库中的可用性,那么必须确定放松一致性的要求。这通常很困难,因为业务涉众和开发人员都倾向于断言一致性对应用程序的成功至关重要。时间上的不一致性无法对最终用户隐藏,因此工程和产品所有者都必须参与选择放松一致性的要求。

下图是一个简单的模式,说明了BASE的一致性注意事项。用户表保存用户信息,包括销售和购买的总金额,这些是总数。交易表保存了每笔交易,涉及卖方和买方以及交易金额。这些都是对实际表格的过度简化,但包含了说明一致性的几个方面的必要元素。

图2

一般来说,功能组之间的一致性比功能组内部的一致性更容易降低要求。示例模式有两个功能组:用户和事务。每次出售商品时,都会向交易表中添加一行,并更新买家和卖家的计数器。使用ACID样式的事务,SQL将如图3所示。

用户表中的总买卖列可以被视为事务表的缓存。这是为了提高系统的效率。有鉴于此,对一致性的约束可以放松。可以设定买方和卖方的预期,使其运行余额不会立即反映交易结果。这种情况并不罕见,事实上,人们经常会在交易和日常余额之间遇到这种延迟(例如ATM取款和手机通话)。

如何修改SQL语句以放松一致性取决于如何定义运行平衡。如果它们只是估计,意味着可能会错过一些事务,那么更改就非常简单,如图4所示。

我们现在已经将用户表和事务表的更新解耦。不能保证表之间的一致性。事实上,第一个和第二个事务之间的故障将导致用户表永久不一致,但如果合同规定运行总数是估计数,这可能就足够了。
但是,如果估计是不可接受的呢?您如何仍然将用户和事务更新解耦?引入持久消息队列可以解决这个问题。实现持久消息有几种选择。然而,实现队列的最关键因素是确保备份持久性与数据库位于同一资源上。这对于允许在不涉及2PC的情况下以事务方式提交队列是必要的。现在,SQL操作看起来有些不同,如图5所示。

这个例子在语法上有一定的自由度,并且过分简化了逻辑来说明这个概念。通过在与insert相同的事务中对持久消息进行排队,可以捕获更新用户上的运行余额所需的信息。事务包含在单个数据库实例中,因此不会影响系统可用性。
一个单独的消息处理组件将对每条消息进行出列,并将信息应用于用户表。这个例子似乎解决了所有问题,但有一个问题。消息持久性在事务主机上,以避免排队期间出现2PC。如果消息在涉及用户主机的事务中出列,我们仍然有2PC的情况。
消息处理组件中2PC的一个解决方案是什么都不做。通过将更新分离到一个单独的后端组件中,可以保持面向客户的组件的可用性。对于业务需求,消息处理器的可用性较低可能是可以接受的。
然而,假设2PC在您的系统中永远不可接受。这个问题怎么解决?首先,你需要理解幂等的概念。如果一个运算可以应用一次或多次并得到相同的结果,则该运算被认为是幂等的。幂等运算之所以有用,是因为它们允许部分故障,因为反复应用它们不会改变系统的最终状态。
所选示例在寻找幂等性时存在问题。更新操作很少是幂等的。该示例在适当的位置增加平衡列。多次使用此操作显然会导致不正确的平衡。然而,即使只是设置值的更新操作,在操作顺序方面也不是幂等的。如果系统无法保证更新将按接收顺序应用,则系统的最终状态将不正确。稍后再详细介绍。
在余额更新的情况下,您需要一种方法来跟踪哪些更新已成功应用,哪些更新尚未完成。一种技术是使用记录已应用的事务标识符的表。
图6所示的表跟踪了交易ID(余额已被更新)和应用余额的用户ID。现在,我们的示例伪代码如图7所示。


本例取决于能否窥视队列中的消息,并在成功处理后将其删除。如有必要,这可以通过两个独立的事务来完成:一个在消息队列上,另一个在用户数据库上。除非数据库操作成功提交,否则不会提交队列操作。该算法现在支持部分故障,并且仍然提供事务性保证,而无需求助于2PC。
如果唯一关心的是排序,那么有一种更简单的方法来确保幂等更新。让我们稍微更改一下示例模式,以说明挑战和解决方案(参见图8)。假设您还希望为用户跟踪最后的销售和购买日期。你可以依靠一个类似的方案,用消息更新日期,但有一个问题。

假设两次购买发生在一个短时间窗口内,我们的消息系统不能确保有序的操作。现在的情况是,根据消息的处理顺序,最后一次购买的值不正确。幸运的是,这种更新只需对SQL稍加修改即可处理,如图9所示。

通过简单地不允许最后一次购买时间倒转,您已经使更新操作顺序独立。您还可以使用这种方法来保护任何更新不受无序更新的影响。除了使用时间,您还可以尝试单调递增的事务ID。

消息队列的排序

对有序消息传递进行简单说明是有必要的。消息系统提供了确保消息按接收顺序发送的能力。这可能需要昂贵的支持,而且往往是不必要的,事实上,有时会给人一种虚假的安全感。

这里提供的示例说明了如何放宽消息排序,并最终提供数据库的一致视图。放宽排序所需的开销是名义上的,并且在大多数情况下明显低于在消息系统中强制排序。

此外,无论交互方式如何,Web应用程序在语义上都是事件驱动的系统。客户端请求以任意顺序到达系统。每个请求所需的处理时间各不相同。整个系统组件中的请求调度是不确定的,导致消息队列不确定。要求保留顺序会给人一种虚假的安全感。简单的现实是,不确定的输入将导致不确定的输出。

软状态/最终一致(Soft State/Eventually Consistent)

到目前为止,重点一直放用一致性换区可用性上。更重要的是需要理解软状态和最终一致性对应用程序设计的影响。

作为软件工程师,我们倾向于将系统视为闭环。我们从可预测的投入产生可预测的产出的角度来考虑他们行为的可预测性。这是创建正确软件系统的必要条件。在许多情况下,好消息是使用BASE不会改变系统作为闭环的可预测性,但它确实需要从整体上观察行为。

一个简单的例子可以帮助说明这一点。考虑一个系统,用户可以将资产转移给其他用户。资产的类型无关紧要——可能是钱,也可能是游戏中的物品。在本例中,我们将假设我们已将两个操作解耦,即从一个用户获取资产,并使用用于提供解耦的消息队列将其提供给另一个用户。

马上,这个系统就会感到不确定性和问题。有一段时间资产离开了一个用户而没有到达另一个用户。此时间窗口的大小可由消息传递系统设计决定。无论如何,在开始和结束状态之间存在一个延迟,其中两个用户似乎都没有资产。

然而,如果我们从用户的角度考虑这一点,这种滞后可能不相关或甚至是已知的。接收用户和发送用户都不知道资产何时到达。如果发送和接收之间的延迟为几秒钟,对于转移资产的双方,是看不到的或者是肯定可以接受的。在这种情况下,系统行为被认为是一致的,用户可以接受,尽管我们依赖于软状态和实现中的最终一致性。

事件驱动架构

如果您确实需要知道状态何时变得一致,该怎么办?您可能有需要应用于该状态的算法,但仅当它达到与传入请求相关的一致状态时。简单的方法是依赖于在状态变得一致时生成的事件。
继续上一个示例,如果需要通知用户资产已到达,该怎么办?在事务中创建将资产提交给接收用户的事件,提供了一种机制,用于在达到已知状态后执行进一步处理。EDA(事件驱动体系结构)可以在可伸缩性和体系结构解耦方面提供显著的改进。关于EDA应用的进一步讨论超出了本文的范围。

总结

将系统扩展到惊人的交易率需要一种新的资源管理方式。当负载需要分布在大量组件上时,传统的事务模型是有问题的。分离操作并依次执行这些操作可以提高可用性和可扩展性,但代价是一致性。BASE为思考这种脱钩提供了一个模型。