Base: ACID外的另外一种选择

Base: ACID外的另外一种选择

 

在分区数据库中,为可用性牺牲一定的一致性能够显著提高可扩展性

DAN PRITCHETT, EBAY

 

在过去的十年中,Web应用变得日益流行。无论您是为最终用户构建应用程序还是为应用程序开发人员(即服务),您希望您的应用程序是最可能被广泛采用的,而广泛的应用将会使事务处理量增长。如果您的应用程序依赖于持久性,那么数据存储很可能将成为你(系统)的瓶颈。

 

任何应用程序可以有两种策略进行扩展。第一种,也是迄今为止最简单的,是垂直扩展:把应用迁移到大型机。垂直扩展对数据来说相当不错,但有一些局限性。最明显的局限性在于系统最大可用容量的扩展。垂直扩展也很昂贵,增加事务处理能力,通常需要采购一个(比现在用的系统)更大的系统。垂直扩展通常也会导致供应商锁定,而这将进一步增加了成本。)

[译者备注:供应商锁定是指你采用了一个技术,即将自己锁定在这家提供商身上,不能轻易转换提供商。]

 

水平扩展则提供了更多的灵活性,但也更复杂。水平数据扩展可以沿两个维度进行。功能扩展涉及到根据功能进行数据分组以及对这些功能组进行跨数据库部署。而将属同一功能域内的数据进行跨数据库的拆分,或叫分片,这是水平扩展的第二个维度。图1展示了水平数据扩展策略。

扫描二维码关注公众号,回复: 1140162 查看本文章

正如图1所示,这两种水平扩展的方法可以一起使用。userproductstransaction,可以分隔在不同的数据库中。此外,每个功能区也可以被分割在多个数据库以提高事务处理能力。正如图中所示,每个功能区可以彼此独立地扩展。

 

功能分区

实现高可扩展性,对功能进行分区是非常重要的。任何一个优秀的数据库架构将依据[不同的]功能把[对应]模式分组拆分到不同的表中。userproducts, transactioncommunication均是各个功能区的例子。利用数据库的概念,如外键,是一种在这些功能区之间保持一致性的通用方法。

 

依托数据库[本身提供的]约束机制来保证各个功能组之间的一致性,这对数据库部署策略来说是一个耦合的模式。应用这些约束的表必须驻留在一个单一的数据库服务器中,随着事务量的增长这将不利于进行水平扩展。在许多情况下,最简单的向外扩展办法是把不同功能组的数据迁移到分散的数据库服务器中。

 

能够扩展到支持非常高事务量的架构将功能不同的数据分布在不同的数据库服务器上。这需要把数据约束从依赖数据库转到依赖应用程序本身。但这会存在一些挑战,这篇文章稍后章节将会重点介绍。

 

CAP定理

埃里克·布鲁尔,加州大学伯克利分校教授,兼lnktomi的联合创始人和首席科学家,提出了一个猜想:Web服务无法确保所有下列的三个属性同时满足(缩写为CAP):

 

一致性:客户感知到一系列操作能够一下子执行完毕。

 

可用性:每一个操作必须有意想中的响应并终止。

 

分区耐受性:操作均能够执行完毕,即使个别组件不可用。

 

具体而言,Web应用程序,任何数据库设计中,最多只有可以支持到这些属性中的两个。显然,任何水平扩展策略是基于对数据进行分区,因此,设计人员不得不在一致性和可用性之间做选择。

 

ACID的解决方案

ACID数据库事务处理大大简化了应用程序开发人员的工作。正如缩写所指,ACID事务提供了以下的保证:

 

原子性:同一事务中的所有操作要么都完成,要么都没有完成。

 

一致性:数据库在事务的开始和结束时将处于一致的状态。

 

隔离性:每个事务的行为就好像它在数据库中是唯一执行的操作一样。

 

永久性:在事务完成后,该操作的结果将无法逆转。

 

数据库厂商很久前就认识到有必要进行数据库分区,并介绍了被称为2PC(两阶段提交)的技术,该技术提供了跨多个数据库实例的ACID保证。该协议分成两个阶段:

 

首先,事务处理协调器要求所涉及的每个数据库进行预提交操作,并上报是否可能进行提交操作。如果所有数据库同意可以进行提交,则第2阶段开始。

