美文网首页
BASE理论应用

BASE理论应用

作者: hdszzwy | 来源:发表于2021-11-15 15:19 被阅读0次

本文翻译了英文文章《In partitioned databases, trading some consistency for availability can lead to dramatic improvement in scalability》,部分段落进行了自我解读。

做个大系统?

受人欢迎的购物网站,人山人海的社交网站,并发量大,数据量大,应用系统纵然能撑住访问的流量,那么假如这个系统依赖于持久化的存储,那么数据存储可能会成为你的系统瓶颈。因为这样的系统一般数据量大,事务量大。那么如何让你的数据存储应对这么大的数据量和事务量呢?两条路可以选择:水平扩展和垂直扩展。相较于垂直扩展,水平式的扩展更加灵活也更加复杂。数据的水平扩展时可以从两个维度考虑:1. 功能上的水平扩展:需要考虑如何按照功能组织数据和如何适应数据库的分布。2. 功能切分后,再考虑在功能内部继续切分数据,称之为分片(sharding)。图一描述了这两个维度。


image-20211113155737755.png

一如图一所示,两个维度可以同时考虑进来。首先,按照功能将用户数据(user),产品数据(products)和交易数据(trans)可以分布在不同的数据库中,之后在功能上继续切分数据,将同一功能上的数据切分至不同的数据库中以提高交易数据的存储量。在功能上切分数据时,与其他功能上的数据是独立考虑的。

功能切分(functional partitioning)

功能切分对于实现高可扩展性是极其重要的,任何一个优秀的数据库架构都在将数据模式切分至数据表时考虑到功能。用户功能,商品功能,交易功能,通讯功能都是切分之后的典型功能模块。使用数据库内部的机制(如外键依赖)以实现不同功能之间的数据一致性是一种常用的策略,但是依赖于数据库系统的内部约束同时也使得数据库模式与数据库部署耦合严重。因为,当不考虑水平扩展时,要用数据库的内部机制,数据表就一定需要部署在一个数据库系统中。在更多的情况中,水平扩展的简单实现是将按照功能切分后的数据分布至不同的服务器上。<u style="box-sizing: border-box;">这时,需要将数据的一致性约束从由数据库约束转向由应用服务进行约束。</u>

CAP理论

Eric Brewer提出了web系统的CAP理论,并且证明了CAP这三个属性,web系统最多只能实现两个。

C:Consistency(一致性),客户无论访问哪一个服务都可以得到相同的结果。

A:Available(可用性),每一个操作必须在短时间内得到响应。

P:Partition tolerance(分区容错性),即便系统的部分功能不可用,操作也必须能够完成。

不论要使用的存储系统是什么,一个web系统最多只能实现以上两个属性。显然,水平扩展一定是Partiton tolerance,那么web系统的设计者就必须在C和A之间做出取舍。

事务的ACID解决方案

基于ACID理论的数据库事务大大减轻了应用开发者的工作,下面是ACID理论的介绍:

A:Atomicity,操作要么全部完成,要么一点儿都没完成。

C:Consistency,当操作结束后,数据库将会处于一个一致性状态。

I:Isolation,各个操作之间互不影响。

D:Durability,操作结束后,对于数据库的影响是持久的

数据库管理系统的提供商在很早边意识到了分布式存储系统的市场,之后引入了2PC(两阶段提交)技术用于在分布式存储的数据库管理系统中支持ACID。两阶段提交技术将事务提交分为如下两个阶段:

第一阶段:事务协调者(2PC中接收事务请求的角色)向各个数据库组成成员预提交事务,以获取事务是否可以在所有的数据库成员上执行,若是所有的数据库成员均告知事务协调者可以执行此事务,那么第二阶段开始。

第二阶段:事务协调者告知所有的数据库组成成员事务可以提交,此时数据库组成成员提交事务。

若是在第一阶段中,任何一个数据库组成成员告知事务协调者不可以执行此事务,那么所有的数据库将进行事务回滚。

