文章目录
分布式系统存在太多可能出错的场景,如果一出故障就停机,未免不太现实。我们需要更加容错的解决方案,即使某些内部组件发生故障,整个系统仍然可以对外提供服务。
本章将讨论构建容错式分布系统的相关算法和协议。为了构建容错系统,最好建立一套通用的抽象机制和与之对应的技术保证,这样只需实现一次,其上的各种应用程序都可以安全地信赖底层的保证。总之,抽象的事务机制可以屏蔽系统内部很多复杂的问题。
分布式系统最重要的抽象之一就是共识:所有的节点就某一项提议达成一致。本章主要研究的就是解决共识问题的相关算法。
1. 一致性保证
最终一致性是一个非常弱的一致性保证。分布式一致性模型和我们之前在事务隔离中讨论的相似,但是总体上有显著区别:事务隔离主要是为了处理并发执行事务时的各种临界条件,而分布式一致性则主要是针对延迟和故障等问题来协调多副本之间的状态。
本章将探索更强的一致性模型。这也意味着更多的代价,例如性能降低或容错性差。但是,更强的保证的好处是使上层应用逻辑更简单,更不易出错。
2. 可线性化
2.1 就近保证
可线性化的思想:让一个系统看起来好像只有一个数据副本,且所有的操作都是原子的。有了这个保证,应用程序就不需要关心系统内部的多个副本。
可线性化的一种就近的保证:在一个可线性化的系统中,一旦某个客户端成功提交写请求,所有客户端的读请求一定都能看到最新的值,直到被再次覆盖。这种看似单一副本的假象意味着它可以保证读取最近最新值,而不是过期的缓存。
2.2 可线性化 VS 可串行化
可线性化与可串行化概念完全不同,需要仔细区分:
可串行化:事务的隔离属性,其中每个事务可以读写多个对象,它确保事务执行的结果与串行执行的结果完全相同。
可线性化:读写寄存器(单个对象)的最新值保证。她它并不要求将操作组合到事务中,因此无法避免写倾斜与幻读等问题,除非采取其他额外措施。
实际以串行执行和两阶段加锁都是典型的可线性化。
可串行的快照隔离则不是线性化的:按照设计,它可以从一致性快照中读取,以避免读、写之间的竞争。一致性快照的要点在于它里面不包括快照点创建时刻之后的写入数据,因此从快照读取肯定不满足线性化。
2.3 线性化的场景
- 加锁与主节点选举
主从复制的系统需要确保有且仅有一个主节点,否则会产生脑裂。选举新的主节点常见的方法就是使用锁:即每个启动的节点都试图获得锁,但其中只有一个可以成功,继而成为主节点。不管锁具体如何实现,它必须满足可线性化:所有的节点都必须同意哪个节点持有锁,否则就会出现问题。
- 约束与唯一性保证
这种情况和加锁非常类似,其实本质都是要确保唯一性。
硬性的唯一性约束,常见如关系型数据库中的主键约束,则需要线性化保证。
- 跨通道的时间依赖
当系统中存在多个不同的通信通道时,如果没有线性化的就近保证,这些通道之间存在竞争条件。
线性化并非避免这种竞争的唯一方法,但却是最容易理解的。
2.4 实现线性化系统
线性化本质上意味着“表现得好像只有一个数据副本”,所以最简单的方案自然是只用一个数据副本。但显然,该方法无法容错:如果仅有的副本所在的节点发生故障,就会导致数据丢失。
系统容错最常见的方法:复制机制。但是非常遗憾,三种主流复制方案,都无法实现完全的可线性化。
(1)主从复制(部分支持可线性化)
同步方式:理论上可以满足线性化。但是,如果某节点自认为是主节点,但事实并非如此,主要是因为他们可能会采用快照隔离的设计,或者实现时存在并发方面的bug。(这显然不满足线性化的就近一致保证)这个“自以为是”的主节点如果对外提供服务,就会违反线性化。
异步方式:故障切换过程中甚至可能会丢失一些已提交的写入,结果是同时违反持久性和线性化。
(2)多主复制(不可线性化)
多主节点复制的系统通常是无法线性化的,这其实正是多副本,从定义上就违背了可线性化。
(3)无主复制(可能不可线性化)
对于无主节点复制的系统,完全取决于具体的quorum的配置,以及如何定义强一致性,它非常有可能并不保证线性化。比如:“最后写入者获胜”冲突解决方法几乎肯定是非线性化的;宽松的quorum也会破坏线性化。
综上所述,单纯依靠三种常见的复制机制都无法安全地实现线性化。我们需要专门设计的共识算法来实现可线性化,这些系统包括:ZooKeeper和etcd。