事务处理协调器要求每个数据库提交数据。

如果有任何数据库否决提交操作,则要求所有的数据库回滚部分的事务。缺点是什么呢?如果布鲁尔是正确的,我们获得了分区的一致性,那么我们就必然会影响到可用性,但怎么可能呢?

 

任何一个系统的[整体]可用性是由操作所涉及到的各个组件的可用性所决定的。该声明的最后部分是最重要的。系统可能用到的但不是必须的组件不应降低系统的整体可用性在两阶段提交中涉及两个数据库的事务的可用性将由每个数据库的可用性决定。例如,如果我们假设每个数据库的可用性是99.9%,那么事务的可用性变为99.8[译者:总体可用性=99.9%*99.9%=99.8%],或额外的停机时间为43分钟/每月。

 

ACID外的另外一种选择

既然ACID已经为分区数据库提供了一种一致性的选择,那么你该如何去获取可用性呢?答案之一是BASE(基本可用,软状态,最终一致)。

 

BASEACID是截然不同的。ACID是悲观的,并强制保证每一个操作完毕后的一致性,而BASE是乐观的,它接受该数据库的一致性将处在不定的状态中。虽然这听起来比较棘手,但在现实中,它是相当易于管理且可以获得ACID中无法获得的可扩展性。

 

BASE的可用性是通过容忍部分失败而不导致系统整体不可用而获得的。下面是一个简单的例子:如果user被划分在5个数据库服务器中,基于BASE的设计则鼓励比较特殊的操作方式:某个user数据库的故障仅影响分布在该主机上的20%的用户。这没有什么神奇的,但能够获得更高的系统可用性。

 

所以,现在你已经把数据分解到不同功能组且把最繁忙的功能组进行跨数据库的分区,那你将如何把BASE用到你的应用程序吗?相比典型的ACID应用,BASE需要更深入的分析一个逻辑事务中的操作情况。你应该关注什么?以下各节提供了一些指引。

 

一致性模式

根据的布鲁尔猜想,如果BASE允许分区数据库的可用性,那么放松一致性的机会是必然的。但这是非常困难的,因为企业的利益相关者和开发商趋向于断言,一致性对应用的成功是最重要的。从最终用户来看暂时的不一致不能被屏蔽,所以工程负责人和产品负责人两者都必须挑选放松应用一致性要求的机会。

【译者:这里的意思应该是由于企业利益相关者和开发商断言一致性非常重要,且暂时不一致对用户来说无法屏蔽,因此采取放松一致性策略的时候,工程和产品负责人需要参与一起讨论协商,达成共识】

 

2是一个简单的例子,展示了BASE在一致性方面的考虑。user表保存用户的信息,包括出售和购买的总金额。这些是运行中的总数。transaction表保存有关的每一笔交易,包括买家和卖家以及交易的金额。虽然这些表相比真正的表过于粗略,但包含了展示一致性方面必要的元素了。

 

一般情况下,跨功能组比在合在同一组内更容易放松一致性。示例模式有两个功能组:usertransaction。每次卖出一个物品,将会往transaction表添加一条记录和更新买方和卖方的计数。若SQL使用ACID式的事务,则对应的SQL将如在图3中所示。

 

user表中的总购买和出售列可以认为是transaction表的一个缓存。这是本系统的效率。鉴于此,一致性的约束可以放宽。我们可以设置买卖双方方的预期,让他们知道他们的余额不会立即反映一个交易的结果。其实这是并不少见,事实上人们常常会遇到这种交易和他们的余额之间延迟的情况(例如,ATM取款和手机打电话)。

 

如何修改SQL语句以支持放松一致性则取决于余额是如何定义的。如果它们是简单的估计,这意味着某些交易可以容忍丢失,那么修改就变得很简单了,如图4所示:

 

现在,我们已经把更新操作从user表和transaction表分离了。表之间的一致性无法得到保证。事实上,若第一和第二个事务之间发生了故障,将导致user表中的永久不一致,但如果规定余额只用于估算的,这可能已经足够了。

 