此时,为了完全分布式的web系统,还有什么问题吗?以上描述中,这样无论web系统如何水平扩展(此时已经实现了CAP的P),事务如何地增加,我们都可以利用数据库实现的一致性。那么,如果Brewer是对的,我们必须要抛弃可用性,这怎么可能,不应该这样啊,因为可用性对web系统来说太重要了。

单单考虑存储系统的话,对于任何一个web系统来说,可用性指组成存储系统的各个组件的可用性的组合。任何一个组件的失效都会影响系统的整体可用性。采用了2PC技术的数据库事务操作的可用性的概率将会是各个组件的可用性概率的乘积,比如说,数据库成员的可用性概率是99.9%,那么数据库系统的整体可用性是99.8%,即每月43分钟,这仍然是不可接受的。

与ACID不同的事务解决方案

如果已经由实现ACID的分布式数据库来实现服务的一致性,那么可用性怎么实现呢?答案是BASE(basically available,soft state,eventually consistent)。

BASE与ACID有明显的不同。ACID是一种悲观实现,要求每次操作必须实现数据一致性;而BASE是一种乐观的实现,可以容忍数据修改过程中的中间状态。尽管这听起来不可思议,但是这使得事务管理很方便,同时因不再强制要求ACID而更加的灵活。

BASE只保证了整个系统不崩溃,而不再保证所有的功能可用。举例来说,如果用户表分布在五个不同的数据库服务器上,那么BASE设计原则鼓励你的web系统可以在一个数据库服务器宕机的情况下继续为其他80%的用户服务(假设用户表均匀分布)。这种设计使得系统的可用性更高。

当数据已经按照功能进行切分并且将计算量大的功能数据分不到了多个数据库服务器上,那么web系统具体如何实现BASE呢?此时,BASE原则需要设计人员对事务的逻辑,相较于基于ACID数据库的原则来设计,有更加深入的认识。

一致性模型

根据Brewer的CAP理论,如果保证了可用性和分区容错性,那么必须要放宽对于一致性的限制。由于商用应用及其开发者将一致性看做是应用服务是否成功的重要指标,但是又很难保证,而短暂的不一致性又有可能被用户感知,所以工程师和产品经理必须放宽对一致性的限制。

下面用图二来说明BASE对一致性的考虑。用户表保存了用户信息,包括总销售额和总消费额,这些额度是实时更新的。交易表中保存了详细的交易记录,消费记录记录了销售方,消费方和交易额度。这两张表是对商用表设计的简化版本,但是包含了本文需要的必要元素。跨功能模块的一致性比功能模块内部的一致性相比,更加容易被放宽。例如,每次商品交易,交易表内需要插入一行,而交易双方的账面额度需要更新。对于这个交易过程,基于ACID风格的事务,SQL语句如下所示:

Begin transaction
   Insert into transaction(id, seller_id, buyer_id, amount);
   Update user set amt_sold = amt_sold + $amount where id = $seller_id;
   Update user set amt_bought = amt_bought +$amount where id = $buyer_id;
End transaction

若是将用户表中的总销售额和总消费额两列看做是交易表的缓存,那么这两列的更新速度可以看做是系统的执行效率(先插入交易看做是写入磁盘,那么可能磁盘写入后缓存没有更新)。而现实的交易中,交易双方的在交易后观察到的账户额度没有及时更新,是可以接受的。在放宽对于一致性的限制后,SQL语句应该如何修改呢?若是账户额度是一个大致的估值,即允许存在交易的丢失,那么,SQL语句可以简单修改为:

# 交易功能模块中执行
Begin transaction
   Insert into transaction(id, seller_id, buyer_id, amount);
End transaction
# 用户功能模块中执行
Begin transaction
   Update user set amt_sold = amt_sold + $amount where id = $seller_id;
   Update user set amt_bought = amt_bought +$amount where id = $buyer_id;
End transaction

