作者简介:
一致性这事也许不是天经地义的。你同意嘛?
5.3. Relaxing Consistency 放宽一致性
一致了自然是好事。但,不幸的是,有时候我们不得不放弃他。在设计系统时,我们总是避免“不一致”现象,然而,若要真正做到这一点,通常需要放弃系统中的其它一些特性,而那些特性却是必不可少的。如此说来,我们就需要放弃一定的一致性来换取其它的特性。因为其它的那些是必不可少的啊。某些架构师把这当成这种灾难,而笔者却把其视为系统设计中必须面对的平衡。此外,各个领域对“不一致性”的容忍度也不同,所以在决策时要考虑容忍度这一因素。
使用单服务器关系数据库的人,应该对怎么处理不一致性很熟悉。在这种情况下,我们确保一致性的主要工具就是事务(transaction),这个事务可以提供强一致性的保障。然而,事务系统通常具备放松“隔离级别”的功能,以允许查询操作读取尚未提交的数据。在实际应用中,大多数的应用都会把一致性放宽到最高级别(如:序列化)以下的某个级别,就是往下调级别,这样虽然没有了强一致性,但却改善了性能。我们经常看到人们使用“read-committed”(读取已提交的数据)事务级别,这样确实避免了“read-write”冲突,但同时也出现了其它的问题。
许多的系统已经彻底的放弃了事务,因为事务对性能的影响实在太大。我们看到有两种不使用事务的方式。一种就是在数据量较小的场景下,人们喜欢使用MySQL,那时候MySQL还不支持事务。许多的网站喜欢MySQL的高速访问能力,他们不打算再使用事务了。还有一种情况就是,那些很大很大的网站,比如eBay,他们为了获得性能也放弃了事务,特别是我们引入分片的情况就更是如此了。甚至没有这些限制,许多的应用构建者需要和remote 系统进行交互,这时候也没法再使用事务了,所以,在更新时不用事务,已经是企业应用中非常普遍的一种现象。
5.3.1. The CAP Theorem CAP定理
在NoSQL的世界里,人们经常会提到CAP定理,因为你需要放宽一致性,容忍一定程度的不一致。这个定理最初是由Eric Brewer在2000年的时候提出的。数年之后,Seth Gilbert 和Nancy Lynch 二位大虾也验证了这个定理。(你也许也听到有人把这个定理叫做“Brewer猜想”Brewer’ s Conjecture)
CAP定理的基本的描述就是说:现在给你三个属性,一致性(Consistency)、可用性(Availability)以及分区耐受性(Partition tolerance),你只能得到其中的两个。也就是不可能所有的都满足。很显然,这很大程度上取决于我们如何定义这三个属性,而且不同的观点和看法会导致对CAP定理的实际效果也存在争论。
“一致性”和我们之前定义的一模一样。可用性在CAP中有特别的定义——它的意思是如果你可以和集群中的某个节点可以通话,那么就认为它具备读取和写入数据的能力。这个定义稍微和我们传统的理解有点不一样,这个问题我们会在后面深入讨论。分区耐受性(Partition tolerance)的意思就是说,如果发生通讯故障,,导致集群被分割成多个无法通信的分区时(这种情况也被叫做“脑裂”),集群依然可以使用。
图5.3 通信线路有两处断了,导致整个集群被分成两组
单服务器(single-server)系统显然就是一个CA系统-一个拥有一致性(Consistency)和可用性(Availability)但没有分区耐受性(Partition tolerance)的系统。一台机器无法继续分割,所以也就不用担心分区耐受性(partition tolerance)的问题了。因为只有一个节点,所以只要它能正常运转,那么系统就可以使用。能正常运转并且能保持一致性的系统,是合理的,没有问题的。现实中大部分的关系数据库都属于这种情况,就是具备CA。
从理论上讲,也有可能存在“CA”的集群。然而,这就意味着如果在集群中出现“分区”,集群中所有的节点都将无法运转(go down),导致客户端无法与各节点对话。按照“可用性”的常规的定义,我们认为这种情况就是缺乏“可用性”,但按照CAP里的“可用性”来解释就感觉怪怪的。CAP里的“可用性”是指“在系统中的每个没问题的节点接收到请求后,必须要响应”。[Lynch and Gilbert]。所以,发生故障并没有响应的节点,并不意味着就缺乏了CAP中的可用性(Availability)。
这确实意味着你可以构建一个CA集群,但是要确保它很少出现分区问题,而且一旦出现分区问题,所有节点必须全部停止工作。这个是能够做到的,至少在一个数据中心内部是可以的,但代价会高得让你无法忍受。你为了把集群中一个分区里所有的节点都停掉,你必须定时的检测是否出现了分区问题,这个事情可要花费你很多的精力。
所以,集群必须要忍受网络分区这种情况。这也正是CAP定理的意义所在。尽管CAP定理经常被描述为“三个中只能保有两个”,但其实是在讲:在一个系统中,如果遇到了分区问题,比如分布式系统中,这时候你就需要在一致性和可用性之间做一个权衡。这不是一个非黑即白的决定;通常你可以稍微丢点一点一致性来获得一些可用性。这样的话,系统就既没有完美的一致性也没有完美的可用性-但这种不完美的结合却能满足你特定的需求。
有个例子可以说明这个问题。Martin和Pramod都想要在系统上预订酒店的最后一套房间,预订酒店的系统是使用了对等分布的模型(peer-to-peer distribution),有两个节点组成(Martin使用位于伦敦的节点,Pramod使用位于孟买的节点)。如果我们想确保一致性,那么当Martin在伦敦的节点上想要预订房间的时候,那么在预订确定前必须要先和在孟买的节点进行通信。实际上,两个节点必须按照相互一致的顺序来处理它们所收到的请求。这个做法保证了一致性——但一旦两个节点的网络连接出现故障,那么导致出现了两个“分区”,这时候两个节点就都无法预订房间了,这样就没有了“可用性”(availability)。
有种改善可用性的方法就是把一个节点作为某个酒店的主节点(master node),然后确保这个酒店所有的订单都由这个master来处理。假设master在孟买,那么当两个节点之间的网络发生故障后它依然可以处理酒店的预订,这样Pramod将会预订到最后的那个房间。如果我们使用主从复制(master-slave replication),伦敦的用户看到了那个不一致的房间,但是他们不能预订,这就导致了“更新不一致”(update inconsistency)。然而,在这种情况下用户也希望这样。所以说,这种在“一致性”和“可用性”之间所做的权衡,也能正确处理上述特殊情况。
上面的这种做法确实改善了状况,但如果网络连接出问题我们依然无法在伦敦的节点上预订酒店的房间,因为master在孟买。在CAP术语中,这就是一个“可用性”的故障(failure of availability),因为Martin可用和伦敦的节点通信,但这个节点却不能更新数据。为了得到更好的可用性,我们其实可以允许两个系统即使在网络连接出现故障的情况下也能够接受酒店的预订。这样做的风险就是Martin 和 Pramod都预订到了最后的那套房间。但是呢?这其实也不是什么严重的问题,因为我们可以通过酒店的线下操作来解决这个问题。通常的话,旅行社是允许一定数量的超额预订的,这样的话,如果有某些客人预订了房间而最终没人入住,那么就可以把这部分空余房间分派给那些超额预订的人。与之相对,有的旅馆也会在名额之外预留几间客房,这样万一哪间客房出现了问题或者在房间订满后又来了一位贵宾,那么酒店就可以把客人安排到预留的空房中。还有的酒店一旦发现预订冲突就会和客户道歉然后取消此预订。这个方案也是一种选择,至少这样要比因网络故障而无法预订要好的多。
允许写入不一致的一个经典案例就是购物车,Dynamo的论文中曾经提到这个例子[Amazon’s Dynamo]。在这种情况下,即使出现了网络故障,你也总是能够修改购物车里的商品。这么做有可能导致出现多个购物车。在结帐的时候,我们再把多个购物车合并,就是把多个购物车的商品拿出来放到一个购物车中。这么做一般不会有问题——但是如果有问题,用户在下单之前也有机会查看自己的购物车里的商品来确认。
所以我们的经验就是:尽管大部分的软件开发者都把更新一致性视为天经地义的事情(The Way Things Must Be),尽管不同用户的请求得到了不一致的结果,我们依然可以优雅的处理这个问题。这些情况和领域(domain)密切相关并且你必须拥有相关的领域知识才能解决这些问题。所以我们不能总是纯粹让开发团队搞定这些问题,而是要去求助于领域专家(domain experts)来解决。如果你找到了处理不一致更新的方法,那么我们就有更多的选择来提高可用性和性能了。作为购物车,意味着购物者应该总是能够购买,并且处理应该很快速。作为一个爱国的中国人,我们应该知道支持我们的零售行业的重要性,嘿嘿(原文是这样:And as Patriotic Americans, we know how vital it is to support Our Retail Destiny)。
这种类似的逻辑也可以应用读取一致性(read consistency)的问题上。如果你正在用交易软件来买卖“金融产品”的话,那么也许不能忍受一点点数据的更新不及时。然而如果是正在一个网站上发帖子,那么旧页面持续几分钟也没什么关系。
在这种情况下,我们就需要知道用户对于陈旧数据的容忍度以及这个不一致窗口得有多长。一般我们会从“平均时长”、“最差时长”等多个维度来衡量不一致窗口,而且还要考虑不同的时长的分布情况等等。不同的数据对于数据陈旧的容忍度也是不同的。因此我们就需要在我们的“复制配置”(replication configuration)中作出不同的配置。
NoSQL的倡导者经常说,与关系型数据库的ACID事务不同,NoSQL系统遵循BASE属性:基本可用,柔性状态,最终一致(Basically Available, Soft state, Eventual consistency) [Brewer]。尽管我们感觉这里有必要提一下“BASE”这个首字母缩略词,但我们认为并没有什么鸟用。这个首字母甚至比ACID还要做作,并且“ basically available” 和 “ soft state”都没有明确的定义。另外需要强调的是,当处Brewer引入BASE这个概念的时候,他说“ACID”和“BASE”不是非你即我的选择,二者之间存在多个逐渐过渡的权衡方案可选。
本文之所以要讨论CAP定理是因为当谈论到权衡分布式数据库的“一致性”时,经常会用到它(甚至是滥用,所以有必要提一下)。然而,与其考虑如何权衡“一致性”和“可用性”,不如思考怎样在“一致性”与“延迟”(latency)之间取舍。在讨论分布式系统一致性的问题时,通常可以概括的说:参与交互操作的节点越多,“一致性”就越好。然而问题是,每新增一个节点,都会使交互操作的响应时间变长。“可用性”可以视为能够忍受的最大延迟时间,一旦延迟过高,我们就放弃操作,并认为数据不可用,这样一来,就和CAP的“可用性”吻合了。