Base:一种Acid的替代方案

本文是Ebay的架构师在2008年发表给ACM的文章,是一篇解释BASE原则,或者说最终一致性的经典文章. 文中Dan讨论了BASE与ACID原则的基本差异, 以及如何设计大型网站以满足不断增长的可伸缩性需求,期间如何对业务做调整与折衷. 以及一些具体的折衷技术的介绍.


在数据库分区中,以一致性换取可用性会显著提高系统伸缩性。
在过去的十年里,网络应用越来越普及。不管你正在为终端用户还是应用程序开发人员构建应用程序(即服务),您都希望您的应用程序得到广泛的采用,而广泛采用将会带来交易增长。如果您的应用程序依赖于持久化,那么数据存储可能会成为您的瓶颈。
有两种方法可用于扩展任何应用程序。第一个,也是最简单的,是垂直扩展:将应用程序移动到更大的计算机上。垂直扩展不影响数据的合理性,但是有几个限制。最明显的限制是超过系统的最大可用容量。垂直扩展也很昂贵,因为增加交易容量通常需要购买下一个更大的系统。垂直扩展通常会带来供应商依赖,从而进一步增加成本
水平扩展提供了更多的灵活性,但也相当复杂。水平数据扩展可以沿着两个维度进行。功能扩展,包括按功能分组数据,还有将不同的功能组分布到不同数据库。第二个维度是在功能区域内分割数据,分布到不同数据库,这也叫分片。图1说明了横向数据扩展策略。
如图1所示,水平扩展的两种方法都可以同时应用。用户、产品和事务可以在不同的数据库中。此外,每个功能区域都可以根据交易量跨多个数据库分割。如图所示,功能区域可以相互独立扩展。
功能分区
功能分区对于获得高的可伸缩性非常重要。任何优秀的数据库架构都会将schema分解为按功能分组的表。用户、产品、交易和通信都是功能的例子。利用诸如外键之类的数据库概念是保持这些功能区域一致性的常见方法。
依赖于数据库约束来确保跨功能组的一致性,会导致数据库在部署策略上的高耦合。为了使约束生效,表必须驻留在单个数据库服务器上,从而不能在交易数量增长时水平伸缩。在许多情况下,最简单的扩展方案是将功能组数据移动到相互独立数据库服务器上。
当交易量非常高的时候,不同的功能数据将在不同的数据库服务器。这需要将数据约束从数据库移出并在应用程序解决。这就带来了本文后面将讨论的几个挑战。
CAP原理
加州大学伯克利分校(University of California, Berkeley)教授、Inktomi的联合创始人兼首席科学家埃里克·布鲁尔(Eric Brewer)提出了一个猜想,即Web服务不能同时确保以下三个属性(缩写为CAP):
一致性。客户端知道一系列操作同时发生。
可用性。每个操作必须在预期的响应中终止。
分区容错性。即使单个组件不可用,操作也将完成。
具体来说,不管数据库如何设计,Web应用程序最多只能支持其中两个属性。显然,任何水平伸缩策略都是基于数据分区的;因此,设计师不得不在一致性和可用性之间做出选择。
ACID解决方案
ACID数据库事务极大地简化了应用程序开发人员的工作。作为首字母缩写,ACID事务提供了以下保证:
原子性。事务中的所有操作要么全部完成,要么都不完成。
一致性。当事务开始和结束时,数据库将处于一致状态。
隔离性。事务将表现为它是在数据库上执行的惟一操作。
持久性。交易完成后,操作将不会被撤销。
数据库供应商很久以前就认识到数据库分区的需求,并引入了一种称为2PC(两阶段提交)的技术,用于在多个数据库实例中提供ACID保证。协议分为两个阶段:
•首先,事务协调器要求涉及的数据库预提交操作,并说明提交是否可行。如果所有数据库都同意commit,那么第2阶段就开始了。
•事务协调器要求每个数据库提交数据。
如果任意数据库不能commit,那么所有数据库都被要求回滚它在事务中的操作。缺点是什么?我们在各个分区之间保持一致,如果Brewer是正确的,那么这必然影响可用性,但这怎么可以呢?
任何系统的可用性都是执行操作相关组件的可用性的产物。这句话的最后一部分是最重要的。系统使用的非必要组件不降低系统可用性。在2PC提交中涉及两个数据库的事务是每个数据库可用性的的产物。例如,假设每个数据库有99.9%的可用性,那么事务的可用性就会达到99.8%,或者每个月额外的停机时间为43分钟。
ACID的替代
如果ACID为分区数据库提供了一致性选择,那么如何实现可用性呢?一个答案是BASE(基本可用,软状态,最终一致性)。
BASE与ACID正好相反。在每个操作结束时,ACID都是悲观的,并且强制一致性,BASE是乐观的,并且接受数据库的一致性将处于变化状态。虽然这听起来是不可能的,但实际上它是很容易管理的,并且获得了ACID无法实现的可伸缩性。
通过允许部分失败,避免完全系统故障,实现了BASE的可用性。这里有一个简单的例子:如果用户在5个数据库服务器上进行分区,那么BASE设计就会鼓励以这种方式操作,而用户数据库的失败只会影响到该主机上20%的用户。没有任何魔法,但这确实带来了系统更高的可用性。
因此,现在已经将数据分解为功能组,并将最繁忙的组划分为多个数据库,那么如何将BASE融入到应用程序中呢?BASE需要对逻辑事务中的操作进行更深入的分析,而不是像ACID那样简单使用。你应该如何分析?以下部分提供了一些指导。
一致性模式
根据Brewer的推测,如果BASE允许在分区数据库中可用,那么就必须识别出弱化一致性的条件。这常常是困难的,因为业务相关者和开发人员都倾向于断言一致性对应用程序的成功是至关重要的。暂时的不一致性也瞒不过终端用户,所以工程和产品的负责人都必须参与弱化一致性的条件选择。
图2是一个简单的模式,它演示了BASE的一致性考虑。用户表保存用户信息,包括售出和购买的总金额。这些都是运行总数。事务表保存每个交易,涉及买卖双方和交易金额。这些表过于粗略但包含了说明一致性那几个方面的必要元素。
一般来说,功能组之间的一致性比功能组更容易弱化。示例模式有两个功能组:用户和交易。每次销售一个商品时,都会在交易表中添加一行,并更新买方和卖方的数据。使用ACID事务,SQL如图3所示。
在用户表中购买和出售额的列可以看作是交易表的缓存。这是为了系统效率。考虑到这一点,对一致性的限制可以弱化。买方和卖方他们的运行余额可以不立即反映交易的结果。这并不少见,事实上,人们经常会在交易和他们的余额之间遇到这种延迟(例如,ATM取款和手机通话)。
如何修改SQL语句来弱化一致性取决于如何定义运行时结算的余额。如果它们只是简单的估计,意味着一些事务可以被忽略,那么更改非常简单,如图4所示。
我们现在已经将对用户和交易表的更新解耦了。不能保证表之间的一致性。事实上,第一次和第二次事务之间的失败将导致用户表永久不一致,但是如果协议规定运行统计总数是估计值,那么这也许就足够的。
但是,如果估计值是不能接受的呢?如何才能将用户和事务更新解耦?引入持久消息队列解决了这个问题。实现持久消息有几种选择。然而,实现队列最关键的因素是确保持久性支持与数据库使用同一资源上。这让队列在不引入2PC的情况下进行事务处理是十分必要的。现在,SQL操作看起来有些不同,如图5所示。
为了说明这个概念,在这个例子中,对语法进行了一些修改,并将逻辑简化。通过将持久性消息排成队列,并且和insert在同一事务中,更新用户余额的信息就被获取了。事务在单个数据库实例中,因此不会影响系统可用性。
一个单独的消息处理组件将对每个消息进行处理,并将信息更新到用户表。这个例子似乎解决了所有的问题,但是有一个问题。消息持久化在同一台主机事务中,以避免在排队时使用2PC。如果消息在涉及用户模块的主机事务中被移除,我们仍然面临2PC的情况。
在消息处理组件中,2PC的一个解决方案是什么都不做。通过将更新分离为一个单独的后端组件,您可以保持面向客户的组件的可用性。对于商业需求,消息处理器的低可用性是可以接受的。
然而,假设2PC在您的系统中是绝对不能接受的。如何解决这个问题?首先,你需要理解幂等性的概念。如果一个操作可以被应用一次或多次,并且得到相同的结果,那么它就被认为是幂等的。幂等运算是有用的,因为它们允许部分失败,因为重复使用它们不会改变系统的最终状态。
在发现幂等性时,所选的例子是有问题的。更新操作不是幂等的。这个示例增加了余额。多次使用此操作显然会导致不正确的余额。然而,即使是简单地设置一个值的更新操作,考虑到操作顺序上,它也不具有幂等性。如果系统不能保证按接收到的顺序更新,系统的最终状态将是不正确的,甚至还更严重。
在余额更新的情况下,您需要一种方法来跟踪哪些更新已经成功操作,哪些还未完成。有一种技术是使用表记录已操作交易的标识。
图6中显示的表跟踪交易ID,哪个余额已被更新,还有用户ID。现在我们的示例伪代码如图7所示。
这个示例依赖于这一机制,它能够在队列中查看消息并在成功处理后删除它。这可以通过两个独立事务来完成:一个在消息队列上,一个在用户数据库上。除非数据库操作成功提交,否则不提交队列操作。该算法现在支持部分故障,并且仍然提供事务保证,而无需求助于2PC。
如果只关注排序,有一种更简单的技术保证幂等更新。让我们稍微改变一下示例模式,说明面临的挑战和解决方案(参见图8)。假设您还希望跟踪用户的最后一次销售和购买日期。您可以使用类似的方法通过消息更新日期,但是这样会产生一个问题。
假设两个购买发生在一个短时间窗口内,而我们的消息系统不能确保有序的操作。您现在的情况是,如果根据处理消息的顺序更新,last_purchase值可能不正确。幸运的是,这种更新可以通过对SQL的小修改来处理,如图9所示。
通过不允许last_purchase时间向后退,您已经使更新操作顺序不相互影响。您还可以使用此方法来防止无序更新。如果不使用时间,您还可以尝试单调递增的事务ID。
消息队列顺序
关于有序消息传递的简短说明是有用的。消息系统提供了确保消息按收到的顺序发送的能力。这一支持代价可能是昂贵的,而且往往是不必要的,而且,事实上,有时会给人一种虚假的安全感。
这里提供的示例说明了消息排序可以弱化要求,最终还是可以保证数据一致性。弱化排序所需要的开销是微不足道的,在大多数情况下比在消息系统中执行排序要少得多。
此外,Web应用程序在语义上是一个事件驱动的系统,不管什么交互形式。客户端请求以随机顺序到达系统。每个请求所需的处理时间各不相同。整个系统组件的请求调度是不确定的,导致消息的不确定性排队。要求顺序被保存会给人一种虚假的安全感。显而易见的事实是,不确定性输入将导致不确定性输出。
软状态/最终一致性
到目前为止,关注的焦点一方面是以一致性换取可用性,另一方面是理解软状态和最终一致性对应用程序设计的影响。
作为软件工程师,我们倾向于把我们的系统看作闭环系统。我们考虑他们行为的可预测性,在可预测的输入中产生可预测的输出。这是创建正确的软件系统的必要条件。在许多情况下,好消息是使用BASE并不会改变系统作为闭环的可预测性,但它确实需要从整体上来进行审视。
一个简单的例子可以说明这一点。考虑一个用户可以将资产转移给其他用户的系统。资产的类型是不相关的——它可能是游戏中的钱或对象。对于本例,我们假设已经通过消息队列解耦了从一个用户获取资产,转给另一个用户的两个操作。
这个系统马上产生不确定性。有一段时间,只是从用户中获取资产,而没有转给另一个用户。这个时间窗口的大小可以由消息传递系统设计来决定。无论如何,在开始和结束状态之间有一个时间差,在这些状态中,两个用户似乎都没有资产。
但是,如果我们从用户的角度来考虑这个问题,那么这种滞后可能并不相关,甚至不为人所知。接收用户和发送用户都不知道资产何时到达。如果发送和接收之间的时间差是几秒钟,那么对于直接进行资产转移的用户来说,它是不可见的,或者是可以容忍的。在这种情况下,系统行为被认为是一致的,并且可以被用户接受,即使我们依赖于软状态和最终一致性。
事件驱动架构
如果你确实需要知道什么时候状态是一致的呢?您可能需要将算法运用于状态,但只有当它达到与传入请求相关的一致状态时才需要。简单的方法是依赖于在状态一致时生成的事件。
继续前面的示例,如果需要通知用户资产已经到达,该怎么办?在将资产提交给接收用户的事务中创建事件,这提供了一种机制,用于在到达预定状态之后执行进一步处理。EDA(事件驱动架构)可以在可伸缩性和架构解耦方面提供显著的改进。关于EDA应用的进一步讨论超出了本文的讨论范围。
结论
将系统扩展到引人注目的交易率需要一种新的管理资源的方式。当负载需要跨越大量组件时,传统的事务模型会产生问题。将操作解耦并依次执行,牺牲一点一致性,可以提高可用性和伸缩性。BASE提供了一种思考这种解耦方式的模型。
参考
1. http://highscalability.com/unorthodox-approach-database-design-coming-shard.
2. http://citeseer.ist.psu.edu/544596.html.
DAN PRITCHETT是易趣的一位技术人员,在过去的四年里他一直是这个架构团队的一员。在任职期间,他与eBay市场、PayPal和Skype的战略、业务、产品和技术团队进行广泛协作。Pritchett拥有超过20年的科技公司工作经验,曾任职于Sun Microsystems, hp,和Silicon Graphics。Pritchett拥有丰富的技术经验,从网络层协议与操作系统到系统设计和软件模式。他曾获得密苏里大学计算机科学学士学位。

作者:DAN PRITCHETT
译者:java达人
来源:https://queue.acm.org/detail.cfm?id=1394128

评论

此博客中的热门博文

近期折腾 tailscale 的一些心得

高可用用户中心设计

群晖硬软件的的各种坑及解决方案

打造一个可国内访问的Blogger(Blogspot)方法

星际蜗牛安装黑裙(群晖)制作家用nas

Cloudflare免费版设置说明

N1 PT下载小钢炮固件下载及安装说明

分析redis key大小的几种方法

Windows7系统目录迁移:Users,Program Files,ProgramData

个性化推荐从入门到精通