但如果估算是不能接受的呢?你将如何能够把更新操作从user表和transaction表解耦呢?这里介绍一个持久消息队列机制来解决这个问题。实现持久性消息有多种选择。但是,在实现队列中,最关键因素是确保在同一资源上的数据库上支持持久性[译者说明:即入队列的操作跟transaction在同一主机上,这样可以避免分布式事务的2PC]。这是必要的,

能够保证跟事务提交队列无需涉及到2PC(两阶段提交)。现在的SQL操作看起来有点不同,如图5所示。

 

这个例子需要一些自由的语法和非常简化的逻辑来说明这个概念。通过在同一事务中进行插入操作把永久消息入队列,能够捕获到更新正在运行的user表所需要的信息,该事务包含在单一的数据库实例中,因此不会影响系统的可用性。

 

一个单独的消息处理组件将出列的每个消息运用到对应的user表。这个例子貌似解决了所有的问题,但有一个问题。在入队列操作中,为了避免2PC,消息持久性是在事务主机上的。若消息出列操作在涉及user所在主机的事务内,则我们仍然需要2PC

 

在消息处理组件中处理2PC问题的一个解决方案是什么都不做。通过把更新操作解耦到一个单独的后端组件,你可以保留面向客户的组件的可用性。对业务需求来说,消息处理器较低的可用性可能是可接受。

 

然而,假设2PC在您的系统中是根本无法接受的。这个问题如何解决呢?首先,你需要了解幂等性的概念。一个操作如果可以执行一次或多次且都具有相同的结果,则该操作认为具有幂等性。具有幂等性的操作是非常有用的,因为它们允许部分失败,他们反复运用和执行但不改变系统的最终状态。

 

寻找幂等性时所选择例子的问题是:更新操作很少幂等。例如增加余额列的地方。应用这个操作不止一次显然会导致一个不正确的余额,哪怕只是简单地设置一个值的更新操作,然而如跟操作顺序相关,则也不是幂等的。如果系统不能保证以接收的先后顺序进行更新操作,系统的最终状态将是不正确的。后面有更多这块的讨论。

 

在余额更新的例子中,你需要一种方法来跟踪哪些更新已成功地执行和哪些依然未执行。其中一种技术是使用一个表,该表记录已被成功执行的事务标识符。

 

在图6中所示的表跟踪记录了余额已被更新的事务ID和余额已经增加的用户ID。现在,我们的示例伪代码如图7所示。

 

 

 

 

【译者说明:例子有些错误:

Select count(*) as processedfrom updates_applied where trans_id=message.trans_id and balance=message.balance and user_id=message.user_id;

用于查找对应的记录是否已经处理,若仍未处理,则进入下一步取出队列处理。】

 

这个例子依赖于在队列中能够提取一个消息并且一旦处理成功可以从队列中移除。如果有必要这可以用两个独立的事务:一个在消息队列和一个对用户数据库。除非数据库操作成功并提交否则队列操作可以不提交。而现在的算法支持部分失败,并仍然提供事务保证而无需借助于2PC

 

有一个简单的技术,以确保幂等的更新,唯一担心的就是顺序。让我们稍微改变一下我们的示例模式来说明这种挑战和解决方案(参见图8)。如果你也想追踪用户的最后购买和销售的日期。你可以依靠类似消息更新日期的模式,但有一个问题。

 

假设两个购买发生在一个短的时间窗口内,并且我们的消息系统并不确保按序操作。现在有一种情况,即依赖于处理消息的顺序,last purchase将会有一个不正确的值。幸运的是,通过稍微修改SQL语句这种更新可以进行处理,如在图9所示。

 

通过不允许last_purchase的值小于向后的某个时间,您所做的更新操作便跟顺序无关【译者说明:即update的时候只能update那些last_purchase小于交易的记录,这样就无需确保消息处理的顺序了,相当于是在user表同步保存了一个最新交易的时间戳】。您也可以使用这个方法来保护任何更新以避免无序。作为使用时间的替代方法,你也可以尝试一个单调递增的事务ID

 

消息队列的顺序

关于有序消息分发的不足也是值得注意的【译者:翻译不是很明确,原句是A short side note on ordered message delivery is relevant.】。消息系统提供了一种确保按他们接收消息顺序进行分发的能力。这可能是昂贵且往往是不必要的,而且,事实上,有时给人一种虚假的安全感。

 

