我们已经看到了如何使用读/写仲裁进行数据复制,以及使用全序广播进行状态机复制。在这种情况下,我们希望副本包含"相同数据的一致备份",但我们还没有准确定义consistent一致是什么意思。
不幸的是,"consistency一致性"这个词在不同的语境中意味着不同的东西。在事务的语境中,ACID中的C代表一致性,是一种状态的属性:也就是说,我们将数据库满足或违反某些不变性称为处于一致或不一致的状态。另一方面,在复制的语境下,我们使用"一致性"来指代副本之间的关系:我们希望一个副本与另一个副本保持一致。
由于一致性缺乏准确的定义,我们转而研究各种consistency models一致性模型。我们已经看到了一致性模型的一个特殊例子,即读写一致性。它限制了当同一节点先前对同一数据项进行写入后,读操作可能返回的值。我们将在本章中看到更多的模型。
7.1 Two-phase commit
让我们从执行distributed transaction分布式事务时出现的一致性问题开始,即多节点读/写事务。多个节点上的数据可能是同一个数据集的副本,或者是一个更大的数据集的不同部分;分布式事务可能发生在任一种情况下。
事务的一个关键属性是atomicity原子性。当一个事务跨越多个节点时,我们仍然希望整个事务具有原子性:也就是说,要么所有节点都必须提交事务并执行更新,要么所有节点都必须中止事务并丢弃或回滚状态。
因此,我们需要在节点之间就是否提交事务达成agreement一致。问题是,这种一致与我们之前讨论的共识consensus是一回事么?答案是否定的。虽然两者都是关于达成一致,但细节上有很大的不同。
最常见的确保多节点原子性提交的算法是two-phase commit (2PC)二阶段提交协议[Gray, 1978]。(还有一个three-phase commit三阶段提交协议,但它假定了不现实的同步系统模型,所以我们在这里不讨论它)。
当使用二阶段提交时,客户端首先在参与交易的每个副本上启动一个常规的单节点交易,并在这些交易中执行常规的读写操作。当客户端准备好提交交易时,它向transaction coordinator交易协调者(一个管理2PC协议的指定节点)发送一个提交请求(在部分系统中,协调器是客户端的一部分)。
协调者首先向参与交易的每个副本发送一个prepare准备消息,每个副本回复一个消息来表明它是否能够提交交易(这是协议的第一阶段)。此时副本实际上还没有提交交易,但它们必须确保,如果协调者发出指示,它们就能够在第二阶段提交交易。这意味着,副本必须将交易的所有更新写入磁盘,并在回复prepare准备消息之前检查完整性约束,同时持有交易的锁。
协调者收集响应,并决定是否实际提交交易。如果所有节点的都回复ok,协调者就决定提交;如果任一节点想放弃,或者任何节点未能在某个超时时间内回复,协调者就决定放弃。然后,协调者将其决定发送给每个副本,它们都按照指示提交或中止(这是第二阶段)。如果决定是提交,每个副本保证能够提交其事务,因为之前的prepare请求打好了基础。如果决定放弃,副本就会回滚该事务。
二阶段提交的问题是,协调者会单点故障。协调者可以通过将commit提交或abort中止决定提前写入稳定的存储空间来容忍故障。但即便如此,协调者崩溃时,可能还有一些已经prepare好但尚未commit / abort的事务(称为in-doubt transactions 存疑事务)。任何存疑事务都必须等到协调者恢复后才能知道自己的安排;它们不能单方面决定提交或中止,因为这种决定最终可能与协调者和其他节点不一致,导致违反原子性。
幸好,我们可以通过使用共识算法或全序广播协议来避免协调者的单点故障。上图显示了一种基于Paxos Commit[Gray and Lamport, 2006]的两阶段容错提交算法。它的理念是,每个参与交易的节点都使用全序广播来传播其关于commit或abort的投票。此外,如果节点A怀疑节点B故障了(比如在某个超时时间内没有收到来自B的投票),那么A可能会试图代表B投票abort。这就引入了一个竞争条件:如果节点B很慢,可能节点B就会广播自己的commit投票,但同时节点A怀疑B失效并代表B投票abort。
这些票数通过全序广播传递给每个节点,每个接收者独立计算票数。在这样做的时候,我们只计算来自任何副本的第一张投票,而忽略来自对方的任何后续投票。由于全序广播保证在每个节点上以相同的顺序递交,所有节点都会根据副本的第一张投票是commit还是abort达成一致,即使竞争条件下多个节点为同一副本广播相互矛盾的投票。
如果节点观察到来自某个副本的第一张投票是abort,那么transaction可以立即中止。否则,它需要等到从每个副本中递交了至少一个投票。一旦这些投票递交完成,并且没有副本投票abort,那该transaction就可以commit。由于全序广播,所有节点都能保证在abort或commit上做出一致的决定,这就保证了原子性。