From 3b33445a076ea273dd83e4093b5d949dff9530cf Mon Sep 17 00:00:00 2001 From: Vonng Date: Mon, 12 Feb 2018 14:53:06 +0800 Subject: [PATCH] =?UTF-8?q?ch8=20=E6=9C=BA=E7=BF=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ddia/README.md | 6 +- ddia/ch8.md | 622 +++++++++++++++++++++++++++++++++++++++++++++++-- ddia/ch9.md | 11 +- 3 files changed, 615 insertions(+), 24 deletions(-) diff --git a/ddia/README.md b/ddia/README.md index 269882b7..89fcd7c1 100644 --- a/ddia/README.md +++ b/ddia/README.md @@ -69,9 +69,9 @@ | 第二部分:分布式数据——概览 | [part-ii.md](part-ii.md) | | 初翻 | | 第五章:复制 | [ch5.md](ch5.md) | | 初翻 | | 第六章:分片 | [ch6.md](ch6.md) | | 初翻 | -| 第七章:事务 | [ch7.md](ch7.md) | | 初翻 35% | -| 第八章:分布式系统的麻烦 | [ch8.md](ch8.md) | | - | -| 第九章:一致性与共识 | [ch9.md](ch9.md) | | - | +| 第七章:事务 | [ch7.md](ch7.md) | | 初翻 | +| 第八章:分布式系统的麻烦 | [ch8.md](ch8.md) | | **机翻 0%** | +| 第九章:一致性与共识 | [ch9.md](ch9.md) | | | | 第三部分:前言 | [part-iii.md](part-iii.md) | | 机翻 | | 第十章:批处理 | [ch10.md](ch10.md) | | - | | 第十一章:流处理 | [ch11.md](ch11.md) | | - | diff --git a/ddia/ch8.md b/ddia/ch8.md index b88d16e1..e234ba04 100644 --- a/ddia/ch8.md +++ b/ddia/ch8.md @@ -16,50 +16,646 @@ [TOC] +最近几章中反复出现的主题是系统如何处理错误的事情。例如,我们讨论了复制故障转移(第156页的“处理节点中断”),复制滞后(第161页的“复制滞后问题”)和事务的控制(第233页的“弱隔离级别”)。当我们了解可能在实际系统中出现的各种边缘情况时,我们会更好地处理它们。 + +但是,尽管我们已经谈了很多错误,但最后几章仍然过于乐观。现实更加黑暗。我们现在将我们的悲观主义转向最大化,并假设任何可能出错的东西**都会**出错[^i]。(经验丰富的系统操作员会告诉你这是一个合理的假设,如果你问得好,他们可能会告诉你一些可怕的故事,护理过去的战斗伤痕。) + +[^i]: 除了一个例外:我们将假定故障是非拜占庭式的(参见“[拜占庭故障](#拜占庭故障)”)。 + +使用分布式系统与在一台计算机上编写软件有着根本的区别,主要的区别在于有许多新的和令人兴奋的方法可以使事情出错[1,2]。在这一章中,我们将了解实践中出现的问题,理解我们能够和不可以依赖的东西。 + +最后,作为工程师,我们的任务是构建能够完成工作的系统(即满足用户期望的保证),尽管一切都出错了。 在第9章中,我们将看看一些可以在分布式系统中提供这种保证的算法的例子。 但首先,在本章中,我们必须了解我们面临的挑战。 + +本章对分布式系统中可能出现的问题进行彻底的悲观和沮丧的总结。 我们将研究网络的问题(“无法访问的网络”,第269页); 时钟和时序问题(第287页上的“不可靠时钟”); 我们将讨论他们可以避免的程度。 所有这些问题的后果都是困惑的,所以我们将探索如何思考一个分布式系统的状态,以及如何推理发生的事情(第300页的“知识,真相和谎言”)。 + ## 故障与部分失效 +当你在一台计算机上编写一个程序时,它通常会以一种相当可预测的方式运行:无论是工作还是不工作。充满错误的软件可能会让人觉得电脑有时候是“糟糕的一天”(这个问题通常是重新启动的问题),但这主要是软件写得不好的结果。 + +单个计算机上的软件没有根本的原因:当硬件正常工作时,相同的操作总是产生相同的结果(这是确定性的)。如果存在硬件问题(例如,内存损坏或连接器松动),其后果通常是整个系统故障(例如,内核恐慌,“蓝屏死机”,启动失败)。具有良好软件的个人计算机通常功能完全或完全破坏,但不是介于两者之间。 + +、这是计算机设计中的一个慎重的选择:如果发生内部错误,我们宁愿电脑完全崩溃,而不是返回错误的结果,因为错误的结果很难处理。因此,计算机隐藏了它们所实现的模糊的物理现实,并呈现出一个理想化的系统模型,并以数学完美的方式运作。 CPU指令总是做同样的事情;如果您将一些数据写入内存或磁盘,那么这些数据将保持不变,并且不会被随机破坏。总是正确的计算这个设计目标一直回到第一台数字计算机[3]。 + +当你编写运行在多台计算机上的软件时,情况根本不同。在分布式系统中,我们不再处于理想化的系统模型中,我们别无选择,只能面对现实世界的混乱现实。而在现实世界中,如此轶事所示,各种各样的事情可能会出现问题[4]: + +> 在我有限的经验中,我已经和很多东西打过交道:单个数据中心(DC)中长期存在的网络分区,配电单元PDU故障,开关故障,整个机架意外的电源短路,全直流主干故障,全直流电源故障,以及一个低血糖的司机把他的福特皮卡撞碎在数据中心的HVAC(加热,通风和空气)系统上。而且我甚至不是一个运维。 +> +> ——柯达黑尔 + +在分布式系统中,尽管系统的其他部分工作正常,但系统的某些部分可能会以某种不可预知的方式被破坏。这被称为部分失败。难点在于部分失败是不确定的:如果你试图做任何涉及多个节点和网络的事情,它有时可能会工作,有时会出现不可预知的失败。正如我们将要看到的,你甚至不知道是否成功了,因为消息通过网络传播的时间也是不确定的! +这种不确定性和部分失效的可能性,使得分布式系统难以工作[5]。 + ### 云计算与超级计算机 +关于如何构建大型计算系统有一系列的哲学: + +* 规模的一端是高性能计算(HPC)领域。具有数千个CPU的超级计算机通常用于计算密集型科学计算任务,如天气预报或分子动力学(模拟原子和分子的运动)。 +* 另一个极端是云计算,这种云计算的定义不是很好[6],但通常与多租户数据中心,连接IP网络的商品计算机(通常是以太网),弹性/按需资源分配以及计量计费。 +* 传统企业数据中心位于这两个极端之间。 + +用这些哲学来处理错误的方法非常不同。在超级计算机中,作业通常会检查计算的状态,以便持久存储。如果一个节点出现故障,通常的解决方案是简单地停止整个集群的工作负载。故障节点修复后,计算从上一个检查点重新开始[7,8]。因此,超级计算机更像是一个单节点计算机而不是分布式系统:它通过让它升级成完全失败来处理部分失败 - 如果系统的任何部分发生故障,只是让所有的事情都崩溃(就像单台机器上的内核恐慌)。 + +在本书中,我们将重点放在实现互联网服务的系统上,这些系统通常与超级计算机看起来有很大不同 + +* 许多与互联网有关的应用程序都在线,因为它们需要能够随时以低延迟服务用户。使服务不可用(例如,停止群集以进行修复)是不可接受的。相比之下,像天气模拟这样的离线(批处理)工作可以停止并重新启动,影响相当小。 + +* 超级计算机通常由专用硬件构建而成,每个节点相当可靠,节点通过共享内存和远程直接内存访问(RDMA)进行通信。另一方面,云服务中的节点是由商品机器构建而成的,由于规模经济,可以以较低的成本提供相同的性能,而且具有较高的故障率。 + +* 大型数据中心网络通常基于IP和以太网,以Clos拓扑排列,以提供高二等分带宽[9]。超级计算机通常使用专门的网络拓扑结构,例如多维网格和toruses [10],这为具有已知通信模式的HPC工作负载提供了更好的性能。 + + ( 系统越大,其组件之一就越有可能发生变化。随着时间的推移,破碎的东西得到修复,新的东西被破坏,但是在一个有成千上万个节点的系统中,有理由认为总是有一些东西被破坏[7]。当错误处理策略由简单的放弃组成时,一个大的系统最终会花费大量时间从错误中恢复,而不是做有用的工作[8]。 + +* 如果系统可以容忍发生故障的节点,并继续保持整体工作状态,那么这对于操作和维护非常有用:例如,可以执行滚动升级(请参阅第4章),一次重新启动一个节点,而服务继续服务用户不中断。在云环境中,如果一台虚拟机运行不佳,可以杀死它并请求一台新的虚拟机(希望新的虚拟机速度更快)。 + +* 在地理位置分散的部署中(保持数据在地理位置上接近用户以减少访问延迟),通信很可能通过互联网进行,与本地网络相比,通信速度缓慢且不可靠。超级计算机通常假设它们的所有节点都靠近在一起。 + +如果要使分布式系统工作,就必须接受部分故障的可能性,并在软件中建立容错机制。换句话说,我们需要从不可靠的组件构建一个可靠的系统。 (正如第6页“可靠性”中所讨论的那样,没有完美的可靠性,所以我们需要理解我们可以实际承诺的限制。) + +即使在只有少数节点的小型系统中,考虑部分故障也是很重要的。在一个小系统中,很可能大部分组件在大部分时间都正常工作。然而,迟早会有一部分系统出现故障,软件必须以某种方式处理。故障处理必须是软件设计的一部分,并且作为软件的操作员,您需要知道在发生故障的情况下,软件可以期望什么样的行为。 + +假设缺陷是罕见的,只是希望最好,是不明智的。考虑一系列可能的错误(甚至是不太可能的错误),并在测试环境中人为地创建这些情况来查看会发生什么是非常重要的。在分布式系统中,怀疑,悲观和偏执狂回报。 + +> #### 从不可靠的组件构建可靠的系统 +> +> 您可能想知道这是否有意义 - 直观地看来,系统只能像其最不可靠的组件(最薄弱的环节)一样可靠。事实并非如此:事实上,从不太可靠的潜在基础构建更可靠的系统是计算机领域的一个古老思想[11]。例如: +> +> * 纠错码允许数字数据在通信信道上准确传输,偶尔会出现一些错误,例如由于无线网络上的无线电干扰[12]。 +> * IP(Internet协议)不可靠:可能丢弃,延迟,复制或重排数据包。 TCP(传输控制协议)在IP之上提供了更可靠的传输层:它确保丢失的数据包被重新传输,消除重复,并且数据包被重新组装成它们被发送的顺序。 +> +> 虽然这个系统可以比它的底层部分更可靠,但它的可靠性总是有限的。例如,纠错码可以处理少量的单比特错误,但是如果你的信号被干扰所淹没,那么通过你的通信信道可以得到多少数据是有根本的限制的[13]。 TCP可以隐藏数据包的丢失,重复和重新排序,但是它不能神奇地消除网络中的延迟。 +> +> 虽然更可靠的高级系统并不完美,但它仍然有用,因为它处理了一些棘手的低级错误,所以其余的错误通常更容易推理和处理。我们将在第519页的“端到端的论点”中进一步探讨这个问题。 + ## 不可靠的网络 +正如在第二部分的介绍中所讨论的那样,我们在本书中关注的分布式系统是无共享的系统,即通过网络连接的一堆机器。网络是这些机器可以通信的唯一途径 - 我们假设每台机器都有自己的内存和磁盘,一台机器不能访问另一台机器的内存或磁盘(除了通过网络向服务器发出请求)。 + +无共享并不是构建系统的唯一方式,但它已经成为构建互联网服务的主要方式,其原因如下:相对便宜,因为它不需要特殊的硬件,可以利用商品化的云计算服务,通过跨多个地理分布的数据中心进行冗余可以实现高可靠性。 + +互联网和数据中心(通常是以太网)中的大多数内部网络都是异步分组网络。在这种网络中,一个节点可以向另一个节点发送一个消息(一个数据包),但是网络不能保证它什么时候到达,或者是否到达。如果您发送请求并期待响应,则很多事情可能会出错(其中一些如图8-1所示): + +1. 您的请求可能已经丢失(可能有人拔掉了网线)。 +2. 您的请求可能正在队列中等待,稍后将交付(也许网络或收件人超载)。 +3. 远程节点可能失败(可能是崩溃或关机)。 +4. 远程节点可能暂时停止了响应(可能会遇到长时间的垃圾回收暂停;请参阅第295页上的“暂停进程”),但稍后会再次响应。 +5. 远程节点可能已经处理了您的请求,但是网络上的响应已经丢失(可能是网络交换机配置错误)。 +6. 远程节点可能已经处理了您的请求,但是响应已经被延迟并且稍后将被传递(可能是网络或者您自己的机器过载)。 + +![](img/fig8-1.png) + +**图8-1 如果发送请求并没有得到响应,则无法区分(a)请求是否丢失,(b)远程节点是否关闭,或(c)响应是否丢失。** + +发送者甚至不能分辨数据包是否被发送:唯一的选择是让接收者发送响应消息,这可能会丢失或延迟。这些问题在异步网络中难以区分:您所拥有的唯一信息是,您尚未收到响应。如果您向另一个节点发送请求并且没有收到响应,则无法说明原因。 + +处理这个问题的通常方法是**超时(Timeout)**:在一段时间之后,您放弃等待,并且认为响应不会到达。但是,当发生超时时,您仍然不知道远程节点是否收到了您的请求(如果请求仍然在某个地方排队,那么即使发件人已经放弃了该请求,仍然可能会将其发送给收件人)。 + ### 真实世界的网络故障 +我们几十年来一直在建立计算机网络 - 人们可能希望现在我们能想出如何使它们可靠。但是,我们似乎还没有成功。 +有一些系统的研究和大量的轶事证据表明,即使在像一家公司运营的数据中心那样的受控环境中,网络问题也可能出乎意料地普遍。在一家中型数据中心进行的一项研究发现,每个月大约有12个网络故障,其中一半断开一台机器,一半断开整个机架[15]。另一项研究测量了架顶式交换机,汇聚交换机和负载平衡器等组件的故障率[16]。它发现添加冗余网络设备不会像您所希望的那样减少故障,因为它不能防范人为错误(例如,错误配置的交换机),这是造成中断的主要原因。 + +诸如EC2之类的公有云服务因频繁的暂态网络故障而臭名昭着[14],管理良好的私有数据中心网络可能是更稳定的环境。尽管如此,没有人不受网络问题的困扰:例如,交换机软件升级过程中的一个问题可能会引发网络拓扑重构,在此期间网络数据包可能会延迟超过一分钟[17]。鲨鱼可能咬住海底电缆并损坏它们[18]。其他令人惊讶的故障包括网络接口有时会丢弃所有入站数据包,但是成功发送出站数据包[19]:仅仅因为网络链接在一个方向上工作并不能保证它也在相反的方向工作。 + +> 网络分区 +> 当网络的一部分由于网络故障而被切断时,有时称为网络分区或网络分割。在本书中,我们通常会坚持更一般的术语网络故障,以避免与存储系统的分区(碎片)混淆,如第6章所述。 + +即使网络故障在您的环境中非常罕见,故障可能发生的事实意味着您的软件需要能够处理它们。无论何时通过网络进行通信,都可能会失败,这是无法实现的。 + +如果网络故障的错误处理没有被定义和测试,那么可能会发生任意错误的事情:例如,即使网络恢复[20],集群可能会死锁,永久无法为请求提供服务,甚至可能会删除所有你的数据[21]。如果软件被置于意料之外的情况下,它可能会出乎意料的事情。 + +处理网络故障并不意味着容忍它们:如果你的网络通常是相当可靠的,一个有效的方法可能是当你的网络遇到问题时,简单地向用户显示一条错误信息。但是,您确实需要知道您的软件如何应对网络问题,并确保系统能够从中恢复。有意识地触发网络问题并测试系统响应(这是Chaos Monkey背后的想法;请参阅第6页上的“[可靠性]()”)。 + ### 检测故障 +许多系统需要自动检测故障节点。例如: + +* 负载平衡器需要停止向已经死亡的节点发送请求(即从轮询列表中移除)。 +* 在单主复制功能的分布式数据库中,如果主库失效,则需要将其中一个从库升级为新主库(请参阅第152页的“处理节点中断”)。 + +不幸的是,网络的不确定性使得很难判断一个节点是否工作。在某些特定的情况下,您可能会收到一些反馈信息,明确告诉您某些事情没有成功: + +* 如果您可以到达运行节点的机器,但没有进程正在侦听目标端口(例如,因为进程崩溃),操作系统将通过发送RST或TCP连接来帮助关闭或拒绝TCP连接FIN数据包在回复。但是,如果节点在处理请求时发生崩溃,则无法知道远程节点实际处理了多少数据[22]。 +* 如果节点进程崩溃(或被管理员杀死),但节点的操作系统仍在运行,则脚本可以通知其他节点有关该崩溃的信息,以便另一个节点可以快速接管,而无需等待超时到期。例如,HBase做这个[23]。 +* 如果您有权访问数据中心网络交换机的管理界面,则可以查询它们以检测硬件级别的链路故障(例如,远程机器是否关闭电源)。如果您通过互联网连接,或者如果您处于共享数据中心而无法访问交换机,或者由于网络问题而无法访问管理界面,则排除此选项。 +* 如果路由器确认您尝试连接的IP地址不可用,则可能会使用ICMP目标不可达数据包回复您。但是,路由器不具备神奇的故障检测能力——它受到与网络其他参与者相同的限制。 + +关于远程节点关闭的快速反馈很有用,但是你不能指望它。即使TCP确认已经传送了一个数据包,应用程序在处理之前可能已经崩溃。如果你想确保一个请求是成功的,你需要应用程序本身的积极响应[24]。 + +相反,如果出了什么问题,你可能会在堆栈的某个层次上得到一个错误响应,但总的来说,你必须假设你根本就没有得到任何回应。您可以重试几次(TCP重试是透明的,但是您也可以在应用程序级别重试),等待超时过期,并且如果在超时时间内没有收到响应,则最终声明节点已经死亡。 + ### 超时与无穷的延迟 +如果超时是检测故障的唯一可靠方法,那么超时应该等待多久?不幸的是没有简单的答案。 + +长时间的超时意味着长时间等待,直到一个节点被宣告死亡(在这段时间内,用户可能不得不等待或者看到错误信息)。短暂的超时可以更快地检测到故障,但是实际上它只是经历了暂时的减速(例如,由于节点或网络上的负载峰值)而导致错误地宣布节点失效的风险更高。 + +过早地声明一个节点已经死了是有问题的:如果这个节点实际上是活着的,并且正在执行一些动作(例如,发送一封电子邮件),而另一个节点接管,那么这个动作可能会最终执行两次。我们将在第300页的“知识,真相和谎言”以及第9章和第11章中更详细地讨论这个问题。 + +当一个节点被宣告死亡时,它的职责需要转移到其他节点,这会给其他节点和网络带来额外的负担。如果系统已经处于高负荷状态,则说明节点过早死亡会使问题更加严重。尤其是,可能发生节点实际上并没有死亡,而是由于过载而只响应缓慢;将其负载转移到其他节点可能会导致级联失败(在极端情况下,所有节点都宣告对方死亡,并且所有节点都停止工作)。 + +设想一个假想的系统,其网络可以保证数据包的最大延迟 - 每个数据包要么在一段时间内传送,要么丢失,但是传送永远不会比d更长。此外,假设您可以保证一个非故障节点总是在一段时间内处理一个请求r。在这种情况下,您可以保证每个成功的请求在2d + r时间内都能收到响应,如果您在此时间内没有收到响应,则知道网络或远程节点不工作。如果这是真的,2d + r将是一个合理的超时使用。 + +不幸的是,我们所使用的大多数系统都没有这些保证:异步网络具有无限的延迟(即尽可能快地传送数据包,但数据包可能需要的时间没有上限到达),并且大多数服务器实现不能保证他们可以在一定的最大时间内处理请求(请参阅“响应时间保证”(第298页))。对于故障检测,系统大部分时间快速运行是不够的:如果您的超时时间很短,往返时间只需要一个瞬间的尖峰就可以使系统失去平衡。 + +#### 网络拥塞和排队 + +在驾驶汽车时,由于交通拥堵,道路交通网络的旅行时间往往不尽相同。同样,计算机网络上数据包延迟的可变性通常是由于排队[25]: + +* 如果多个不同的节点同时尝试将数据包发送到同一目的地,则网络交换机必须将它们排队并将它们逐个送入目标网络链路(如图8-2所示)。在繁忙的网络链路上,数据包可能需要等待一段时间才能获得一个插槽(这称为网络连接)。如果传入的数据太多,交换机队列填满,数据包将被丢弃,因此需要重新发送数据包 - 即使网络运行良好。 +* 当数据包到达目标机器时,如果所有CPU内核当前都处于繁忙状态,则来自网络的传入请求将被操作系统排队,直到应用程序准备好处理它为止。根据机器上的负载,这可能需要一段任意的时间。 +* 在虚拟化环境中,正在运行的操作系统经常暂停几十毫秒,而另一个虚拟机使用CPU内核。在这段时间内,虚拟机不能从网络中消耗任何数据,所以传入的数据被虚拟机监视器[26]排队(缓冲),进一步增加了网络延迟的可变性。 +* TCP执行流量控制(也称为拥塞避免或背压),其中节点限制自己的发送速率以避免网络链路或接收节点过载[27]。这意味着在数据甚至进入网络之前,在发送者处额外排队。 + +![](img/fig8-2.png) + +**图8-2 如果有多台机器将网络流量发送到同一目的地,则其交换机队列可能会被填满。在这里,端口1,2和4都试图发送数据包到端口3。** + +而且,如果TCP在某个超时时间内没有被确认(这是根据观察的往返时间计算的),则认为数据包丢失,丢失的数据包将自动重新发送。尽管应用程序没有看到数据包丢失和重新传输,但它看到了延迟(等待超时到期,然后等待重新传输的数据包得到确认)。 +      + +> ### TCP与UDP +> +> 一些对延迟敏感的应用程序(如视频会议和IP语音(VoIP))使用UDP而不是TCP。这是延迟的可靠性和可变性之间的折衷:由于UDP不执行流量控制并且不重传丢失的分组,所以避免了可变网络延迟的一些原因(尽管它仍然易受切换队列和调度延迟的影响)。 +> +> 在延迟数据毫无价值的情况下,UDP是一个不错的选择。例如,在VoIP电话呼叫中,可能没有足够的时间重新发送丢失的数据包,之后其数据将在扬声器上播放。在这种情况下,重发数据包没有意义 - 应用程序必须使用静音填充丢失数据包的时隙(导致声音短暂中断),然后在数据流中继续。重试发生在人类层。 (“请你再说一遍吗?”声音刚刚熄灭了一会儿。“) + + + +所有这些因素都会造成网络延迟的变化。当系统接近其最大容量时,排队延迟的范围特别广泛: + +拥有足够的备用容量的系统可以轻松排队,而在高度利用的系统中,排队很长,可以很快建立起来。 + +在公共云和多租户数据中心中,资源被许多客户共享:网络链接和交换机,甚至每个机器的网络接口和CPU(在虚拟机上运行时)共享。批处理工作负载(如MapReduce)(请参阅第10章)可能很容易使网络链接饱和。由于您无法控制或了解其他客户对共享资源的使用情况,如果您附近的某个人(嘈杂的邻居)正在使用大量资源,则网络延迟可能会发生很大变化[28,29]。 + +在这种环境下,您只能通过实验方式选择超时:测量延长的网络往返时间和多台机器的分布,以确定延迟的预期可变性。然后,考虑到应用程序的特性,可以确定故障检测延迟与过早超时风险之间的适当折衷。 + +更好的是,系统不是使用配置的常量超时,而是连续测量响应时间及其变化(抖动),并根据观察到的响应时间分布自动调整超时。这可以用Phi Accrual故障检测器[30]来完成,该检测器例如在Akka和Cassandra [31]中使用。 TCP重传超时也同样起作用[27]。 + ### 同步网络 vs 异步网络 +如果我们可以依靠网络来传递一些固定的最大延迟的数据包,而不是丢弃数据包,那么分布式系统就会简单得多。为什么我们不能在硬件层面上解决这个问题,使网络可靠,使软件不必担心呢? + +为了回答这个问题,将数据中心网络与非常可靠的传统固定电话网络(非蜂窝,非VoIP)进行比较是很有趣的:延迟音频帧和掉话是非常罕见的。一个电话需要一个低端的端到端的延迟和足够的带宽来传输你的声音的音频样本。在计算机网络中具有类似的可靠性和可预测性不是很好吗? + +当您通过电话网络拨打电话时,它会建立一个电路:在两个呼叫者之间的整个路线上为呼叫分配一个固定的,有保证的带宽量。这个电路保持在通话结束的地方[32]。例如,ISDN网络以每秒4000帧的固定速率运行。呼叫建立时,每个帧内(每个方向)分配16位空间。因此,在通话期间,每一方都保证能够每250微秒发送一个精确的16位音频数据[33,34]。 + +这种网络是同步的:即使数据经过多个路由器,也不会受到排队的影响,因为呼叫的16位空间已经在网络的下一跳中保留了下来。而且由于没有排队,网络的最大端到端延迟是固定的。我们称之为有限延迟。 + +#### 我们不能简单地使网络延迟可预测吗? + +请注意,电话网络中的电路与TCP连接有很大不同:电路是固定数量的预留带宽,在电路建立时没有其他人可以使用,而TCP连接的数据包机会性地使用任何可用的网络带宽。您可以给TCP一个可变大小的数据块(例如,一个电子邮件或一个网页),它会尽可能在最短的时间内传输它。 TCP连接空闲时,不使用任何带宽[^ii]。 + +[^ii]: 除了偶尔的keepalive数据包,如果TCP keepalive被启用。 + +如果数据中心网络和互联网是电路交换网络,那么在建立电路时就可以建立一个保证的最大往返时间。但是,它们并不是:以太网和IP是分组交换协议,这些协议可以从排队中获得,从而使网络无限延迟。这些协议没有电路的概念。 + +为什么数据中心网络和互联网使用分组交换?答案是,它们针对**突发流量(bursty truffic)**进行了优化。一个电路适用于音频或视频通话,在通话期间需要每秒传送相当数量的比特。另一方面,请求网页,发送电子邮件或传输文件没有任何特定的带宽要求 - 我们只是希望它尽快完成。 + +如果你想通过电路传输文件,你将不得不猜测一个带宽分配。如果您猜的太低,传输速度会不必要的太慢,导致网络容量不能使用。如果你猜的太高,电路就无法建立(因为如果无法保证其带宽分配,网络不能建立电路)。因此,使用用于突发数据传输的电路浪费网络容量,并且使传输不必要地缓慢。相比之下,TCP动态调整数据传输速率以适应可用的网络容量。 + +已经有一些尝试去建立支持电路交换和分组交换的混合网络,比如ATM[^iii] InfiniBand有一些相似之处[35]:它在链路层实现了端到端的流量控制,从而减少了在网络中排队,尽管它仍然可能因链路拥塞而受到延迟[36]。通过仔细使用服务质量(QoS,数据包的优先级和调度)和准入控制(限速发送器),可以仿真分组网络上的电路交换,或提供统计上有界的延迟[25,32]。 + +[^iii]: 异步传输模式(ATM)在20世纪80年代是以太网的竞争对手[32],但在电话网核心交换机之外并没有得到太多的采用。与自动柜员机(也称为自动取款机)无关,尽管共用一个缩写词。或许,在一些平行的世界里,互联网是基于像ATM这样的东西,因为互联网视频通话可能比我们的更可靠,因为它们不会遭受丢包和延迟的包裹。 + +但是,目前在多租户数据中心和公共云或通过互联网[^iv]进行通信时,此类服务质量尚未启用。当前部署的技术不允许我们对网络的延迟或可靠性作出任何保证:我们必须假设 网络拥塞,排队和无限的延迟将会发生。 因此,超时时间没有“正确”的价值 - 他们需要通过实验确定。 + +[^iv]: 互联网服务提供商之间的对等协议和通过BGP网关协议(BGP)建立路由之间的对等协议,与电路交换本身相比,与电路交换更接近。在这个级别上,可以购买专用带宽。但是,互联网路由在网络级别运行,而不是主机之间的单独连接,而且运行时间要长得多。 + +> ### 延迟和资源利用 +> +> 更一般地说,您可以将动态资源分区的结果视为可变延迟。 +> +> 假设两台电话交换机之间有一条线路,可以同时进行10,000个呼叫。通过此线路切换的每个电路都占用其中一个呼叫插槽。因此,您可以将线路视为可由多达10,000个并发用户共享的资源。资源以静态方式分配:即使您现在是电话上唯一的电话,并且所有其他9,999个插槽都未使用,您的电路仍将分配与导线充分利用时相同的固定数量的带宽。 +> +> 相比之下,互联网动态分享网络带宽。发送者互相推挤并互相推挤以尽可能快地通过网络获得它们的分组,并且网络交换机决定从一个时刻到另一个时刻发送哪个分组(即,带宽分配)。这种方法有排队的缺点,但其优点是它最大限度地利用了电线。电线固定成本,所以如果你更好地利用它,你通过电线发送的每个字节都更便宜。 +> +> CPU也会出现类似的情况:如果您在多个线程间动态共享每个CPU内核,则有一个线程有时必须等待操作系统的运行队列,而另一个线程正在运行,这样线程可以暂停不同的时间长度。但是,与为每个线程分配静态数量的CPU周期相比,这会更好地利用硬件(请参阅第298页的“响应时间保证”)。更好的硬件利用率也是使用虚拟机的重要动机。 +> +> 如果资源是静态分区的(例如,专用硬件和专用带宽分配),则在某些环境中可实现延迟保证。但是,这是以降低利用率为代价的 - 换句话说,它是更昂贵的。另一方面,动态资源分配的多租户提供了更好的利用率,所以它更便宜,但它具有可变延迟的缺点。 +> +> 网络中的可变延迟不是一种自然规律,而只是成本/收益权衡的结果。 + ## 不可靠的时钟 -### 单调时钟与Time-Of-Day时钟 +时钟和时间很重要。应用程序以各种方式依赖于时钟来回答以下问题: + +1. 这个请求是否超时了? +2. 这项服务的第99百分位响应时间是多少? +3. 在过去五分钟内,该服务平均每秒处理多少个查询? +4. 用户在我们的网站上花了多长时间? +5. 本文何时发表? +6. 在什么时间发送提醒邮件? +7. 这个缓存条目何时到期? +8. 日志文件中此错误消息的时间戳是什么? + +[例1-4]()测量[持续时间]()(例如,发送请求与正在接收的响应之间的时间间隔),而示例5-8描述**时间点(point in time)**(在特定日期,特定时间发生的事件)。 + +在分布式系统中,时间是一件棘手的事情,因为通信不是即时的:消息通过网络从一台机器传送到另一台机器需要时间。收到消息的时间总是晚于发送的时间,但是由于网络中的可变延迟,我们不知道多少时间。这个事实有时很难确定在涉及多台机器时发生事情的顺序。 + +而且,网络上的每台机器都有自己的时钟,这是一个实际的硬件设备:通常是石英晶体振荡器。这些设备不是完全准确的,所以每台机器都有自己的时间概念,可能比其他机器稍快或更慢。可以在一定程度上同步时钟:最常用的机制是网络时间协议(NTP),它允许根据一组服务器报告的时间来调整计算机时钟[37]。服务器则从更精确的时间源(如GPS接收机)获取时间。 + +### 单调钟与时钟 + +现代计算机至少有两种不同的时钟:时钟和单调钟。尽管它们都衡量时间,但区分这两者很重要,因为它们有不同的目的。 + +#### 时钟 + +时钟是您直观地了解时钟的依据:它根据某个日历(也称为**挂钟时间(wall-clock time)**)返回当前日期和时间。例如,Linux[^v]上的`clock_gettime(CLOCK_REALTIME)`和Java中的`System.currentTimeMillis()`返回自epoch(1970年1月1日 午夜 UTC,格里高利历)以来的秒数(或毫秒),根据公历日历,不包括闰秒。有些系统使用其他日期作为参考点。 + +[^v]: 虽然时钟被称为实时时钟,但它与实时操作系统无关,如第298页上的“响应时间保证”中所述。 + +时钟通常与NTP同步,这意味着来自一台机器的时间戳(理想情况下)意味着与另一台机器上的时间戳相同。但是,如下节所述,时钟也具有各种各样的奇特性。特别是,如果本地时钟在NTP服务器之前太远,则它可能会被强制重置并显示跳回到先前的时间点。这些跳跃以及他们经常忽略闰秒的事实使时钟不能用于测量经过时间[38]。 + +时钟还具有相当粗略的分辨率,例如,在较早的Windows系统上以10毫秒为单位前进[39]。在最近的系统中,这不是一个问题。 + +#### 单调钟 + +单调时钟适用于测量持续时间(时间间隔),例如超时或服务的响应时间:Linux上的`clock_gettime(CLOCK_MONOTONIC)`和Java中的`System.nanoTime()`是单调时钟。这个名字来源于他们保证总是前进的事实(而时间的时钟可以及时跳回)。 + + ### 时钟同步与准确性 +单调时钟不需要同步,但是需要根据NTP服务器或其他外部时间源来设置时钟,以便有用。不幸的是,我们获取时钟的方法并不像您所希望的那样可靠或准确 - 硬件时钟和NTP可能会变幻莫测。举几个例子: + +计算机中的石英钟不够精确:漂移(运行速度快于或慢于预期)。时钟漂移取决于机器的温度。 Google为其服务器假定时钟漂移为200 ppm(百万分之一)[41],相当于每30秒与服务器重新同步一次的时钟为6 ms漂移,或者重新同步的时钟为17秒漂移 - 一天一次。即使一切工作正常,此漂移也会限制您可以达到的最佳准确度。 + +* 如果计算机的时钟与NTP服务器的时钟差别太大,可能会拒绝同步,或者本地时钟将被强制重置[37]。任何观察重置前后时间的应用程序都可能会看到时间倒退或突然跳跃。 +* 如果某个节点被NTP服务器意外阻塞,可能会在一段时间内忽略错误配置。有证据表明,这在实践中确实发生了。 +* NTP同步只能和网络延迟一样好,所以当您在拥有可变数据包延迟的拥塞网络上时,NTP同步的准确性会受到限制。一个实验表明,当通过互联网同步时,35毫秒的最小误差是可以实现的,尽管偶尔的网络延迟峰值会导致大约一秒的误差。根据配置,较大的网络延迟会导致NTP客户端完全放弃。 +* 一些NTP服务器错误或配置错误,报告时间已经过去了几个小时[43,44]。 NTP客户端非常强大,因为他们查询多个服务器并忽略异常值。尽管如此,在互联网上陌生人告诉你的时候,你的系统的正确性还是值得担忧的。 +* 闰秒导致59分钟或61秒长的分钟,这混淆了未设计闰秒的系统中的时序假设[45]。事实上,闰秒已经使许多大型系统崩溃[38,46],这表明对于时钟偷偷进入系统的错误假设是多么的容易。处理闰秒的最佳方法可能是通过在一天中逐渐执行闰秒调整(这被称为拖尾)[47,48],使NTP服务器“撒谎”,虽然实际的NTP服务器行为在练习[49]。 +* 在虚拟机中,硬件时钟被虚拟化,这对于需要精确计时的应用程序提出了额外的挑战[50]。当一个CPU核心在虚拟机之间共享时,每个虚拟机都会暂停几十毫秒,而另一个虚拟机正在运行。从应用程序的角度来看,这种停顿表现为时钟突然向前跳跃[26]。 +* 如果您在未完全控制的设备上运行软件(例如,移动设备或嵌入式设备),则可能完全不信任该设备的硬件时钟。一些用户故意将其硬件时钟设置为不正确的日期和时间,例如为了规避游戏中的时间限制。因此,时钟可能会在过去或未来大放异彩。 + +如果您足够关心,投入大量资源,就可以达到非常好的时钟精度。例如,针对金融机构的欧洲法规草案MiFID II要求所有高频率交易基金在UTC时间100微秒内同步时钟,以便调试“闪崩”等市场异常现象,并帮助检测市场操纵 【51】。 + +使用GPS接收机,精确时间协议(PTP)[52]以及仔细的部署和监测可以实现这种精确度。然而,这需要很大的努力和专业知识,并且有很多方法可以导致时钟同步错误。如果您的NTP守护程序配置错误,或者防火墙阻止了NTP通信,由漂移引起的时钟错误可能会很快变大。 + ### 依赖同步时钟 +时钟的问题在于,虽然它们看起来简单易用,但却具有令人惊讶的缺陷:一天可能不会有86,400秒,时钟会按时间向后移动,而一个节点上的时间可能与另一个节点上的时间完全不同。 + +本章早些时候,我们讨论了网络丢包和任意延迟包的问题。尽管网络在大多数情况下表现良好,但软件的设计必须假定网络偶尔会出现故障,而软件必须正常处理这些故障。时钟也是如此:尽管大多数时间都工作得很好,但需要准备强大的软件来处理不正确的时钟。 + +部分问题是不正确的时钟很容易被忽视。如果一台机器的CPU出现故障或者网络配置错误,很可能根本无法工作,所以很快就会被注意和修复。另一方面,如果它的石英时钟有缺陷,或者它的NTP客户端配置错误,大部分事情似乎仍然可以正常工作,即使它的时钟逐渐偏离现实。如果某个软件依赖于精确同步的时钟,那么结果就更可能是沉默的,而且数据的细微丢失要比一次剧烈的崩溃更为严重[53,54]。 + +因此,如果您使用需要同步时钟的软件,则必须仔细监控所有机器之间的时钟偏移。任何节点的时钟偏离其他时钟应该被宣布死亡,并从集群中删除。这样的监控可以确保您注意到破损的时钟,然后才能造成太多的损害。 + +#### 有序事件的时间戳 + +让我们考虑一个特别的情况,即依靠时钟诱惑但危险的事情:在多个节点上对事件进行排序。 例如,如果两个客户端写入分布式数据库,谁先到达那里? 哪一个是最新的? + +图8-3显示了在具有多领导者复制的数据库中对时钟的危险使用(该例子类似于图5-9)。 客户端A在节点1上写入`x = 1`; 写入被复制到节点3; 客户端B在节点3上增加x(我们现在有`x = 2`); 最后,这两个写入都被复制到节点2。 + +![](img/fig8-3.png) + +**图8-3 客户端B的写入比客户端A的写入要晚,但是B的写入具有较早的时间戳。** + +在[图8-3]()中,当一个写入被复制到其他节点时,它会根据发生写入的节点上的时钟时钟标记一个时间戳。在这个例子中,时钟同步是非常好的:节点1和节点3之间的偏差小于3ms,这可能比你在实践中预期的更好。 + +尽管如此,图8-3中的时间戳却无法正确排列事件:写入x = 1的时间戳为42.004秒,但写入x = 2的时间戳为42.003秒,即使x = 2在稍后出现。当节点2接收到这两个事件时,会错误地推断出x = 1是最近的值,而写入x = 2。实际上,客户端B的增量操作将会丢失。 + +这种冲突解决策略被称为最后写赢(LWW),它在多领导者复制和无领导者数据库(如Cassandra [53]和Riak [54])中被广泛使用(请参见“最后写赢(放弃并发写)”一节)。有些实现会在客户端而不是服务器上生成时间戳,但这并不能改变LWW的基本问题: + +* 数据库写入可能会神秘地消失:具有滞后时钟的节点无法用快速时钟覆盖之前由节点写入的值,直到节点之间的时钟偏差过去[54,55]。此方案可能会导致任意数量的数据悄悄丢弃,而不会向应用程序报告任何错误。 +* LWW无法区分快速连续发生的写入(在图8-3中,客户端A的写入之后客户端B的增量肯定发生),并且写入是真正并发的(写入者不知道其他写入者)。需要额外的因果关系跟踪机制(例如版本向量),以防止因果关系的冲突(请参阅“检测并发写入”第169页)。 +* 两个节点可以独立生成具有相同时间戳的写入,特别是在时钟仅具有毫秒分辨率的情况下。为了解决这样的冲突,还需要一个额外的tiebreaker值(可以简单地说是一个大的随机数),但这种方法也会导致违反因果关系[53]。 + +因此,尽管通过保持最“最近”的价值和放弃其他价值来解决冲突是很有诱惑力的,但是要注意,“最近”的定义取决于当地时间的时钟,这很可能是不正确的。即使紧密NTP同步时钟,您也可以在时间戳100毫秒(根据发送者的时钟)发送一个数据包,并在时间戳99毫秒(根据接收者的时钟)到达 - 看起来好像数据包已到达在发送之前,这是不可能的。 + +NTP同步是否足够准确,以至于这种不正确的排序不会发生?可能不会,因为NTP的同步精度本身受到网络往返时间的限制,除了石英漂移等其他误差源之外。对于正确的排序,你需要的时钟源比你测量的东西(即网络延迟)要精确得多。 + +所谓的逻辑时钟[56,57]是基于递增计数器而不是振荡石英晶体,对于排序事件来说是更安全的选择(请参见“检测并发写”(第184页))。逻辑时钟不测量一天中的时间或经过的秒数,而仅测量事件的相对顺序(无论一个事件发生在另一个事件之前还是之后)。相反,测量实际经过时间的时间和单调时钟也被称为物理时钟。我们将在第339页的“订购保证”中查看更多订购信息。 + +#### 时钟读数有一个置信区间 + +您可能能够以微秒或甚至纳秒的分辨率读取机器的时钟。但即使可以得到如此细致的测量结果,这并不意味着这个值对于这样的精度实际上是准确的。实际上,如前所述,即使您每分钟与本地网络上的NTP服务器进行同步,很可能也不会像前面提到的那样,在不精确的石英时钟上漂移几毫秒。使用公共互联网上的NTP服务器,最好的准确度可能达到几十毫秒,而且当网络拥塞时,误差可能会超过100毫秒[57]。 + +因此,将时钟读数视为一个时间点(在一个置信区间内更像是一个时间范围)是没有意义的:例如,一个系统可能95%的确信时间在一分钟过去了10.3秒和10.5秒,但是现在还不知道比这更精确[58]。如果我们只知道+/- 100毫秒的时间,那么时间戳中的微秒数基本上是没有意义的。 + +不确定性界限可以根据你的时间源来计算。如果您的GPS接收器或原子(铯)时钟直接连接到您的计算机上,预期的错误范围由制造商报告。如果从服务器获得时间,则不确定性取决于自上次与服务器同步以来的预期石英漂移,加上NTP服务器的不确定性,再加上到服务器的网络往返时间(第一次近似,并假设你相信服务器)。 + +不幸的是,大多数系统不公开这种不确定性:例如,当您调用`clock_gettime()`时,返回值不会告诉您时间戳的预期错误,所以您不知道其置信区间是5毫秒还是5年。 + +一个有趣的例外是Spanner中的Google TrueTime API [41],它明确地报告了本地时钟的置信区间。当你询问当前时间时,你会得到两个值:[最早,最近],这是最早可能的时间戳和最新的时间戳。时钟基于其不确定性计算,知道实际当前时间在该时间间隔内。间隔的宽度取决于自从本地石英钟最后与更精确的时钟源同步以来已经过了多长时间。 + +#### 全局快照的同步时钟 + +在第237页的“快照隔离和可重复读取”中,我们讨论了快照隔离,这是数据库中非常有用的功能,需要支持小型快速读写事务和大型长时间运行的只读事务,用于备份或分析)。它允许只读事务在特定的时间点查看数据库处于一致状态,而不会锁定和干扰读写事务。 + +快照隔离最常见的实现需要单调递增的事务ID。如果写入比快照晚(即,写入具有比快照更大的事务ID),则该写入对于快照事务是不可见的。在单节点数据库上,一个简单的计数器就足以生成事务ID。 + +但是,当数据库分布在许多机器上,可能在多个数据中心中时,由于需要协调,所以难以产生全局的,单调递增的事务处理ID(跨所有分区)。事务ID必须反映因果关系:如果事务B读取由事务A写入的值,则B必须具有比A更高的事务ID,否则快照就无法保持一致。由于大量的小规模,高速交易,在分布式系统中创建事务ID成为一个站不住脚的瓶颈[^vi]。 + +[^vi]: 存在分布式序列号生成器,例如Twitter的雪花(Snowflake),其以可扩展的方式(例如,通过将ID空间的块分配给不同节点)近似单调地增加唯一ID。但是,它们通常无法保证与因果关系一致的排序,因为分配的ID块的时间范围比数据库读取和写入的时间范围要长。另请参阅第339页上的“订购保证”。 + +我们可以使用同步时钟的时间戳作为事务ID吗?如果我们能够获得足够好的同步性,那么他们将拥有合适的性能:以后的交易会有更高的时间戳。这个问题当然是关于时钟精度的不确定性。 + +Spanner以这种方式实现跨数据中心的快照隔离[59,60]。它使用TrueTime API报告的时钟置信区间,并基于以下观察结果:如果您有两个置信区间,每个置信区间包含最早和最近可能的时间戳(A = [最早,Alatest]和B = (最早的,Blatest)),这两个区间不重叠(即最早的 #### 实时真的是真的吗? +> +> 在嵌入式系统中,实时是指系统经过精心设计和测试,以满足所有情况下的特定时间保证。这个含义与Web上实时术语的模糊使用相反,它描述了服务器将数据推送到客户端以及流处理,而没有严格的响应时间限制(见第11章)。 + +例如,如果车载传感器检测到当前正在经历碰撞,你肯定不希望安全气囊释放系统因为GC暂停而延迟弹出。 + +在系统中提供实时保证需要各级软件栈的支持:一个实时操作系统(RTOS),允许在指定的时间间隔内保证CPU时间的分配。图书馆 + +函数必须记录最坏情况下的执行时间;动态内存分配可能受到限制或完全不允许(实时垃圾收集器存在,但是应用程序仍然必须确保它不会给GC太多的工作);必须进行大量的测试和测量,以确保达到保证。 + +所有这些都需要大量额外的工作,严重限制了可以使用的编程语言,库和工具的范围(因为大多数语言和工具不提供实时保证)。由于这些原因,开发实时系统非常昂贵,并且它们通常用于安全关键的嵌入式设备。而且,“实时”与“高性能”不一样 - 事实上,实时系统可能具有较低的吞吐量,因为他们必须优先考虑及时响应高于一切(另请参见“延迟和资源利用“)。 + +对于大多数服务器端数据处理系统来说,实时保证是不经济或不合适的。因此,这些系统必须承受在非实时环境中运行的暂停和时钟不稳定性。 + +#### 限制垃圾收集的影响 + +过程暂停的负面影响可以在不诉诸昂贵的实时调度保证的情况下得到缓解。语言运行时在计划垃圾回收时具有一定的灵活性,因为它们可以跟踪对象分配的速度和随着时间的推移剩余的空闲内存。 + +一个新兴的想法是将GC暂停视为一个节点的短暂计划中断,并让其他节点处理来自客户端的请求,同时一个节点正在收集其垃圾。如果运行时可以警告应用程序一个节点很快需要GC暂停,那么应用程序可以停止向该节点发送新的请求,等待它完成处理未完成的请求,然后在没有请求正在进行时执行GC。这个技巧隐藏了来自客户端的GC暂停,并降低了响应时间的高百分比[70,71]。一些对延迟敏感的金融交易系统[72]使用这种方法。 + +这个想法的一个变种是只使用垃圾收集器来处理短暂的对象(这些对象要快速收集)并定期重新启动进程,然后再累积足够长的对象来需要一个长寿命对象的完整GC [65,73]。一次可以重新启动一个节点,在计划重新启动之前,流量可以从节点移开,就像滚动升级(参见第4章)一样。 + +这些措施不能完全阻止垃圾收集暂停,但可以有效地减少它们对应用程序的影响。 + ## 知识、真相与谎言 -### 真理掌握在大多数人手中 +本章到目前为止,我们已经探索了分布式系统与运行在单台计算机上的程序的不同之处:没有共享内存,只有通过可变延迟的不可靠网络传递消息,系统可能遭受部分失败,不可靠的时钟和处理暂停。 + +如果您不习惯于分布式系统,那么这些问题的后果就会让人迷惑不解。网络中的一个节点无法确切地知道任何事情——它只能根据它通过网络接收(或不接收)的消息进行猜测。节点只能通过交换消息来找出另一个节点所处的状态(存储了哪些数据,是否正确运行等等)。如果远程节点没有响应,则无法知道它处于什么状态,因为网络中的问题不能可靠地与节点上的问题区分开来。 + +这些系统的讨论与哲学有关:我们知道在我们的系统中是真实的还是虚假的?如果知觉和测量的机制是不可靠的,那么我们如何确定这些知识呢?软件系统应该遵循我们对物理世界所期望的法则,如因果关系吗? + +幸运的是,我们不需要去搞清楚生命的意义。在分布式系统中,我们可以陈述关于行为(系统模型)的假设,并以满足这些假设的方式设计实际系统。算法可以被证明在某个系统模型中正确运行。这意味着即使底层系统模型提供了很少的保证,也可以实现可靠的行为。 + +但是,尽管可以使软件在不可靠的系统模型中表现良好,但这并不是直截了当的。在本章的其余部分中,我们将进一步探讨分布式系统中的知识和真理的概念,这将有助于我们思考我们可以做出的各种假设以及我们可能希望提供的保证。在第9章中,我们将着眼于分布式系统的一些例子,这些算法在特定的假设条件下提供了特定的保证。 + +### 真理由多数所定义 + +设想一个具有不对称故障的网络:一个节点能够接收发送给它的所有消息,但是来自该节点的任何传出消息被丢弃或延迟[19]。即使该节点运行良好,并且正在接收来自其他节点的请求,其他节点也无法听到其响应。经过一段时间后,其他节点宣布它已经死亡,因为他们没有听到节点的消息。这种情况就像恶梦一样展开:半连接的节点被拖向墓地,敲打尖叫着“我没死!” ——但是由于没有人能听到它的尖叫,葬礼队伍继续以坚忍的决心行事。 + +在一个稍微少一些的恶梦场景中,半断开的节点可能会注意到它发送的消息没有被其他节点确认,因此意识到网络中必定存在故障。尽管如此,节点被其他节点错误地宣告为死亡,而半断开的节点对此无能为力。 + +作为第三种情况,想象一个经历了一个长时间停止世界垃圾收集暂停的节点。所有节点的线程被GC抢占并暂停一分钟,因此没有请求被处理,也没有响应被发送。其他节点等待,重试,不耐烦,并最终宣布节点死亡,并将其加载到灵车上。最后,GC完成,节点的线程继续,好像什么也没有发生。其他节点感到惊讶,因为所谓的死亡节点突然从棺材中抬起头来,身体健康,开始和旁观者高兴地聊天。起初,GCing节点甚至没有意识到已经过了整整一分钟,从它的角度来看它已经宣告死了,自从最后一次与其他节点交谈以来,几乎没有任何时间能够通过。 + +这些故事的寓意是节点不一定相信自己对情况的判断。分布式系统不能完全依赖单个节点,因为节点可能随时失败,可能会使系统停滞不前,无法恢复。相反,许多分布式算法都依赖于法定人数,即在节点之间进行投票(请参阅第179页上的“读写法定人数”):决策需要来自多个节点的最小投票数,以减少对任何节点的依赖特定节点。 + +这包括关于声明节点的决定。如果法定数量的节点声明另一个节点已经死亡,那么即使该节点仍感觉活着,它也必须被认为是死的。个体节点必须遵守法定决定并下台。 + +最常见的法定人数是绝大多数节点的一半以上(尽管其他类型的法定人数也是可能的)。多数法定人数允许系统继续工作,如果单个节点发生故障(三个节点,一个故障可以容忍;五个节点,两个故障是可以容忍的)。但是,它仍然是安全的,因为在这个制度中只能有一个多数 - 不能同时存在两个相互冲突的多数决定。当我们在第9章中讨论一致性算法时,我们将更详细地讨论问题的使用。 + +#### 领导和锁 + +通常情况下,一些东西在一个系统中只能有一个。例如: + +* 只允许一个节点成为数据库分区的领导者,以避免**脑裂(split brain)**(参阅“[处理节点中断]()”)。 +* 只有一个事务或客户端被允许持有特定资源的锁定或对象,以防止同时写入和破坏它。 +* 只有一个用户可以注册一个特定的用户名,因为用户名必须唯一标识一个用户。 + +在分布式系统中实现这一点需要注意:即使一个节点认为它是“天选者”(分区的负责人,锁的持有者,成功抓取用户名的用户的请求处理程序),但这并不一定意味着有法定人数的节点同意!一个节点可能以前是领导者,但是如果其他节点在此期间宣布它死亡(例如,由于网络中断或GC暂停),则它可能已被降级并且另一个领导者可能已经被选举。 + +如果一个节点继续作为选定的节点,即使大多数节点已经声明它已经死了,但是这可能会导致系统中的问题不被仔细设计。这样的节点可以按照自定义的容量向其他节点发送消息,如果其他节点相信,整个系统可能会做一些不正确的事情。 + +例如,图8-4显示了由于不正确的锁定实施而导致的数据损坏错误。 (这个错误不是理论上的:HBase曾经有这个问题[74,75])假设你要确保一个存储服务中的文件一次只能被一个客户访问,因为如果多个客户试图写对此,该文件将被损坏。您尝试通过在访问文件之前要求客户端从锁定服务获取租约来实现此目的。 + +![](img/fig8-4.png) + +**图8-4 分布式锁的实现不正确:客户端1认为它仍然具有有效的租约,即使它已经过期,从而破坏了存储中的文件** + +这个问题就是我们在第295页的“进程暂停”中讨论过的一个例子:如果持有租约的客户端暂停太久,它的租约将到期。另一个客户端可以获得同一文件的租约,并开始写入文件。当暂停的客户端回来时,它认为(不正确)它仍然有一个有效的租约,并继续写入文件。结果,客户的写入冲突和损坏的文件。 + +#### 击剑令牌 + +当使用锁或租约来保护对某些资源(如图8-4中的文件存储)的访问时,需要确保一个被误认为自己是“天选者”的节点不能中断系统的其它部分。实现这一目标的一个相当简单的技术就是**击剑(fencing)**,如[图8-5]()所示 + +![](img/fig8-5.png) + +**图8-5 只允许以增加屏蔽令牌的顺序进行写操作,从而保证存储安全** + +我们假设每次锁定服务器授予锁或租约时,它还会返回一个**击剑令牌(fencing token)**,这个数字在每次授予锁定时都会增加(例如,由锁定服务增加)。然后,我们可以要求客户端每次向存储服务发送写入请求时,都必须包含当前的屏蔽令牌。 + +在图8-5中,客户端1以33的令牌获得租约,但随后进入一个长时间的停顿并且租约到期。客户端2以34的令牌(该数字总是增加)获取租约,然后将其写入请求发送到存储服务,包括34的令牌。稍后,客户端1恢复生机并将其写入存储服务,包括其令牌值33.但是,存储服务器会记住它已经处理了一个具有更高令牌编号(34)的写入,因此它会拒绝带有令牌33的请求。 + +如果将ZooKeeper用作锁定服务,则可将事务标识`zxid`或节点版本`cversion`用作防护令牌。由于它们保证单调递增,因此它们具有所需的属性[74]。 + +请注意,这种机制要求资源本身在检查令牌方面发挥积极作用,通过拒绝使用旧的令牌,而不是已经被处理的令牌来进行写操作——仅仅依靠客户端检查自己的锁状态是不够的。对于不明确支持击剑令牌的资源,您可能仍然可以解决此限制(例如,在文件存储服务的情况下,可以将防护令牌包含在文件名中)。但是,为了避免在锁的保护之外处理请求,需要进行某种检查。 + +在服务器端检查一个令牌可能看起来像是一个缺点,但这可以说是一件好事:一个服务假定它的客户总是表现得很好是不明智的,因为客户经常由优先级非常高的人与服务人员的优先权不同[76]。因此,任何服务保护自己免受意外客户的滥用是一个好主意。 ### 拜占庭故障 +防护令牌可以检测和阻止无意中发生错误的节点(例如,因为它尚未发现其租约已过期)。但是,如果节点有意破坏系统的保证,则可以通过使用假屏蔽令牌发送消息来轻松完成此操作。 + +在本书中,我们假设节点是不可靠但诚实的:它们可能很慢或者从不响应(由于故障),并且它们的状态可能已经过时(由于GC暂停或网络延迟),但是我们假设如果节点它做出了回应,它正在说出“真相”:尽其所知,它正在按照议定书的规则发挥作用。 + +如果存在节点可能“撒谎”(发送任意错误或损坏的响应)的风险,则分布式系统问题变得更加困难 - 例如,如果节点可能声称其实际上没有收到特定的消息。这种行为被称为拜占庭故障,在这个不信任的环境中达成共识的问题被称为拜占庭将军问题[77]。 + +> ### 拜占庭将军问题 +> +> 拜占庭将军问题是所谓“两将军问题”的概括[78],它想象两个将军需要就战斗计划达成一致的情况。由于他们在两个不同的地点建立了营地,他们只能通过信使进行沟通,信使有时会被延迟或丢失(就像网络中的信息包一样)。我们将在第9章讨论这个共识问题。 +> +> 在这个拜占庭式的问题中,有n位将军需要同意,他们的努力因为有一些叛徒在他们中间而受到阻碍。大多数的将军都是忠诚的,因而发出了真实的信息,但是叛徒可能会试图通过发送虚假或不真实的信息来欺骗和混淆他人(在试图保持未被发现的同时)。事先并不知道叛徒是谁。 +> +> 拜占庭是后来成为君士坦丁堡的古希腊城市,现在在土耳其的伊斯坦布尔。没有任何历史证据表明拜占庭将军比其他地方更容易出现阴谋和阴谋。相反,这个名字来源于拜占庭式的过度复杂,官僚,迂回等意义,早在计算机之前就已经在政治中被使用了[79]。Lamport想要选一个不会冒犯任何读者的国家,他被告知将其称为阿尔巴尼亚将军问题并不是一个好主意[80]。 + +当一个系统在部分节点发生故障、不遵守协议、甚至恶意攻击、扰乱网络时仍然能继续正确工作,称之为**拜占庭容错(Byzantine fault-tolerant)**的,在特定场景下,这种担忧在是有意义的: + +* 在航空航天环境中,计算机内存或CPU寄存器中的数据可能被辐射破坏,导致其以任意不可预知的方式响应其他节点。由于系统故障将非常昂贵(例如,飞机撞毁和炸死船上所有人员,或火箭与国际空间站相撞),飞行控制系统必须容忍拜占庭故障[81,82]。 +* 在多个参与组织的系统中,一些参与者可能会试图欺骗或欺骗他人。在这种情况下,节点仅仅信任另一个节点的消息是不安全的,因为它们可能是出于恶意的目的而被发送的。例如,像比特币和其他区块链一样的对等网络可以被认为是让互不信任的各方同意交易是否发生的一种方式,而不依赖于中央当局[83]。 + +然而,在本书讨论的那些系统中,我们通常可以安全地假设没有拜占庭式的错误。在你的数据中心里,所有的节点都是由你的组织控制的(所以他们可以信任),辐射水平足够低,内存损坏不是一个大问题。制作拜占庭容错系统的协议相当复杂[84],而容错嵌入式系统依赖于硬件层面的支持[81]。在大多数服务器端数据系统中,部署拜占庭容错解决方案的成本使其变得不切实际。 + +Web应用程序确实需要预期受终端用户控制的客户端(如Web浏览器)的任意和恶意行为。这就是为什么输入验证,清理和输出转义如此重要:例如,防止SQL注入和跨站点脚本。但是,我们通常不使用拜占庭容错协议,而只是让服务器决定什么是客户端行为,而不是允许的。在没有这种中心授权的对等网络中,拜占庭容错更为重要。 + +软件中的一个错误可能被认为是拜占庭式的错误,但是如果您将相同的软件部署到所有节点上,那么拜占庭式的容错算法不能为您节省。大多数拜占庭式容错算法要求超过三分之二的节点能够正常工作(即,如果有四个节点,最多只能有一个故障)。要使用这种方法对付bug,你必须有四个独立的相同软件的实现,并希望一个bug只出现在四个实现之一中。 + +同样,如果一个协议可以保护我们免受漏洞,安全妥协和恶意攻击,那么这将是有吸引力的。不幸的是,这也是不现实的:在大多数系统中,如果攻击者可以危害一个节点,他们可能会渗透所有这些节点,因为它们可能运行相同的软件。因此,传统机制(认证,访问控制,加密,防火墙等)仍然是攻击者的主要保护措施。 + +#### 弱谎言形式 + +尽管我们假设节点通常是诚实的,但值得向软件中添加防止“撒谎”弱形式的机制 - 例如,由硬件问题导致的无效消息,软件错误和错误配置。这种保护机制并不是完全的拜占庭容错,因为它们不能抵挡决心坚定的对手,但它们仍然是简单而实用的步骤,以提高可靠性。例如: + +* 由于硬件问题或操作系统,驱动程序,路由器等中的错误,网络数据包有时会受到损坏。通常,内建于TCP和UDP中的校验和会俘获损坏的数据包,但有时它们会逃避检测[85,86,87] 。简单的措施通常是防止这种破坏的充分保护,例如应用程序级协议中的校验和。 +* 可公开访问的应用程序必须仔细清理来自用户的任何输入,例如检查值是否在合理的范围内,并限制字符串的大小以防止通过大内存分配拒绝服务。防火墙后面的内部服务可能能够在对输入进行较不严格的检查的情况下逃脱,但是一些基本的理智检查(例如,在协议解析中)是一个好主意。 +* NTP客户端可以配置多个服务器地址。同步时,客户联系所有人,估计他们的错误,并检查大多数服务器在某个时间范围内达成一致。只要大多数的服务器没问题,一个配置错误的NTP服务器报告的时间不正确 + ### 系统模型与现实 +已经设计了许多算法来解决分布式系统问题 - 例如,我们将在第9章讨论共识问题的解决方案。为了有用,这些算法需要容忍我们在本章中讨论的分布式系统的各种故障。 + +算法的编写方式并不过分依赖于运行的硬件和软件配置的细节。这又要求我们以某种方式将我们期望在系统中发生的错误形式化。我们通过定义一个系统模型来做到这一点,这个模型是一个抽象,描述一个算法可能承担的事情。 +关于定时假设,三种系统模型是常用的: + +***同步模型*** + +同步模型假定有界网络延迟,有界过程保持和有界时钟误差。这并不意味着完全同步的时钟或零网络延迟;这只意味着你知道网络延迟,暂停和时钟漂移将永远不会超过某个固定的上限[88]。同步模型并不是大多数实际系统的实际模型,因为(如本章所讨论的)无限延迟和暂停确实发生。 + +***部分同步模型*** + +部分同步意味着一个系统在大多数情况下像一个同步系统一样运行,但有时候会超出网络延迟,进程暂停和时钟漂移的界限[88]。这是一个很多系统的现实模型:大多数情况下,网络和流程是相当好的,否则我们永远无法完成任何事情,但是我们必须考虑到任何时间假设偶尔都会被破坏。发生这种情况时,网络延迟,暂停和时钟错误可能会变得非常大。 + +***异步模型*** + +在这个模型中,一个算法不允许做任何时间假设 - 事实上,它甚至没有时钟(所以它不能使用超时)。一些算法可以被设计用于异步模型,但是这是非常严格的。 + +而且,除了时间问题,我们还要考虑**节点故障**。三种最常见的节点系统模型是: + +***崩溃-停止故障*** + +在碰撞停止模型中,算法可能会假设一个节点只能以一种方式失效,即通过崩溃。这意味着节点可能在任何时候突然停止响应,此后该节点永远消失 - 它永远不会回来。 + +***崩溃-恢复故障*** + +我们假设节点可能会在任何时候崩溃,也许在未知的时间之后再次开始响应。在故障恢复模型中,假设节点具有稳定的存储(即,非易失性磁盘存储),其在崩溃中被保存,而内存中状态被假定为丢失。 + +***拜占庭(任意)故障*** + +节点可以做任何事情,包括试图欺骗和欺骗其他节点,如上一节所述。 + +对于真实系统的建模,具有故障恢复故障的部分同步模型通常是最有用的模型。但分布式算法如何应对这种模式? + +***算法的正确性*** + +为了定义算法是正确的,我们可以描述它的属性。例如,排序算法的输出具有如下特性:对于输出列表中的任何两个不同的元素,左边的元素比右边的元素小。这只是一个正式的方式来定义对列表进行排序的含义。 + +同样,我们可以写下我们想要的分布式算法的属性来定义它的正确含义。例如,如果我们正在为一个锁生成击剑令牌(请参阅第291页上的“防护令牌”),我们可能要求算法具有以下属性: + +***唯一性*** + +没有两个屏蔽令牌请求返回相同的值。 + +***单调序列*** + +如果请求x返回了令牌tx,并且请求y返回了令牌ty,并且x在y开始之前完成了,那么tx