From 2fff2043a4b080ea843d1a71015e80197bea85fe Mon Sep 17 00:00:00 2001 From: Yin Gang Date: Mon, 9 Aug 2021 15:46:58 +0800 Subject: [PATCH] finish chapter 8 --- ch8.md | 250 +++++++++++++++++++++++++--------------------------- glossary.md | 4 +- 2 files changed, 124 insertions(+), 130 deletions(-) diff --git a/ch8.md b/ch8.md index 2ffbf9e..ecce496 100644 --- a/ch8.md +++ b/ch8.md @@ -16,7 +16,7 @@ [TOC] -​ 最近几章中反复出现的主题是,系统如何处理错误的事情。例如,我们讨论了**副本故障切换**(“[处理节点中断](#ch5.md#处理节点宕机)”),**复制延迟**(“[复制延迟问题](ch6.md#复制延迟问题)”)和事务控制(“[弱隔离级别](ch7.md#弱隔离级别)”)。当我们了解可能在实际系统中出现的各种边缘情况时,我们会更好地处理它们。 +​ 最近几章中反复出现的主题是,系统如何处理错误的事情。例如,我们讨论了**副本故障切换**(“[处理节点中断](ch5.md#处理节点宕机)”),**复制延迟**(“[复制延迟问题](ch5.md#复制延迟问题)”)和事务控制(“[弱隔离级别](ch7.md#弱隔离级别)”)。当我们了解可能在实际系统中出现的各种边缘情况时,我们会更好地处理它们。 ​ 但是,尽管我们已经谈了很多错误,但之前几章仍然过于乐观。现实更加黑暗。我们现在将悲观主义最大化,假设任何可能出错的东西**都会**出错[^i]。(经验丰富的系统运维会告诉你,这是一个合理的假设。如果你问得好,他们可能会一边治疗心理创伤一边告诉你一些可怕的故事) @@ -26,17 +26,16 @@ ​ 最后,作为工程师,我们的任务是构建能够完成工作的系统(即满足用户期望的保证),尽管一切都出错了。 在[第9章](ch9.md)中,我们将看看一些可以在分布式系统中提供这种保证的算法的例子。 但首先,在本章中,我们必须了解我们面临的挑战。 -​ 本章对分布式系统中可能出现的问题进行彻底的悲观和沮丧的总结。 我们将研究网络的问题(“[无法访问的网络](#无法访问的网络)”); 时钟和时序问题(“[不可靠时钟](#不可靠时钟)”); 我们将讨论他们可以避免的程度。 所有这些问题的后果都是困惑的,所以我们将探索如何思考一个分布式系统的状态,以及如何推理发生的事情(“[知识,真相和谎言](#知识,真相和谎言)”)。 - +​ 本章对分布式系统中可能出现的问题进行彻底的悲观和沮丧的总结。 我们将研究网络的问题(“[不可靠的网络](#不可靠的网络)”); 时钟和时序问题(“[不可靠的时钟](#不可靠的时钟)”); 我们将讨论他们可以避免的程度。 所有这些问题的后果都是困惑的,所以我们将探索如何思考一个分布式系统的状态,以及如何推理发生的事情(“[知识、真相与谎言](#知识、真相与谎言)”)。 ## 故障与部分失效 -​ 当你在一台计算机上编写一个程序时,它通常会以一种相当可预测的方式运行:无论是工作还是不工作。充满错误的软件可能会让人觉得电脑有时候是“糟糕的一天”(这个问题通常是重新启动的问题),但这主要是软件写得不好的结果。 +​ 当你在一台计算机上编写一个程序时,它通常会以一种相当可预测的方式运行:无论是工作还是不工作。充满错误的软件可能会让人觉得电脑有时候也会有“糟糕的一天”(这种问题通常是重新启动就恢复了),但这主要是软件写得不好的结果。 ​ 单个计算机上的软件没有根本性的不可靠原因:当硬件正常工作时,相同的操作总是产生相同的结果(这是确定性的)。如果存在硬件问题(例如,内存损坏或连接器松动),其后果通常是整个系统故障(例如,内核恐慌,“蓝屏死机”,启动失败)。装有良好软件的个人计算机通常要么功能完好,要么完全失效,而不是介于两者之间。 -​ 这是计算机设计中的一个慎重的选择:如果发生内部错误,我们宁愿电脑完全崩溃,而不是返回错误的结果,因为错误的结果很难处理。因为计算机隐藏了模糊不清的物理实现,并呈现出一个理想化的系统模型,并以数学一样的完美的方式运作。 CPU指令总是做同样的事情;如果您将一些数据写入内存或磁盘,那么这些数据将保持不变,并且不会被随机破坏。从第一台数字计算机开始,*始终正确地计算*这个设计目标贯穿始终【3】。 +​ 这是计算机设计中的一个有意的选择:如果发生内部错误,我们宁愿电脑完全崩溃,而不是返回错误的结果,因为错误的结果很难处理。因为计算机隐藏了模糊不清的物理实现,并呈现出一个理想化的系统模型,并以数学一样的完美的方式运作。 CPU指令总是做同样的事情;如果您将一些数据写入内存或磁盘,那么这些数据将保持不变,并且不会被随机破坏。从第一台数字计算机开始,*始终正确地计算*这个设计目标贯穿始终【3】。 ​ 当你编写运行在多台计算机上的软件时,情况有本质上的区别。在分布式系统中,我们不再处于理想化的系统模型中,我们别无选择,只能面对现实世界的混乱现实。而在现实世界中,各种各样的事情都可能会出现问题【4】,如下面的轶事所述: @@ -52,38 +51,38 @@ 关于如何构建大型计算系统有一系列的哲学: -* 规模的一端是高性能计算(HPC)领域。具有数千个CPU的超级计算机通常用于计算密集型科学计算任务,如天气预报或分子动力学(模拟原子和分子的运动)。 -* 另一个极端是**云计算(cloud computing)**,云计算并不是一个良好定义的概念【6】,但通常与多租户数据中心,连接IP网络的商品计算机(通常是以太网),弹性/按需资源分配以及计量计费等相关联。 +* 一个极端是高性能计算(HPC)领域。具有数千个CPU的超级计算机通常用于计算密集型科学计算任务,如天气预报或分子动力学(模拟原子和分子的运动)。 +* 另一个极端是**云计算(cloud computing)**,云计算并不是一个良好定义的概念【6】,但通常与多租户数据中心,连接IP网络(通常是以太网)的商用计算机,弹性/按需资源分配以及计量计费等相关联。 * 传统企业数据中心位于这两个极端之间。 不同的哲学会导致不同的故障处理方式。在超级计算机中,作业通常会不时地会将计算的状态存盘到持久存储中。如果一个节点出现故障,通常的解决方案是简单地停止整个集群的工作负载。故障节点修复后,计算从上一个检查点重新开始【7,8】。因此,超级计算机更像是一个单节点计算机而不是分布式系统:通过让部分失败升级为完全失败来处理部分失败——如果系统的任何部分发生故障,只是让所有的东西都崩溃(就像单台机器上的内核恐慌一样)。 -在本书中,我们将重点放在实现互联网服务的系统上,这些系统通常与超级计算机看起来有很大不同 +在本书中,我们将重点放在实现互联网服务的系统上,这些系统通常与超级计算机看起来有很大不同: * 许多与互联网有关的应用程序都是**在线(online)**的,因为它们需要能够随时以低延迟服务用户。使服务不可用(例如,停止群集以进行修复)是不可接受的。相比之下,像天气模拟这样的离线(批处理)工作可以停止并重新启动,影响相当小。 -* 超级计算机通常由专用硬件构建而成,每个节点相当可靠,节点通过共享内存和**远程直接内存访问(RDMA)**进行通信。另一方面,云服务中的节点是由商品机器构建而成的,由于规模经济,可以以较低的成本提供相同的性能,而且具有较高的故障率。 +* 超级计算机通常由专用硬件构建而成,每个节点相当可靠,节点通过共享内存和**远程直接内存访问(RDMA)**进行通信。另一方面,云服务中的节点是由商用机器构建而成的,由于规模经济,可以以较低的成本提供相同的性能,而且具有较高的故障率。 -* 大型数据中心网络通常基于IP和以太网,以闭合拓扑排列,以提供更高的二等分带宽【9】。超级计算机通常使用专门的网络拓扑结构,例如多维网格和环面 【10】,这为具有已知通信模式的HPC工作负载提供了更好的性能。 +* 大型数据中心网络通常基于IP和以太网,以CLOS拓扑排列,以提供更高的对分(bisection)带宽【9】。超级计算机通常使用专门的网络拓扑结构,例如多维网格和Torus网络 【10】,这为具有已知通信模式的HPC工作负载提供了更好的性能。 -* 系统越大,其组件之一就越有可能坏掉。随着时间的推移,坏掉的东西得到修复,新的东西又坏掉,但是在一个有成千上万个节点的系统中,有理由认为总是有一些东西是坏掉的【7】。当错误处理策略由简单的放弃组成时,一个大的系统最终会花费大量时间从错误中恢复,而不是做有用的工作【8】。 +* 系统越大,其组件之一就越有可能坏掉。随着时间的推移,坏掉的东西得到修复,新的东西又坏掉,但是在一个有成千上万个节点的系统中,有理由认为总是有一些东西是坏掉的【7】。当错误处理的策略只由简单放弃组成时,一个大的系统最终会花费大量时间从错误中恢复,而不是做有用的工作【8】。 -* 如果系统可以容忍发生故障的节点,并继续保持整体工作状态,那么这对于操作和维护非常有用:例如,可以执行滚动升级(参阅[第4章](ch4.md)),一次重新启动一个节点,而服务继续服务用户不中断。在云环境中,如果一台虚拟机运行不佳,可以杀死它并请求一台新的虚拟机(希望新的虚拟机速度更快)。 +* 如果系统可以容忍发生故障的节点,并继续保持整体工作状态,那么这对于运营和维护非常有用:例如,可以执行滚动升级(参阅[第4章](ch4.md)),一次重新启动一个节点,同时继续给用户提供不中断的服务。在云环境中,如果一台虚拟机运行不佳,可以杀死它并请求一台新的虚拟机(希望新的虚拟机速度更快)。 * 在地理位置分散的部署中(保持数据在地理位置上接近用户以减少访问延迟),通信很可能通过互联网进行,与本地网络相比,通信速度缓慢且不可靠。超级计算机通常假设它们的所有节点都靠近在一起。 -如果要使分布式系统工作,就必须接受部分故障的可能性,并在软件中建立容错机制。换句话说,我们需要从不可靠的组件构建一个可靠的系统。 (正如“[可靠性](ch1.md#可靠性)”中所讨论的那样,没有完美的可靠性,所以我们需要理解我们可以实际承诺的限制。) +如果要使分布式系统工作,就必须接受部分故障的可能性,并在软件中建立容错机制。换句话说,我们需要从不可靠的组件构建一个可靠的系统。 (正如“[可靠性](ch1.md#可靠性)”中所讨论的那样,没有完美的可靠性,所以我们需要理解我们可以实际承诺的极限。) ​ 即使在只有少数节点的小型系统中,考虑部分故障也是很重要的。在一个小系统中,很可能大部分组件在大部分时间都正常工作。然而,迟早会有一部分系统出现故障,软件必须以某种方式处理。故障处理必须是软件设计的一部分,并且作为软件的运维,您需要知道在发生故障的情况下,软件可能会表现出怎样的行为。 -​ 简单地假设缺陷很罕见,只是希望始终保持最好的状况是不明智的。考虑一系列可能的错误(甚至是不太可能的错误),并在测试环境中人为地创建这些情况来查看会发生什么是非常重要的。在分布式系统中,怀疑,悲观和偏执狂是值得的。 +​ 简单地假设缺陷很罕见并希望始终保持最好的状况是不明智的。考虑一系列可能的错误(甚至是不太可能的错误),并在测试环境中人为地创建这些情况来查看会发生什么是非常重要的。在分布式系统中,怀疑,悲观和偏执狂是值得的。 > #### 从不可靠的组件构建可靠的系统 > > 您可能想知道这是否有意义——直观地看来,系统只能像其最不可靠的组件(最薄弱的环节)一样可靠。事实并非如此:事实上,从不太可靠的潜在基础构建更可靠的系统是计算机领域的一个古老思想【11】。例如: > > * 纠错码允许数字数据在通信信道上准确传输,偶尔会出现一些错误,例如由于无线网络上的无线电干扰【12】。 -> * **互联网协议(Internet Protocol, IP)**不可靠:可能丢弃,延迟,复制或重排数据包。 传输控制协议(Transmission Control Protocol, TCP)在互联网协议(IP)之上提供了更可靠的传输层:它确保丢失的数据包被重新传输,消除重复,并且数据包被重新组装成它们被发送的顺序。 +> * **互联网协议(Internet Protocol, IP)**不可靠:可能丢弃,延迟,重复或重排数据包。 传输控制协议(Transmission Control Protocol, TCP)在互联网协议(IP)之上提供了更可靠的传输层:它确保丢失的数据包被重新传输,消除重复,并且数据包被重新组装成它们被发送的顺序。 > > 虽然这个系统可以比它的底层部分更可靠,但它的可靠性总是有限的。例如,纠错码可以处理少量的单比特错误,但是如果你的信号被干扰所淹没,那么通过信道可以得到多少数据,是有根本性的限制的【13】。 TCP可以隐藏数据包的丢失,重复和重新排序,但是它不能神奇地消除网络中的延迟。 > @@ -93,16 +92,16 @@ ## 不可靠的网络 -​ 正如在第二部分的介绍中所讨论的那样,我们在本书中关注的分布式系统是无共享的系统,即通过网络连接的一堆机器。网络是这些机器可以通信的唯一途径——我们假设每台机器都有自己的内存和磁盘,一台机器不能访问另一台机器的内存或磁盘(除了通过网络向服务器发出请求)。 +​ 正如在[第二部分](part-ii.md)的介绍中所讨论的那样,我们在本书中关注的分布式系统是无共享的系统,即通过网络连接的一堆机器。网络是这些机器可以通信的唯一途径——我们假设每台机器都有自己的内存和磁盘,一台机器不能访问另一台机器的内存或磁盘(除了通过网络向服务器发出请求)。 ​ **无共享**并不是构建系统的唯一方式,但它已经成为构建互联网服务的主要方式,其原因如下:相对便宜,因为它不需要特殊的硬件,可以利用商品化的云计算服务,通过跨多个地理分布的数据中心进行冗余可以实现高可靠性。 ​ 互联网和数据中心(通常是以太网)中的大多数内部网络都是**异步分组网络(asynchronous packet networks)**。在这种网络中,一个节点可以向另一个节点发送一个消息(一个数据包),但是网络不能保证它什么时候到达,或者是否到达。如果您发送请求并期待响应,则很多事情可能会出错(其中一些如[图8-1](img/fig8-1.png)所示): 1. 请求可能已经丢失(可能有人拔掉了网线)。 -2. 请求可能正在排队,稍后将交付(也许网络或收件人超载)。 +2. 请求可能正在排队,稍后将交付(也许网络或接收方过载)。 3. 远程节点可能已经失效(可能是崩溃或关机)。 -4. 远程节点可能暂时停止了响应(可能会遇到长时间的垃圾回收暂停;参阅“[暂停进程](#暂停进程)”),但稍后会再次响应。 +4. 远程节点可能暂时停止了响应(可能会遇到长时间的垃圾回收暂停;参阅“[进程暂停](#进程暂停)”),但稍后会再次响应。 5. 远程节点可能已经处理了请求,但是网络上的响应已经丢失(可能是网络交换机配置错误)。 6. 远程节点可能已经处理了请求,但是响应已经被延迟,并且稍后将被传递(可能是网络或者你自己的机器过载)。 @@ -110,9 +109,9 @@ **图8-1 如果发送请求并没有得到响应,则无法区分(a)请求是否丢失,(b)远程节点是否关闭,或(c)响应是否丢失。** -​ 发送者甚至不能分辨数据包是否被发送:唯一的选择是让接收者发送响应消息,这可能会丢失或延迟。这些问题在异步网络中难以区分:您所拥有的唯一信息是,您尚未收到响应。如果您向另一个节点发送请求并且没有收到响应,则无法说明原因。 +​ 发送者甚至不能分辨数据包是否被发送:唯一的选择是让接收者发送响应消息,这可能会丢失或延迟。这些问题在异步网络中难以区分:您所拥有的唯一信息是,您尚未收到响应。如果您向另一个节点发送请求并且没有收到响应,则不可能判断是什么原因。 -​ 处理这个问题的通常方法是**超时(Timeout)**:在一段时间之后放弃等待,并且认为响应不会到达。但是,当发生超时时,你仍然不知道远程节点是否收到了请求(如果请求仍然在某个地方排队,那么即使发件人已经放弃了该请求,仍然可能会将其发送给收件人)。 +​ 处理这个问题的通常方法是**超时(Timeout)**:在一段时间之后放弃等待,并且认为响应不会到达。但是,当发生超时时,你仍然不知道远程节点是否收到了请求(如果请求仍然在某个地方排队,那么即使发送者已经放弃了该请求,仍然可能会将其发送给接收者)。 ### 真实世界的网络故障 @@ -141,60 +140,58 @@ 不幸的是,网络的不确定性使得很难判断一个节点是否工作。在某些特定的情况下,您可能会收到一些反馈信息,明确告诉您某些事情没有成功: -* 如果你可以到达运行节点的机器,但没有进程正在侦听目标端口(例如,因为进程崩溃),操作系统将通过发送FIN或RST来关闭并重用TCP连接。但是,如果节点在处理请求时发生崩溃,则无法知道远程节点实际处理了多少数据【22】。 -* 如果节点进程崩溃(或被管理员杀死),但节点的操作系统仍在运行,则脚本可以通知其他节点有关该崩溃的信息,以便另一个节点可以快速接管,而无需等待超时到期。例如,HBase做这个【23】。 -* 如果您有权访问数据中心网络交换机的管理界面,则可以查询它们以检测硬件级别的链路故障(例如,远程机器是否关闭电源)。如果您通过互联网连接,或者如果您处于共享数据中心而无法访问交换机,或者由于网络问题而无法访问管理界面,则排除此选项。 +* 如果你可以连接到运行节点的机器,但没有进程正在侦听目标端口(例如,因为进程崩溃),操作系统将通过发送FIN或RST来关闭并重用TCP连接。但是,如果节点在处理请求时发生崩溃,则无法知道远程节点实际处理了多少数据【22】。 +* 如果节点进程崩溃(或被管理员杀死),但节点的操作系统仍在运行,则脚本可以通知其他节点有关该崩溃的信息,以便另一个节点可以快速接管,而无需等待超时到期。例如,HBase就是这么做的【23】。 +* 如果您有权访问数据中心网络交换机的管理界面,则可以通过它们检测硬件级别的链路故障(例如,远程机器是否关闭电源)。如果您通过互联网连接,或者如果您处于共享数据中心而无法访问交换机,或者由于网络问题而无法访问管理界面,则排除此选项。 * 如果路由器确认您尝试连接的IP地址不可用,则可能会使用ICMP目标不可达数据包回复您。但是,路由器不具备神奇的故障检测能力——它受到与网络其他参与者相同的限制。 -关于远程节点关闭的快速反馈很有用,但是你不能指望它。即使TCP确认已经传送了一个数据包,应用程序在处理之前可能已经崩溃。如果你想确保一个请求是成功的,你需要应用程序本身的积极响应【24】。 +关于远程节点关闭的快速反馈很有用,但是你不能指望它。即使TCP确认已经传送了一个数据包,应用程序在处理之前可能已经崩溃。如果你想确保一个请求是成功的,你需要应用程序本身的正确响应【24】。 -​ 相反,如果出了什么问题,你可能会在堆栈的某个层次上得到一个错误响应,但总的来说,你必须假设你根本就没有得到任何回应。您可以重试几次(TCP重试是透明的,但是您也可以在应用程序级别重试),等待超时过期,并且如果在超时时间内没有收到响应,则最终声明节点已经死亡。 +​ 相反,如果出了什么问题,你可能会在堆栈的某个层次上得到一个错误响应,但总的来说,你必须假设你可能根本就得不到任何回应。您可以重试几次(TCP重试是透明的,但是您也可以在应用程序级别重试),等待超时过期,并且如果在超时时间内没有收到响应,则最终声明节点已经死亡。 ### 超时与无穷的延迟 ​ 如果超时是检测故障的唯一可靠方法,那么超时应该等待多久?不幸的是没有简单的答案。 -​ 长时间的超时意味着长时间等待,直到一个节点被宣告死亡(在这段时间内,用户可能不得不等待,或者看到错误信息)。短暂的超时可以更快地检测到故障,但是实际上它只是经历了暂时的减速(例如,由于节点或网络上的负载峰值)而导致错误地宣布节点失效的风险更高。 +​ 长时间的超时意味着长时间等待,直到一个节点被宣告死亡(在这段时间内,用户可能不得不等待,或者看到错误信息)。短的超时可以更快地检测到故障,但有更高地风险误将一个节点宣布为失效,而该节点实际上只是暂时地变慢了(例如由于节点或网络上的负载峰值)。 -​ 过早地声明一个节点已经死了是有问题的:如果这个节点实际上是活着的,并且正在执行一些动作(例如,发送一封电子邮件),而另一个节点接管,那么这个动作可能会最终执行两次。我们将在“[知识,真相和谎言](#知识,真相和谎言)”以及[第9章](ch9.md)和[第11章](ch11.md)中更详细地讨论这个问题。 +​ 过早地声明一个节点已经死了是有问题的:如果这个节点实际上是活着的,并且正在执行一些动作(例如,发送一封电子邮件),而另一个节点接管,那么这个动作可能会最终执行两次。我们将在“[知识、真相与谎言](#知识、真相与谎言)”以及[第9章](ch9.md)和[第11章](ch11.md)中更详细地讨论这个问题。 -​ 当一个节点被宣告死亡时,它的职责需要转移到其他节点,这会给其他节点和网络带来额外的负担。如果系统已经处于高负荷状态,则过早宣告节点死亡会使问题更严重。尤其是可能发生,节点实际上并没有死亡,而是由于过载导致响应缓慢;将其负载转移到其他节点可能会导致**级联失效(cascading failure)**(在极端情况下,所有节点都宣告对方死亡,并且所有节点都停止工作)。 +​ 当一个节点被宣告死亡时,它的职责需要转移到其他节点,这会给其他节点和网络带来额外的负担。如果系统已经处于高负荷状态,则过早宣告节点死亡会使问题更严重。特别是如果节点实际上没有死亡,只是由于过载导致其响应缓慢;这时将其负载转移到其他节点可能会导致**级联失效(cascading failure)**(在极端情况下,所有节点都宣告对方死亡,所有节点都将停止工作)。 ​ 设想一个虚构的系统,其网络可以保证数据包的最大延迟——每个数据包要么在一段时间内传送,要么丢失,但是传递永远不会比$d$更长。此外,假设你可以保证一个非故障节点总是在一段时间内处理一个请求$r$。在这种情况下,您可以保证每个成功的请求在$2d + r$时间内都能收到响应,如果您在此时间内没有收到响应,则知道网络或远程节点不工作。如果这是成立的,$2d + r$ 会是一个合理的超时设置。 -​ 不幸的是,我们所使用的大多数系统都没有这些保证:异步网络具有无限的延迟(即尽可能快地传送数据包,但数据包到达可能需要的时间没有上限),并且大多数服务器实现并不能保证它们可以在一定的最大时间内处理请求(请参阅“[响应时间保证](#响应时间保证)”)。对于故障检测,系统大部分时间快速运行是不够的:如果你的超时时间很短,往返时间只需要一个瞬时尖峰就可以使系统失衡。 +​ 不幸的是,我们所使用的大多数系统都没有这些保证:异步网络具有无限的延迟(即尽可能快地传送数据包,但数据包到达可能需要的时间没有上限),并且大多数服务器实现并不能保证它们可以在一定的最大时间内处理请求(请参阅“[响应时间保证](#响应时间保证)”)。对于故障检测,即使系统大部分时间快速运行也是不够的:如果你的超时时间很短,往返时间只需要一个瞬时尖峰就可以使系统失衡。 #### 网络拥塞和排队 ​ 在驾驶汽车时,由于交通拥堵,道路交通网络的通行时间往往不尽相同。同样,计算机网络上数据包延迟的可变性通常是由于排队【25】: -* 如果多个不同的节点同时尝试将数据包发送到同一目的地,则网络交换机必须将它们排队并将它们逐个送入目标网络链路(如[图8-2](img/fig8-2.png)所示)。在繁忙的网络链路上,数据包可能需要等待一段时间才能获得一个插槽(这称为网络连接)。如果传入的数据太多,交换机队列填满,数据包将被丢弃,因此需要重新发送数据包 - 即使网络运行良好。 +* 如果多个不同的节点同时尝试将数据包发送到同一目的地,则网络交换机必须将它们排队并将它们逐个送入目标网络链路(如[图8-2](img/fig8-2.png)所示)。在繁忙的网络链路上,数据包可能需要等待一段时间才能获得一个插槽(这称为网络拥塞)。如果传入的数据太多,交换机队列填满,数据包将被丢弃,因此需要重新发送数据包 - 即使网络运行良好。 * 当数据包到达目标机器时,如果所有CPU内核当前都处于繁忙状态,则来自网络的传入请求将被操作系统排队,直到应用程序准备好处理它为止。根据机器上的负载,这可能需要一段任意的时间。 -* 在虚拟化环境中,正在运行的操作系统经常暂停几十毫秒,而另一个虚拟机使用CPU内核。在这段时间内,虚拟机不能从网络中消耗任何数据,所以传入的数据被虚拟机监视器 【26】排队(缓冲),进一步增加了网络延迟的可变性。 -* TCP执行**流量控制(flow control)**(也称为**拥塞避免(congestion avoidance)**或**背压(backpressure)**),其中节点限制自己的发送速率以避免网络链路或接收节点过载【27】。这意味着在数据甚至进入网络之前,在发送者处需要进行额外的排队。 +* 在虚拟化环境中,正在运行的操作系统经常暂停几十毫秒,因为另一个虚拟机正在使用CPU内核。在这段时间内,虚拟机不能从网络中消耗任何数据,所以传入的数据被虚拟机监视器 【26】排队(缓冲),进一步增加了网络延迟的可变性。 +* TCP执行**流量控制(flow control)**(也称为**拥塞避免(congestion avoidance)**或**背压(backpressure)**),其中节点会限制自己的发送速率以避免网络链路或接收节点过载【27】。这意味着甚至在数据进入网络之前,在发送者处就需要进行额外的排队。 ![](img/fig8-2.png) **图8-2 如果有多台机器将网络流量发送到同一目的地,则其交换机队列可能会被填满。在这里,端口1,2和4都试图发送数据包到端口3** ​ 而且,如果TCP在某个超时时间内没有被确认(这是根据观察的往返时间计算的),则认为数据包丢失,丢失的数据包将自动重新发送。尽管应用程序没有看到数据包丢失和重新传输,但它看到了延迟(等待超时到期,然后等待重新传输的数据包得到确认)。 -      + > ### TCP与UDP > -> ​ 一些对延迟敏感的应用程序(如视频会议和IP语音(VoIP))使用UDP而不是TCP。这是在可靠性和和延迟可变性之间的折衷:由于UDP不执行流量控制并且不重传丢失的分组,所以避免了可变网络延迟的一些原因(尽管它仍然易受切换队列和调度延迟的影响)。 +> ​ 一些对延迟敏感的应用程序(如视频会议和IP语音(VoIP))使用UDP而不是TCP。这是在可靠性和和延迟变化之间的折衷:由于UDP不执行流量控制并且不重传丢失的分组,所以避免了网络延迟变化的一些原因(尽管它仍然易受切换队列和调度延迟的影响)。 > > ​ 在延迟数据毫无价值的情况下,UDP是一个不错的选择。例如,在VoIP电话呼叫中,可能没有足够的时间重新发送丢失的数据包,并在扬声器上播放数据。在这种情况下,重发数据包没有意义——应用程序必须使用静音填充丢失数据包的时隙(导致声音短暂中断),然后在数据流中继续。重试发生在人类层。 (“你能再说一遍吗?声音刚刚断了一会儿。“) -​ 所有这些因素都会造成网络延迟的变化。当系统接近其最大容量时,排队延迟的范围特别广泛: +​ 所有这些因素都会造成网络延迟的变化。当系统接近其最大容量时,排队延迟的变化范围特别大:拥有足够备用容量的系统可以轻松排空队列,而在高利用率的系统中,很快就能积累很长的队列。 -​ 拥有足够备用容量的系统可以轻松排空队列,而在高利用率的系统中,很快就能积累很长的队列。 +​ 在公共云和多租户数据中心中,资源被许多客户共享:网络链接和交换机,甚至每个机器的网卡和CPU(在虚拟机上运行时)。批处理工作负载(如MapReduce)(参阅[第10章](ch10.md))能够很容易使网络链接饱和。由于无法控制或了解其他客户对共享资源的使用情况,如果附近的某个人(嘈杂的邻居)正在使用大量资源,则网络延迟可能会发生剧烈变化【28,29】。 -​ 在公共云和多租户数据中心中,资源被许多客户共享:网络链接和交换机,甚至每个机器的网卡和CPU(在虚拟机上运行时)。批处理工作负载(如MapReduce)(参阅[第10章](ch10.md))可能很容易使网络链接饱和。由于无法控制或了解其他客户对共享资源的使用情况,如果附近的某个人(嘈杂的邻居)正在使用大量资源,则网络延迟可能会发生剧烈抖动【28,29】。 +​ 在这种环境下,您只能通过实验方式选择超时:在一段较长的时期内、在多台机器上测量网络往返时间的分布,以确定延迟的预期变化。然后,考虑到应用程序的特性,可以确定**故障检测延迟**与**过早超时风险**之间的适当折衷。 -​ 在这种环境下,您只能通过实验方式选择超时:测量延长的网络往返时间和多台机器的分布,以确定延迟的预期可变性。然后,考虑到应用程序的特性,可以确定**故障检测延迟**与**过早超时风险**之间的适当折衷。 - -​ 更好的一种做法是,系统不是使用配置的常量超时时间,而是连续测量响应时间及其变化(抖动),并根据观察到的响应时间分布自动调整超时时间。这可以通过Phi Accrual故障检测器【30】来完成,该检测器在例如Akka和Cassandra 【31】中使用。 TCP超时重传机制也同样起作用【27】。 +​ 更好的一种做法是,系统不是使用配置的常量超时时间,而是连续测量响应时间及其变化(抖动),并根据观察到的响应时间分布自动调整超时时间。这可以通过Phi Accrual故障检测器【30】来完成,该检测器在例如Akka和Cassandra 【31】中使用。 TCP的超时重传机制也是以类似的方式工作【27】。 ### 同步网络 vs 异步网络 @@ -212,19 +209,19 @@ [^ii]: 除了偶尔的keepalive数据包,如果TCP keepalive被启用。 -​ 如果数据中心网络和互联网是电路交换网络,那么在建立电路时就可以建立一个保证的最大往返时间。但是,它们并不是:以太网和IP是**分组交换协议**,不得不忍受排队的折磨,及其导致的网络无限延迟。这些协议没有电路的概念。 +​ 如果数据中心网络和互联网是电路交换网络,那么在建立电路时就可以建立一个受保证的最大往返时间。但是,它们并不是:以太网和IP是**分组交换协议**,不得不忍受排队的折磨,及其导致的网络无限延迟。这些协议没有电路的概念。 ​ 为什么数据中心网络和互联网使用分组交换?答案是,它们针对**突发流量(bursty traffic)**进行了优化。一个电路适用于音频或视频通话,在通话期间需要每秒传送相当数量的比特。另一方面,请求网页,发送电子邮件或传输文件没有任何特定的带宽要求——我们只是希望它尽快完成。 -​ 如果想通过电路传输文件,你得预测一个带宽分配。如果你猜的太低,传输速度会不必要的太慢,导致网络容量闲置。如果你猜的太高,电路就无法建立(因为如果无法保证其带宽分配,网络不能建立电路)。因此,使用用于突发数据传输的电路浪费网络容量,并且使传输不必要地缓慢。相比之下,TCP动态调整数据传输速率以适应可用的网络容量。 +​ 如果想通过电路传输文件,你得预测一个带宽分配。如果你猜的太低,传输速度会不必要的太慢,导致网络容量闲置。如果你猜的太高,电路就无法建立(因为如果无法保证其带宽分配,网络不能建立电路)。因此,将电路用于突发数据传输会浪费网络容量,并且使传输不必要地缓慢。相比之下,TCP动态调整数据传输速率以适应可用的网络容量。 -​ 已经有一些尝试去建立支持电路交换和分组交换的混合网络,比如ATM[^iii] InfiniBand有一些相似之处【35】:它在链路层实现了端到端的流量控制,从而减少了在网络中排队,尽管它仍然可能因链路拥塞而受到延迟【36】。通过仔细使用**服务质量(quality of service,)**(QoS,数据包的优先级和调度)和**准入控制(admission control)**(限速发送器),可以仿真分组网络上的电路交换,或提供统计上的**有限延迟**【25,32】。 +​ 已经有一些尝试去建立同时支持电路交换和分组交换的混合网络,比如ATM[^iii]。InfiniBand有一些相似之处【35】:它在链路层实现了端到端的流量控制,从而减少了在网络中排队的需要,尽管它仍然可能因链路拥塞而受到延迟【36】。通过仔细使用**服务质量(quality of service,)**(QoS,数据包的优先级和调度)和**准入控制(admission control)**(限速发送器),可以在分组网络上模拟电路交换,或提供统计上的**有限延迟**【25,32】。 -[^iii]: **异步传输模式(Asynchronous TransferMode, ATM)**在20世纪80年代是以太网的竞争对手【32】,但在电话网核心交换机之外并没有得到太多的采用。与自动柜员机(也称为自动取款机)无关,尽管共用一个缩写词。或许,在一些平行的世界里,互联网是基于像ATM这样的东西,因此它们的互联网视频通话可能比我们的更可靠,因为它们不会遭受丢包和延迟的包裹。 +[^iii]: **异步传输模式(Asynchronous Transfer Mode, ATM)**在20世纪80年代是以太网的竞争对手【32】,但在电话网核心交换机之外并没有得到太多的采用。它与自动柜员机(也称为自动取款机)无关,尽管共用一个缩写词。或许,在一些平行的世界里,互联网是基于像ATM这样的东西,因此它们的互联网视频通话可能比我们的更可靠,因为它们不会遭受包的丢失和延迟。 ​ 但是,目前在多租户数据中心和公共云或通过互联网[^iv]进行通信时,此类服务质量尚未启用。当前部署的技术不允许我们对网络的延迟或可靠性作出任何保证:我们必须假设网络拥塞,排队和无限的延迟总是会发生。因此,超时时间没有“正确”的值——它需要通过实验来确定。 -[^iv]: 互联网服务提供商之间的对等协议和通过**BGP网关协议(BGP)**建立路由之间的对等协议,与电路交换本身相比,与电路交换更接近。在这个级别上,可以购买专用带宽。但是,互联网路由在网络级别运行,而不是主机之间的单独连接,而且运行时间要长得多。 +[^iv]: 互联网服务提供商之间的对等协议和通过**BGP网关协议(BGP)**建立的路由,与IP协议相比,更接近于电路交换。在这个级别上,可以购买专用带宽。但是,互联网路由在网络级别运行,而不是主机之间的单独连接,而且运行时间要长得多。 > ### 延迟和资源利用 > @@ -232,16 +229,15 @@ > > ​ 假设两台电话交换机之间有一条线路,可以同时进行10,000个呼叫。通过此线路切换的每个电路都占用其中一个呼叫插槽。因此,您可以将线路视为可由多达10,000个并发用户共享的资源。资源以静态方式分配:即使您现在是电话上唯一的电话,并且所有其他9,999个插槽都未使用,您的电路仍将分配与导线充分利用时相同的固定数量的带宽。 > -> ​ 相比之下,互联网动态分享网络带宽。发送者互相推挤和争夺,以尽可能快地通过网络获得它们的分组,并且网络交换机决定从一个时刻到另一个时刻发送哪个分组(即,带宽分配)。这种方法有排队的缺点,但其优点是它最大限度地利用了电线。电线固定成本,所以如果你更好地利用它,你通过电线发送的每个字节都会更便宜。 +> ​ 相比之下,互联网动态分享网络带宽。发送者互相推挤和争夺,以让他们的数据包尽可能快地通过网络,并且网络交换机决定从一个时刻到另一个时刻发送哪个分组(即,带宽分配)。这种方法有排队的缺点,但其优点是它最大限度地利用了电线。电线固定成本,所以如果你更好地利用它,你通过电线发送的每个字节都会更便宜。 > -> ​ CPU也会出现类似的情况:如果您在多个线程间动态共享每个CPU内核,则有一个线程有时必须等待操作系统的运行队列,而另一个线程正在运行,这样线程可以暂停不同的时间长度。但是,与为每个线程分配静态数量的CPU周期相比,这会更好地利用硬件(参阅“[响应时间保证](#响应时间保证)”)。更好的硬件利用率也是使用虚拟机的重要动机。 +> ​ CPU也会出现类似的情况:如果您在多个线程间动态共享每个CPU内核,则一个线程有时必须在操作系统的运行队列里等待,而另一个线程正在运行,这样每个线程都有可能被暂停一个不定的时间长度。但是,与为每个线程分配静态数量的CPU周期相比,这会更好地利用硬件(参阅“[响应时间保证](#响应时间保证)”)。更好的硬件利用率也是使用虚拟机的重要动机。 > > ​ 如果资源是静态分区的(例如,专用硬件和专用带宽分配),则在某些环境中可以实现**延迟保证**。但是,这是以降低利用率为代价的——换句话说,它是更昂贵的。另一方面,动态资源分配的多租户提供了更好的利用率,所以它更便宜,但它具有可变延迟的缺点。 > > ​ 网络中的可变延迟不是一种自然规律,而只是成本/收益权衡的结果。 - ## 不可靠的时钟 时钟和时间很重要。应用程序以各种方式依赖于时钟来回答以下问题: @@ -255,29 +251,29 @@ 7. 这个缓存条目何时到期? 8. 日志文件中此错误消息的时间戳是什么? -[例1-4](ch1.md)测量[持续时间]()(例如,发送请求与正在接收的响应之间的时间间隔),而[例5-8](ch5.md)描述**时间点(point in time)**(在特定日期,特定时间发生的事件)。 +[例1-4](ch1.md)测量了**持续时间(durations)**(例如,请求发送与响应接收之间的时间间隔),而[例5-8](ch5.md)描述了**时间点(point in time)**(在特定日期,特定时间发生的事件)。 -​ 在分布式系统中,时间是一件棘手的事情,因为通信不是即时的:消息通过网络从一台机器传送到另一台机器需要时间。收到消息的时间总是晚于发送的时间,但是由于网络中的可变延迟,我们不知道多少时间。这个事实有时很难确定在涉及多台机器时发生事情的顺序。 +​ 在分布式系统中,时间是一件棘手的事情,因为通信不是即时的:消息通过网络从一台机器传送到另一台机器需要时间。收到消息的时间总是晚于发送的时间,但是由于网络中的可变延迟,我们不知道晚了多少时间。这个事实导致有时很难确定在涉及多台机器时发生事情的顺序。 ​ 而且,网络上的每台机器都有自己的时钟,这是一个实际的硬件设备:通常是石英晶体振荡器。这些设备不是完全准确的,所以每台机器都有自己的时间概念,可能比其他机器稍快或更慢。可以在一定程度上同步时钟:最常用的机制是**网络时间协议(NTP)**,它允许根据一组服务器报告的时间来调整计算机时钟【37】。服务器则从更精确的时间源(如GPS接收机)获取时间。 -### 单调钟与时钟 +### 单调钟与日历时钟 -​ 现代计算机至少有两种不同的时钟:时钟和单调钟。尽管它们都衡量时间,但区分这两者很重要,因为它们有不同的目的。 +​ 现代计算机至少有两种不同的时钟:日历时钟(time-of-day clock)和单调钟(monotonic clock)。尽管它们都衡量时间,但区分这两者很重要,因为它们有不同的目的。 -#### 时钟 +#### 日历时钟 -​ 时钟是您直观地了解时钟的依据:它根据某个日历(也称为**挂钟时间(wall-clock time)**)返回当前日期和时间。例如,Linux[^v]上的`clock_gettime(CLOCK_REALTIME)`和Java中的`System.currentTimeMillis()`返回自epoch(1970年1月1日 午夜 UTC,格里高利历)以来的秒数(或毫秒),根据公历日历,不包括闰秒。有些系统使用其他日期作为参考点。 +​ 日历时钟是您直观地了解时钟的依据:它根据某个日历(也称为**挂钟时间(wall-clock time)**)返回当前日期和时间。例如,Linux上的`clock_gettime(CLOCK_REALTIME)`[^v]和Java中的`System.currentTimeMillis()`返回自epoch(UTC时间1970年1月1日午夜)以来的秒数(或毫秒),根据公历(Gregorian)日历,不包括闰秒。有些系统使用其他日期作为参考点。 -[^v]: 虽然时钟被称为实时时钟,但它与实时操作系统无关,如第298页上的“[响应时间保证](#响应时间保证)”中所述。 +[^v]: 虽然该时钟被称为实时时钟,但它与实时操作系统无关,如“[响应时间保证](#响应时间保证)”中所述。 -​ 时钟通常与NTP同步,这意味着来自一台机器的时间戳(理想情况下)与另一台机器上的时间戳相同。但是如下节所述,时钟也具有各种各样的奇特之处。特别是,如果本地时钟在NTP服务器之前太远,则它可能会被强制重置,看上去好像跳回了先前的时间点。这些跳跃以及他们经常忽略闰秒的事实,使时钟不能用于测量经过时间【38】。 +​ 日历时钟通常与NTP同步,这意味着来自一台机器的时间戳(理想情况下)与另一台机器上的时间戳相同。但是如下节所述,日历时钟也具有各种各样的奇特之处。特别是,如果本地时钟在NTP服务器之前太远,则它可能会被强制重置,看上去好像跳回了先前的时间点。这些跳跃以及他们经常忽略闰秒的事实,使日历时钟不能用于测量经过时间(elapsed time)【38】。 -​ 时钟还具有相当粗略的分辨率,例如,在较早的Windows系统上以10毫秒为单位前进【39】。在最近的系统中这已经不是一个问题了。 +​ 历史上的日历时钟还具有相当粗略的分辨率,例如,在较早的Windows系统上以10毫秒为单位前进【39】。在最近的系统中这已经不是一个问题了。 #### 单调钟 -​ 单调钟适用于测量持续时间(时间间隔),例如超时或服务的响应时间:Linux上的`clock_gettime(CLOCK_MONOTONIC)`,和Java中的`System.nanoTime()`都是单调时钟。这个名字来源于他们保证总是前进的事实(而时钟可以及时跳回)。 +​ 单调钟适用于测量持续时间(时间间隔),例如超时或服务的响应时间:Linux上的`clock_gettime(CLOCK_MONOTONIC)`,和Java中的`System.nanoTime()`都是单调时钟。这个名字来源于他们保证总是往前走的事实(而日历时钟可以往回跳)。 ​ 你可以在某个时间点检查单调钟的值,做一些事情,且稍后再次检查它。这两个值之间的差异告诉你两次检查之间经过了多长时间。但单调钟的绝对值是毫无意义的:它可能是计算机启动以来的纳秒数,或类似的任意值。特别是比较来自两台不同计算机的单调钟的值是没有意义的,因为它们并不是一回事。 @@ -289,25 +285,24 @@ ### 时钟同步与准确性 -​ 单调钟不需要同步,但是时钟需要根据NTP服务器或其他外部时间源来设置才能有用。不幸的是,我们获取时钟的方法并不像你所希望的那样可靠或准确——硬件时钟和NTP可能会变幻莫测。举几个例子: - -​ 计算机中的石英钟不够精确:它会**漂移(drifts)**(运行速度快于或慢于预期)。时钟漂移取决于机器的温度。 Google假设其服务器时钟漂移为200 ppm(百万分之一)【41】,相当于每30秒与服务器重新同步一次的时钟漂移为6毫秒,或者每天重新同步的时钟漂移为17秒。即使一切工作正常,此漂移也会限制可以达到的最佳准确度。 +​ 单调钟不需要同步,但是日历时钟需要根据NTP服务器或其他外部时间源来设置才能有用。不幸的是,我们获取时钟的方法并不像你所希望的那样可靠或准确——硬件时钟和NTP可能会变幻莫测。举几个例子: +* 计算机中的石英钟不够精确:它会**漂移(drifts)**(运行速度快于或慢于预期)。时钟漂移取决于机器的温度。 Google假设其服务器时钟漂移为200 ppm(百万分之一)【41】,相当于每30秒与服务器重新同步一次的时钟漂移为6毫秒,或者每天重新同步的时钟漂移为17秒。即使一切工作正常,此漂移也会限制可以达到的最佳准确度。 * 如果计算机的时钟与NTP服务器的时钟差别太大,可能会拒绝同步,或者本地时钟将被强制重置【37】。任何观察重置前后时间的应用程序都可能会看到时间倒退或突然跳跃。 -* 如果某个节点被NTP服务器意外阻塞,可能会在一段时间内忽略错误配置。有证据表明,这在实践中确实发生过。 +* 如果某个节点被NTP服务器的防火墙意外阻塞,有可能会持续一段时间都没有人会注意到。有证据表明,这在实践中确实发生过。 * NTP同步只能和网络延迟一样好,所以当您在拥有可变数据包延迟的拥塞网络上时,NTP同步的准确性会受到限制。一个实验表明,当通过互联网同步时,35毫秒的最小误差是可以实现的,尽管偶尔的网络延迟峰值会导致大约一秒的误差。根据配置,较大的网络延迟会导致NTP客户端完全放弃。 -* 一些NTP服务器错误或配置错误,报告时间已经过去了几个小时【43,44】。 NTP客户端非常强大,因为他们查询多个服务器并忽略异常值。尽管如此,在互联网上陌生人告诉你的时候,你的系统的正确性还是值得担忧的。 -* 闰秒导致59分钟或61秒长的分钟,这混淆了未设计闰秒的系统中的时序假设【45】。闰秒已经使许多大型系统崩溃【38,46】的事实说明了,关于时钟的假设是多么容易偷偷溜入系统中。处理闰秒的最佳方法可能是通过在一天中逐渐执行闰秒调整(这被称为**拖尾(smearing)**)【47,48】,使NTP服务器“撒谎”,虽然实际的NTP服务器表现各异【49】。 +* 一些NTP服务器是错误的或者配置错误的,报告的时间可能相差几个小时【43,44】。还好NTP客户端非常健壮,因为他们会查询多个服务器并忽略异常值。无论如何,依赖于互联网上的陌生人所告诉你的时间来保证你的系统的正确性,这还挺让人担忧的。 +* 闰秒导致一分钟可能有59秒或61秒,这会打破一些在设计之时未考虑闰秒的系统的时序假设【45】。闰秒已经使许多大型系统崩溃的事实【38,46】说明了,关于时钟的错误假设是多么容易偷偷溜入系统中。处理闰秒的最佳方法可能是让NTP服务器“撒谎”,并在一天中逐渐执行闰秒调整(这被称为**拖尾(smearing)**)【47,48】,虽然实际的NTP服务器表现各异【49】。 * 在虚拟机中,硬件时钟被虚拟化,这对于需要精确计时的应用程序提出了额外的挑战【50】。当一个CPU核心在虚拟机之间共享时,每个虚拟机都会暂停几十毫秒,与此同时另一个虚拟机正在运行。从应用程序的角度来看,这种停顿表现为时钟突然向前跳跃【26】。 -* 如果您在未完全控制的设备上运行软件(例如,移动设备或嵌入式设备),则可能完全不能信任该设备的硬件时钟。一些用户故意将其硬件时钟设置为不正确的日期和时间,例如,为了规避游戏中的时间限制,时钟可能会被设置到很远的过去或将来。 +* 如果您在没有完整控制权的设备(例如,移动设备或嵌入式设备)上运行软件,则可能完全不能信任该设备的硬件时钟。一些用户故意将其硬件时钟设置为不正确的日期和时间,例如,为了规避游戏中的时间限制,时钟可能会被设置到很远的过去或将来。 -如果你足够在乎这件事并投入大量资源,就可以达到非常好的时钟精度。例如,针对金融机构的欧洲法规草案MiFID II要求所有高频率交易基金在UTC时间100微秒内同步时钟,以便调试“闪崩”等市场异常现象,并帮助检测市场操纵 【51】。 +如果你足够在乎这件事并投入大量资源,就可以达到非常好的时钟精度。例如,针对金融机构的欧洲法规草案MiFID II要求所有高频率交易基金在UTC时间100微秒内同步时钟,以便调试“闪崩”等市场异常现象,并帮助检测市场操纵【51】。 ​ 通过GPS接收机,精确时间协议(PTP)【52】以及仔细的部署和监测可以实现这种精确度。然而,这需要很多努力和专业知识,而且有很多东西都会导致时钟同步错误。如果你的NTP守护进程配置错误,或者防火墙阻止了NTP通信,由漂移引起的时钟误差可能很快就会变大。 ### 依赖同步时钟 -​ 时钟的问题在于,虽然它们看起来简单易用,但却具有令人惊讶的缺陷:一天可能不会有精确的86,400秒,**时钟**可能会前后跳跃,而一个节点上的时间可能与另一个节点上的时间完全不同。 +​ 时钟的问题在于,虽然它们看起来简单易用,但却具有令人惊讶的缺陷:一天可能不会有精确的86,400秒,**日历时钟**可能会前后跳跃,而一个节点上的时间可能与另一个节点上的时间完全不同。 ​ 本章早些时候,我们讨论了网络丢包和任意延迟包的问题。尽管网络在大多数情况下表现良好,但软件的设计必须假定网络偶尔会出现故障,而软件必须正常处理这些故障。时钟也是如此:尽管大多数时间都工作得很好,但需要准备健壮的软件来处理不正确的时钟。 @@ -325,21 +320,21 @@ **图8-3 客户端B的写入比客户端A的写入要晚,但是B的写入具有较早的时间戳。** -​ 在[图8-3](img/fig8-3.png)中,当一个写入被复制到其他节点时,它会根据发生写入的节点上的时钟时钟标记一个时间戳。在这个例子中,时钟同步是非常好的:节点1和节点3之间的偏差小于3ms,这可能比你在实践中预期的更好。 +​ 在[图8-3](img/fig8-3.png)中,当一个写入被复制到其他节点时,它会根据发生写入的节点上的日历时钟标记一个时间戳。在这个例子中,时钟同步是非常好的:节点1和节点3之间的偏差小于3ms,这可能比你在实践中能预期的更好。 ​ 尽管如此,[图8-3](img/fig8-3.png)中的时间戳却无法正确排列事件:写入`x = 1`的时间戳为42.004秒,但写入`x = 2`的时间戳为42.003秒,即使`x = 2`在稍后出现。当节点2接收到这两个事件时,会错误地推断出`x = 1`是最近的值,而丢弃写入`x = 2`。效果上表现为,客户端B的增量操作会丢失。 -​ 这种冲突解决策略被称为**最后写入胜利(LWW)**,它在多领导者复制和无领导者数据库(如Cassandra 【53】和Riak 【54】)中被广泛使用(参见“[最后写入胜利(丢弃并发写入)](#最后写入胜利(丢弃并发写入))”一节)。有些实现会在客户端而不是服务器上生成时间戳,但这并不能改变LWW的基本问题: +​ 这种冲突解决策略被称为**最后写入胜利(LWW)**,它在多领导者复制和无领导者数据库(如Cassandra 【53】和Riak 【54】)中被广泛使用(参见“[最后写入胜利(丢弃并发写入)](ch5.md#最后写入胜利(丢弃并发写入))”一节)。有些实现会在客户端而不是服务器上生成时间戳,但这并不能改变LWW的基本问题: * 数据库写入可能会神秘地消失:具有滞后时钟的节点无法覆盖之前具有快速时钟的节点写入的值,直到节点之间的时钟偏差消逝【54,55】。此方案可能导致一定数量的数据被悄悄丢弃,而未向应用报告任何错误。 -* LWW无法区分**高频顺序写入**(在[图8-3](img/fig8-3.png)中,客户端B的增量操作**一定**发生在客户端A的写入之后)和**真正并发写入**(写入者意识不到其他写入者)。需要额外的因果关系跟踪机制(例如版本向量),以防止因果关系的冲突(请参阅“[检测并发写入](ch5.md#检测并发写入)”)。 +* LWW无法区分**高频顺序写入**(在[图8-3](img/fig8-3.png)中,客户端B的增量操作**一定**发生在客户端A的写入之后)和**真正并发写入**(写入者意识不到其他写入者)。需要额外的因果关系跟踪机制(例如版本向量),以防止违背因果关系(请参阅“[检测并发写入](ch5.md#检测并发写入)”)。 * 两个节点很可能独立地生成具有相同时间戳的写入,特别是在时钟仅具有毫秒分辨率的情况下。为了解决这样的冲突,还需要一个额外的**决胜值(tiebreaker)**(可以简单地是一个大随机数),但这种方法也可能会导致违背因果关系【53】。 -因此,尽管通过保留最“最近”的值并放弃其他值来解决冲突是很诱惑人的,但是要注意,“最近”的定义取决于本地的**时钟**,这很可能是不正确的。即使用频繁同步的NTP时钟,一个数据包也可能在时间戳100毫秒(根据发送者的时钟)时发送,并在时间戳99毫秒(根据接收者的时钟)处到达——看起来好像数据包在发送之前已经到达,这是不可能的。 +因此,尽管通过保留最“最近”的值并放弃其他值来解决冲突是很诱惑人的,但是要注意,“最近”的定义取决于本地的**日历时钟**,这很可能是不正确的。即使用严格同步的NTP时钟,一个数据包也可能在时间戳100毫秒(根据发送者的时钟)时发送,并在时间戳99毫秒(根据接收者的时钟)处到达——看起来好像数据包在发送之前已经到达,这是不可能的。 -​ NTP同步是否能足够准确,以至于这种不正确的排序不会发生?也许不能,因为NTP的同步精度本身受到网络往返时间的限制,除了石英钟漂移这类误差源之外。为了进行正确的排序,你需要一个比测量对象(即网络延迟)要精确得多的时钟。 +​ NTP同步是否能足够准确,以至于这种不正确的排序不会发生?也许不能,因为NTP的同步精度本身,除了石英钟漂移这类误差源之外,还受到网络往返时间的限制。为了进行正确的排序,你需要一个比测量对象(即网络延迟)要精确得多的时钟。 -​ 所谓的**逻辑时钟(logic clock)**【56,57】是基于递增计数器而不是振荡石英晶体,对于排序事件来说是更安全的选择(请参见“[检测并发写入](ch5.md#检测并发写入)”)。逻辑时钟不测量一天中的时间或经过的秒数,而仅测量事件的相对顺序(无论一个事件发生在另一个事件之前还是之后)。相反,用来测量实际经过时间的**时钟**和**单调钟**也被称为**物理时钟(physical clock)**。我们将在“[顺序保证](#顺序保证)”中来看顺序问题。 +​ 所谓的**逻辑时钟(logic clock)**【56,57】是基于递增计数器而不是振荡石英晶体,对于排序事件来说是更安全的选择(请参见“[检测并发写入](ch5.md#检测并发写入)”)。逻辑时钟不测量一天中的时间或经过的秒数,而仅测量事件的相对顺序(无论一个事件发生在另一个事件之前还是之后)。相反,用来测量实际经过时间的**日历时钟**和**单调钟**也被称为**物理时钟(physical clock)**。我们将在“[顺序保证](ch9.md#顺序保证)”中来看顺序问题。 #### 时钟读数存在置信区间 @@ -355,23 +350,23 @@ #### 全局快照的同步时钟 -​ 在“[快照隔离和可重复读取](ch7.md#快照隔离和可重复读取)”中,我们讨论了快照隔离,这是数据库中非常有用的功能,需要支持小型快速读写事务和大型长时间运行的只读事务,用于备份或分析)。它允许只读事务看到特定时间点的处于一致状态的数据库,且不会锁定和干扰读写事务。 +​ 在“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”中,我们讨论了快照隔离,这是数据库中非常有用的功能,需要支持小型快速读写事务和大型长时间运行的只读事务(用于备份或分析)。它允许只读事务看到特定时间点的处于一致状态的数据库,且不会锁定和干扰读写事务。 ​ 快照隔离最常见的实现需要单调递增的事务ID。如果写入比快照晚(即,写入具有比快照更大的事务ID),则该写入对于快照事务是不可见的。在单节点数据库上,一个简单的计数器就足以生成事务ID。 -​ 但是当数据库分布在许多机器上,也许可能在多个数据中心中时,由于需要协调,(跨所有分区)全局单调递增的事务ID会很难生成。事务ID必须反映因果关系:如果事务B读取由事务A写入的值,则B必须具有比A更大的事务ID,否则快照就无法保持一致。在有大量的小规模、高频率的事务情景下,在分布式系统中创建事务ID成为一个难以防守的瓶颈[^vi]。 +​ 但是当数据库分布在许多机器上,也许可能在多个数据中心中时,由于需要协调,(跨所有分区)全局单调递增的事务ID会很难生成。事务ID必须反映因果关系:如果事务B读取由事务A写入的值,则B必须具有比A更大的事务ID,否则快照就无法保持一致。在有大量的小规模、高频率的事务情景下,在分布式系统中创建事务ID成为一个难以处理的瓶颈[^vi]。 -[^vi]: 存在分布式序列号生成器,例如Twitter的雪花(Snowflake),其以可伸缩的方式(例如,通过将ID空间的块分配给不同节点)近似单调地增加唯一ID。但是,它们通常无法保证与因果关系一致的排序,因为分配的ID块的时间范围比数据库读取和写入的时间范围要长。另请参阅“[顺序保证](#顺序保证)”。 +[^vi]: 存在分布式序列号生成器,例如Twitter的雪花(Snowflake),其以可伸缩的方式(例如,通过将ID空间的块分配给不同节点)近似单调地增加唯一ID。但是,它们通常无法保证与因果关系一致的排序,因为分配的ID块的时间范围比数据库读取和写入的时间范围要长。另请参阅“[顺序保证](ch9.md#顺序保证)”。 ​ 我们可以使用同步时钟的时间戳作为事务ID吗?如果我们能够获得足够好的同步性,那么这种方法将具有很合适的属性:更晚的事务会有更大的时间戳。当然,问题在于时钟精度的不确定性。 ​ Spanner以这种方式实现跨数据中心的快照隔离【59,60】。它使用TrueTime API报告的时钟置信区间,并基于以下观察结果:如果您有两个置信区间,每个置信区间包含最早和最晚可能的时间戳( $A = [A_{earliest}, A_{latest}]$, $B=[B_{earliest}, B_{latest}]$),这两个区间不重叠(即:$A_{earliest} < A_{latest} < B_{earliest} < B_{latest}$)的话,那么B肯定发生在A之后——这是毫无疑问的。只有当区间重叠时,我们才不确定A和B发生的顺序。 -​ 为了确保事务时间戳反映因果关系,在提交读写事务之前,Spanner在提交读写事务时,会故意等待置信区间长度的时间。通过这样,它可以确保任何可能读取数据的事务处于足够晚的时间,因此它们的置信区间不会重叠。为了保持尽可能短的等待时间,Spanner需要保持尽可能小的时钟不确定性,为此,Google在每个数据中心都部署了一个GPS接收器或原子钟,这允许时钟在大约7毫秒内同步【41】。 +​ 为了确保事务时间戳反映因果关系,在提交读写事务之前,Spanner在提交读写事务时,会故意等待置信区间长度的时间。通过这样,它可以确保任何可能读取数据的事务处于足够晚的时间,因此它们的置信区间不会重叠。为了保持尽可能短的等待时间,Spanner需要保持尽可能小的时钟不确定性,为此,Google在每个数据中心都部署了一个GPS接收器或原子钟,这允许时钟同步到大约7毫秒以内【41】。 ​ 对分布式事务语义使用时钟同步是一个活跃的研究领域【57,61,62】。这些想法很有趣,但是它们还没有在谷歌之外的主流数据库中实现。 -### 暂停进程 +### 进程暂停 ​ 让我们考虑在分布式系统中使用危险时钟的另一个例子。假设你有一个数据库,每个分区只有一个领导者。只有领导被允许接受写入。一个节点如何知道它仍然是领导者(它并没有被别人宣告为死亡),并且它可以安全地接受写入? @@ -382,33 +377,33 @@ ​ 可以想象,请求处理循环看起来像这样: ```java -while(true){ - request=getIncomingRequest(); +while (true) { + request = getIncomingRequest(); // 确保租约还剩下至少10秒 - if (lease.expiryTimeMillis-System.currentTimeMillis() < 10000){ + if (lease.expiryTimeMillis - System.currentTimeMillis() < 10000){ lease = lease.renew(); } - if(lease.isValid()){ + if (lease.isValid()) { process(request); } } ``` -​  这个代码有什么问题?首先,它依赖于同步时钟:租约到期时间由另一台机器设置(例如,当前时间加上30秒,计算到期时间),并将其与本地系统时钟进行比较。如果时钟超过几秒不同步,这段代码将开始做奇怪的事情。 +​  这个代码有什么问题?首先,它依赖于同步时钟:租约到期时间由另一台机器设置(例如,当前时间加上30秒,计算到期时间),并将其与本地系统时钟进行比较。如果时钟不同步超过几秒,这段代码将开始做奇怪的事情。 ​ 其次,即使我们将协议更改为仅使用本地单调时钟,也存在另一个问题:代码假定在执行剩余时间检查`System.currentTimeMillis()`和实际执行请求`process(request)`中间的时间间隔非常短。通常情况下,这段代码运行得非常快,所以10秒的缓冲区已经足够确保**租约**在请求处理到一半时不会过期。 -​ 但是,如果程序执行中出现了意外的停顿呢?例如,想象一下,线程在`lease.isValid()`行周围停止15秒,然后才终止。在这种情况下,在请求被处理的时候,租约可能已经过期,而另一个节点已经接管了领导。然而,没有什么可以告诉这个线程已经暂停了这么长时间了,所以这段代码不会注意到租约已经到期了,直到循环的下一个迭代 ——到那个时候它可能已经做了一些不安全的处理请求。 +​ 但是,如果程序执行中出现了意外的停顿呢?例如,想象一下,线程在`lease.isValid()`行周围停止15秒,然后才继续。在这种情况下,在请求被处理的时候,租约可能已经过期,而另一个节点已经接管了领导。然而,没有什么可以告诉这个线程已经暂停了这么长时间了,所以这段代码不会注意到租约已经到期了,直到循环的下一个迭代 ——到那个时候它可能已经做了一些不安全的处理请求。 ​ 假设一个线程可能会暂停很长时间,这是疯了吗?不幸的是,这种情况发生的原因有很多种: * 许多编程语言运行时(如Java虚拟机)都有一个垃圾收集器(GC),偶尔需要停止所有正在运行的线程。这些“**停止所有处理(stop-the-world)**”GC暂停有时会持续几分钟【64】!甚至像HotSpot JVM的CMS这样的所谓的“并行”垃圾收集器也不能完全与应用程序代码并行运行,它需要不时地停止所有处理【65】。尽管通常可以通过改变分配模式或调整GC设置来减少暂停【66】,但是如果我们想要提供健壮的保证,就必须假设最坏的情况发生。 * 在虚拟化环境中,可以**挂起(suspend)**虚拟机(暂停执行所有进程并将内存内容保存到磁盘)并恢复(恢复内存内容并继续执行)。这个暂停可以在进程执行的任何时候发生,并且可以持续任意长的时间。这个功能有时用于虚拟机从一个主机到另一个主机的实时迁移,而不需要重新启动,在这种情况下,暂停的长度取决于进程写入内存的速率【67】。 * 在最终用户的设备(如笔记本电脑)上,执行也可能被暂停并随意恢复,例如当用户关闭笔记本电脑的盖子时。 -* 当操作系统上下文切换到另一个线程时,或者当管理程序切换到另一个虚拟机时(在虚拟机中运行时),当前正在运行的线程可以在代码中的任意点处暂停。在虚拟机的情况下,在其他虚拟机中花费的CPU时间被称为**窃取时间(steal time)**。如果机器处于沉重的负载下(即,如果等待运行的线程很长),暂停的线程再次运行可能需要一些时间。 +* 当操作系统上下文切换到另一个线程时,或者当管理程序切换到另一个虚拟机时(在虚拟机中运行时),当前正在运行的线程可能在代码中的任意点处暂停。在虚拟机的情况下,在其他虚拟机中花费的CPU时间被称为**窃取时间(steal time)**。如果机器处于沉重的负载下(即,如果等待运行的线程队列很长),暂停的线程再次运行可能需要一些时间。 * 如果应用程序执行同步磁盘访问,则线程可能暂停,等待缓慢的磁盘I/O操作完成【68】。在许多语言中,即使代码没有包含文件访问,磁盘访问也可能出乎意料地发生——例如,Java类加载器在第一次使用时惰性加载类文件,这可能在程序执行过程中随时发生。 I/O暂停和GC暂停甚至可能合谋组合它们的延迟【69】。如果磁盘实际上是一个网络文件系统或网络块设备(如亚马逊的EBS),I/O延迟进一步受到网络延迟变化的影响【29】。 -* 如果操作系统配置为允许交换到磁盘(分页),则简单的内存访问可能导致**页面错误(page fault)**,要求将磁盘中的页面装入内存。当这个缓慢的I/O操作发生时,线程暂停。如果内存压力很高,则可能需要将不同的页面换出到磁盘。在极端情况下,操作系统可能花费大部分时间将页面交换到内存中,而实际上完成的工作很少(这被称为**抖动(thrashing)**)。为了避免这个问题,通常在服务器机器上禁用页面调度(如果你宁愿干掉一个进程来释放内存,也不愿意冒抖动风险)。 +* 如果操作系统配置为允许交换到磁盘(页面交换),则简单的内存访问可能导致**页面错误(page fault)**,要求将磁盘中的页面装入内存。当这个缓慢的I/O操作发生时,线程暂停。如果内存压力很高,则可能需要将另一个页面换出到磁盘。在极端情况下,操作系统可能花费大部分时间将页面交换到内存中,而实际上完成的工作很少(这被称为**抖动(thrashing)**)。为了避免这个问题,通常在服务器机器上禁用页面调度(如果你宁愿干掉一个进程来释放内存,也不愿意冒抖动风险)。 * 可以通过发送SIGSTOP信号来暂停Unix进程,例如通过在shell中按下Ctrl-Z。 这个信号立即阻止进程继续执行更多的CPU周期,直到SIGCONT恢复为止,此时它将继续运行。 即使你的环境通常不使用SIGSTOP,也可能由运维工程师意外发送。 所有这些事件都可以随时**抢占(preempt)**正在运行的线程,并在稍后的时间恢复运行,而线程甚至不会注意到这一点。这个问题类似于在单个机器上使多线程代码线程安全:你不能对时机做任何假设,因为随时可能发生上下文切换,或者出现并行运行。 @@ -421,27 +416,27 @@ while(true){ ​ 在许多编程语言和操作系统中,线程和进程可能暂停一段无限制的时间,正如讨论的那样。如果你足够努力,导致暂停的原因是**可以**消除的。 -​ 某些软件的运行环境要求很高,不能在特定时间内响应可能会导致严重的损失:飞机主控计算机,火箭,机器人,汽车和其他物体的计算机必须对其传感器输入做出快速而可预测的响应。在这些系统中,软件必须有一个特定的**截止时间(deadline)**,如果截止时间不满足,可能会导致整个系统的故障。这就是所谓的**硬实时(hard real-time)**系统。 +​ 某些软件的运行环境要求很高,不能在特定时间内响应可能会导致严重的损失:控制飞机、火箭、机器人、汽车和其他物体的计算机必须对其传感器输入做出快速而可预测的响应。在这些系统中,软件必须有一个特定的**截止时间(deadline)**,如果截止时间不满足,可能会导致整个系统的故障。这就是所谓的**硬实时(hard real-time)**系统。 > #### 实时是真的吗? > -> 在嵌入式系统中,实时是指系统经过精心设计和测试,以满足所有情况下的特定时间保证。这个含义与Web上实时术语的模糊使用相反,它描述了服务器将数据推送到客户端以及流处理,而没有严格的响应时间限制(见[第11章](ch11.md))。 +> 在嵌入式系统中,实时是指系统经过精心设计和测试,以满足所有情况下的特定时间保证。这个含义与Web上对实时术语的模糊使用相反,后者描述了服务器将数据推送到客户端以及没有严格的响应时间限制的流处理(见[第11章](ch11.md))。 例如,如果车载传感器检测到当前正在经历碰撞,你肯定不希望安全气囊释放系统因为GC暂停而延迟弹出。 -​ 在系统中提供**实时保证**需要各级软件栈的支持:一个实时操作系统(RTOS),允许在指定的时间间隔内保证CPU时间的分配。库函数必须记录最坏情况下的执行时间;动态内存分配可能受到限制或完全不允许(实时垃圾收集器存在,但是应用程序仍然必须确保它不会给GC太多的负担);必须进行大量的测试和测量,以确保达到保证。 +​ 在系统中提供**实时保证**需要各级软件栈的支持:一个实时操作系统(RTOS),允许在指定的时间间隔内保证CPU时间的分配。库函数必须申明最坏情况下的执行时间;动态内存分配可能受到限制或完全不允许(实时垃圾收集器存在,但是应用程序仍然必须确保它不会给GC太多的负担);必须进行大量的测试和测量,以确保达到保证。 -​ 所有这些都需要大量额外的工作,严重限制了可以使用的编程语言,库和工具的范围(因为大多数语言和工具不提供实时保证)。由于这些原因,开发实时系统非常昂贵,并且它们通常用于安全关键的嵌入式设备。而且,“**实时**”与“**高性能**”不一样——事实上,实时系统可能具有较低的吞吐量,因为他们必须优先考虑及时响应高于一切(另请参见“[延迟和资源利用](#延迟和资源利用)“)。 +​ 所有这些都需要大量额外的工作,严重限制了可以使用的编程语言、库和工具的范围(因为大多数语言和工具不提供实时保证)。由于这些原因,开发实时系统非常昂贵,并且它们通常用于安全关键的嵌入式设备。而且,“**实时**”与“**高性能**”不一样——事实上,实时系统可能具有较低的吞吐量,因为他们必须让及时响应的优先级高于一切(另请参见“[延迟和资源利用](#延迟和资源利用)“)。 ​ 对于大多数服务器端数据处理系统来说,实时保证是不经济或不合适的。因此,这些系统必须承受在非实时环境中运行的暂停和时钟不稳定性。 #### 限制垃圾收集的影响 -​ 过程暂停的负面影响可以在不诉诸昂贵的实时调度保证的情况下得到缓解。语言运行时在计划垃圾回收时具有一定的灵活性,因为它们可以跟踪对象分配的速度和随着时间的推移剩余的空闲内存。 +​ 进程暂停的负面影响可以在不诉诸昂贵的实时调度保证的情况下得到缓解。语言运行时在计划垃圾回收时具有一定的灵活性,因为它们可以跟踪对象分配的速度和随着时间的推移剩余的空闲内存。 -​ 一个新兴的想法是将GC暂停视为一个节点的短暂计划中断,并让其他节点处理来自客户端的请求,同时一个节点正在收集其垃圾。如果运行时可以警告应用程序一个节点很快需要GC暂停,那么应用程序可以停止向该节点发送新的请求,等待它完成处理未完成的请求,然后在没有请求正在进行时执行GC。这个技巧隐藏了来自客户端的GC暂停,并降低了响应时间的高百分比【70,71】。一些对延迟敏感的金融交易系统【72】使用这种方法。 +​ 一个新兴的想法是将GC暂停视为一个节点的短暂计划中断,并在这个节点收集其垃圾的同时,让其他节点处理来自客户端的请求。如果运行时可以警告应用程序一个节点很快需要GC暂停,那么应用程序可以停止向该节点发送新的请求,等待它完成处理未完成的请求,然后在没有请求正在进行时执行GC。这个技巧向客户端隐藏了GC暂停,并降低了响应时间的高百分比【70,71】。一些对延迟敏感的金融交易系统【72】使用这种方法。 -​ 这个想法的一个变种是只用垃圾收集器来处理短命对象(这些对象要快速收集),并定期在积累大量长寿对象(因此需要完整GC)之前重新启动进程【65,73】。一次可以重新启动一个节点,在计划重新启动之前,流量可以从节点移开,就像[滚动升级](ch4.md#滚动升级)一样。 +​ 这个想法的一个变种是只用垃圾收集器来处理短命对象(这些对象可以快速收集),并定期在积累大量长寿对象(因此需要完整GC)之前重新启动进程【65,73】。一次可以重新启动一个节点,在计划重新启动之前,流量可以从该节点移开,就像[第4章](ch4.md)里描述的滚动升级一样。 ​ 这些措施不能完全阻止垃圾回收暂停,但可以有效地减少它们对应用的影响。 @@ -457,9 +452,9 @@ while(true){ ​ 幸运的是,我们不需要去搞清楚生命的意义。在分布式系统中,我们可以陈述关于行为(系统模型)的假设,并以满足这些假设的方式设计实际系统。算法可以被证明在某个系统模型中正确运行。这意味着即使底层系统模型提供了很少的保证,也可以实现可靠的行为。 -​ 但是,尽管可以使软件在不可靠的系统模型中表现良好,但这并不是可以直截了当实现的。在本章的其余部分中,我们将进一步探讨分布式系统中的知识和真理的概念,这将有助于我们思考我们可以做出的各种假设以及我们可能希望提供的保证。在[第9章](ch9.md)中,我们将着眼于分布式系统的一些例子,这些算法在特定的假设条件下提供了特定的保证。 +​ 但是,尽管可以使软件在不可靠的系统模型中表现良好,但这并不是可以直截了当实现的。在本章的其余部分中,我们将进一步探讨分布式系统中的知识和真相的概念,这将有助于我们思考我们可以做出的各种假设以及我们可能希望提供的保证。在[第9章](ch9.md)中,我们将着眼于分布式系统的一些例子,这些算法在特定的假设条件下提供了特定的保证。 -### 真理由多数所定义 +### 真相由多数所定义 ​ 设想一个具有不对称故障的网络:一个节点能够接收发送给它的所有消息,但是来自该节点的任何传出消息被丢弃或延迟【19】。即使该节点运行良好,并且正在接收来自其他节点的请求,其他节点也无法听到其响应。经过一段时间后,其他节点宣布它已经死亡,因为他们没有听到节点的消息。这种情况就像梦魇一样:**半断开(semi-disconnected)**的节点被拖向墓地,敲打尖叫道“我没死!” ——但是由于没有人能听到它的尖叫,葬礼队伍继续以坚忍的决心继续行进。 @@ -491,7 +486,7 @@ while(true){ **图8-4 分布式锁的实现不正确:客户端1认为它仍然具有有效的租约,即使它已经过期,从而破坏了存储中的文件** -​ 这个问题就是我们先前在“[进程暂停](#进程暂停)”中讨论过的一个例子:如果持有租约的客户端暂停太久,它的租约将到期。另一个客户端可以获得同一文件的租约,并开始写入文件。当暂停的客户端回来时,它认为(不正确)它仍然有一个有效的租约,并继续写入文件。结果,客户的写入冲突和损坏的文件。 +​ 这个问题就是我们先前在“[进程暂停](#进程暂停)”中讨论过的一个例子:如果持有租约的客户端暂停太久,它的租约将到期。另一个客户端可以获得同一文件的租约,并开始写入文件。当暂停的客户端回来时,它认为(不正确)它仍然有一个有效的租约,并继续写入文件。结果,客户的写入将产生冲突并损坏文件。 #### 防护令牌 @@ -521,55 +516,55 @@ while(true){ > ### 拜占庭将军问题 > -> ​ 拜占庭将军问题是所谓“两将军问题”的概括【78】,它想象两个将军需要就战斗计划达成一致的情况。由于他们在两个不同的地点建立了营地,他们只能通过信使进行沟通,信使有时会被延迟或丢失(就像网络中的信息包一样)。我们将在[第9章](ch9.md)讨论这个共识问题。 +> ​ 拜占庭将军问题是对所谓“两将军问题”的泛化【78】,它想象两个将军需要就战斗计划达成一致的情况。由于他们在两个不同的地点建立了营地,他们只能通过信使进行沟通,信使有时会被延迟或丢失(就像网络中的信息包一样)。我们将在[第9章](ch9.md)讨论这个共识问题。 > -> ​ 在这个拜占庭式的问题中,有n位将军需要同意,他们的努力因为有一些叛徒在他们中间而受到阻碍。大多数的将军都是忠诚的,因而发出了真实的信息,但是叛徒可能会试图通过发送虚假或不真实的信息来欺骗和混淆他人(在试图保持未被发现的同时)。事先并不知道叛徒是谁。 +> ​ 在这个问题的拜占庭版本里,有n位将军需要同意,他们的努力因为有一些叛徒在他们中间而受到阻碍。大多数的将军都是忠诚的,因而发出了真实的信息,但是叛徒可能会试图通过发送虚假或不真实的信息来欺骗和混淆他人(在试图保持未被发现的同时)。事先并不知道叛徒是谁。 > > ​ 拜占庭是后来成为君士坦丁堡的古希腊城市,现在在土耳其的伊斯坦布尔。没有任何历史证据表明拜占庭将军比其他地方更容易出现阴谋和阴谋。相反,这个名字来源于拜占庭式的过度复杂,官僚,迂回等意义,早在计算机之前就已经在政治中被使用了【79】。Lamport想要选一个不会冒犯任何读者的国家,他被告知将其称为阿尔巴尼亚将军问题并不是一个好主意【80】。 ​ 当一个系统在部分节点发生故障、不遵守协议、甚至恶意攻击、扰乱网络时仍然能继续正确工作,称之为**拜占庭容错(Byzantine fault-tolerant)**的,在特定场景下,这种担忧在是有意义的: * 在航空航天环境中,计算机内存或CPU寄存器中的数据可能被辐射破坏,导致其以任意不可预知的方式响应其他节点。由于系统故障非常昂贵(例如,飞机撞毁和炸死船上所有人员,或火箭与国际空间站相撞),飞行控制系统必须容忍拜占庭故障【81,82】。 -* 在多个参与组织的系统中,一些参与者可能会试图欺骗或欺骗他人。在这种情况下,节点仅仅信任另一个节点的消息是不安全的,因为它们可能是出于恶意的目的而被发送的。例如,像比特币和其他区块链一样的对等网络可以被认为是让互不信任的各方同意交易是否发生的一种方式,而不依赖于中央当局【83】。 +* 在多个参与组织的系统中,一些参与者可能会试图欺骗或欺骗他人。在这种情况下,节点仅仅信任另一个节点的消息是不安全的,因为它们可能是出于恶意的目的而被发送的。例如,像比特币和其他区块链一样的对等网络可以被认为是让互不信任的各方同意交易是否发生的一种方式,而不依赖于中心机构(central authority)【83】。 然而,在本书讨论的那些系统中,我们通常可以安全地假设没有拜占庭式的错误。在你的数据中心里,所有的节点都是由你的组织控制的(所以他们可以信任),辐射水平足够低,内存损坏不是一个大问题。制作拜占庭容错系统的协议相当复杂【84】,而容错嵌入式系统依赖于硬件层面的支持【81】。在大多数服务器端数据系统中,部署拜占庭容错解决方案的成本使其变得不切实际。 -​ Web应用程序确实需要预期受终端用户控制的客户端(如Web浏览器)的任意和恶意行为。这就是为什么输入验证,数据清洗和输出转义如此重要:例如,防止SQL注入和跨站点脚本。但是,我们通常不使用拜占庭容错协议,而只是让服务器决定是否允许客户端行为。在没有这种中心授权的对等网络中,拜占庭容错更为重要。 +​ Web应用程序确实需要预期受终端用户控制的客户端(如Web浏览器)的任意和恶意行为。这就是为什么输入验证,数据清洗和输出转义如此重要:例如,防止SQL注入和跨站点脚本。然而,我们通常不在这里使用拜占庭容错协议,而只是让服务器有权决定是否允许客户端行为。但在没有这种中心机构的对等网络中,拜占庭容错更为重要。 -​ 软件中的一个错误可能被认为是拜占庭式的错误,但是如果您将相同的软件部署到所有节点上,那么拜占庭式的容错算法不能为您节省。大多数拜占庭式容错算法要求超过三分之二的节点能够正常工作(即,如果有四个节点,最多只能有一个故障)。要使用这种方法对付bug,你必须有四个独立的相同软件的实现,并希望一个bug只出现在四个实现之一中。 +​ 软件中的一个错误(bug)可能被认为是拜占庭式的错误,但是如果您将相同的软件部署到所有节点上,那么拜占庭式的容错算法帮不到你。大多数拜占庭式容错算法要求超过三分之二的节点能够正常工作(即,如果有四个节点,最多只能有一个故障)。要使用这种方法对付bug,你必须有四个独立的相同软件的实现,并希望一个bug只出现在四个实现之一中。 -​ 同样,如果一个协议可以保护我们免受漏洞,安全妥协和恶意攻击,那么这将是有吸引力的。不幸的是,这也是不现实的:在大多数系统中,如果攻击者可以渗透一个节点,那他们可能会渗透所有这些节点,因为它们可能运行相同的软件。因此传统机制(认证,访问控制,加密,防火墙等)仍然是抵御攻击者的主要保护措施。 +​ 同样,如果一个协议可以保护我们免受漏洞,安全渗透和恶意攻击,那么这将是有吸引力的。不幸的是,这也是不现实的:在大多数系统中,如果攻击者可以渗透一个节点,那他们可能会渗透所有这些节点,因为它们可能都运行着相同的软件。因此,传统机制(认证,访问控制,加密,防火墙等)仍然是抵御攻击者的主要保护措施。 #### 弱谎言形式 ​ 尽管我们假设节点通常是诚实的,但值得向软件中添加防止“撒谎”弱形式的机制——例如,由硬件问题导致的无效消息,软件错误和错误配置。这种保护机制并不是完全的拜占庭容错,因为它们不能抵挡决心坚定的对手,但它们仍然是简单而实用的步骤,以提高可靠性。例如: -* 由于硬件问题或操作系统,驱动程序,路由器等中的错误,网络数据包有时会受到损坏。通常,内建于TCP和UDP中的校验和会俘获损坏的数据包,但有时它们会逃避检测【85,86,87】 。简单的措施通常是采用充分的保护来防止这种破坏,例如应用程序级协议中的校验和。 -* 可公开访问的应用程序必须仔细清理来自用户的任何输入,例如检查值是否在合理的范围内,并限制字符串的大小以防止通过大内存分配拒绝服务。防火墙后面的内部服务可能能够在对输入进行较不严格的检查的情况下逃脱,但是一些基本的理智检查(例如,在协议解析中)是一个好主意。 -* NTP客户端可以配置多个服务器地址。同步时,客户端联系所有的服务器,估计它们的误差,并检查大多数服务器是否在对某个时间范围内达成一致。只要大多数的服务器没问题,一个配置错误的NTP服务器报告的时间会被当成特异值从同步中排除【37】。使用多个服务器使NTP更健壮(比起只用单个服务器来)。 +* 由于硬件问题或操作系统、驱动程序、路由器等中的错误,网络数据包有时会受到损坏。通常,损坏的数据包会被内建于TCP和UDP中的校验和所俘获,但有时它们也会逃脱检测【85,86,87】 。要对付这种破坏通常使用简单的方法就可以做到,例如应用程序级协议中的校验和。 +* 可公开访问的应用程序必须仔细清理来自用户的任何输入,例如检查值是否在合理的范围内,并限制字符串的大小以防止通过大内存分配的拒绝服务。防火墙后面的内部服务对于输入也许可以只采取一些不那么严格的检查,但是采取一些基本的合理性检查(例如,在协议解析中)仍然是一个好主意。 +* NTP客户端可以配置多个服务器地址。同步时,客户端联系所有的服务器,估计它们的误差,并检查大多数服务器是否对某个时间范围达成一致。只要大多数的服务器没问题,一个配置错误的NTP服务器报告的时间会被当成特异值从同步中排除【37】。使用多个服务器使NTP更健壮(比起只用单个服务器来)。 ### 系统模型与现实 ​ 已经有很多算法被设计以解决分布式系统问题——例如,我们将在[第9章](ch9.md)讨论共识问题的解决方案。为了有用,这些算法需要容忍我们在本章中讨论的分布式系统的各种故障。 -​ 算法的编写方式并不过分依赖于运行的硬件和软件配置的细节。这又要求我们以某种方式将我们期望在系统中发生的错误形式化。我们通过定义一个系统模型来做到这一点,这个模型是一个抽象,描述一个算法可能承担的事情。 -关于定时假设,三种系统模型是常用的: +​ 算法的编写方式不应该过分依赖于运行的硬件和软件配置的细节。这就要求我们以某种方式将我们期望在系统中发生的错误形式化。我们通过定义一个系统模型来做到这一点,这个模型是一个抽象,描述一个算法可以假设的事情。 + +关于时序假设,三种系统模型是常用的: ***同步模型*** -​ **同步模型(synchronous model)**假设网络延迟,进程暂停和和时钟误差都是有界限的。这并不意味着完全同步的时钟或零网络延迟;这只意味着你知道网络延迟,暂停和时钟漂移将永远不会超过某个固定的上限【88】。同步模型并不是大多数实际系统的现实模型,因为(如本章所讨论的)无限延迟和暂停确实会发生。 +​ **同步模型(synchronous model)**假设网络延迟、进程暂停和和时钟误差都是受限的。这并不意味着完全同步的时钟或零网络延迟;这只意味着你知道网络延迟、暂停和时钟漂移将永远不会超过某个固定的上限【88】。同步模型并不是大多数实际系统的现实模型,因为(如本章所讨论的)无限延迟和暂停确实会发生。 ***部分同步模型*** -​ **部分同步(partial synchronous)**意味着一个系统在大多数情况下像一个同步系统一样运行,但有时候会超出网络延迟,进程暂停和时钟漂移的界限【88】。这是很多系统的现实模型:大多数情况下,网络和进程表现良好,否则我们永远无法完成任何事情,但是我们必须承认,在任何时刻假设都存在偶然被破坏的事实。发生这种情况时,网络延迟,暂停和时钟错误可能会变得相当大。 +​ **部分同步(partial synchronous)**意味着一个系统在大多数情况下像一个同步系统一样运行,但有时候会超出网络延迟,进程暂停和时钟漂移的界限【88】。这是很多系统的现实模型:大多数情况下,网络和进程表现良好,否则我们永远无法完成任何事情,但是我们必须承认,在任何时刻都存在时序假设偶然被破坏的事实。发生这种情况时,网络延迟、暂停和时钟错误可能会变得相当大。 ***异步模型*** -​ 在这个模型中,一个算法不允许对时机做任何假设——事实上它甚至没有时钟(所以它不能使用超时)。一些算法被设计为可用于异步模型,但非常受限。 +​ 在这个模型中,一个算法不允许对时序做任何假设——事实上它甚至没有时钟(所以它不能使用超时)。一些算法被设计为可用于异步模型,但非常受限。 - -进一步来说,除了时间问题,我们还要考虑**节点失效**。三种最常见的节点系统模型是: +进一步来说,除了时序问题,我们还要考虑**节点失效**。三种最常见的节点系统模型是: ***崩溃-停止故障*** @@ -589,7 +584,7 @@ while(true){ ​ 为了定义算法是正确的,我们可以描述它的属性。例如,排序算法的输出具有如下特性:对于输出列表中的任何两个不同的元素,左边的元素比右边的元素小。这只是定义对列表进行排序含义的一种形式方式。 -​ 同样,我们可以写下我们想要的分布式算法的属性来定义它的正确含义。例如,如果我们正在为一个锁生成防护令牌(参阅“[防护令牌](防护令牌)”),我们可能要求算法具有以下属性: +​ 同样,我们可以写下我们想要的分布式算法的属性来定义它的正确含义。例如,如果我们正在为一个锁生成防护令牌(参阅“[防护令牌](#防护令牌)”),我们可能要求算法具有以下属性: ***唯一性*** @@ -603,7 +598,7 @@ while(true){ ​ 请求防护令牌并且不会崩溃的节点,最终会收到响应。 -如果一个系统模型中的算法总是满足它在我们假设可能发生的所有情况下的性质,那么这个算法是正确的。但这如何有意义?如果所有的节点崩溃,或者所有的网络延迟突然变得无限长,那么没有任何算法能够完成任何事情。 +如果一个系统模型中的算法总是满足它在所有我们假设可能发生的情况下的性质,那么这个算法是正确的。但这如何有意义?如果所有的节点崩溃,或者所有的网络延迟突然变得无限长,那么没有任何算法能够完成任何事情。 #### 安全性和活性 @@ -613,8 +608,8 @@ while(true){ ​ 安全性通常被非正式地定义为,**没有坏事发生**,而活性通常就类似:**最终好事发生**。但是,最好不要过多地阅读那些非正式的定义,因为好与坏的含义是主观的。安全性和活性的实际定义是精确的和数学的【90】: -* 如果安全属性被违反,我们可以指向一个特定的时间点(例如,如果违反了唯一性属性,我们可以确定重复的防护令牌返回的特定操作) 。违反安全属性后,违规行为不能撤销——损失已经发生。 -* 活性属性反过来:在某个时间点(例如,一个节点可能发送了一个请求,但还没有收到响应),它可能不成立,但总是希望在未来(即通过接受答复)。 +* 如果安全属性被违反,我们可以指向一个特定的安全属性被破坏的时间点(例如,如果违反了唯一性属性,我们可以确定重复的防护令牌被返回的特定操作)。违反安全属性后,违规行为不能被撤销——损失已经发生。 +* 活性属性反过来:在某个时间点(例如,一个节点可能发送了一个请求,但还没有收到响应),它可能不成立,但总是希望在未来能成立(即通过接受答复)。 区分安全性和活性属性的一个优点是可以帮助我们处理困难的系统模型。对于分布式算法,在系统模型的所有可能情况下,要求**始终**保持安全属性是常见的【88】。也就是说,即使所有节点崩溃,或者整个网络出现故障,算法仍然必须确保它不会返回错误的结果(即保证安全性得到满足)。 @@ -624,37 +619,36 @@ while(true){ ​ 安全性和活性属性以及系统模型对于推理分布式算法的正确性非常有用。然而,在实践中实施算法时,现实的混乱事实再一次地让你咬牙切齿,很明显系统模型是对现实的简化抽象。 -​ 例如,在故障恢复模型中的算法通常假设稳定存储器中的数据经历了崩溃。但是,如果磁盘上的数据被破坏,或者由于硬件错误或错误配置导致数据被清除,会发生什么情况?如果服务器存在固件错误并且在重新启动时无法识别其硬盘驱动器,即使驱动器已正确连接到服务器,那又会发生什么情况? +​ 例如,在崩溃-恢复(crash-recovery)模型中的算法通常假设稳定存储器中的数据在崩溃后可以幸存。但是,如果磁盘上的数据被破坏,或者由于硬件错误或错误配置导致数据被清除,会发生什么情况【91】?如果服务器存在固件错误并且在重新启动时无法识别其硬盘驱动器,即使驱动器已正确连接到服务器,那又会发生什么情况【92】? -​ 法定人数算法(参见“[读写法定人数](ch5.md#读写法定人数)”)依赖节点来记住它声称存储的数据。如果一个节点可能患有健忘症,忘记了以前存储的数据,这会打破法定条件,从而破坏算法的正确性。也许需要一个新的系统模型,在这个模型中,我们假设稳定的存储大多存在崩溃,但有时可能会丢失。但是那个模型就变得更难以推理了。 +​ 法定人数算法(参见“[读写法定人数](ch5.md#读写法定人数)”)依赖节点来记住它声称存储的数据。如果一个节点可能患有健忘症,忘记了以前存储的数据,这会打破法定条件,从而破坏算法的正确性。也许需要一个新的系统模型,在这个模型中,我们假设稳定的存储大多能在崩溃后幸存,但有时也可能会丢失。但是那个模型就变得更难以推理了。 -​ 算法的理论描述可以简单宣称一些事在假设上是不会发生的——在非拜占庭式系统中。但实际上我们还是需要对可能发生和不可能发生的故障做出假设,真实世界的实现,仍然会包括处理“假设上不可能”情况的代码,即使代码可能就是`printf("you sucks")`和`exit(666)`,实际上也就是留给运维来擦屁股。(这可以说是计算机科学和软件工程间的一个差异)。 +​ 算法的理论描述可以简单宣称一些事是不会发生的——在非拜占庭式系统中,我们确实需要对可能发生和不可能发生的故障做出假设。然而,真实世界的实现,仍然会包括处理“假设上不可能”情况的代码,即使代码可能就是`printf("Sucks to be you")`和`exit(666)`,实际上也就是留给运维来擦屁股【93】。(这可以说是计算机科学和软件工程间的一个差异)。 -​ 这并不是说理论上抽象的系统模型是毫无价值的,恰恰相反。它们对于将实际系统的复杂性降低到一个我们可以推理的可处理的错误是非常有帮助的,以便我们能够理解这个问题,并试图系统地解决这个问题。我们可以证明算法是正确的,通过表明它们的属性总是保持在某个系统模型中。 - -​ 证明算法正确并不意味着它在真实系统上的实现必然总是正确的。但这迈出了很好的第一步,因为理论分析可以发现算法中的问题,这种问题可能会在现实系统中长期潜伏,直到你的假设(例如,时间)因为不寻常的情况被打破。理论分析与经验测试同样重要。 +​ 这并不是说理论上抽象的系统模型是毫无价值的,恰恰相反。它们对于将实际系统的复杂性提取成一个个我们可以推理的可处理的错误类型是非常有帮助的,以便我们能够理解这个问题,并试图系统地解决这个问题。我们可以证明算法是正确的,通过表明它们的属性在某个系统模型中总是成立的。 +​ 证明算法正确并不意味着它在真实系统上的实现必然总是正确的。但这迈出了很好的第一步,因为理论分析可以发现算法中的问题,这种问题可能会在现实系统中长期潜伏,直到你的假设(例如,时序)因为不寻常的情况被打破。理论分析与经验测试同样重要。 ## 本章小结 在本章中,我们讨论了分布式系统中可能发生的各种问题,包括: -* 当您尝试通过网络发送数据包时,数据包可能会丢失或任意延迟。同样,答复可能会丢失或延迟,所以如果你没有得到答复,你不知道消息是否通过。 -* 节点的时钟可能会与其他节点显著不同步(尽管您尽最大努力设置NTP),它可能会突然跳转或跳回,依靠它是很危险的,因为您很可能没有好的测量你的时钟的错误间隔。 +* 当您尝试通过网络发送数据包时,数据包可能会丢失或任意延迟。同样,答复可能会丢失或延迟,所以如果你没有得到答复,你不知道消息是否发送成功了。 +* 节点的时钟可能会与其他节点显著不同步(尽管您尽最大努力设置NTP),它可能会突然跳转或跳回,依靠它是很危险的,因为您很可能没有好的方法来测量你的时钟的错误间隔。 * 一个进程可能会在其执行的任何时候暂停一段相当长的时间(可能是因为停止所有处理的垃圾收集器),被其他节点宣告死亡,然后再次复活,却没有意识到它被暂停了。 -这类**部分失效**可能发生的事实是分布式系统的决定性特征。每当软件试图做任何涉及其他节点的事情时,偶尔就有可能会失败,或者随机变慢,或者根本没有响应(最终超时)。在分布式系统中,我们试图在软件中建立**部分失效**的容错机制,这样整个系统即使在某些组成部分被破坏的情况下,也可以继续运行。 +这类**部分失效(partial failure)**可能发生的事实是分布式系统的决定性特征。每当软件试图做任何涉及其他节点的事情时,偶尔就有可能会失败,或者随机变慢,或者根本没有响应(最终超时)。在分布式系统中,我们试图在软件中建立**部分失效**的容错机制,这样整个系统在即使某些组成部分被破坏的情况下,也可以继续运行。 -​ 为了容忍错误,第一步是**检测**它们,但即使这样也很难。大多数系统没有检测节点是否发生故障的准确机制,所以大多数分布式算法依靠**超时**来确定远程节点是否仍然可用。但是,超时无法区分网络失效和节点失效,并且可变的网络延迟有时会导致节点被错误地怀疑发生故障。此外,有时一个节点可能处于降级状态:例如,由于驱动程序错误【94】,千兆网卡可能突然下降到1 Kb/s的吞吐量。这样一个“跛行”而不是死掉的节点可能比一个干净的失效节点更难处理。 +​ 为了容忍错误,第一步是**检测**它们,但即使这样也很难。大多数系统没有检测节点是否发生故障的准确机制,所以大多数分布式算法依靠**超时**来确定远程节点是否仍然可用。但是,超时无法区分网络失效和节点失效,并且可变的网络延迟有时会导致节点被错误地怀疑发生故障。此外,有时一个节点可能处于降级状态:例如,由于驱动程序错误,千兆网卡可能突然下降到1 Kb/s的吞吐量【94】。这样一个“跛行”而不是死掉的节点可能比一个干净的失效节点更难处理。 ​ 一旦检测到故障,使系统容忍它也并不容易:没有全局变量,没有共享内存,没有共同的知识,或机器之间任何其他种类的共享状态。节点甚至不能就现在是什么时间达成一致,就不用说更深奥的了。信息从一个节点流向另一个节点的唯一方法是通过不可靠的网络发送信息。重大决策不能由一个节点安全地完成,因此我们需要一个能从其他节点获得帮助的协议,并争取达到法定人数以达成一致。 -​ 如果你习惯于在理想化的数学完美(同一个操作总能确定地返回相同的结果)的单机环境中编写软件,那么转向分布式系统的凌乱的物理现实可能会有些令人震惊。相反,如果能够在单台计算机上解决一个问题,那么分布式系统工程师通常会认为这个问题是平凡的【5】,现在单个计算机确实可以做很多事情【95】。如果你可以避免打开潘多拉的盒子,把东西放在一台机器上,那么通常是值得的。 +​ 如果你习惯于在理想化的数学完美的单机环境(同一个操作总能确定地返回相同的结果)中编写软件,那么转向分布式系统的凌乱的物理现实可能会有些令人震惊。相反,如果能够在单台计算机上解决一个问题,那么分布式系统工程师通常会认为这个问题是平凡的【5】,现在单个计算机确实可以做很多事情【95】。如果你可以避免打开潘多拉的盒子,把东西放在一台机器上,那么通常是值得的。 ​ 但是,正如在[第二部分](part-ii.md)的介绍中所讨论的那样,可伸缩性并不是使用分布式系统的唯一原因。容错和低延迟(通过将数据放置在距离用户较近的地方)是同等重要的目标,而这些不能用单个节点实现。 -​ 在本章中,我们也转换了几次话题,探讨了网络,时钟和进程的不可靠性是否是不可避免的自然规律。我们看到这并不是:有可能给网络提供硬实时的响应保证和有限的延迟,但是这样做非常昂贵,且导致硬件资源的利用率降低。大多数非安全关键系统会选择**便宜而不可靠**,而不是**昂贵和可靠**。 +​ 在本章中,我们也转换了几次话题,探讨了网络、时钟和进程的不可靠性是否是不可避免的自然规律。我们看到这并不是:有可能给网络提供硬实时的响应保证和有限的延迟,但是这样做非常昂贵,且导致硬件资源的利用率降低。大多数非安全关键系统会选择**便宜而不可靠**,而不是**昂贵和可靠**。 ​ 我们还谈到了超级计算机,它们采用可靠的组件,因此当组件发生故障时必须完全停止并重新启动。相比之下,分布式系统可以永久运行而不会在服务层面中断,因为所有的错误和维护都可以在节点级别进行处理——至少在理论上是如此。 (实际上,如果一个错误的配置变更被应用到所有的节点,仍然会使分布式系统瘫痪)。 diff --git a/glossary.md b/glossary.md index e19ba12..00e9cc5 100644 --- a/glossary.md +++ b/glossary.md @@ -292,9 +292,9 @@ -### 可序列化(serializable) +### 可串行化(serializable) -保证多个并发事务同时执行时,它们的行为与按顺序逐个执行事务相同。 请参阅第251页上的“可序列化”。 +保证多个并发事务同时执行时,它们的行为与按顺序逐个执行事务相同。 请参阅第7章的“[可串行化](ch7.md#可串行化)”。