本章的线性一致性是在铺垫了多副本、网络问题、时钟问题后的一个综合探讨。首先探讨了线性一致的内涵:让系统表现得好像只有一个数据副本。然后讨论如何实现线性一致性,以及背后所做出的的取舍考量。其间花了一些笔墨探讨 CAP,可以看出作者很不喜欢 CAP 的模糊性。
如前所述,分布式系统中很多事情都有可能出错。解决出错最简单粗暴的方法是让整个系统宕机,并给出出错原因。但在实际生产中,这种方式多不可接受,此时我们就需要找到容错(tolerating faults)的方法。即,即使系统构件出现了一些问题,我们能保证系统仍然正常运行。
本章我们将会讨论一些用于构建具有容错性分布式系统的算法和协议(alogrithm and protocol)。在设计算法和协议时,我们假设第八章提到的分布式系统中的问题都会存在:
- 数据包可能会丢失、乱序、重复和不确定延迟
- 多机时钟最好的情况也就是近似一致
- 机器节点可能会不确定停顿、宕机重启
构建一个容错系统最好的方法是:找到一些基本抽象,可以对上提供某些承诺,应用层可以依赖这些承诺来构建系统,而不必关心底层细节。在第七章中,通过使用事务,应用层可以假设不会发生宕机(原子性,意思是不会因为宕机出现让事务停留在半成功的状态),没有其他应用并发访问数据库数据(隔离性),且存储系统非常可靠(持久性)。事务模型会隐藏节点宕机、竞态条件(race conditions)、硬盘故障等底层细节,即使这些问题出现了,应用层也不必关心。
本章将继续讨论一些可以减轻应用层负担的分布式系统中的基本抽象。比如,分布式系统中最重要的一个抽象——共识(consensus),即,_让所有节点在某件事情上达成一致_。在本章稍后的讨论可以看出,让系统中的所有节点在有网络故障和节点宕机的情况下达成共识,是一件非常棘手的事情。
为什么共识协议如此重要呢?他和真实系统的连接点在于哪里?答曰,操作日志。而大部分数据系统都可以抽象为一系列数据操作的依次施加,即状态机模型。而共识协议可以让多机对某个确定的操作序列达成共识,进而对系统的任意状态达成共识。
一旦我们实现了共识协议,应用层可以依赖其做很多事情。例如,你有一个使用单主模型的数据库,如果主副本所在节点宕机,我们便可以使用共识协议选出新的主。在第五章处理节点下线(Handling Node Outages)一节中我们提到过,只有唯一的主,并且所有副本都认可该主,是一个需要确保的非常重要的特性。如果有超过一个节点都认为自己是主,我们称之为脑裂(split brain)。脑裂很容易导致数据丢失,而正确实现的共识协议能够避免该问题。
在本章稍后的地方,“分布式事务和共识协议”一小节里,我们将会详细讨论用以解决共识相关问题的算法。但在此之前,我们需要探索下分布式系统中我们可以提供的保证和抽象有哪些。
我们需要理解容错的边界,哪些事情可以做、哪些事情做不了:在某些情况下,系统可以容忍某些故障;但在另外情况下,系统却容忍不了。我们将通过理论证明和具体实现来深入探讨,可能与不可能的边界限制。我们会对诸多基本限制有个概览式串讲。
分布式系统领域针对这些主题的研究已经持续了数十载,因此积累了很多材料,但我们只能进行简要介绍其皮毛。由于篇幅所限,我们不会详细探究其严谨的模型描述和详细证明,相反,我们只会给一些其背后的直觉(informal intuitions)。如果你感兴趣,章节末尾的参考文献应该可以提供一些足够深入的细节。
一致性保证
在第五章中日志滞后问题(Problems with Replication Lag)小节,我们分析了一些多副本数据所遇到的时序问题。在相同时刻,如果对比多副本数据库中一份数据的两个副本,我们可能会看到不一致的数据。这是因为,写请求到达不同的数据节点,总会存在一个时间差。无论我们使用什么数据副本模型(单主、多主和无主),这种数据的不一致性都有可能会发生。
大部分多副本数据库(replicated databases)提供最终一致性(eventually consistency)的保证,这意味着,只要你对数据库停写,并等待足够长的时间,则所有对相同数据的读取请求最终会返回相同的结果。从另一个角度来说,所有的不一致都是暂时的,最终都会被解决(当然,这得是在网络故障能最终修复的假设之下)。描述相同意思的一个更好的名字可能:收敛性(convergence),即最终,所有副本都会收敛到相同的值。
但这是一个相当不靠谱的保证——没有提供任何关于何时收敛的信息。而在收敛之前,对于相同数据的读取,可能会返回任意值甚至不返回。举个例子,你向多副本数据库中写入了一条数据,并立即读取他。你能读到什么,最终一致性对此不会提供任何保证,因为读取请求可能会被路由到任何其他副本。
最终一致性对于应用层开发者很不友好,因为它表现出的行为和单线程程序中的变量完全不一致。在单线程模型里,如果对某个变量赋值后立即读取,我们默认一定会读到刚才的赋值,而不是读到旧值或者读取失败。数据库在对外表现上很像一组可读写的变量集,但具有复杂得多的语义。
在使用只提供弱保证的数据库时,我们需要时刻记得其限制,而不能偶尔自己增加额外假设,否则,会产生非常致命且难以察觉的 BUG。因为大部分时间里,应用层表现得毫无波澜,只有在系统中出现故障(网络拥塞、节点宕机)或在高负载场景下,这些边缘情况才会被触发。
本章我们会一起探究一些更强的一致性模型,但选择这些模型是有代价的。相对弱一致性模型系统来说,这么做要么会牺牲性能,要么会牺牲可用性。但提供强保证会让应用层能更加容易、正确的使用。但当然,我们最终还是得根据具体场景,来选择使用何等强度一致性模型。
在实践中,我们常会使用分层策略,让某些底层解决可用性、性能和容量的问题,让上层解决一致性的问题。比如云上各种基于 aws s3 的关系型数据库。另外,也有些系统会同时提供多种一致性模型供用户选择,在一致性和性能间进行取舍。
分布式系统中的一致性模型的强弱和第七章讲的事物的隔离级别层次有一些共通之处,比如在性能和隔离性/一致性间做取舍。但他们是相对独立的抽象:
- 事务隔离级别是为了解决并发所引起的竞态条件
- 分布式一致性是处理由于多副本间延迟和故障所引入的数据同步问题
本章涉及到很多主题,乍看起来很宽泛,但其内里是互相勾连的:
- 首先,我们从常用的最强的一致性模型:线性一致性(linearizability)开始,探究其优缺点。
- 接着,我们会考察分布式系统中时间的顺序问题,尤其是关于因果关系(causality)和全序问题(total ordering)。
- 最后,在第三部分,我们会探索如何原子性地提交一个分布式事务,最终导出共识问题的解决方法。