这里提供的例子说明如何可以放宽消息的顺序,并仍然提供了一个数据库最终一致性的看法。放松顺序所需的开销是名义上的,并在大多数情况下是显著低于在消息系统中强制有序性[的开销]

 

此外,Web应用程序在语义上是一个事件驱动的系统而跟交互的风格无关。客户端请求以任意顺序到达系统。处理每个请求需要的时间各不相同。贯穿在整个系统各个组件的请求调度是不确定的,这造成非确定性的消息队列。顺序要求被保留只不过是以给人一种虚假的安全感。而简单的事实是不确定的输入会导致不确定的输出。

 

软状态/最终一致性

到此刻为止,焦点在于用一致性换取可用性。另一面是理解软状态和最终一致性对应用程序设计上的影响。

 

作为软件工程师,我们往往看我们的系统是一个封闭循环。我们认为,系统的行为具有可预性,即可预见的输入产生可预测输出。这对建立正确的软件系统是必要的。在许多情况下,好消息是,使用BASE并不改变为闭环系统的可预测性,但它确实需要研究[系统的]总体行为。

 

一个简单的例子可以说明这一点。考虑一个系统,用户可以将资产转让给其他用户。这里资产的类别是无关的,可以是在游戏中的金钱或物品。在这个例子中,我们将假设使用消息队列机制用于解耦,且我们已经分离了两个操作 -资产从一个用户转出和资产从另外一个用户存入。

 

随即,[我们]可以感到该系统是不确定和有问题的。资产从一个用户转出且未转入另外一个用户之间存在一段时间的延迟。这个时间窗口的大小可以由消息传送系统的设计来确定。无论如何,在开始和结束状态之间存在一个滞后时间,这个滞后时间中两个用户貌似资产都消失了[译者说明:此时用户A已经转出资产了,但用户B没收到资产,所以两个用户都没有资产,貌似资产消失了]

 

然而,如果我们从用户的角度来看,这种滞后可能是不相干或用户是感知不到的。无论是接收用户还是发送的用户可能均不知道什么时候该资产到帐。如果发送和接收之间的时间差只是几秒钟,这对资产转移进行直接沟通的用户来说无疑是感知不到或是可以一定容忍的。在这种情况下,系统的行为被认为是一致的且用户能够接受的,即使我们在实现中依赖软状态和最终一致性。

 

事件驱动的架构

如果你确实需要知道什么时候状态已经成为一致的吗?您可能需要一些运用于状态的算法,但只有当它已达到跟输入请求相关的一致的状态,简单的方法是依赖随状态变一致所产生的事件。【[译者说明:翻译不是很准,大家有好的可以分享,原句是:What if you do need to know when state has become consistent? You may have algorithms that need to be applied to the state but only when it has reached a consistent state relevant to an incoming request. The simple approach is to rely on events that are generated as state becomes consistent.

 

继续前面的例子,如果你需要通知用户的资产已经到账呢?在事务中创建一个提交资产给接收用户的事件,该事件提供了一个一旦到达某个已知的状态进行进一步处理的机制。 EDA(事件驱动的架构)可以提供显著改善的可扩展性和架构解耦。关于EDA应用的进一步的讨论则超出了本文的范围。

 

结论

扩展系统以显着支持大量事务需要一种新的方式考虑资源管理。当负载需要分散到大量的组件上时,传统的事务模式是有问题的。操作解耦和执行,在牺牲一定的一致性下,反过来可以提高可用性和可扩展性。BASE提供了一种思考这种解耦的模式。

 

参考文献

http://highscalability.com/unorthodox-approach-database-design-coming-shard

http://citeseer.ist.psu.edu/544596.html

DAN PRITCHETT一直是eBay的一名技术人员,在过去的四年里,他已经是ebay架构团队的成员。In this role, he interfaces with the strategy, business, product, and technology teams across eBay marketplaces, PayPal, and Skype.。他在如Sun Microsystems, Hewlett-Packard Silicon Graphics公司有拥有超过20年的经验,在技术方面,Pritchett有极深的经验,领域涉及包括从网络级协议和操作系统到系统设计以及软件模式。他从密苏里大学罗拉分校的计算机科学获得了理学学士。

猜你喜欢

转载自ituski.iteye.com/blog/2196775