ch8 机翻

This commit is contained in:
Vonng 2018-02-12 14:53:06 +08:00
parent 8b595b31bc
commit 3b33445a07
3 changed files with 615 additions and 24 deletions

View File

@ -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) | | - |

View File

@ -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]。
> * IPInternet协议不可靠可能丢弃延迟复制或重排数据包。 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()`返回自epoch1970年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以这种方式实现跨数据中心的快照隔离[5960]。它使用TrueTime API报告的时钟置信区间并基于以下观察结果如果您有两个置信区间每个置信区间包含最早和最近可能的时间戳A = [最早Alatest]和B = 最早的Blatest这两个区间不重叠即最早的<Alatest <最早的<Blatest那么B肯定发生在A之后 - 这是毫无疑问的只有间隔重叠我们才能确定A和B发生的顺序
为了确保事务时间戳反映因果关系在提交读写事务之前Spanner故意等待置信区间的长度。通过这样做它可以确保任何可能读取数据的事务处于足够晚的时间因此它们的置信区间不会重叠。为了保持尽可能短的等待时间Spanner需要保持尽可能小的时钟不确定性为此Google在每个数据中心都部署了一个GPS接收器或原子钟允许时钟在大约7 ms内同步[41]。
对分布式事务语义使用时钟同步是一个积极的研究领域[57,61,62]。这些想法很有趣,但是它们还没有在谷歌之外的主流数据库中实现。
### 暂停进程
让我们考虑在分布式系统中使用危险时钟的另一个例子。假设你有一个数据库,每个分区只有一个领导者。只有领导被允许接受写入。一个节点如何知道它仍然是领导者(它并没有被别人宣告为死亡),并且它可以安全地接受写入?
一种选择是领导者从其他节点获得一个**租约lease**,类似一个带超时的锁[63]。只有一个节点可以在任何时间持有租约——因此,当一个节点获得一个租约时,它知道它在某段时间内是领导者,直到租约到期。为了保持领导,节点必须定期更新
租期到期。如果节点发生故障,则停止续租,所以另一个节点过期时可以接管。
你可以想象请求处理循环看起来像这样:
```java
while(true){
request=getIncomingRequest();
// 确保租约还剩下至少10秒
if (lease.expiryTimeMillis-System.currentTimeMillis()< 10000){
lease = lease.renew();
}
if(lease.isValid()){
process(request);
}}
}
```
 这个代码有什么问题首先它依赖于同步时钟租约到期时间由另一台机器设置例如当前时间加上30秒计算到期时间并将其与本地系统时钟进行比较。如果时钟超过几秒不同步这段代码将开始做奇怪的事情。
其次,即使我们将协议更改为仅使用本地单调时钟,也存在另一个问题:代码假定在执行剩余时间检查`System.currentTimeMillis()`和实际执行请求`process(request)`中间的时间间隔非常短。通常情况下这段代码运行得非常快所以10秒的缓冲区已经足够确保**租约**在请求处理到一半时不会过期。
但是,如果程序执行中出现了意外的停顿呢?例如,想象一下,线程在`lease.isValid()`行周围停止15秒然后才终止。在这种情况下在请求被处理的时候租约可能已经过期而另一个节点已经接管了领导。然而没有什么可以告诉这个线程已经暂停了这么长时间了所以这段代码不会注意到租约已经到期了直到循环的下一个迭代 - 到那个时候它可能已经做了一些不安全的处理请求。
假设一个线程可能会暂停很长时间,这是疯了吗?不幸的是,这种情况发生的原因有很多种:
* 许多编程语言运行时如Java虚拟机都有一个垃圾收集器GC偶尔需要停止所有正在运行的线程。这些“停止世界”GC暂停有时被称为持续几分钟[64]甚至像HotSpot JVM的CMS这样的所谓的“并发”垃圾收集器也不能完全与应用程序代码并行运行即使它们需要不时地停止世界[65]。尽管通常可以通过改变分配模式或调整GC设置来减少暂停[66],但是如果我们想要提供有力的保证,我们必须假设最差。
* 在虚拟化环境中,可以暂停虚拟机(暂停执行所有进程并将内存内容保存到磁盘)并恢复(恢复内存内容并继续执行)。这个暂停可以在进程执行的任何时候发生,并且可以持续任意长的时间。这个功能有时用于虚拟机从一个主机到另一个主机的实时迁移,而不需要重新启动,在这种情况下,暂停的长度取决于进程写入内存的速率[67]。
* 在最终用户的设备(如笔记本电脑)上,执行也可能被暂停并随意恢复,例如当用户关闭笔记本电脑的盖子时。
* 当操作系统上下文切换到另一个线程时或者当管理程序切换到另一个虚拟机时在虚拟机中运行时当前正在运行的线程可以在代码中的任意点处暂停。在虚拟机的情况下在其他虚拟机中花费的CPU时间被称为窃取时间。如果机器处于沉重的负载下如果等待运行的线程很长暂停的线程再次运行可能需要一些时间。
* 如果应用程序执行同步磁盘访问则线程可能暂停等待缓慢的磁盘I / O操作完成[68]。在许多语言中,即使代码没有明确提到文件访问,磁盘访问也可能出乎意料地发生 - 例如Java类加载器在第一次使用时懒惰地加载类文件这可能在程序执行过程中随时发生。 I / O暂停和GC暂停甚至可能合谋结合它们的延迟[69]。如果磁盘实际上是一个网络文件系统或网络块设备如亚马逊的EBSI / O延迟进一步受到网络延迟变化的影响[29]。
* 如果操作系统配置为允许交换到磁盘分页则简单的内存访问可能导致页面错误要求将磁盘中的页面装入内存。当这个缓慢的I / O操作发生时线程暂停。如果内存压力很高则可能需要将不同的页面换出到磁盘。在极端情况下操作系统可能花费大部分时间将页面交换到内存中而实际上完成的工作很少这被称为抖动。为了避免这个问题通常在服务器机器上禁用页面调度如果你宁愿干掉一个进程来释放内存也不愿意冒风险
* 可以通过发送SIGSTOP信号来暂停Unix进程例如通过在shell中按下Ctrl-Z。 这个信号立即阻止进程继续执行更多的CPU周期直到SIGCONT恢复为止此时它将继续运行。 即使您的环境通常不使用SIGSTOP也可能由操作工程师意外发送。
所有这些事件都可以随时抢占正在运行的线程,并在稍后的时间恢复运行,而线程甚至不会注意到这一点。这个问题类似于在单个机器上使多线程代码线程安全:您不能假定任何关于时间的事情,因为可能发生任意的上下文切换和并行性。
当在一台机器上编写多线程代码时,我们有相当好的工具来实现线程安全:互斥量,信号量,原子计数器,无锁数据结构,阻塞队列等等。不幸的是,这些工具并不直接转化为分布式系统,因为分布式系统没有通过不可靠的网络发送共享内存消息。
分布式系统中的一个节点必须假定它的执行可以在任何时候都暂停很长一段时间,即使是在一个函数的中间。在暂停期间,世界其他地方一直在移动,甚至可能因为没有响应而宣布暂停的节点已经死亡。最终,暂停的节点可能会继续运行,甚至没有注意到它一直睡着,直到稍后再检查它的时钟。
#### 响应时间保证
在许多编程语言和操作系统中,线程和进程可能暂停一段无限的时间,正如讨论的那样。如果你足够努力,那么暂停的原因就可以消除。
某些软件运行在指定时间内未能响应的环境中会造成严重损害:控制飞机,火箭,机器人,汽车和其他物体的计算机必须对其传感器输入做出快速而可预测的响应。在这些系统中,软件必须有一个特定的截止日期,如果不符合截止日期,可能会导致整个系统的故障。这就是所谓的硬实时系统。
> #### 实时真的是真的吗?
>
> 在嵌入式系统中实时是指系统经过精心设计和测试以满足所有情况下的特定时间保证。这个含义与Web上实时术语的模糊使用相反它描述了服务器将数据推送到客户端以及流处理而没有严格的响应时间限制见第11章
例如如果车载传感器检测到当前正在经历碰撞你肯定不希望安全气囊释放系统因为GC暂停而延迟弹出。
在系统中提供实时保证需要各级软件栈的支持一个实时操作系统RTOS允许在指定的时间间隔内保证CPU时间的分配。图书馆
函数必须记录最坏情况下的执行时间;动态内存分配可能受到限制或完全不允许实时垃圾收集器存在但是应用程序仍然必须确保它不会给GC太多的工作;必须进行大量的测试和测量,以确保达到保证。
所有这些都需要大量额外的工作,严重限制了可以使用的编程语言,库和工具的范围(因为大多数语言和工具不提供实时保证)。由于这些原因,开发实时系统非常昂贵,并且它们通常用于安全关键的嵌入式设备。而且,“实时”与“高性能”不一样 - 事实上,实时系统可能具有较低的吞吐量,因为他们必须优先考虑及时响应高于一切(另请参见“延迟和资源利用“)。
对于大多数服务器端数据处理系统来说,实时保证是不经济或不合适的。因此,这些系统必须承受在非实时环境中运行的暂停和时钟不稳定性。
#### 限制垃圾收集的影响
过程暂停的负面影响可以在不诉诸昂贵的实时调度保证的情况下得到缓解。语言运行时在计划垃圾回收时具有一定的灵活性,因为它们可以跟踪对象分配的速度和随着时间的推移剩余的空闲内存。
一个新兴的想法是将GC暂停视为一个节点的短暂计划中断并让其他节点处理来自客户端的请求同时一个节点正在收集其垃圾。如果运行时可以警告应用程序一个节点很快需要GC暂停那么应用程序可以停止向该节点发送新的请求等待它完成处理未完成的请求然后在没有请求正在进行时执行GC。这个技巧隐藏了来自客户端的GC暂停并降低了响应时间的高百分比[70,71]。一些对延迟敏感的金融交易系统[72]使用这种方法。
这个想法的一个变种是只使用垃圾收集器来处理短暂的对象这些对象要快速收集并定期重新启动进程然后再累积足够长的对象来需要一个长寿命对象的完整GC [6573]。一次可以重新启动一个节点在计划重新启动之前流量可以从节点移开就像滚动升级参见第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 <ty
***可用性***
请求防护令牌并且不会崩溃的节点最终会收到响应。
如果一个系统模型中的算法总是满足它在我们假设可能发生的所有情况下的性质,那么这个算法是正确的。但是,这是如何有意义的?如果所有的节点崩溃,或者所有的网络延迟突然变得无限长,那么没有任何算法能够完成任何事情。
#### 安全性和活性
为了澄清这种情况,有必要区分两种不同的性质:**安全性safety**和**活性liveness**。在刚刚给出的例子中,**唯一性uniqueness**和**单调序列monotonic sequence**是安全属性,但**可用性**是**活性liveness**属性。
这两种性质有什么区别?一个实锤就是活性属性通常在定义中通常包括“最终”一词。 (是的,你猜对了——最终一致性是一个活性属性[89]。)
安全性通常被非正式地定义为没有什么不好的事情发生,而活着就像最终发生的事情一样。但是,最好不要过多地阅读那些非正式的定义,因为好与坏的含义是主观的。安全性和活性的实际定义是精确的和数学的[90]
* 如果安全属性被违反,我们可以指向一个特定的时间点(例如,如果违反了唯一性属性,我们可以确定重复的防护令牌返回的特定操作) 。违反安全财产后,违规行为不能撤销 - 损害已经完成。
* 活性属性反过来:在某个时间点(例如,一个节点可能发送了一个请求,但还没有收到响应),它可能不成立,但总是希望在未来(即通过接受答复)。
区分安全性和活性属性的一个优点是可以帮助我们处理困难的系统模型。对于分布式算法,在系统模型的所有可能情况下,要求安全属性始终保持是常见的[88]。也就是说,即使所有节点崩溃,或者整个网络出现故障,算法仍然必须确保它不会返回错误的结果(即保证安全性得到满足)。
但是,对于活性属性,我们可以提出一些注意事项:例如,只有在大多数节点没有崩溃的情况下,只有当网络最终从中断中恢复时,我们才可以说请求需要接收响应。部分同步模型的定义要求系统最终返回到同步状态 - 即任何网络中断的时间段只会持续一段有限的时间,然后进行修复。
#### 将系统模型映射到现实世界
安全性和活性属性以及系统模型对于推理分布式算法的正确性非常有用。然而,在实践中实施算法时,现实的混乱事实再一次地让你咬牙切齿,很明显系统模型是对现实的简化抽象。
例如,在故障恢复模型中的算法通常假设稳定存储器中的数据经历了崩溃。但是,如果磁盘上的数据被破坏,或者由于硬件错误或错误配置导致数据被清除,会发生什么情况?如果服务器存在固件错误并且在重新启动时无法识别其硬盘驱动器,即使驱动器已正确连接到服务器,也会发生什么情况?
法定人数算法请参见第179页的“读写法定人数”依赖节点来记住它声称存储的数据。如果一个节点可能患有健忘症忘记了以前存储的数据这会打破法定条件从而破坏算法的正确性。也许需要一个新的系统模型在这个模型中我们假设稳定的存储大多存在崩溃但有时可能会丢失。但是那个模型就变得更难以推理了。
算法的理论描述可以宣称,简单假设在非拜占庭式系统中,一些事是不会发生的。但实际上我们还是需要对可能发生和不可能发生的故障做出假设,真实世界的实现仍然会包括那些处理“假设上不可能”情况的代码,即使代码可以归结为`printf("你逊爆了")`和`exit(666)`,实际上也就是留给运维来人肉清理烂摊子(这可以说是计算机科学和软件工程间的一个差异)。
这并不是说理论上抽象的系统模型是毫无价值的,恰恰相反。它们对于将实际系统的复杂性降低到一个我们可以推理的可处理的错误是非常有帮助的,以便我们能够理解这个问题,并试图系统地解决这个问题。我们可以证明算法是正确的,通过显示它们的属性总是保持在某个系统模型中
证明算法正确并不意味着它在真实系统上的实现必然总是正确的。但这是一个非常好的第一步,因为理论分析可以发现一个算法中的问题,这个算法可能会在真实系统中长时间隐藏,而当你的假设(例如,时间)是由于不寻常的情况而被击败。理论分析与经验
## 本章小结
在本章中,我们讨论了分布式系统中可能发生的各种问题,包括:
* 当您尝试通过网络发送数据包时,数据包可能会丢失或任意延迟。同样,答复可能会丢失或延迟,所以如果你没有得到答复,你不知道消息是否通过。
* 节点的时钟可能会与其他节点显着不同步尽管您尽最大努力设置NTP它可能会突然跳转或跳回依靠它是很危险的因为您很可能没有好的测量你的时钟的错误间隔。
* 一个进程可能会在其执行的任何时候暂停一段相当长的时间(可能是因为世界上的垃圾收集器),被其他节点宣告死亡,然后再次复活,却没有意识到它被暂停了。
这种部分失效可能发生的事实是分布式系统的决定性特征。每当软件试图做任何涉及其他节点的事情时,就有可能偶尔会失败,或者随机变慢,或者根本没有响应(最终超时)。在分布式系统中,我们试图将部分故障的容忍度建立到软件中,这样整个系统即使在某些组成部分被破坏的情况下也可以继续运行。
为了容忍错误,第一步是检测它们,但即使这样也很难。大多数系统没有检测节点是否发生故障的准确机制,所以大多数分布式算法依靠超时来确定远程节点是否仍然可用。但是,超时无法区分网络和节点故障,并且可变的网络延迟有时会导致节点被错误地怀疑发生故障。此外,有时一个节点可能处于降级状态:例如,由于驱动程序错误[94]千兆网络接口可能突然下降到1 Kb / s的吞吐量。这样一个“跛行”而不是死的节点可能比一个干净的故障节点更难处理。
一旦检测到故障,使系统容忍它是不容易的:没有全局变量,没有共享内存,没有共同的知识或机器之间的任何其他种类的共享状态。节点甚至不知道现在几点了,更不用说更深刻的了。信息从一个节点流向另一个节点的唯一方法是通过不可靠的网络发送信息。重大决策不能由一个节点安全地完成,因此我们需要从其他节点获得帮助的协议,并争取达到法定人数。
如果您习惯于将软件编写在单台计算机的理想化的数学完美中,那么同一个操作总能确定地返回相同的结果,那么转向分布式系统的凌乱的物理现实可能会有些震惊。相反,如果分布式系统工程师能够在一台计算机上解决这个问题,那么分布式系统工程师通常会认为这个问题是琐事的[5],现在单个计算机的确可以做很多事情[95]。如果你可以避免打开潘多拉的盒子,把东西放在一台机器上,那么通常是值得的。
但是,正如在第二部分的介绍中所讨论的那样,可扩展性并不是想要使用分布式系统的唯一原因。容错和低延迟(通过将数据放置在距离用户较近的地方)是同等重要的目标,而这些不能用单个节点实现。
在本章中,我们也探讨了一些切线,以探讨网络,时钟和过程的不可靠性是不可避免的自然规律。我们看到这不是:有可能给网络提供硬实时的响应保证和有限的延迟,但是这样做非常昂贵,导致硬件资源的利用率降低。大多数非安全关键系统选择便宜,不可靠而不昂贵和可靠。
我们还谈到了超级计算机,它们采用可靠的组件,因此当组件发生故障时必须完全停止并重新启动。相比之下,分布式系统可以永久运行而不会在服务级别中断,因为所有的错误和维护都可以在节点级别进行处理 - 至少在理论上是如此。 (实际上,如果一个错误的配置变更被转移到所有的节点,这仍然会使分布式系统瘫痪)。
这一章一直是关于问题,给我们带来了一个黯淡的前景。在下一章中,我们将继续讨论解决方案,并讨论一些旨在解决分布式系统中所有问题的算法。
## 参考文献
@ -338,23 +934,9 @@
------
![](img/fig8-1.png)
| 上一章 | 目录 | 下一章 |
| ---------------------- | ------------------------------- | ------------------------------ |
| [第七章:事务](ch7.md) | [设计数据密集型应用](README.md) | [第九章:一致性与共识](ch9.md) |
图8-1
![](img/fig8-2.png)
图8-2
![](img/fig8-3.png)
图8-3
![](img/fig8-4.png)
图8-4
![](img/fig8-5.png)
图8-5

View File

@ -544,4 +544,13 @@
![](img/fig9-10.png)
图9-10
图9-10
------
| 上一章 | 目录 | 下一章 |
| ---------------------------------- | ------------------------------- | --------------------------------- |
| [第八章:分布式系统的麻烦](ch8.md) | [设计数据密集型应用](README.md) | [第三部分:派生数据](part-iii.md) |