数据存储提供的一致性保证往往有悖于你的直觉,分布式存储尤是如此。有许多一致性模型对各种一致性保证给出了定义,本文将借助这些模型探讨这些一致性保证的区别是什么,你需要结合自己的需要做出怎样的权衡。
本文最初发布于Roberto Vitillo个人博客,InfoQ中文站翻译并分享。
设象一下,如果给一个变量赋了值,然后立即读取,却发现读不到它,会不会很是让人抓狂呀!
x = 42
assert(x == 42) # 抛出异常
然而,使用具有弱一致性保证的分布式数据存储时还真有可能发生这样的情况。“但是等等,难道不应该由数据库为我处理一致性问题吗?”你可能会提出这样的问题。答案是,能够尽早读取到更新的值取决于数据库提供的保证。
为了提供高可用性和性能,一些数据库所提供的一致性保证与你的直觉并不相符。还有一些软件(比如Azure的Cosmos DB和Cassandra)则将选择权交给你,由你自己选择是想要更好的性能还是更强的保证。因此,你应该知道需要做哪些权衡。
让我们看看,当你向数据库发出一个请求时会发生什么。在理想的世界中,你的请求会被立即执行:
但现实是残酷的,我们的真实世界可没那么理想——你的请求需要先发到数据存储,由数据存储进行处理,最终向你返回一个响应。所有这些动作都需要时间,而且都不是瞬时发生的:
数据库能够提供的最佳保证是在调用时和完成时之间执行这个请求。你可能会认为这似乎没什么大不了的(毕竟,你常常写的是一些单线程应用程序),如果你给x赋值为1然后读取它,那么你希望得到的就是1,前提是没有其他线程也给x赋值。但是,一旦你开始与数据存储打交道,为了实现高可用性和可伸缩性而把它们复制到多台机器上,那么读取到什么值就真的很难预测了。为了搞明白为什么会这样,我们将探讨一下,设计师在实现分布式数据库的简化模型时需要做出哪些权衡。
假设我们有一个分布式的键-值存储,它由一组副本组成。副本之间选举出了一个leader,这是一个唯一可以接受写操作的节点。当这个leader接收到一个写请求时,它会将其异步广播给其他副本。尽管所有副本以相同的顺序接收相同的更新,但它们接收到更新的时间会有所不同。
现在要求你想一个策略来处理读请求,你会怎么做呢?嗯……,读取可以由leader负责,也可以由副本负责。如果所有读取都要通过leader,那么吞吐量将受到单一节点处理能力的限制。或者,任何副本都可以负责任何读取请求——这样肯定是可伸缩的,但是两个客户端(或观察者)对系统状态的看法可能不同,因为副本可能落后于leader,或者介乎各复本之间。
直觉告诉我们,观察者对系统的看法是否一致与系统的性能和可用性之间需要进行权衡。要理解这种关系,我们需要来精确地定义什么是一致性。我们将借助于一些一致性模型,它们就观察者能够体验到的对系统状态看法给出了正式的定义。
如果客户端仅向leader发送写和读,那么每个请求似乎都是以原子的方式发生于一个非常特定的时间点,就好像只有一个数据副本一样。无论有多少副本,或者落后多少,只要客户端总是直接询问leader,以它们的视角来看,就只有一个数据副本。
因为请求不是即时提供服务的,而且只有一个节点为其提供服务,所以请求它是在调用和完成之间的某一时刻执行的。换一种方式来思考,一旦某一请求完成,它的副作用对所有观察者都是可见的:
由于请求在其调用和完成之间的时间内对所有其他参与者都可见,因此必须实施实时保证——这种保证被定义为一种称为线性化或强一致性的一致性模型。线性化是系统能够为单对象请求提供的最强的一致性保证。
如果客户端向leader发送了一个读请求,当请求到达时,接收请求的服务器认为它仍然是leader,而实际上已经被废黜了呢?如果是前任leader来处理这个请求,系统将不再是强一致的。为了防止这种情况,这个假定的leader首先需要与大多数副本取得联系,以确认它是否仍然是leader。只有当它还是leader的时候,才被允许执行请求并将响应发送回客户端。这大大增加了读取服务所需的时间。
现在,我们已经讨论了通过leader序列化所有读取操作。但是这么做会产生一个单一的阻塞点,从而限制系统的吞吐量。其中最重要的一点是,为了处理读取,leader需要与大多数副本交互。为了提高读取性能,我们也可以允许副本来处理请求。
尽管某个副本的版本可能会落后于leader,但它总是会以与leader相同的顺序接收新的更新。如果客户端a只查询副本1,而客户端B只查询副本2,两个客户端会看到状态是在不同的时间演变的,因为副本并不完全同步:
这个一致性模型称为顺序一致性,在此,操作对于所有观察者而言都是以相同的顺序发生的,但就操作的副作用何时对观察者可见不提供任何实时保证。缺乏实时保证是顺序一致性与线性化的不同。
保持与队列同步的生产者/消费者系统是该模型的一种简单应用——生产者节点将条目写入队列,消费者读取队列。生产者和消费者以相同的顺序查看这些条目,但是消费者落后于生产者。
尽管我们设法提高了读取吞吐量,但我们不得不把客户端与副本绑定在一起,如果副本宕机了怎么办?我们可以通过允许客户端查询任一副本来提高商店的可用性。但是,这会在一致性方面付出高昂的代价。假设有副本1和副本2两个副本,其中副本2落后于副本1。如果客户端查询了副本1,然后再查询副本2,它将看到过去的状态,这可能会令人非常困惑。客户端的唯一保证是,当对系统的写操作停止时,所有副本最终将收敛到最终状态。这种一致性模型称为最终一致性。
在最终一致性的数据存储之上构建应用程序是一项挑战,因为其行为与你在编写单线程应用程序时所习惯的行为不同。有一些小错误可能会出现,它们难以调试和重现。然而,若要保证最终一致性,并不需要线性化所有的应用程序。你需要自主做出选择,判断数据存储所提供的一致性保证是否满足应用程序的需求。如果你想要跟踪你的网站的访问用户数,那么最终一致性的存储则非常合适,因为查询返回的数字稍微有些过时并不重要。但如果是支付处理,则肯定需要强一致性。
除本文提到的之外,还有很多其他的一致性模型。但无论是哪个模型,其背后的规律均不会与直觉相违背:一致性保证越强,单个操作的延迟就越高,出现故障时存储的可用性就越低。
PACELC定理已经明确阐述了这一关系。它指出,如果分布式计算机系统存在网络分区§,则必须在可用性(a)和一致性©之间做出权衡;反之(E)如果没有,也必须在时延(L)和一致性©之间做出权衡。
我编写了《系统设计》一书,在这本书中我探讨了为保证高可用性和性能还可以做哪些数据存储的权衡,比如隔离保证,即防止一个事务内的操作干扰并发运行的其他事务。
原文链接:
https://robertovitillo.com/what-every-developer-should-know-about-database-consistency
译者简介:冬雨,小小技术宅一枚,从事研发过程改进及质量改进方面的工作,关注编程、软件工程、敏捷、DevOps、云计算等领域,非常乐意将国外新鲜的IT资讯和深度技术文章翻译分享给大家,已翻译出版《深入敏捷测试》、《持续交付实战》。
领取专属 10元无门槛券
私享最新 技术干货