此时,交易表和用户表已经实现了事务上的解耦,但是数据的一致性却没有保证,而一次的不一致将会导致永远的不一致。若产品经理设计就只是个估值,那目前就可以了。但是实际应用中,这是不可接受的,那该怎么办?我又想解耦又想保证数据一致性呢!引入一个可持久化的消息队列就可以了。这种消息队保证的实现方式很多,此处不提,但是在使用消息队列时,必须保证操作的对象在执行此操作时还未发生改变(就像是对这个对象加了锁的意思,但实际上没有加锁)。向队列发送消息应该是事务性的(这应该由消息队列系统来保证),只有这样才能保证事务正确提交。引入消息队列后的执行语句如下(非完全的SQL):

Begin transaction
   Insert into transaction(id, seller_id, buyer_id, amount);
   Queue message "update user('seller', seller_id, amount)";
   Queue message "update user('buyer', buyer_id, amount)";
End transaction
For each message in Queue
   Begin transaction
   Dequeue message
   If message.balance == 'seller':
     Update user set amt_sold = amt_sold + $amount where id = message,id;
   Else 
     Update user set amt_bought = amt_bought +$amount where id = message,id;
   End if
 End transaction
End for

这段代码使用一些非SQL的语法以简化逻辑说明原理,在将更新语句写入消息队列时,同时将用户表中的相关用户锁定并在更新后解除锁定,这样就可以保证执行更新用户信息时的操作对象还未改变,同时不影响系统的可用性。这里我们注意一个细节,发送消息的程序被部署在交易功能模块运行的主机上,以防止执行insert时由于事务提交失败。

引入一个独立的消息队列的消费者依次读取消息队列中的消息并执行相关操作,看起来解决了一致性的所有问题,但真的是这样吗?不,还有个问题。若是用户功能在进行更新时失败的情况(这是交易信息已经插入到交易表里面了),怎么办?一种消极的解决方案是什么都不做,这时你保证了面向客户的显示上的可用性(尽管不能进行交易)。这种低可用性可能是可接受的,但若是不可接受的呢?这时,我们要用到幂等的概念。如果一个操作执行一次和执行多次得到的结果是相同的,那么就称这个操作是幂等的。幂等的操作在操作时部分失败时是非常有用的,因为多次提交不会影响系统的最终状态。

更新操作很少是幂等的,如上面提到的更新额度的操作,多次操作显然会导致数据不一致。即使更新操作只是一个纯粹的set操作,那也可能不是幂等的,因为多个set操作的顺序无法保证。上面提到的额度更新操作,需要开发者考虑如何更新操作是否全部执行成功。一种方法是创建一个表用于记录每个交易ID是否已经成功更新了用户表,结构如下表所示。

image-20211115100042234.png

当加入此表后,我们交易中对数据库的整个流程如下。

Begin transaction
   Insert into transaction(id, seller_id, buyer_id, amount);
   Queue message "update user('seller', seller_id, amount)";
   Queue message "update user('buyer', buyer_id, amount)";
End transaction
For each message in Queue
   Peek message
   Begin transaction
   Select count(*) as processed where trans_id = message.trans_id and
   balance = message.balance and user_id = message.user_id
   If processed == 0
     If message.balance == 'seller':
       Update user set amt_sold = amt_sold + $amount where id = message,id;
     Else 
       Update user set amt_bought = amt_bought +$amount where id = message,id;
     End if
     Insert into updates_applied(message.trans_id, message.balance, message.user_id);
   End if
 End transaction
 If transaction successful
   Remove message from queue
 End if
End for

上述代码中,消息队列的消费者从列中取出一条消息后,并在执行成功后从消息队列移除这条消息。消息队列中的操作信息只有在真正提交成功后才会被删除,这样可以实现在部分代码执行失败时仍然保证web系统的事务性。

