原文: NET Core微服务之路:再谈分布式系统中一致性问题分析
前言
一致性:很多时候表现在IT系统中,通常在分布式系统中,必须(或最终)为多个节点的数据保持一致。世间万物,也有存在相同的特征或相似,比如儿时的双胞胎,一批工厂流水线的产品,当然,我们不去讨论非IT以外的知识点。
注:我们一定要明白一个词叫“信息不对称”,不论是人、事、物,信息不对称是永远都存在的,要知道,在IT系统中,能引起信息不对称的因素有很多,比如网络上,有丢包、有延迟。硬件上,有不同性能的计算能力和处理能力。
在传统的IT时代,一致性通常是指强一致性,比如一个单体的WEB程序中,从数据库到缓存,再到呈现出来的界面,数据均是相同的;
而在现在的互联网时代,特别是分布式架构下,一致性的含义远远超出她原有的含义,由于互联网的特点,信息量巨大,每个人(或者不同的每个业务)获取到的信息不对称,最终都会造成不一致。
换句话说,传统的单体应用无法满足巨大的信息量,而转向为多节点、多服务的分布式架构模型(正如CPU,从单核变为多核来支撑更多的计算能力),多个服务多节点必然会产生数据不一致的问题,正如虽然人多力量大,可如果每个人都往不同方向使劲,这个力量就是分散的,不一致的,没有任何作用的,所以,我们需要管理,需要告诉每个人使劲的方向,告诉每个人的排列顺序等等一系列有效的分配和管理,于是乎,我们将这个话题转化为互联网思维就是“拆”(非拆迁办)。
我们很多时候都在讨论这个程序如何拆分,比如拆成业务层、数据层、提供层、UI层、缓存层等等,而每一层的部署大部分情况下都不会存在于一个相同的宿主中(否者怎么还叫拆),这样称为水平拆分(或横向拆分),将这些不同的层部署成多个节点,多个节点如果具有相同的一致性功能,那么再组成一个服务域,这样,把这个服务域作为一个处理中心,在处理能力和计算能力上,远远会超过单体程序的整体性能。
这样的优势显而易见,但是,最大的问题(不能说是缺点)是,由于拆分后的系统或者服务化的系统,存在多个元功能模块,或者一个服务域中的多个节点,如何去保证她们数据的一致呢?
提出问题
直接用A,B,C等等常规化表述形式去描述不一致问题,网上文章很多,我们尝试换一种方式,以实际的、你我都会接触到的现实场景,来理解不同情况下所产生的不一致问题。
Q1.生活案例:意愿
假如,别假如了,就拿笔者来说吧。当初跟妻子去买婚戒的时候,只想买个一般的对戒便可,因为日后的日子里用钱的地方太多(相信任何一个已婚男士都有这种体会),可妻子喜欢那种布灵布灵的,要知道,这种布灵布灵的,可比一般的要贵出许多许多,那么,这个不一致就开始形成了,你的意愿和她的意愿并没保持一致,日后生活问题会越来越大。当然,这只是一个几年前的例子,显然我妻子已经接受了我当初的观点。
Q2.银行案例:转账
转账是不一致案例中最经典的例子。这么来说,假如你想通过某个平台,比如支付宝、微信、银行进行转账,这个平台的流程是这样的:首先减去你账户上的余额,然后加到你指定账户上去。如果平台减去你的账户余额成功了,而增加其他账户的的余额失败了,那么你将损失这笔钱;如果减去你的账户余额失败,而增加其他账户的余额成功,那么银行将损失这笔钱;这种资金上的不一致是绝不允许存在,否则这个平台将面临破产和倒闭。
Q3.常见案例:下单和扣库存
不管是电商还是企业仓库,都会存在一个经典案例。比如,你在某个界面上进行了下单,比如她显示的库存是100件,而你订购了1件,并支付了应该的费用,可是,这个库存数量并未任何变化,那么,如果有101个人前来购买,那么会出现超卖的情况,也就是这多出来的1个人将买不到这件商品,这就是下单和库存不一致所导致的。反之,永远下单不成功,而库存却在减少,这对企业来说都是增加运营成本的。
Q4.常见案例:掉单
掉单一般常见于协作工作的系统的流程中,分别对彼此的上游和下游。比如上面的案例,你已成功下单并支付,按照流程,平台应该告诉物流我有物品需要配送,过来取单,可物流终究收不到这个收单请求,导致物品配送失败,严重会导致赔付,这样的不一致性通常是一个系统中的两个请求不一致而造成的。
Q5.系统案例:系统状态
这个案例跟上面的掉单案例类似,只是需要区分引起这个掉单的原因,其实就是两个系统的状态不一致所造成。比如上游及时收到请求并响应,而下游却因为某个状态(原因)而没响应这个请求,最终导致不一致形成。
Q6.系统案例:本地缓存和数据库
现在存储都依赖于数据库,而关系型数据库都具备ACID特性,但是在大规模高并发的互联网系统里,一些特殊的场景对读的性能要求极高,为止,服务在这个上面的数据库将难以抗住大规模的读流量,为了应对这样的问题,一般都会在数据库前增加缓存,那么缓存和数据库之间的数据是如何保持一致?是需要强一致还是若一致?
Q7.系统案例:节点缓存
一个服务域上的多个节点为了满足较高的性能要求,需要使用到本地缓存,使用了本地缓存,每个节点都会有一份缓存数据的拷贝,如果这些数据是静态的、不变的,那永远都不会有任何问题。但如果这些数据是动态的、经常更新的。那么问题就来了,当被更新的时候,各个节点的更新都会存在先后顺序,而正是这先后顺序的一瞬间,各个节点的数据将会不一致。想象一个高流量读的场景中,一个请求拿到的数据是1,而另一个请求拿到的是0,这将导致灾难性的后果。
Q8.系统案例:超时
服务化的系统间调用常常因为网络问题导致系统间调用超时,这是在即便在网络最好的机房下、在上亿次的前提下,同步调用超时也是必然会存在的,正如上面提到的不同状态类似,假如当下单和物流存在极高请求的情况下,物流并未及时反馈响应,而下单却并不知道物流是否已经接到订单,或者定已收到订单,只是掉包了等现象,这样的不一致,也是会导致灾难性后果的。
解决模型
对于Q1问题,我们可以有两套方案,一是不结了,不过显然这是行不通的;二是慢慢的补偿,先买个一般的,等手头资金充裕了,或者某天中了500万,或者天上掉个金块,再给她个惊喜,于是,这个问题解决了,大家的意愿都一致了,都开心了。可见,这样的解决模式会存在一个现象---过渡时期,当这个过渡时期到最终双方都达成一致时,问题就解决了。因此,我们不能要求在买对戒的时候,双方都达到强一致的要求,生活是必须要过下去的,不可能说散就散,那么我们要考虑去补偿她,尽最大的努力达到最终一致。
在化学单词中,“ACID”是酸,我想这绝对不是巧合,原文可参考百科
https://en.wikipedia.org/wiki/ACID_(computer_science)。在关系型数据库中我们都知道ACID原理,关系型数据库天生解决具有复杂场景问题,需要保证_一致的,每个事务都是原子的,要么都成功或要么都失败,事务间是相互隔离的,而且相互完全不影响,而最终结果是持久的_。
(image form Wilfred Springer,)
因此,数据库会从一个明确的状态到另外一个明确的状态,中间的临时状态是不会出现的,EF的追踪功能也正是遵循这个原则进行设计,任何一个操作状态在中途是不会存在改变,如果出现改变,将会给你提示错误,当然,你可以关掉它,不过这样EF的核心点就不存在了。
A: Atomicity,原子性
C: Consistency,一致性
I: Isolation,隔离性
D: Durability,持久性
回到Q2和Q3的问题上,使用关系型数据库可以解决这样的强一致性需求,然而,单纯通过强一致性的数据库去面对不断拆分的元组,是难以满足互联网高流量的需求的,或许你会说使用服务器固态硬盘和读写分离的模式去应对,但这绝对只是一个应对方案而已。因此,在拆分的时候尽量把转账的相关账户放入一个数据库分片,而Q3上,把订单和存库放入一个分片,因为中途不会存在任何改变,从0到100必须保证任何状态都是原始的初始状态。
但是,我们就Q2假设另外一个场景,假设账户的数量巨大,对账户存储进行了拆分,关系型数据库分为8个实例,每个实例8个库,每个库8张表,共512张表,假如转账的账户正好在一个库里,这个问题依赖关系型数据库的事务来保持强一致性,但是,如果两个账户在不同的库里,这个事务就无法封装在同一个数据库中的,这样就会发生一个账户扣款成功,而另外一个库的账户增加失败的情况。
这时,我们就需要考虑另外一种理论:CAP理论和BASE理论,就CAP原文可参考百科https://en.wikipedia.org/wiki/CAP_theorem。而BASE原文可参考百科https://en.wikipedia.org/wiki/Eventual_consistency。
帽子理论证明,任何分布式系统同时只可满足两点,没法三者兼顾。
C:Consistency,一致性, 数据一致更新,所有数据变动都是同步的
A:Availability,可用性, 好的响应性能,完全的可用性指的是在任何故障模型下,服务都会在有限的时间处理响应
P:Partition tolerance,分区容错性,可靠性
关系型数据库由于关系型数据库是单节点的,因此,不具有分区容错性,但是具有一致性和可用性,而分布式的服务化系统都需要满足分区容错性,那么我们必须在一致性和可用性中进行权衡,具体表现在服务化系统处理的异常请求在某一个时间段内可能是不完全的,但是经过自动的或者手工的补偿后,达到了最终的一致性。
(image form Wilfred Springer,)
而BASE理论的提出,解决了CAP在分布式系统中的一致性和可用性不可兼得的问题。“BASE”在化学单词中是指碱,因此我们可以想到一个词语叫“酸碱平衡”,而在实际的场景中,我们可以分别使用ACID和BASE来解决分布式服务化系统的一致性问题。BASE理论与ACID理论完全不同,她满足CAP理论,通过牺牲强一致性而获得可用性,一般应用在服务化系统的应用层,通过达到最终一致性来尽量满足不同业务上的需求。
BA:Basically Available,基本可用
S:Soft State,软状态,状态可以有一段时间不同步
E:Eventually Consistent,最终一致,最终数据是一致的就可以了,而不是时时保持强一致
(image form Wilfred Springer,)
按照BASE模型实现的系统,由于不保证强一致性,系统在处理请求的过程中,可以存在短暂的不一致状态。系统在做每一步操作的时候,通过记录每一个临时状态,在系统出现故障的时候,可以从这些临时状态中继续完成未完成的请求处理,或者回退到原始状态,最后达到一致的状态。
例如Q1的转账状态为例,我们把两个账户的转账情况粗分为四个状态:
第一个状态为准备状态,用户准备向另外一个进行用户转账;
第二个状态为扣额状态,系统将从转账用户中扣去相应余额;
第三个状态为加额状态,系统将从收款用户中增加相应余额;
第四个状态为完成状态,系统转账完成后的确认;
在这过程中,系统需要将每一步的操作状态都进行记录,一旦某个环节出现故障,系统能够发现故障环节并继续完成未完成的任务,最终完成任务,达到一致的最终状态。在实际的业务生产环境中,通常每个阶段的状态都是通过持久化的执行任务,一旦出现了问题,定时任务会检查未完成的任务,继续执行未完成的任务,直到执行完成为止;再或者,该状态是属于取消状态,跟数据库事务执行方式一样,那么这个过程中已经完成的状态应该回滚到原始状态,整个过程还可以细化到如下更加详细实际转账流程:
不过,由于这种方法在每个状态执行的时候都需要记录下来,而且需要更新数据库中的状态信息,一旦在大规模高并发下,性能将会是一个严重的瓶颈。
一种更好的解决办法是用Write-Ahead Logging(WAL),也叫预写日志,详细介绍可参考维基百科https://en.wikipedia.org/wiki/Write-ahead_logging,一种提高性能的方法,作用是在做每个操作的时候,都先写入到日志,如果操作遇到问题而停止的时候,可以读取日志进行恢复,最后达到一致,由于他的日志是追加模式,磁盘写操作只有传统的回滚日志一半左右,大大提高了数据库在高并发下的性能。
总结
如果钱不是问题,那么最最简单的方式是使用向上扩展,利用强悍的硬件性能来运行专业的关系数据库,能否保证强一致性,比如Orcele和DB2这样符合工业标准的数据库。
如果钱是个问题,那么相关的数据分到数据库的同一个分片,能够保证使用关系型数据库实现强一致性,比如Mysql。
如果是业务限制,无法将相关的数据分到同一个片,就需要实现最终一致性,通过记录事务的状态来判断,一旦处理不一致,可通过自动化(如定时)或者人工干预来继续执行,并修复不一致的情况。
下一篇我们再多介绍一下一致性的更多具体实现方案。
感谢阅读!
参考
《浅析分布式一致性模型》http://loopjump.com/distributed_consistency_model/