1.3RPC
对于远程函数的调用与局部函数的使用不同,虽然可以给函数一个类似的签名,但是调用远程功能会出现许多错误情况,比如函数中断,仅仅处理了局部请求等。
接口定义语言,对于平台上不同编程语言实现的系统,要有一个能互通的,或者说,指定类型签名或函数调用的方式不是特定于任何一种编程语言(IDL)
2.3系统模型
两个将军问题:假设相互诚实,但是消息可能会出现丢失,即不可靠消息;拜占庭问题:消息是可靠的,但是节点不诚实。
现在将两个问题放一起,节点和网络都可能以各种方式出错,这也是分布式系统的算法基础。
系统模型中的假设包括:1.网络行为(例如消息丢失) 2.节点行为(例如崩溃)3.时序行为(例如延迟) 每个部分的模型选择。
系统模型模型:网络行为
假设两个节点之间进行双向点对点通信,其中之一是:1.可靠(完美)链接:当且仅当发送消息时才接收到消息。消息可能会重新排序。2.Fair-loss 链接:消息可能会丢失、重复或重新排序。如果您继续重试,消息最终会通过。3.任意链接(主动攻击者):恶意攻击者可能会干扰消息(窃听、修改、丢弃、欺骗、重放)。网络分区:一些链接长时间丢弃/延迟所有消息
系统模型:节点行为
每个节点执行指定的算法,假设以下之一:1.崩溃停止(fail-stop):如果节点崩溃(在任何时刻),则节点出现故障。崩溃后,它永远停止执行。2.崩溃恢复(fail-recovery):一个节点可能随时崩溃,失去它的内存状态。它可能会在稍后的某个时间恢复执行。3.Byzantine (fail-arbitrary):如果一个节点偏离了算法,它就是有故障的。故障节点可能会做任何事情,包括崩溃或恶意行为。没有故障的节点称为“正确”
系统模型:同步(定时)假设
对网络和节点假设以下之一:1.同步:消息延迟不大于已知上限。节点以已知速度执行算法。2.部分同步:系统在某些有限(但未知)的时间段内是异步的,否则是同步的。3.异步:消息可以任意延迟。节点可以任意暂停执行。根本没有时间保证。注意:计算机科学的其他部分以不同的方式使用术语“同步”和“异步”。
实践中的同步冲突
网络通常具有相当可预测的延迟,以下行为可能会导致延迟: 1.消息丢失和重新传输,延迟会无限增加,特别是如果我们必须等待网络分区被修复,然后消息才能通过 2.导致排队的拥塞/争用 3. 网络/路由重新配置。节点通常以可预测的速度执行代码,偶尔会暂停,下面是可能导致暂停的原因:1.操作系统调度问题,例如优先级反转 2.Stop-the-world 垃圾收集暂停3.Page faults, swap, thrashing 实时操作系统 (RTOS) 提供调度保证,但大多数分布式系统不使用 RTOS。
结合可变网络延迟的许多原因,这意味着在实际系统中,假设同步系统模型很少是安全的。大多数分布式算法需要针对异步或部分同步模型进行设计。
系统模型总结
对于三个部分中的每一个,选择一个进行分布式设计:1.网络:可靠、公平损失或任意 2.节点:crash-stop、crash-recovery 或 Byzantine 3.时序:同步、部分同步或异步,这是任何分布式算法的基础。如果您的假设是错误的,那么所有的赌注都会失败!
2.4容错和高可用性
从业务的角度来看,通常最重要的是服务的可用性。
故障(例如节点崩溃或网络中断)是不可用的常见原因。为了提高可用性,我们可以降低故障频率,或者我们可以设计系统以在其某些组件出现故障的情况下继续工作;后一种方法称为容错。
实现高可用性:容错。
失败:整个系统不工作。
故障:系统的某些部分不工作。节点故障:崩溃(崩溃停止/崩溃恢复),偏离算法(拜占庭) .网络故障:丢弃或显着延迟消息 .
容错:系统作为一个整体继续工作,尽管有故障(假设故障的最大数量) .
单点故障(SPOF):故障导致故障的节点/网络链路
容忍故障的第一步是检测故障,这通常使用故障检测器来完成。 (“故障检测器”更符合逻辑,但“故障检测器”是传统术语。)故障检测器通常检测崩溃故障。拜占庭故障并不总是可以检测到,尽管在某些情况下拜占庭行为确实留下了可用于识别和排除恶意节点的证据。
在大多数情况下,故障检测器通过定期向其他节点发送消息来工作,如果在预期时间内没有收到响应,则将节点标记为崩溃。理想情况下,我们希望当且仅当节点真的崩溃时才会发生超时(这被称为完美故障检测器)。然而,两位将军的问题告诉我们,这并不是检测崩溃的完全准确的方法,因为没有响应也可能是由于消息丢失或延迟。完美的基于超时的故障检测器只存在于具有可靠链路的同步崩溃停止系统中;在部分同步系统中,不存在完美的故障检测器。此外,在异步系统中,不存在基于超时的故障,因为超时在异步模型中毫无意义。然而,部分同步系统中存在一个有用的故障检测器:最终完美的故障检测器
3 时间、时钟和事件的顺序
对时序的假设构成了分布式算法所依赖的系统模型的关键部分。例如,基于超时的故障检测器需要测量时间以确定何时超时。操作系统广泛依赖计时器和时间测量来安排任务、跟踪 CPU 使用率和许多其他目的。
3.1物理时钟
由于闰秒,一个小时总是有 3600 秒,而一天总是有 86,400 秒是不正确的。在 UTC 时间刻度中,由于闰秒,一天可以是 86,399 秒、86,400 秒或 86,401 秒。这使需要处理日期和时间的软件变得复杂。
但是,操作系统和分布式系统通常确实依赖高分辨率时间戳来准确测量时间,其中一秒的差异非常明显。在这种情况下,忽略闰秒可能很危险。
由于 Linux 内核中的错误,闰秒很可能在运行多线程进程时触发活锁条件。即使重新启动也不能解决问题,而是设置系统时钟会重置内核中的不良状态。
今天,一些软件明确处理闰秒,而其他程序继续忽略它们。今天广泛使用的一个实用的解决方案是,当一个正闰秒发生时,而不是在 23:59:59 和 00:00:00 之间插入它,额外的一秒被故意分散在该时间之前和之后的几个小时内在此期间减慢时钟(或在负闰秒的情况下加快时钟)。这种方法叫做涂抹闰秒,也不是没有问题。然而,它是一种实用的替代方法,可以让所有软件都意识到闰秒并对其鲁棒,这很可能是不可行的。
3.2 时钟同步和单调时钟
协议:网络时间协议 (NTP)、精确时间协议 (PTP)。 NTP 如何估计客户端和服务器之间的时钟偏差?
3.3因果关系和happens-before
分布式的一个问题:
为了将我们在这种场景中“正确”顺序的含义形式化,我们使用幻灯片 62 中定义的happensbefore 关系。该定义假设每个节点只有一个执行线程,因此对于一个节点的任何两个执行步骤节点,很明显哪个先发生。更正式地说,我们假设在同一节点发生的事件有严格的总顺序。多线程进程可以通过使用单独的节点来表示每个线程来建模。然后,我们通过定义在接收到相同消息之前发送消息来跨节点扩展此顺序(换句话说,我们排除了时间旅行:不可能接收尚未发送的消息)。为方便起见,我们假设每条发送的消息都是唯一的,因此当收到一条消息时,我们总是可以明确地知道该消息是在何时何地发送的。在实践中,可能存在重复消息,但我们可以使它们唯一,例如通过在每条消息中包含发送者节点的 ID 和序列号。最后,我们采用传递闭包,结果是happens-before关系。这是一个偏序,这意味着对于某些事件 a 和 b,可能 a 既没有发生在 b 之前,b 也没有发生在 a 之前。在这种情况下,我们称 a 和 b 并发。请注意,这里的“并发”并不是字面上的“同时”,而是 a 和 b 是独立的,因为没有从一个消息到另一个的消息序列。
4 广播协议和逻辑时间
在本章节中,我们将研究广播协议(也称为多播协议),即用于将一条消息传递给多个接收者的算法。正如我们将在第 5 讲中看到的,这些是高级分布式算法的有用构建块。在实践中使用了几种不同的广播协议,它们的主要区别在于它们传递消息的顺序。
4.1逻辑时间
逻辑时钟专注于正确捕获分布式系统中的事件顺序。我们不关注于时间,或者说过去了多少秒,相反关注的是发生了事件,因此,逻辑时间戳是,本质说是在某些事情发生时,计次。
4.1.1 lamport时钟
Lamport 时间戳本质上是一个整数,用于计算已发生事件的数量。因此,它与物理时间没有直接关系。在每个节点上,时间都会增加,因为整数在每个事件上都会增加。该算法假设一个崩溃停止模型(或者如果时间戳保存在稳定的存储中,即在磁盘上,则为崩溃恢复模型)。当通过网络发送消息时,发送者会将其当前的 Lamport 时间戳附加到该消息。
Lamport 时间戳的属性是,如果 a 发生在 b 之前,那么 b 总是比 a 具有更大的时间戳;换句话说,时间戳与因果关系一致。然而,反之则不成立。示例中,节点 A 上的第三个事件和节点 B 上的第一个事件的时间戳均为 3。如果我们需要为每个事件提供唯一的时间戳,则可以使用节点的名称或标识符扩展每个时间戳该事件发生的时间。在单个节点的范围内,每个事件都被分配一个唯一的时间戳;因此,假设每个节点都有一个唯一的名称,时间戳和节点名称的组合是全局唯一的(跨所有节点)。
给定两个事件的 Lamport 时间戳,通常无法判断这些事件是并发的还是一个事件发生在另一个事件之前。如果我们确实想检测事件何时并发,我们需要一种不同类型的逻辑时间:矢量时钟。
4.1.2 向量时钟。
给定 Lamport 时间戳 L(a) 和 L(b),且 L(a) < L(b),我们无法判断是 a → b 还是 b→a。如果我们想检测哪些事件是并发的,我们需要矢量时钟:假设系统中有 n 个节点,N = hN1, N2, . . . ,。事件 a 的向量时间戳为 V (a) = ht1, t2, 。 . . tni。 ti 是节点 Ni 观察到的事件数。每个节点都有一个当前向量时间戳 T。在节点 Ni 发生事件时,增加向量元素 T [i]。将当前向量时间戳附加到每条消息。接收方将消息向量合并到其本地向量中。除了标量和向量之间的区别之外,向量时钟算法与 Lamport 时钟非常相似)。一个节点初始化它的向量时钟以包含系统中每个节点的零。每当节点 Ni 发生事件时,它都会在其矢量时钟中增加第 i 个条目(它自己的条目)。 (在实践中,这个向量通常实现为从节点 ID 到整数的映射,而不是整数数组。)当通过网络发送消息时,发送者的当前向量时间戳会附加到消息中。最后,当接收到消息时,接收者通过取两个向量的元素最大值将消息中的向量时间戳与其本地时间戳合并,然后接收者增加自己的条目。
请注意,当 C 从 B 接收到消息 m2 时,A 的向量条目也更新为 2,因为此事件与发生在 A 的两个事件具有间接因果关系。这样,向量时间戳反映了发生的传递性- 关系之前。
然后,我们定义向量时间戳的偏序,如幻灯片 72 所示。如果第一个向量的每个元素都小于或等于第二个向量的相应元素,我们就说一个向量小于或等于另一个向量。如果一个向量小于或等于另一个向量,并且它们至少在一个元素上不同,则它们严格小于另一个向量。但是,如果一个向量在一个元素中具有更大的值,而另一个向量在不同元素中具有更大的值,则两个向量是不可比的
4.2广播协议中的交付顺序
广播协议,它概括了网络,以便将消息发送到某个组中的所有节点。组成员身份可以是固定的,或者系统可以提供节点加入和离开组的机制。一些局域网在硬件级别提供多播或广播(例如,IP 多播),但 Internet 上的通信通常只允许单播。此外,硬件级多播通常是在尽力而为的基础上提供的,它允许丢弃消息;要想使其可靠,可能需要之前提到的,多次重复发送消息的重传协议(一个节点会尝试向所有节点发送消息,尽管它可能不会到达,例如在发件人崩溃的情况下?)就像在系统模型的上下文中假设的一样,假设不是同步的,而是异步或者部分异步,也就是我们不会假设消息的上限延迟,因此在可靠的广播协议中,我们可以说消息最终会消失,并且不会对可能需要多少时间做出承诺,一直到消息通过为止,这是广播的背景。
下面的都是可靠的广播协议,但是区别在于消息顺序不同。
4.2.1 FIFO广播
最弱的广播类型称为 FIFO 广播,它与 FIFO 链接密切相关(参见练习 2)。在此模型中,同一节点发送的消息按发送顺序传递。例如,在幻灯片 76 上,m1 必须在 m3 之前交付,因为它们都是由 A 发送的。但是,m2 可以在 m1 和 m3 之前、之间或之后的任何时间交付。关于这些广播协议的另一个细节:我们假设每当一个节点广播一条消息时,它也会将该消息传递给它自己(在幻灯片 76 上表示为一个环回箭头)。起初这似乎没有必要——毕竟,节点知道它自己广播了哪些消息! – 但我们将需要这个来进行总订单广播。
4.2.2 causal广播
消息按因果顺序传递:也就是说,如果一条消息的广播发生在另一条消息的广播之前,那么所有节点都必须按该顺序传递这两条消息。如果同时广播两条消息,则节点可以按任一顺序传递它们。在幻灯片 76 上的示例中,如果节点 C 在 m1 之前接收到 m2,则 C 的广播算法将不得不阻止(延迟或缓冲) m2 直到 m1 被传递,以确保消息按因果顺序传递。在幻灯片 77 的示例中,消息 m2 和 m3 同时广播。节点 A 和 C 以 m1、m3、m2 的顺序传递消息,而节点 B 以 m1、m2、m3 的顺序传递消息。这些交货单中的任何一个都是可以接受的,因为它们都符合因果关系。
4.2.3 全单广播
第三种广播是全序广播,有时也称为原子广播。虽然 FIFO 和因果广播允许不同的节点以不同的顺序传递消息,但总顺序广播在节点之间强制执行一致性,确保所有节点以相同的顺序传递消息。精确的交货顺序没有定义,只要在所有节点上都相同。幻灯片 78 和 79 显示了总订单广播的两个示例执行。在幻灯片 78 上,所有三个节点都按 m1、m2、m3 的顺序传递消息,而在幻灯片 79 上,所有三个节点都按 m1、m3、m2 的顺序传递消息。只要节点同意,这些执行中的任何一个都是有效的。
与因果广播一样,节点可能需要保留消息,等待需要首先传递的其他消息。例如,节点 C 可以按任意顺序接收消息 m2 和 m3。如果算法已经确定 m3 应该在 m2 之前交付,但如果节点 C 先收到 m2,则 C 将需要阻止 m2 直到收到 m3 之后。在这些图中可以看到另一个重要的细节:在 FIFO 和因果广播的情况下,当一个节点广播一条消息时,它可以立即将该消息传递给自己,而无需等待与任何其他节点的通信。这在全序广播中不再适用:例如,在幻灯片 78 上,m2 需要在 m3 之前交付,因此节点 A 向自身交付 m3 必须等到 A 从 B 收到 m2 之后。同样,在幻灯片 79 上,节点 B 向自己交付 m2 必须等待 m3。
4.2.4 FIFO和全单结合
最后,FIFO-total order 广播类似于 Total order 广播,但有一个额外的 FIFO 要求,即同一节点广播的任何消息都按照它们发送的顺序传递。幻灯片 78 和 79 上的示例实际上是有效的 FIFO-total order 广播执行,因为在两者中 m1 在 m3 之前交付。
4.3 广播算法
我们现在将继续讨论实现广播的算法。粗略地说,这包括两个步骤:首先,确保每个节点都接收到每条消息;其次,以正确的顺序传递这些信息。我们将首先研究可靠地传播消息。我们可能尝试的第一个算法是:当一个节点想要广播一条消息时,它会使用幻灯片 33 中讨论的可靠链接将该消息单独发送到每个其他节点(即重新传输丢弃的消息)。但是,可能会发生消息被丢弃,并且发件人在重新传输之前崩溃的情况。在这种情况下,其中一个节点将永远不会收到该消息。
为了提高可靠性,我们可以寻求其他节点的帮助。例如,我们可以说一个节点第一次收到特定消息时,它会将其转发给其他每个节点(这称为急切可靠广播)。该算法确保即使某些节点崩溃,所有剩余(非故障)节点也会收到每条消息。然而,该算法效率相当低:在没有故障的情况下,每条消息在一组 n 个节点中被发送 O(n2) 次,因为每个节点将接收每条消息 n - 1 次。这意味着它使用了大量的冗余网络流量
一个特别常见的广播算法系列是八卦协议(也称为流行协议)。在这些协议中,希望广播消息的节点将其发送到随机选择的少量固定数量的节点。在第一次收到消息时,节点将其转发给固定数量的随机选择的节点。这类似于流言蜚语、谣言或传染病在人群中传播的方式。 Gossip 协议并不严格保证所有节点都会收到消息:有可能在随机选择节点时,总是会省略某些节点。但是,如果算法的参数选择得当,消息不被传递的概率可能非常小。 Gossip 协议之所以吸引人,是因为通过正确的参数,它们对消息丢失和节点崩溃具有很强的弹性,同时还能保持高效。
现在有了可靠的广播,我们可以在之上构建FIFO、因果或者全序广播。
节点 Ni 发送的每条 FIFO 广播消息都标记有发送节点编号 i 和序列号,Ni 发送的第一个消息为 0,第二个消息为 1,以此类推。每个节点的本地状态由序列号 sendSeq(计算该节点广播的消息数)、已传递(每个节点有一个条目的向量,计算该节点已传递的每个发送者的消息数)和缓冲区(用于保留消息直到它们准备好传送的缓冲区)。该算法检查来自任何发送者的与预期的下一个序列号匹配的消息,然后递增该数字,确保来自每个特定发送者的消息按序列号递增的顺序传递
因果广播算法有点类似于 FIFO 广播;我们不是为每条广播的消息附加一个序列号,而是附加一个整数向量。该算法有时被称为矢量时钟算法,尽管它与幻灯片 70 上的算法完全不同。在幻灯片 70 中的矢量时钟算法中,矢量元素计算每个节点上发生的事件数,而在因果关系中广播算法,向量元素计算来自每个发送者的已传递消息的数量。每个节点的本地状态由 sendSeq、delivered 和 buffer 组成,它们与 FIFO 广播算法中的含义相同。当一个节点想要广播一条消息时,我们附加发送节点号 i 和 deps,一个表示该消息的因果依赖关系的向量。我们通过获取传递的副本来构建 deps,该向量计算来自每个发送者的消息已在该节点传递的数量。这表明在此广播之前已在本地传递的所有消息必须按因果顺序出现在广播消息之前。然后我们将这个向量的发送节点自己的元素更新为等于 sendSeq,这样可以确保该节点广播的每条消息都与同一节点广播的前一条消息有因果关系。当接收到消息时,算法首先将其添加到缓冲区中,就像 FIFO 广播一样,然后在缓冲区中搜索任何准备好发送的消息。比较 deps ≤ Delivered 在幻灯片 72 上定义的向量上使用 ≤ 运算符。如果此节点已经按因果顺序传递了必须在此消息之前的所有消息,则此比较为真。任何因果就绪的消息随后都会传递给应用程序并从缓冲区中删除,并且传递的向量的适当元素会递增。
最后,总订单广播(和 FIFO 总订单广播)更棘手。幻灯片 86 概述了两种简单的方法,一种基于指定的领导节点,另一种是使用 Lamport 时间戳的无领导算法。然而,这两种方法都不是容错的:在这两种情况下,单个节点的崩溃都会阻止所有其他节点传递消息。在单领导者方法中,领导者是单点故障。我们将在第 6 课中回到容错全序广播问题。
五.复制
复制问题,这意味着在多个节点上维护相同数据的副本,每个节点称为副本。复制是许多分布式数据库、文件系统和其他存储系统的标准功能。这是我们实现容错的主要机制之一:如果一个副本出现故障,我们可以继续访问其他副本上的数据副本。
5.1 远程操作状态
让我们以在社交网络上“喜欢”状态更新的行为为例。当您单击“喜欢”按钮时,您喜欢它的事实以及喜欢它的人数需要存储在某个地方,以便向您和其他用户显示它们。这通常发生在社交网络服务器上的数据库中。我们可以将存储在数据库中的数据视为其状态。更新数据库的请求可能会在网络中丢失,或者可能会丢失已执行更新的确认。像往常一样,我们可以通过重试请求来提高可靠性。但是,如果我们不小心,重试可能会导致请求被多次处理,从而导致数据库中的状态不正确。
例如上图推特,关注人数出现负数,如果这个是银行账号呢?数据库操作本质上是一样的,但是执行这个操作太多次可能会出现意外。
防止更新多次生效的一种方法是对请求进行重复数据删除。但是,在崩溃恢复系统模型中,这需要将请求(或有关请求的一些元数据,例如矢量时钟)存储在稳定的存储中,以便即使在崩溃之后也可以准确地检测到重复。记录重复数据删除请求的另一种方法是使请求具有幂等性。
递增计数器不是幂等的,但将元素添加到集合中是幂等的。因此,如果需要一个计数器(如点赞数),最好实际维护数据库中的元素集,并通过计算其基数从集合中导出计数器值。幂等更新可以安全地重试,因为执行多次与执行一次具有相同的效果。幂等性允许更新具有完全一次的语义:也就是说,更新实际上可能会被应用多次,但效果就像它被应用了一次一样。
但是,幂等性有一个限制,当有多个更新正在进行时就会变得很明显。在幻灯片 91 上,客户 1 将用户 ID 添加到帖子的喜欢集合中,但确认丢失。客户端 2 从数据库中读取一组点赞数(包括客户端 1 添加的用户 ID),然后再次发出删除用户 ID 的请求。同时,客户端 1 重试其请求,不知道客户端 2 所做的更新。因此,重试具有将用户 ID 再次添加到集合中的效果。这是出乎意料的,因为客户端 2 观察到客户端 1 的更改,因此删除是在添加集合元素之后因果发生的,因此我们可能期望在最终状态下,用户 ID 不应该出现在集合中。在这种情况下,将元素添加到集合是幂等的这一事实不足以使重试安全。
类似的问题发生在幻灯片 92 上,我们有两个副本。在第一个场景中,客户端首先将 x 添加到数据库的两个副本中,然后尝试再次从两个副本中删除 x。但是,对副本 B 的删除请求丢失了,并且客户端在能够重试之前就崩溃了。在第二种情况下,客户端尝试将 x 添加到两个副本,但对副本 A 的请求丢失,客户端再次崩溃。
在这两种情况下,结果是相同的:x 存在于副本 B 中,而副本 A 中不存在。但预期的效果不同:在第一种情况下,客户端希望从两个副本中删除 x,而在第二种情况下,客户希望 x 出现在两个副本上。当两个副本协调它们不一致的状态时,我们希望它们都以客户端预期的状态结束。但是,如果副本无法区分这两种情况,则这是不可能的。为了解决这个问题,我们可以做两件事。首先,我们为每个更新操作附加一个逻辑时间戳,并将该时间戳作为更新写入的数据的一部分存储在数据库中。其次,当被要求从数据库中删除一条记录时,我们实际上并没有删除它,而是编写了一种特殊类型的更新(称为墓碑)将其标记为已删除。在幻灯片 93 上,包含 false 的记录是墓碑。
在许多复制系统中,副本运行一个协议来检测和协调任何差异(这称为反熵),以便副本最终保持相同数据的一致副本。多亏了墓碑,反熵过程可以区分已删除的记录和尚未创建的记录。多亏了时间戳,我们可以分辨出哪个版本的记录较旧,哪个版本较新。然后,反熵过程保留较新的记录并丢弃较旧的记录。这种方法还有助于解决幻灯片 91 上的问题:重试请求与原始请求具有相同的时间戳,因此重试不会覆盖由具有更大时间戳的因果稍后请求写入的值。
将时间戳附加到每个更新的技术对于处理并发更新也很有用。在幻灯片 95 上,客户端 1 想要将键 x 设置为值 v1(带有时间戳 t1),而同时客户端 2 想要将相同的键 x 设置为值 v2(带有时间戳 t2)。副本 A 首先接收 v2,然后是 v1,而副本 B 以相反的顺序接收更新。为了确保两个副本最终处于相同的状态,我们不依赖于它们接收请求的顺序,而是它们的时间戳顺序。
这种方法的细节取决于使用的时间戳类型。如果我们使用 Lamport 时钟(总顺序在幻灯片 68 中定义),两个并发更新将任意排序,具体取决于时间戳的分配方式。在这种情况下,我们得到了所谓的最后写入者获胜 (L WW) 语义:时间戳最大的更新生效,而对同一键的时间戳较低的任何并发更新都将被丢弃。这种方法使用起来很简单,但是当同时执行多个更新时,它确实意味着数据丢失。这是否是一个问题取决于应用程序:在某些系统中,丢弃并发更新是可以的。当丢弃并发更新不可接受时,我们需要使用一种时间戳,它允许我们检测更新何时并发发生,例如矢量时钟。使用这种部分排序的时间戳,我们可以判断新值何时应该覆盖旧值(旧更新发生在新更新之前),以及当多个更新同时发生时,我们可以保留所有同时写入的值。这些同时写入的值称为冲突,有时称为兄弟值。应用程序可以稍后将冲突合并回单个值,如第 8 讲中所讨论的。矢量时钟的一个缺点是它们可能变得昂贵:每个客户端都需要矢量中的一个条目,并且在具有大量客户端的系统中(或在客户端每次重新启动时都会假设一个新的身份),这些向量可能会变得很大,可能会占用比数据本身更多的空间。已经开发了更多类型的逻辑时钟,例如点版本向量,以优化此类系统。
5.2Quorums(仲裁?)
如何在复制中实现容错?
首先,考虑幻灯片 97 上的示例。假设我们有两个副本,A 和 B,它们最初都将键 x 与值 v0(和时间戳 t0)相关联。客户端尝试将 x 的值更新为 v1(时间戳为 t1)。更新 B 成功,但更新 A 失败,因为 A 暂时不可用。随后,客户端尝试读回它写入的值;读取在 A 处成功,但在 B 处失败。因此,读取不会返回同一客户端先前写入的值 v1,而是返回初始值 v0。
这种情况是有问题的,因为从客户端的角度来看,它写入的值看起来好像已经丢失了。
因此许多系统需要 read-afterwrite 一致性(也称为 read-your-writes 一致性),其中我们确保在客户端写入值后,同一个客户端将能够读回相同的值数据库中的值。严格来说,在写后读一致性的情况下,客户端在写入后可能不会读取它写入的值,因为同时另一个客户端可能已经覆盖了该值。因此,我们说写后读一致性要求读取最后写入的值或稍后写入的值。在幻灯片 97 上,我们可以通过确保始终写入两个副本和/或从两个副本读取来保证写入后读取的一致性。但是,这意味着读取和/或写入不再具有容错性:如果一个副本不可用,则需要来自两个副本的响应的写入或读取将无法完成。我们可以通过使用三个副本来解决这个难题,如幻灯片 98 所示。
我们将每个读写请求都发送到所有三个副本,但只要我们收到≥ 2 个响应,我们就认为请求成功。在该示例中,副本 B 和 C 上的写入成功,而副本 A 和 B 上的读取成功。对于读取和写入都采用“三选二”的策略,可以保证对副本的至少一个响应读取来自看到最近写入的副本(在示例中,这是副本 B)。
不同的副本可能对同一个读取请求返回不同的响应:在幻灯片 98 上,A 处的读取返回初始值 (t0, v0),而 B 处的读取返回此客户端之前写入的值 (t1, v1)。使用时间戳,客户端可以判断哪个响应是最近的,并将 v1 返回给应用程序。
在这个例子中,响应写入请求的副本集合 {B, C} 是写入仲裁,响应读取的副本集合 {A, B} 是读取仲裁。一般来说,仲裁是必须响应某些请求才能成功的最小节点集。 (该术语来自政治,其中法定人数是指做出有效决定所需的最低票数,例如在议会或委员会中。)为了确保写入后读取的一致性,写入的法定人数和读 quorum 必须有一个非空交集:换句话说,读 quorum 必须包含至少一个已确认写入的节点。在分布式系统中,一个常见的仲裁选择是多数仲裁,它是任何包含严格超过一半节点的节点子集。使用多数仲裁,这意味着三个副本的系统可以容忍一个副本不可用,一个五个副本的系统可以容忍两个不可用,依此类推
在这种复制的仲裁方法中,在任何给定时刻,某些副本可能会丢失某些更新:例如,在幻灯片 98 上,副本 A 中缺少 (t1, v1) 更新,因为该写入请求已被删除。为了使副本彼此恢复同步,一种方法是依赖反熵过程,如幻灯片 94 中所述。
另一种选择是让客户帮助传播更新。例如,在幻灯片 100 上,客户端从 B 读取 (t1, v1),但它从 A 接收到较旧的值 (t0, v0),而 C 没有响应。由于客户端现在知道更新 (t1, v1 ) 需要传播到 A,它可以将该更新发送到 A(使用原始时间戳 t1,因为这不是新的更新,只是对先前更新的重试)。客户端也可以将更新发送给 C,即使它不知道 C 是否需要它(如果事实证明 C 已经有这个更新,那么只会浪费少量的网络带宽)。这个过程称为读修复。客户端可以对其发出的任何读取请求执行读取修复,无论它是否是最初执行相关更新的客户端。使用这种复制模型的数据库通常被称为 Dynamo 风格。
5.3 使用广播进行复制
第 5.2 节的仲裁方法本质上使用尽力而为广播:客户端将每个读取或写入请求广播到所有副本,但协议不可靠(请求可能丢失)并且不提供排序保证。复制的另一种方法是使用第 4 课中的广播协议。让我们首先考虑 FIFO-total order 广播,这是我们见过的最强的广播形式。
使用 FIFO-total order 广播可以很容易地构建一个复制系统:我们将每个更新请求广播到副本,副本根据每条消息在传递时更新它们的状态。这称为状态机复制 (SMR),因为副本充当状态机,其输入是消息传递。我们只要求更新逻辑是确定性的:任何两个处于相同状态并被赋予相同输入的副本必须以相同的下一个状态结束。甚至错误也必须是确定性的:如果更新在一个副本上成功但在另一个副本上失败,它们将变得不一致。 SMR 的一个出色特性是,从一个状态转移到下一个状态的逻辑可以任意复杂,只要它是确定性的。例如,可以执行具有任意业务逻辑的整个数据库事务,该逻辑既可以依赖于广播消息,也可以依赖于数据库的当前状态。一些分布式数据库以这种方式执行复制,每个副本独立地执行相同的确定性事务代码(这称为主动复制)。这一原则也是区块链、加密货币和分布式账本的基础:区块链中的“区块链”只不过是由全序广播协议传递的消息序列(第 6 讲会详细介绍),每个副本都确定性地执行这些区块中描述的交易以确定分类帐的状态(例如,谁拥有哪些钱)。 “智能合约”只是一个确定性程序,副本在传递特定消息时执行。
状态机复制的缺点是总订单广播的限制。如 4.2 节所述,当节点想要通过全序广播来广播消息时,它不能立即将该消息传递给自己。因此,在使用状态机复制时,想要更新其状态的副本不能立即这样做,而是必须经过广播过程,与其他节点协调,并等待更新传递回自己。状态机复制的容错性取决于底层全订单广播的容错性,我们将在第 6 课中讨论。然而,基于全订单广播的复制被广泛使用。
回想一下幻灯片 86,实现全订单广播的一种方法是指定一个节点作为领导者,并通过它路由所有广播消息以强制执行交付订单。这一原则也广泛用于数据库复制:许多数据库系统将一个副本指定为领导者、主副本或主副本。任何希望修改数据库的事务都必须在领导副本上执行。如幻灯片 103 所示,领导者可以同时执行多个事务;但是,它以总顺序提交这些事务。当事务提交时,领导者副本将该事务的数据更改广播到所有跟随者副本,并且跟随者按提交顺序应用这些更改。这种方式称为被动复制,我们可以看到它相当于事务提交记录的全序广播。