另外一种实现幂等更新的方式是保证消息处理的顺序。若是在用户表中添加最近一次销售日期last_sale和最近一次消费日期last_purchase字段,开发者在更新用户额度的同时更新这两个字段。我们又应该如何保证消息处理的顺序以保证last_purchase字段正确呢?使用以下代码可以保证last_purchase只增不减,可一定程序上防止更新顺序从而保证幂等性。

Begin transaction
   Insert into transaction(id, seller_id, buyer_id, amount);
   Queue message "update user('seller', seller_id, amount, trans_date)";
   Queue message "update user('buyer', buyer_id, amount, trans_date)";
End transaction
For each message in Queue
   Peek message
   Begin transaction
   If message.balance == 'seller':
     Update user set last_purchase = message.trans_date , amt_sold = amt_sold + $amount where id = message.buyer_id and last_purchase < message.trans_date;
   Else
     Update user set last_purchase = message.trans_date , amt_bought = amt_bought + $amount where id = message.buyer_id and last_purchase < message.trans_date;
   End if
 End transaction
 If transaction successful
   Remove message from queue
 End if
End for

消息队列的顺序

消息队列提供了先进先出的服务,这增加了系统实现的复杂性,并且有可能导致安全问题。大多数的应用都是事件驱动的网站,而事件的发生顺序由于网络等原因无法进行真正的排序,因此系统不应该过于严格的限制事件的顺序,以防止出现不确定性的结果。

软状态/最终一致性

上面的讨论中,焦点聚集在可用性与一致性之间的权衡上,此时,我们开始讨论BASE对应用系统设计的影响。软件工程师喜欢将系统开左是一个闭环操作,任何一个确定性的输入都会导致一个确定性的输出,这对于设计一个软件是很有必要的。基于BASE原则的设计并不会改变系统结果的可预测性,但是整个系统的行为需要从全局着手来看。比如,用户A与用户B之间发生了转账交易,而用户A和用户B的账户修改操作是解耦的。在某个瞬间,系统的行为是不确定的,可能用户A账面金额减少而用户B还未收到转过来的钱,账面上的钱不知道去哪儿了,但是这个状态只会维持几秒钟,而且对于用户来说是可以接受的。尽管存在软状态,但是最终是一致的,这种情况下,我们可以认为系统实现了一致性,并且对于用户来说是可以接受的。

事件驱动框架(EDA)的引入

如果我需要知道何时系统达到了一致性呢?例如,我需要在系统达到一致后进行某种统计计算。一种简单的解决方案便是在系统达到一致性后进行通知,此时系统将更加完善。上面的转账过程中,若是在用户B的账面上收到用户A转账过来的钱(即系统达到了一致性)时,产生一个消息用以通知用户B。

总结

扩展系统以应付更多事务时,我们需要深入思考资源如何进行管理的问题。BASE设计原则提供一种新的系统解耦设计方法。

相关文章

  • BASE理论应用

    本文翻译了英文文章《In partitioned databases, trading some consiste...

  • BASE理论

    BASE理论 BASE理论是指,Basically Available(基本可用)、Soft-state( 软状态...

  • BASE 理论

    BASE 是 Basically Available(基本可用) 、Soft-state(软状态) 和 Event...

  • Base理论

    Base理论是基于CAP理论逐步演化而来,由ebay的架构师提出,其来源于对大型互联网分布式实践的总结。其核心思想...

  • BASE 理论

    zhexy geekbang 学习笔记 分布式 Markdown BASE理论包括基本可用(Basically A...

  • BASE理论

    BASE理论 BASE是Basically Available(基本可用)、Soft state(软状态)和Eve...

  • Base理论

    BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually...

  • BASE理论

    BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventua...

  • Base 理论

    BASE 理论是对 CAP 中一致性和可用性权衡的结果,是基于 CAP 定理逐步演化而来的。它的核心思想是,如果不...

  • BASE理论

    BASE理论是由Basically Available(基本可用)、Soft state(软状态)、Eventua...

网友评论

      本文标题:BASE理论应用

      本文链接:https://www.haomeiwen.com/subject/pvdqtrtx.html