第9章 分布式系统的麻烦
意外是件有趣的事。在你遇到它们之前,你永远不会拥有它们。 ——A.A.米尔恩,《小熊维尼角的房子》(1928)
如“可靠性与容错”(第43页)所述,使系统可靠意味着确保即使出现问题(即发生故障)时,系统整体仍能继续工作。然而,预料所有可能的故障并加以处理并非易事。作为开发者,我们很容易主要关注顺利路径(毕竟大多数时候一切正常!),而忽视故障,因为故障会引入大量边缘情况。
如果你希望在存在故障的情况下系统仍然可靠,你必须彻底改变思维方式,专注于可能出错的地方,即使这种可能性很小。无论概率是否只有百万分之一,在一个足够大的系统中,百万分之一的事件每天都会发生。经验丰富的系统运维人员会告诉你:任何可能出错的事情终将出错。
处理分布式系统与在单台计算机上编写软件有着根本不同——主要区别在于,有许多全新且令人兴奋的方式可能出错[1, 2]。在本章中,你将了解实际中会出现的问题,并理解哪些是可以依赖的,哪些不能。
为了理解我们面临的挑战,我们将把悲观情绪调到最大,探索分布式系统中可能出现的各种问题,包括网络问题以及时钟和时间问题。这些问题的后果令人困惑,因此我们还将探讨如何思考分布式系统的状态以及如何推理已经发生的事情。在第10章中,我们将看一些示例,说明如何在这些事件面前实现容错。
故障与部分失效
当你在单台计算机上编写程序时,它的行为通常相当可预测:要么工作,要么不工作。有缺陷的软件可能让计算机看起来“今天状态不佳”(通常重启即可解决),但这主要是糟糕编写软件的结果。
单台计算机上的软件没有根本理由不可靠。当硬件正常工作时,相同的操作总是产生相同的结果(它是确定性的)。如果有硬件问题(例如内存损坏或接口松动),后果通常是系统完全失效(例如内核恐慌、“蓝屏死机”、无法启动)。一台运行良好软件的计算机通常要么完全功能正常,要么完全损坏,而不是介于两者之间。
这是计算机设计中的有意选择。如果发生内部故障,我们更希望计算机完全崩溃,而不是返回错误结果,因为错误结果难以处理且令人困惑。因此,计算机隐藏了其赖以实现的模糊物理现实,呈现出一种以数学完美性运行的理想化系统模型。CPU指令始终执行相同操作;如果你将数据写入内存或磁盘,这些数据会保持完整,不会随机损坏。如“硬件故障与软件故障”(第44页)所述,这实际上并不成立——现实中数据确实会静默损坏,CPU有时也会静默返回错误结果——但这种情况发生得足够罕见,以至于我们可以忽略它。
当你编写运行在多台计算机上并通过网络连接的软件时,情况就根本不同了。在分布式系统中,故障发生得频繁得多,我们不能再忽视它们——我们别无选择,只能面对物理世界的混乱现实。而在物理世界中,可能出错的事情范围惊人地广泛,如下面的轶事所示[3]:
在我有限的经历中,我处理过单个数据中心(DC)内的长期网络分区、PDU[配电单元]故障、交换机故障、整个机架的意外断电、整个数据中心骨干网故障、整个数据中心断电,以及一位低血糖司机开着福特皮卡撞进数据中心的HVAC[供暖、通风和空调]系统。而且我甚至不是运维人员。 ——Coda Hale
在分布式系统中,很可能系统的某些部分以不可预测的方式损坏,而其他部分却正常工作。这被称为部分失效。困难在于部分失效是非确定性的:如果你尝试任何涉及多个节点和网络的操作,它有时可能成功,有时可能无法预测地失败。正如我们将看到的,你甚至可能不知道某个操作是否成功!
这种非确定性和部分失效的可能性使得分布式系统难以处理[4]。另一方面,如果分布式系统能够容忍部分失效,那将打开强大的可能性——例如,这意味着我们可以执行滚动升级,一次重启一个节点以安装软件更新,而系统整体则不间断地继续工作。因此,容错使我们能够构建比单节点系统更可靠的分布式系统;我们可以用不可靠的组件构建可靠的系统。
但在实现容错之前,我们需要更多地了解我们应当容忍的故障。重要的是要考虑各种可能的故障——即使是相当不可能发生的故障——并在测试环境中人为制造这些情况,看看会发生什么。在分布式系统中,怀疑、悲观和偏执是值得的。
不可靠的网络
过去,像大型机这样的旧式计算机通过确保单个组件冗余来变得可靠——例如,使用RAID来承受单个磁盘故障。如“共享内存、共享磁盘与无共享架构”(第51页)所述,本书重点关注的分布式系统大多是无共享系统:一组通过网络连接的机器。无共享系统不是在单台机器内部进行组件冗余,而是通过跨不同机器进行复制来实现冗余。网络是这些机器通信的唯一方式。我们假设每台机器都有自己的内存和磁盘,一台机器无法访问另一台机器的内存或磁盘(除了通过网络向服务发出请求)。即使存储是共享的,例如对象存储,机器也是通过网络与共享存储服务通信。
互联网和大多数数据中心内部网络(通常是以太网)是异步分组网络。在这种网络中,一个节点可以向另一个节点发送消息(一个数据包),但网络对消息何时到达或是否到达不提供任何保证。如果你发送请求并期望得到响应,许多事情可能出错(其中一些如图9-1所示):
- 你的请求可能已丢失(也许有人拔掉了网线)。
- 你的请求可能正在队列中等待,稍后才会被传递(也许网络或接收方负载过重)。
- 远程节点可能已失效(也许它崩溃了或电源被切断)。
- 远程节点可能暂时停止响应(也许它正在经历长时间的GC暂停;参见“进程暂停”,第366页),但稍后会再次开始响应。
- 远程节点可能已处理了你的请求,但响应在网络中丢失(也许网络交换机配置错误)。
- 远程节点可能已处理了你的请求,但响应被延迟,稍后才会被传递(也许网络或你自己的机器负载过重)。

发送方甚至无法判断数据包是否已送达。唯一的办法是由接收方发回一条响应消息,而这条消息也可能丢失或延迟。在异步网络中,这些问题无法区分;你唯一拥有的信息是你还没有收到响应。如果你向另一个节点发送请求但没有收到响应,就不可能知道原因。
处理此问题的常用方法是超时:一段时间后,你放弃等待,并假设响应不会到达。然而,当超时发生时,你仍然不知道远程节点是否收到了你的请求(如果请求仍然在某个地方排队,即使你已经放弃了这种可能性,它可能仍然会被送达接收方)。
TCP的局限性
网络数据包有一个最大尺寸(通常为几千字节),但许多应用程序需要发送的消息(请求、响应)太大,无法容纳在一个数据包中。这些应用程序最常使用TCP(传输控制协议)来建立连接,该协议将大数据流分解为单个数据包,并在接收端重新组装。
NOTE
这里关于TCP的大部分内容也适用于其较新的替代方案QUIC,以及WebRTC中使用的流控制传输协议(SCTP)、BitTorrent uTP协议和其他传输协议。与UDP的比较,请参见“TCP与UDP”(第354页)。
[继续下一部分…]
第9章 分布式系统的麻烦
TCP 通常被描述为提供“可靠”的交付,在这个意义上,它能检测并重传丢失的数据包,能检测乱序的数据包并将其重新排列正确,还能通过简单的校验和检测数据包损坏。它还能计算出它可以多快地发送数据,以便尽可能快地传输,同时不会使网络或接收节点过载;这被称为拥塞控制、流量控制或背压[5]。
当你通过向套接字写入数据“发送”数据时,它不会立即被发送;它被放入操作系统管理的缓冲区中。当拥塞控制算法决定它有能力发送一个数据包时,它从该缓冲区中取出下一个数据包大小的数据,并将其传递给网络接口。该数据包经过多个交换机和路由器,最终接收节点的操作系统将数据包的数据放入接收缓冲区,并向发送方发送一个确认数据包。只有在那之后,接收方的操作系统才通知应用程序有更多的数据到达[6]。
那么,如果 TCP 提供了“可靠性”,这是否意味着我们不再需要担心网络不可靠?不幸的是,并非如此。TCP 决定如果没有在某个超时时间内收到确认,则数据包一定已丢失,但它无法判断是发出的数据包丢失还是确认丢失。尽管它可以重发数据包,但它不能保证新数据包一定能通过(例如,如果网线被拔掉,TCP 无法为你重新插上)。最终,在一个可配置的超时时间之后,它放弃并给应用程序发出错误信号。TCP 的去重和重传能力仅适用于单个连接,因此如果应用程序重新连接并重传,数据可能被重复。
如果 TCP 连接因错误而关闭——可能是因为远程节点崩溃或网络中断——你无法知道远程节点实际处理了多少数据[6]。即使你收到数据包已送达的确认,这也仅意味着远程节点上的操作系统内核收到了它;应用程序可能在处理该数据之前就崩溃了。如果你要确保请求成功,你需要来自应用程序本身的肯定响应[7]。
尽管如此,TCP 非常有用,因为它提供了一种方便的方式来发送和接收太大以至于无法装入一个数据包的消息。一旦 TCP 连接建立,你还可以用它发送多个请求和响应。这通常通过首先发送一个指示后续消息字节长度的头部,然后发送实际消息来完成。HTTP 和许多 RPC 协议(参见第 180 页的“服务间的数据流:REST 和 RPC”)就是这样工作的。
不可靠的网络
| | 349
网络故障的实际情况
几十年来我们一直在构建计算机网络——我们可能希望到现在我们已经弄清楚了如何让它们变得可靠。不幸的是,我们还没有成功。一些系统研究和大量的轶事证据表明,网络问题令人惊讶地常见,即使在像由一家公司运营的数据中心这样的受控环境中也是如此[8]:
- 一项在中等规模数据中心的研究发现,每月约有 12 起网络故障,其中一半断开了单台机器,一半断开了整个机架[9]。
- 另一项研究测量了诸如架顶交换机、汇聚交换机和负载均衡器等组件的故障率[10]。它发现增加冗余网络设备并不能像你预期的那样减少故障,因为它不能防范人为错误(例如,交换机配置错误),而这是导致停机的主要原因。
- 广域光纤链路的中断曾被归咎于奶牛[11]、河狸[12]和鲨鱼[13](尽管随着海底电缆屏蔽的改善,鲨鱼咬伤已变得罕见[14])。人类也常常是罪魁祸首,原因包括意外的配置错误[15]、盗猎[16]或蓄意破坏[17]。
- 在各个云区域之间,高百分位数的往返时间可达数分钟[18,表3]。即使在一个数据中心内,在由交换机软件升级期间的问题引发的网络拓扑重新配置过程中,也可能出现超过一分钟的数据包延迟[19]。因此,我们必须假设消息可能被任意延迟。
- 有时通信是部分中断的,取决于你与谁通信——例如,A 和 B 可以通信,B 和 C 可以通信,但 A 和 C 不能[20,21]。其他令人惊讶的故障包括网络接口有时丢弃所有入站数据包,但成功发送出站数据包[22]。仅仅因为一个网络链路在一个方向上工作,并不能保证它在相反方向上也工作。
- 即使是短暂的网络中断,也可能产生持续远长于原始问题的影响[8,20,23]。
即使网络故障在你的环境中很少见,但故障可能发生这一事实意味着你的软件需要能够处理它们。每当通过网络发生任何通信时,都可能失败——这是无法回避的。
350 | 第9章 分布式系统的麻烦
网络分区
术语:网络分区
术语 网络分区 或 网络分裂 ,有时用于指网络的一部分因网络故障而与其余部分隔离的情况。然而,这与其他类型的网络中断并没有本质区别。网络分区与存储系统的分片(有时也称为分区,参见第7章)无关。
如果网络故障的错误处理没有得到定义和测试,可能会发生任意糟糕的事情——例如,集群可能陷入死锁并永久无法服务请求,即使网络恢复后也是如此[24],或者可能潜在地删除你的所有数据[25]。如果软件被置于意外的情况中,它可能做出任意的意外行为。
处理网络故障不一定意味着容忍它们。如果你的网络通常相当可靠,一种有效的方法可能是在网络出现问题时简单地向用户显示错误消息。然而,你确实需要了解你的软件如何对网络问题做出反应,并确保系统能够从中恢复。有意触发网络问题并测试系统的响应(参见第 385 页的“故障注入”)可能是合理的。
故障检测
许多系统需要自动检测故障节点。例如:
- 负载均衡器需要停止向已死亡的节点发送请求(即,将其移出轮转)。
- 在具有单主节点复制的分布式数据库中,如果主节点发生故障,则需要将一个从节点提升为新的主节点(参见第 204 页的“处理节点宕机”)。
不幸的是,网络的不确定性使得判断节点是否在工作变得困难。在特定情况下,你可能会收到反馈来明确告诉你某些事情不工作:
-
如果你可以到达节点应该运行的机器,但没有任何进程在监听目标端口(例如,因为进程崩溃了),操作系统将通过发送 RST 或 FIN 数据包作为回复来友好地关闭或拒绝 TCP 连接。
-
如果节点进程崩溃了(或被管理员杀死),但节点的操作系统仍在运行,脚本可以通知其他节点该崩溃,以便另一个节点可以快速接管,而不必等待超时到期。例如,HBase 就是这样做的[26]。
不可靠的网络 | 351
-
如果你可以访问数据中心中网络交换机的管理接口,你可以查询它们以在硬件级别检测链路故障(例如,如果远程机器断电)。如果你通过互联网连接,或者位于共享数据中心且无法访问交换机本身,或者由于网络问题无法访问管理接口,那么这种方案就不适用。
-
如果路由器确信你试图连接的 IP 地址不可达,它可能会回复你一个 ICMP 目标不可达数据包。然而,路由器也没有神奇的故障检测能力;它受到与网络中其他参与者相同的限制。
关于远程节点宕机的快速反馈很有用,但你不能依赖它。如果出了问题,你可能会在栈的某个级别收到错误响应,但通常你必须假设你将根本收不到任何响应。你可以重试几次,等待超时时间过去,并最终如果在超时时间内没有收到回复,则宣布该节点死亡。由于节点实际上可能还活着,你需要在误报和漏报之间取得平衡:过短的超时会导致将活着的节点错误地怀疑为死亡,而过长的超时会导致等待死亡节点时出现不必要的延迟。
超时和无界延迟
如果超时是检测故障的唯一可靠方法,那么超时应该设置多长时间?不幸的是,没有简单的答案。
长时间超时意味着在节点被宣布死亡之前需要等待很长时间(在此期间,用户可能需要等待或看到错误消息)。短时间超时能更快地检测到故障,但风险更高,可能错误地宣布一个节点死亡,而实际上它只是遭受了暂时的速度下降(例如,由于节点或网络上的负载峰值)。
过早地宣布节点死亡是有问题的。如果该节点实际上还活着,并且正在执行某个操作(例如,发送电子邮件),而另一个节点接管了,那么该操作可能最终被执行两次。我们将在第 371 页的“知识、真理与谎言”以及第 10 章和第 12 章中更详细地讨论这个问题。
当一个节点被宣布死亡时,其职责需要转移到其他节点,这会给这些节点和网络带来额外的负载。如果系统已经在高负载下挣扎,过早地宣布节点死亡会使问题变得更糟。特别是,可能发生这样的情况:该节点实际上并没有死亡,只是由于过载而响应缓慢;将其负载转移到其他节点可能导致级联故障(在极端情况下,所有节点都相互宣布死亡,一切都停止工作——参见第 38 页的“当过载系统无法恢复时”)。
352 |
第9章:分布式系统的麻烦
假设存在一个虚构系统,其网络保证数据包的最大延迟。每个数据包要么在时间 d 内送达,要么丢失,且送达时间绝不会超过 d。此外,假设你可以保证一个未发生故障的节点总能在时间 r 内处理完请求。在这种情况下,你可以保证每个成功的请求在 2d + r 时间内收到响应——如果在此时限内未收到响应,你就知道网络或远程节点出现了问题。如果这一假设成立,那么 2d + r 将是一个合理的超时阈值。
不幸的是,我们日常使用的绝大多数系统都不具备这两种保证。异步网络存在无界延迟(它们会尽可能快地交付数据包,但数据包到达的时间没有上限),而且大多数服务器实现也无法保证能在最大时间内处理完请求(参见第 369 页的“提供响应时间保证”)。
对于故障检测而言,系统大多数时候快速响应是不够的:如果超时设置得过低,只需一次短暂往返时间的尖峰波动,就足以使系统失去平衡。
网络拥塞与排队
开车时,道路网络的通行时间往往因交通拥堵而剧烈变化。类似地,计算机网络中数据包延迟的可变性也大多源于排队 [27]:
- 如果多个节点同时尝试向同一目的地发送数据包,网络交换机必须将它们排成队列,并逐一向目标网络链路上馈送数据(如图 9-2 所示)。在网络链路繁忙时,数据包可能需要等待一段时间才能获得一个发送时隙(这称为网络拥塞)。如果入站数据过多,交换机队列填满,数据包就会被丢弃,因此需要重新发送——尽管网络本身运行正常。
- 当数据包到达目标机器时,如果所有 CPU 核心或应用程序线程当前都处于繁忙状态,来自网络的请求将被操作系统排队,直到应用程序准备好处理它。根据机器的负载情况,这一等待时间可能任意长 [28]。
- 在虚拟化环境中,一个正在运行的操作系统常常会被暂停数十毫秒,让另一个虚拟机(VM)使用某个 CPU 核心。在此期间,该虚拟机无法从网络接收任何数据,因此入站数据被虚拟机监控器(VMM)排队(缓冲)[29],进一步增加了网络延迟的可变性。
- 如前所述,为了避免网络过载,TCP 会限制其发送数据的速率。这意味着在数据甚至还未进入网络之前,发送方就已经进行了额外的排队。
图 9-2. 如果多台机器向同一目的地发送网络流量,其交换机队列可能会被填满。此处,端口 1、2 和 4 都试图向端口 3 发送数据包。
(图 9-2 展示了三台机器向一个交换机发数据,交换机队列正在排队的场景。)
此外,当 TCP 检测到数据包丢失并进行自动重传时,应用程序不会直接看到丢包,但会看到由此产生的延迟(等待超时到期,然后等待重传的数据包被确认)。
TCP 与 UDP
某些对延迟敏感的应用(如视频会议和 IP 语音,VoIP)会使用 UDP 而非 TCP。这一选择是在可靠性与延迟可变性之间做出的权衡:由于 UDP 不执行流量控制,也不重传丢失的数据包,它避免了网络延迟不稳定的某些原因(尽管它仍然会受到交换队列和调度延迟的影响)。
当延迟的数据毫无价值时,UDP 是一个不错的选择。例如,在 VoIP 电话通话中,几乎没有足够的时间在数据本该通过扬声器播放之前重传丢失的数据包。此时,重传数据包毫无意义——应用程序必须用静音填充丢失的数据包时隙(导致声音短暂中断),然后继续处理流中的后续数据。重试发生在人类层面(“能请你再说一遍吗?刚才声音断了一下。”)。
网络延迟的可变性
所有这些因素共同导致了网络延迟的可变性。当系统接近其最大容量时,排队延迟的范围尤其大。具有充足空闲容量的系统可以轻松排空队列,而高度利用的系统则可能迅速积累长队列。
在公有云和多租户数据中心,资源在许多客户之间共享。网络链路和交换机,甚至每台机器的网络接口和 CPU(在虚拟机上运行时),都是共享的。处理大量数据可能会用尽网络链路的全部容量(使其饱和)。由于你无法控制或了解其他客户对共享资源的使用情况,如果附近有人(一个吵闹的邻居)正在使用大量资源,网络延迟可能会高度可变 [30, 31]。
在这样的环境中,你只能通过实验来选择超时设置:长时间跨越多台机器测量网络往返时间的分布,以确定延迟的预期可变性。然后,结合应用程序的特性,确定故障检测延迟与过早超时风险之间的适当权衡。
更好的做法是,系统不必使用配置的固定超时,而是持续测量响应时间及其可变性(抖动),并根据观察到的响应时间分布自动调整超时。Phi 累积故障检测器 [32](例如,用于 Akka 和 Cassandra [33])就是这样一种实现方式。TCP 重传超时的工作原理类似 [5]。
同步网络与异步网络
如果我们可以依赖网络以固定的最大延迟交付数据包并且不丢弃数据包,分布式系统将简单得多。为什么我们不能在硬件层面解决这个问题,使网络变得可靠,从而让软件无需为此担忧呢?
要回答这个问题,将数据中心网络与传统固定电话网络(非蜂窝、非 VoIP)进行比较是很有趣的;固定电话网络极其可靠,音频帧延迟和呼叫中断非常罕见。电话呼叫需要持续的低端到端延迟以及足够的带宽来传输你的语音音频样本。在计算机网络中拥有类似的可靠性和可预测性岂不是很好?
当你通过电话网络拨打电话时,它会建立一条电路:在两条电话线之间的整个路径上,为这次呼叫分配一个固定且保证的带宽量。这条电路在通话结束前一直存在 [34]。例如,ISDN 网络以每秒 4000 帧的固定速率运行。当建立呼叫时,会在每一帧中(每个方向)分配 16 位空间。因此,在通话期间,每一侧都保证能够每 250 微秒精确发送 16 位的音频数据 [35]。
这种网络是同步的:即使数据经过多个路由器,它也不会受到排队的影响,因为呼叫的 16 位空间已经在网络的下一跳中预留好了。由于没有排队,网络的端到端最大延迟是固定的。我们称之为有界延迟。
难道我们不能让网络延迟变得可预测吗?
注意,电话网络中的电路与 TCP 连接截然不同。电路具有固定的预留带宽,只要电路建立,任何人都无法使用;而 TCP 连接的包则机会主义地利用所有可用的网络带宽。你可以向 TCP 提供一个可变大小的数据块(例如,一封电子邮件或一个网页),它会尝试在最短的时间内传输它。当 TCP 连接空闲时,它不使用任何带宽(可能除了偶尔的保活数据包)。
如果数据中心网络和互联网是电路交换网络,则在建立电路时就有可能保证最大往返时间。然而,它们不是。以太网和 IP 是分组交换协议,会受到排队的影响,因此网络中延迟无界。这些协议没有电路的概念。
为什么数据中心网络和互联网要使用分组交换?答案是它们针对突发流量进行了优化。电路适用于音频或视频通话,这类通话需要在通话期间每秒钟传输相当恒定数量的比特。另一方面,请求网页、发送电子邮件或传输文件并没有特定的带宽需求——我们只希望它尽快完成。
如果你想通过电路传输一个文件,你必须猜测一个带宽分配。如果猜得太低,传输将不必要地缓慢,导致网络容量闲置。如果猜得太高,电路无法建立(因为如果无法保证其带宽分配,网络不允许创建电路)。相比之下,TCP 会根据可用网络容量动态调整数据传输速率。
延迟与资源利用率
更一般地说,你可以将可变延迟视为动态资源划分的结果。
假设在两个电话交换机之间有一条电缆(或光纤),它可以同时承载多达 10,000 个通话。在这条电缆上交换的每个电路占据其中一个通话时隙。因此,你可以将这条电缆视为一种资源,可由最多 10,000 个同时用户共享。资源以静态方式划分:即使你是线路上唯一的通话,其他 9,999 个时隙未被使用,你的电路仍然被分配与线路完全利用时相同的固定带宽。
相比之下,互联网动态地共享网络带宽。发送方相互推挤,以尽可能快地将它们的包发送到线路上,网络交换机在每一时刻决定发送哪个包(即带宽分配)。这种方法有排队的缺点,但
第9章:分布式系统的麻烦
不可靠的网络
…网络带宽是动态共享的。发送方相互推搡争抢,尽可能快地将数据包发送到线路上,网络交换机则逐时刻决定哪些数据包应该发送(即带宽分配)。这种方法存在排队等待的缺点,但优点是最大化线路利用率。线路有固定成本,如果利用率更高,每个字节的发送成本就更低。
CPU也面临类似情况。如果在多个线程之间动态共享每个CPU核心,一个线程有时必须在操作系统的运行队列中等待,而另一个线程正在运行,因此线程可能被暂停不同长度的时间[36]。然而,这比给每个线程分配静态数量的CPU周期(参见第369页的”提供响应时间保证”)能更好地利用硬件。更好的硬件利用率也是云平台在同一台物理机上运行来自不同客户的多个虚拟机的原因。
在某些环境中,如果资源是静态划分的(例如专用硬件和独占带宽分配),可以实现延迟保证。然而,这些保证是以降低利用率为代价的——换句话说,成本更高。另一方面,采用动态资源分区的多租户模式提供了更高的利用率,因此成本更低,但缺点是延迟可变。
网络中的可变延迟并非自然法则,而只是成本/收益权衡的结果。
电路交换与分组交换的结合
一些尝试曾致力于构建同时支持电路交换和分组交换的混合网络。异步传输模式(Asynchronous Transfer Mode,ATM)在20世纪80年代曾是以太网的竞争对手,但除了电话网络核心交换机外,并未获得太多采用。InfiniBand有一些相似之处[37]:它在链路层实现了端到端流量控制,减少了网络中的排队需求,但仍可能因链路拥塞而遭受延迟[38]。
通过谨慎使用服务质量(Quality of Service,QoS)机制,例如数据包的优先级和调度以及准入控制(限制发送方速率),可以在分组网络上模拟电路交换,或提供统计有界延迟[27, 34]。像低延迟、低损耗和可扩展吞吐量(Low Latency, Low Loss, and Scalable Throughput,L4S)这样的新网络算法试图在客户端和路由器级别缓解一些排队和拥塞控制问题。Linux的流量控制器(Traffic Controller,TC)也允许应用程序出于QoS目的重新设置数据包的优先级。
然而,此类QoS机制目前并未在多租户数据中心和公共云中启用,也未在通过互联网通信时启用。当前部署的技术不允许我们对网络的延迟或可靠性做出任何保证;我们必须假设网络拥塞、排队和无界延迟会发生。因此,超时没有“正确”的值——它们需要通过实验来确定。
互联网服务提供商之间的对等协议以及通过边界网关协议(Border Gateway Protocol,BGP)建立路由,在更接近电路交换的层次上运行,而非典型的IP分组路由。在这个层次上,可以购买专用带宽。不过,互联网路由运行在网络级别,而非主机之间的单个连接级别,并且时间尺度要长得多。
不可靠的时钟
时钟和时间非常重要。应用程序以各种方式依赖时钟来回答以下问题:
- 此请求是否已超时?
- 该服务的第99百分位响应时间是多少?
- 在过去五分钟内,该服务平均每秒处理了多少查询?
- 用户在网站上花费了多长时间?
- 这篇文章是何时发布的?
- 应何时发送提醒邮件?
- 此缓存条目何时过期?
- 日志文件中此错误消息的时间戳是什么?
问题1-4测量持续时间(例如,请求发送和响应接收之间的时间间隔),而问题5-8描述时间点(事件发生在特定日期、特定时间)。
在分布式系统中,时间是一个棘手的问题,因为通信不是即时的;消息穿越网络从一台机器到另一台机器需要时间。消息接收的时间总是晚于发送的时间,但由于网络中的可变延迟,我们不知道晚多少。当涉及多台机器时,这一事实有时使得确定事件发生的顺序变得困难。
此外,网络上的每台机器都有自己的时钟,这是一种硬件设备——通常是石英晶体振荡器。这些设备并不完全精确,因此每台机器都有自己的时间概念,可能比其他机器稍快或稍慢。可以在一定程度上同步时钟;最常用的机制是网络时间协议(Network Time Protocol,NTP),它允许计算机时钟根据一组服务器报告的时间进行调整[39]。这些服务器反过来从更精确的时间源获取时间,例如GPS接收器。
单调时钟与墙上时钟
现代计算机至少有两种时钟:墙上时钟和单调时钟。虽然两者都测量时间,但区分它们非常重要,因为它们服务于不同的目的。
墙上时钟
墙上时钟按照你直觉上对时钟的期望工作:它根据日历返回当前日期和时间(也称为挂钟时间)。例如,Linux上的clock_gettime(CLOCK_REALTIME)和Java中的System.currentTimeMillis返回自纪元以来的秒数(或毫秒数),纪元定义为公历1970年1月1日午夜UTC,不考虑闰秒。一些系统使用其他日期作为参考点。(虽然Linux时钟被称为实时,但它与实时操作系统无关,如第369页的”提供响应时间保证”所述。)
墙上时钟通常与NTP同步,这意味着来自一台机器的时间戳(理想情况下)与另一台机器上的时间戳含义相同。然而,墙上时钟也存在各种古怪之处,如下一节所述。特别是,如果本地时钟比NTP服务器超前太多,它可能会被强制重置并看似跳回到之前的某个时间点。这些跳跃,以及闰秒引起的类似跳跃,使得墙上时钟不适合测量经过的时间[40]。
墙上时钟还可能因夏令时(DST)的开始和结束而经历跳跃,尽管可以通过始终使用UTC作为时区来避免,UTC没有夏令时。历史上,这些时钟的粒度相当粗(例如,在较旧的Windows系统上以10毫秒的步长前进[41])。在较新的系统上,这个问题不那么严重。
单调时钟
单调时钟适合测量持续时间(时间间隔),例如超时或服务的响应时间;Linux上的clock_gettime(CLOCK_MONOTONIC)或clock_gettime(CLOCK_BOOTTIME)[42]以及Java中的System.nanoTime使用单调时钟测量时间。这个名称源于此类时钟保证始终向前移动(而墙上时钟可能向后跳转)。
你可以在一个时间点检查单调时钟的值,执行一些操作,然后在稍后的时间再次检查时钟。两个值之间的差值告诉你两个检查之间经过了多长时间——更像是一个秒表而不是挂钟。然而,时钟的绝对值没有意义;它可能是自计算机启动以来的纳秒数,或类似的任意值。特别是,比较两台计算机的单调时钟值没有意义,因为它们含义不同。
在具有多个CPU插槽的服务器上,每个CPU可能有一个单独的计时器,这些计时器不一定与其他CPU同步[43]。操作系统会补偿任何差异,并试图向应用程序线程呈现单调的时钟视图,即使它们被调度到多个CPU上。然而,最好对这种单调性保证持保留态度[44]。
NTP可能会调整单调时钟前进的频率(这被称为倾斜时钟),如果它检测到计算机的本地石英比NTP服务器更快或更慢。默认情况下,NTP允许时钟速率加快或减慢最多0.05%,但不能导致单调时钟向前或向后跳跃。单调时钟的分辨率通常很好:在大多数系统上,它们可以测量微秒或更小的时间间隔。
在分布式系统中,使用单调时钟测量经过的时间(例如,超时)通常是可行的,因为它不假设不同节点时钟之间的任何同步,并且对测量的微小不准确不敏感。
时钟同步与准确性
单调时钟不需要同步,但墙上时钟需要根据NTP服务器或其他外部时间源进行设置才能有用。不幸的是,我们让时钟显示正确时间的方法并不像你希望的那样可靠和准确——硬件时钟和NTP可能是善变的野兽。仅举几个例子:
- 典型计算机中的石英时钟并不非常准确:它会漂移(运行速度比应有的快或慢)。时钟漂移随机器温度变化。Google假设其服务器的时钟漂移高达200 ppm(百万分之一)[45],这相当于每30秒与服务器重新同步一次时漂移6毫秒,或者每每天重新同步一次时漂移17秒。即使一切正常工作,这种漂移也限制了可达到的最佳精度。
- 如果计算机的时钟与NTP服务器相差太大,它可能会拒绝同步或被强制重置[39]。在此重置前后观察时间的任何应用程序可能会看到时间向后或突然向前跳转。
- 如果节点意外地被防火墙隔离而无法访问NTP服务器,这种错误配置可能在一段时间内未被察觉,在此期间漂移可能累积到与该节点和其他节点时钟之间的巨大差异。轶事证据表明,这在实际中确实会发生。
第9章:分布式系统的麻烦
不可靠的时钟
NTP 同步的局限性
- NTP 同步的精度受限于网络延迟,因此在网络拥堵、数据包延迟变化大的环境中,其准确性有限。
- 一项实验表明,通过互联网进行同步时,最小误差可达35毫秒[46],但偶尔的网络延迟尖峰可能导致约一秒的误差。根据配置不同,较大的网络延迟可能导致NTP客户端完全放弃同步。
- 某些NTP服务器本身错误或配置不当,报告的时间可能偏差数小时[47, 48]。NTP客户端通过查询多个服务器并忽略异常值来缓解此类错误。然而,将系统的正确性依赖于互联网上某个陌生人告知的时间,多少令人担忧。
- 闰秒会导致一分钟变为59秒或61秒,这会打乱未考虑闰秒的系统的时序假设[49]。闰秒曾导致许多大型系统崩溃[40, 50],这一事实表明关于时钟的错误假设多么容易潜入系统。处理闰秒的最佳方式可能是让NTP服务器“撒谎”,例如在一天内逐渐调整闰秒(称为“涂抹”)[51, 52],尽管实际NTP服务器的行为各不相同[53]。值得庆幸的是,从2035年起将不再使用闰秒,因此这个问题将不复存在。
- 在虚拟机中,硬件时钟被虚拟化,这给需要精确计时的应用程序带来了额外挑战[54]。当CPU核心在多个VM之间共享时,每个VM可能在另一个VM运行时暂停数十毫秒。从应用程序的角度看,这种暂停表现为当VM继续运行时时钟突然向前跳跃[29]。VM内部运行的NTP客户端无法感知暂停何时发生,因此可能错误地报告时钟精度[55]。
- 如果你在无法完全控制的设备(例如移动或嵌入式设备)上运行软件,你很可能根本无法信任它们的硬件时钟。有些用户会故意将设备硬件时钟设置为错误日期和时间,例如为了在游戏中作弊[56]。结果,时钟可能被设置为严重不准确的时间。
实现高精度时钟
如果愿意投入足够的资源,可以实现非常高的时钟精度。例如,欧洲金融工具市场指令II(MiFID II)要求所有高频交易基金将其时钟同步到UTC的100微秒以内,以帮助调试市场异常(如“闪崩”)并检测市场操纵行为[57]。
这种精度可以通过专用硬件(GPS接收器和/或原子钟)、精确时间协议(PTP)以及精心的部署和监控来实现[58, 59]。仅依赖GPS可能存在风险,因为GPS信号容易被干扰。在某些地点(例如靠近军事设施的地方),这种情况频繁发生[60]。一些云提供商已开始为其虚拟机提供高精度时钟同步[61],但时钟同步仍需非常谨慎。如果NTP守护进程配置错误或防火墙阻止了NTP流量,由漂移引起的时钟误差可能迅速变得很大。
关键要点
时钟看似简单易用,却有许多陷阱。一天可能并非恰好86400秒;一天中的时钟(wall clock)可能回拨;一个节点上的时间可能与另一个节点上的时间相差甚远。
依赖同步时钟的风险
我们在本章前面讨论了网络丢弃和任意延迟数据包的问题。尽管网络在大多数时候表现良好,但软件必须假设网络偶尔会出错,并优雅地处理此类故障。时钟也是如此:尽管它们在大多数时候工作正常,但健壮的软件需要准备好处理不正确的时钟。
问题的部分原因在于,不正确的时钟很容易被忽视。如果机器的CPU有缺陷或网络配置错误,它很可能根本无法工作,问题会很快被发现和修复。另一方面,如果其石英钟有缺陷或NTP客户端配置错误,大多数功能看起来仍然正常,尽管其时钟逐渐偏离现实越来越远。如果软件依赖准确的同步时钟,结果更可能是悄无声息且不易察觉的数据丢失,而不是戏剧性的崩溃[62, 63]。
因此,如果使用需要同步时钟的软件,务必仔细监控集群中所有机器之间的时钟偏移。任何时钟漂移过大的节点都应被宣布死亡并移除。这样的监控能确保你在错误时钟造成太大破坏之前注意到它们。
用于事件排序的时间戳
考虑一个特殊场景:跨多个节点的事件排序——这是一个容易诱使我们依赖时钟,但这样做很危险的例子[64]。例如,如果两个客户端写入一个分布式数据库,谁的操作先到达?哪个写入更“新”?
图9-3展示了一个在多主复制数据库中使用一天中时钟的危险示例(该示例类似于图6-8)。客户端A在节点1上写入 x = 1;该写入被复制到节点3;客户端B在节点3上递增 x(现在 x = 2);最后,两个写入都被复制到节点2。如图所示,当写入被复制到其他节点时,会根据写入来源节点的一天中时钟打上时间戳。在这个例子中,时钟同步非常好;节点1和节点3之间的偏差小于3毫秒,这在实际中可能比你预期的要好。
由于递增操作建立在之前的写入 x = 1 之上,我们可能期望 x = 2 的写入拥有更大的时间戳。不幸的是,图9-3中并非如此。写入 x = 1 的时间戳是42.004秒,但写入 x = 2 的时间戳却是42.003秒。换句话说,客户端B的写入在因果关系上晚于客户端A的写入,但B的写入却有一个更早的时间戳。
图9-3. 当一天中时钟没有完美同步时,依赖时间戳进行事件排序可能会导致问题。
如“处理冲突的写入”章节所述(第222页),解决不同节点上的并发写入冲突的一种方法是最后写入获胜(LWW),即对于给定键保留最大时间戳的写入,丢弃所有具有更旧时间戳的写入。在图9-3中,当节点2接收到这两个事件时,它将错误地认为 x = 1 是更新近的值,并丢弃 x = 2 的写入,从而导致递增操作丢失。
重要警告
即使使用紧密同步的NTP时钟,也可能出现这样的情况:发送方在时间戳100ms(根据发送方时钟)发送数据包,但接收方却在时间戳99ms(根据接收方时钟)收到——看起来数据包在发送之前就到达了,这是不可能的。NTP同步的精度本身受网络往返时间的限制,因此无法保证绝对正确的排序。
逻辑时钟 vs. 物理时钟
为了防止此类问题,可以确保在覆盖一个值时,新值总是拥有比被覆盖值更大的时间戳,即使该时间戳超前于写入者的本地时钟。然而,这会带来额外读取最大现有时间戳的成本。一些系统,包括Cassandra和ScyllaDB,被设计为避免这种额外的往返,而直接使用客户端时钟的时间戳配合LWW策略[62]。这种方法存在一些严重问题:
- 数据库写入可能神秘消失。 时钟偏慢的节点在时钟偏快节点写入后,需要经过两者的时钟偏差时间才能覆盖之前的值[63, 65]。这种情况可能导致大量数据在没有任何错误报告给应用程序的情况下被静默丢弃。
- LWW无法区分快速连续发生的写入(如图9-3中客户端B的递增显然发生在客户端A的写入之后)和真正并发的写入(两者彼此不知情)。 需要额外的因果关系追踪机制,如版本向量,以防止违反因果关系(参见“检测并发写入”第237页)。
- 两个节点可能独立生成具有相同时间戳的写入,尤其在时钟分辨率仅为毫秒时。 需要一个额外的决胜值(可以是一个大的随机数)来解决此类冲突,但这种方法也可能导致违反因果关系[62]。
因此,尽管通过保留最“新”的值并丢弃其他值来解决冲突很诱人,但务必要注意,“最新”的定义依赖于本地的一天中时钟,而该时钟可能并不准确。即使使用紧密同步的NTP时钟,你仍然可能在发送方时钟100ms时发送一个数据包,而接收方时钟却在99ms时收到——这看起来像是数据包在发送之前就到达了,这是不可能的。
那么,NTP同步能否精确到足以防止此类错误排序?很可能不能,因为NTP的同步精度本身受到网络往返时间的限制,此外还有石英漂移等其他误差源。要保证正确的排序,你需要时钟误差显著低于网络延迟,这是不可能的。
所谓逻辑时钟[66],基于递增的计数器而非振荡的石英晶体,是用于事件排序的更安全替代方案(参见“检测并发写入”第237页)。逻辑时钟不测量一天中的时间或经过的秒数,只测量事件的相对顺序(一个事件是否发生在另一个之前或之后)。相比之下,测量实际经过时间的一天中时钟和单调时钟被称为物理时钟。我们将在“ID生成器与逻辑时钟”(第417页)中更详细地探讨逻辑时钟。
带置信区间的时钟读数
你可能能够以微秒甚至纳秒分辨率读取机器的一天中时钟。但即使能得到如此精细的测量值,也不意味着该值实际上具有那样的精度。事实上,很可能并非如此。如前所述,不精确的石英时钟的漂移很容易达到几毫秒,即使每分钟都与本地网络上的NTP服务器同步一次也是如此。对于公共互联网上的NTP服务器,最佳误差也可能在35毫秒左右[46],并且偶尔的网络延迟尖峰可能导致约一秒的误差。
Chapter 9: 分布式系统的麻烦
可能达到的精度。实际上,很可能并非如此。如前所述,不精确的石英钟的漂移很容易达到几毫秒,即使每分钟与本地网络上的 NTP 服务器同步一次也是如此。如果使用公共互联网上的 NTP 服务器,最佳精度大概在几十毫秒以内,而网络拥塞时误差可能轻易超过 100 毫秒。
因此,将时钟读数视为一个时间点是没有意义的。它更像是一个时间范围,在一个置信区间内——例如,系统可能有 95% 的置信度认为当前时间在整分钟的 10.3 到 10.5 秒之间,但无法知道更精确的信息 [67]。如果我们只知道时间 +/- 100 毫秒,那么时间戳中的微秒数字基本上毫无意义。
不确定性边界可以根据你的时间源来计算。如果计算机直接连接了 GPS 接收器或原子钟,则预期的误差范围由设备决定,对于 GPS 来说,还取决于来自卫星的信号质量。如果你从服务器获取时间,则不确定性取决于自上次与服务器同步以来石英钟预期的漂移量,加上 NTP 服务器的不确定性,再加上到服务器的网络往返时间(作为一阶近似,假设你信任该服务器)。
不幸的是,大多数系统并不暴露这种不确定性;例如,当你调用 clock_gettime 时,返回值并不会告诉你时间戳的预期误差,因此你无法知道其置信区间是五毫秒还是五年。
当然也有例外。Google Spanner [45] 中的 TrueTime API 和 Amazon ClockBound 明确报告本地时钟的置信区间。当你请求当前时间时,你会得到两个值:[earliest, latest],即最早可能的时间戳和最晚可能的时间戳。基于其不确定性计算,时钟知道实际当前时间位于该区间内的某处。区间的宽度取决于多种因素,其中包括本地石英钟上次与更精确时钟源同步以来所经过的时间。
同步时钟用于全局快照
在 293 页的“快照隔离与可重复读”中,我们讨论了多版本并发控制 (MVCC),这对于需要同时支持小型快速读写事务和大型长时间运行的只读事务(例如备份或分析)的数据库来说是一个非常有用的特性。它允许只读事务在不锁定且不干扰读写事务的情况下,查看数据库在某个特定时间点的一致状态快照。
通常,MVCC 需要一个单调递增的事务 ID。如果写入发生在快照之后(即写入的事务 ID 大于快照的事务 ID),那么该写入对快照事务不可见。在单节点数据库上,简单的计数器足以生成事务 ID。
然而,当数据库分布在多台机器上,甚至跨多个数据中心时,生成全局单调递增的事务 ID(跨所有分片)就变得困难,因为需要协调。事务 ID 必须反映因果关系:如果事务 B 读取或覆盖了事务 A 先前写入的值,那么 B 必须拥有比 A 更高的事务 ID——否则快照将不一致。在大量小型快速事务的情况下,在分布式系统中创建事务 ID 会成为一个难以承受的瓶颈。(我们将在 417 页的“ID 生成器与逻辑时钟”中讨论此类 ID 生成器。)
我们可以使用同步的时间时钟的时间戳作为事务 ID 吗?如果能够做到足够好的同步,它们将具有正确的属性,因为较晚的事务具有较高的时间戳。问题在于时钟精度的不确定性。
Spanner 以这种方式实现了跨数据中心的快照隔离 [68, 69]。它使用 TrueTime API 报告的时钟置信区间,并基于以下观察:如果你有两个置信区间,每个区间由最早和最晚可能的时间戳组成(A = [Aearliest, Alatest] 和 B = [Bearliest, Blatest]),并且这两个区间不重叠(即 Aearliest < Alatest < Bearliest < Blatest),那么 B 肯定发生在 A 之后——毫无疑问。只有当区间重叠时,我们才不确定 A 和 B 发生的顺序。
为了确保事务时间戳反映因果关系,Spanner 在提交读写事务之前故意等待置信区间的长度。通过这样做,它确保任何可能读取该数据的事务发生在足够晚的时间,使得它们的置信区间不重叠。为了尽可能缩短等待时间,Spanner 需要将时钟不确定性降到最低;为此,Google 在每个数据中心部署了 GPS 接收器或原子钟,使时钟能够在约 7 毫秒内同步 [45]。
原子钟和 GPS 接收器在 Spanner 中并非严格必要。重要的是拥有一个置信区间——精确的时钟源只是帮助保持该区间较小。其他系统也开始采用类似的方法——例如,YugabyteDB 在 AWS 上运行时可以利用 ClockBound [70],现在还有其他几个系统在不同程度上依赖时钟同步 [71, 72]。
进程暂停
让我们考虑分布式系统中时钟危险使用的另一个例子。假设你有一个每个分片只有一个领导者的数据库。只有领导者才允许接受写入。一个节点如何知道它仍然是领导者(尚未被其他节点宣告死亡)并且可以安全地接受写入?
一种选择是领导者从其他节点获取一个租约,这类似于带有超时的锁 [73]。任意时刻只有一个节点能持有租约。因此,当一个节点获取租约时,它知道自己在租约到期前的特定时间内是领导者。为了保持领导者地位,节点必须在租约到期前定期续租。如果节点发生故障,它将停止续租,因此另一个节点可以在租约到期时接管。
你可以想象请求处理循环类似于这样:
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 秒,然后才继续。在这种情况下,当请求被处理时,租约很可能已经过期,而另一个节点已经接管成为领导者。但是,没有东西告诉这个线程它暂停了这么久,因此代码直到循环的下一次迭代才会注意到租约已过期——而此时它可能已经通过处理请求做出了不安全的事情。
认为线程可能暂停这么长时间合理吗?不幸的是,是的。有多种原因可能导致这种情况:
- 线程争用:访问共享资源(如锁或队列)的线程竞争可能导致线程花费大量时间等待。在具有更多 CPU 核心的机器上,此类问题通常更严重,并且争用问题可能难以诊断 [74]。
- 垃圾收集:许多编程语言运行时(例如 JVM)有一个垃圾收集器,偶尔需要停止所有正在运行的线程。在过去,此类“停止世界”的垃圾收集暂停有时会持续几分钟 [75]!使用现代 GC 算法,这个问题不那么严重,但 GC 暂停仍然可能显著(见 370 页的“限制垃圾收集的影响”)。
- 虚拟化环境:虚拟机可以被挂起(暂停所有进程的执行并将内存内容保存到磁盘)并恢复(恢复内存内容并继续执行)。这种暂停可能发生在进程执行的任何时刻,并且可以持续任意长的时间。此功能有时用于将虚拟机从一个主机实时迁移到另一个主机而无需重启,在这种情况下,暂停的持续时间取决于进程写入内存的速率 [76]。
- 终端用户设备:在笔记本电脑和手机等终端用户设备上,执行也可能被任意挂起和恢复(例如,当用户合上笔记本电脑的盖子时)。
- 上下文切换:当操作系统切换到另一个线程,或 hypervisor 切换到另一个虚拟机(在虚拟机中运行时),当前正在运行的线程可能会在代码中的任意点暂停。对于虚拟机来说,在其他虚拟机上花费的 CPU 时间被称为 窃取时间 (steal time)。如果机器负载很高——即有一个很长的线程队列等待运行——那么暂停的线程可能需要一段时间才能再次运行。
- 同步磁盘访问:如果应用程序执行同步磁盘访问,线程可能会暂停等待缓慢的磁盘 I/O 操作完成 [77]。在许多语言中,磁盘访问可能意外发生,即使代码没有明确提到文件访问——例如,Java 类加载器在类首次使用时延迟加载类文件,这可能发生在程序执行的任何时刻。I/O 暂停和 GC 暂停甚至可能共同导致延迟叠加 [78]。如果磁盘实际上是网络文件系统或网络块设备(如 Amazon EBS),则 I/O 延迟进一步受网络延迟变化的影响 [31]。
- 交换/分页:如果操作系统配置了磁盘交换(分页),简单的内存访问可能会导致页面错误,需要将磁盘上的页面加载到内存中。在这个缓慢的 I/O 操作期间,线程被暂停。如果内存压力很大,这可能反过来要求将不同的页面换出到磁盘。在极端情况下,操作系统可能花费大部分时间将页面换入和换出内存,而实际完成的工作很少(这被称为 抖动 (thrashing))。为了避免这个问题,服务器上通常禁用分页(如果你宁愿杀死进程来释放内存也不愿冒抖动风险)。
第9章:分布式系统的麻烦
- Unix 进程可以通过发送
SIGSTOP信号被暂停——例如,在 shell 中按下 Ctrl-Z。该信号会立即停止进程获取任何 CPU 时间片,直到通过SIGCONT信号恢复,此时它会从暂停处继续运行。即使你的环境通常不使用SIGSTOP,操作工程师也可能意外地发送该信号。
所有这些事件都可以在任何时刻抢占正在运行的线程,并在稍后恢复执行,而线程甚至不会察觉。这个问题类似于使单机上的多线程代码具有线程安全性;你不能对时序做任何假设,因为可能发生任意的上下文切换和并行。
在单机上编写多线程代码时,我们有相当好的工具来使其线程安全:互斥锁、信号量、原子计数器、无锁数据结构、阻塞队列等。不幸的是,这些工具并不能直接移植到分布式系统中,因为分布式系统没有共享内存——只有通过不可靠网络发送的消息。
分布式系统中的节点必须假设其执行过程可以在任何时刻暂停一段显著长的时间,甚至在函数执行的中途。在暂停期间,外部世界持续运转,甚至可能因为暂停节点未响应而宣布其死亡。最终,暂停的节点可能继续运行,甚至没有意识到自己曾处于休眠状态,直到稍后检查时钟时才发现。
提供响应时间保证
正如我们刚刚讨论的,在许多编程语言和操作系统中,线程和进程可能暂停无限长的时间。然而,如果付出足够努力,这些暂停原因是可以消除的。
某些软件运行在指定时间内未能响应可能导致严重损害的环境中。例如,控制飞机、火箭、机器人、汽车和其他物理对象运动的计算机必须快速且可预测地响应传感器输入。在这些所谓的硬实时系统中,软件必须在指定截止时间前响应;未能满足截止时间可能导致整个系统失效。
在嵌入式系统中,实时意味着系统经过精心设计和测试,能够在所有情况下满足指定的时序保证。这与 Web 上更模糊的“实时”用法形成对比,后者描述的是服务器向客户端推送数据以及没有硬性响应时间限制的流处理(见第 12 章)。
例如,如果车载传感器检测到你当前正在经历碰撞,你不会希望气囊的释放因气囊释放系统中不合时宜的 GC 暂停而延迟。
在系统中提供实时保证需要软件栈各层的支持。需要实时操作系统(RTOS),它允许进程在指定时间间隔内获得保证的 CPU 时间分配;库函数必须记录其最坏情况执行时间;动态内存分配可能受到限制或被完全禁止(存在实时垃圾收集器,但应用程序仍必须确保不交给垃圾收集器过多工作);还需要进行大量测试和测量,以确保保证得到满足。
所有这些都需要大量额外工作,并严重限制了可用的编程语言、库和工具的范围(因为大多数语言和工具不提供实时保证)。由于这些原因,开发实时系统非常昂贵,并且它们最常用于安全关键的嵌入式设备。此外,“实时”并不等同于“高性能”——事实上,实时系统的吞吐量可能更低,因为它们必须优先保证及时响应而非其他一切(参见第 356 页的“延迟与资源利用率”)。
对于大多数服务端数据处理系统而言,实时保证在经济上或技术上并不合适。因此,这些系统必须承受在非实时环境中运行所带来的暂停和时钟不稳定性。
限制垃圾收集的影响
垃圾收集曾经是进程暂停的最大原因之一 [79],但幸运的是,GC 算法已大幅改进。现在,经过适当调优的收集器通常只会使进程暂停几毫秒。Java 运行时提供了诸如并发标记清除(CMS)、垃圾优先(G1)、Z 垃圾收集器(ZGC)、Epsilon 和 Shenandoah 等收集器。每种收集器针对不同的内存配置进行了优化,例如高频对象创建、大堆等。相比之下,Go 提供了更简单的并发标记清除垃圾收集器,并试图自我优化。
如果需要完全避免 GC 暂停,一种选择是使用没有垃圾收集器的语言。例如,Swift 使用自动引用计数来确定何时可以释放内存,而 Rust 和 Mojo 通过类型系统跟踪对象生命周期,使编译器能够确定内存需要分配多长时间。
也可以在使用垃圾收集语言的同时减轻暂停的影响。例如,对象可以存储并重用池中,而不是丢弃,或者数据可以在堆外分配。更极端的方法是,将 GC 暂停视为节点短暂的计划停机,并让其他节点在某个节点进行垃圾收集时处理客户端的请求。如果运行时能够提前警告应用程序节点即将需要 GC 暂停,应用程序可以停止向该节点发送新请求,等待其完成处理未完成的请求,然后在没有请求进行时执行 GC。这种技巧对客户端隐藏了 GC 暂停,并降低了响应时间的高百分位数 [80, 81]。
该想法的一个变体是仅对短生命周期对象使用垃圾收集器(这些对象收集速度快),并定期重启进程,以避免积累足够多的长生命周期对象而需要对它们进行完整 GC [79, 82]。一次可以重启一个节点,并在计划重启前将流量移离该节点,类似滚动升级(见第 5 章)。
这些措施不能完全防止 GC 暂停,但可以有用地减少其对应用程序的影响。
知识、真相与谎言
到目前为止,本章探索了分布式系统与单机运行程序的不同之处。分布式系统没有共享内存,只有通过延迟可变的不稳定网络传递消息,并且系统可能遭受部分失效、不可靠时钟和处理暂停。
如果你不熟悉分布式系统,这些问题的影响会令人深感困惑。网络中的节点无法确切知道其他节点的任何信息——它只能根据接收到的(或未接收到的)消息做出猜测。节点只能通过与其他节点交换消息来了解另一个节点的状态(存储了什么数据,是否正常运行等)。如果远程节点没有响应,就无法知道其状态,因为网络问题无法可靠地与节点问题区分开来。
关于这些系统的讨论近乎哲学:在我们的系统中,我们如何知道什么是真的,什么是假的?如果感知和测量的机制不可靠,我们对这种知识能有多大的把握 [83]?软件系统是否应该遵循我们对物理世界所期望的规律,例如因果律?
幸运的是,我们不必走得太远,去思考生命的意义。在分布式系统中,我们可以陈述对行为的假设(系统模型),并以满足这些假设的方式设计实际系统。算法可以在特定系统模型内被证明是正确的。这意味着即使底层系统模型提供的保证非常少,也能实现可靠的行为。
然而,尽管可以在不可靠的系统模型中使软件行为良好,但做到这一点并不简单。在本章的剩余部分,我们将进一步探讨分布式系统中知识和真相的概念,这将帮助我们思考我们可以做出哪些假设,以及我们可能希望提供哪些保证。在第 10 章中,我们将继续研究一些分布式算法的例子,这些算法在特定假设下提供特定保证。
多数原则
想象一个存在不对称故障的网络:一个节点能够接收到发送给它的所有消息,但从该节点发出的任何消息都被丢弃或延迟 [22]。尽管该节点工作完全正常,并能接收来自其他节点的请求,但其他节点无法听到它的响应。超时后,其他节点宣布它死亡,因为它们没有收到该节点的消息。情况像噩梦一样展开——半断开的节点被拖向坟墓,边踢边喊“我没死!”——但由于没人能听到它的喊叫,送葬队伍冷静地继续前进。
在一个稍微不那么噩梦般的场景中,半断开的节点可能注意到它发送的消息没有被其他节点确认,从而意识到网络中一定存在故障。尽管如此,该节点被其他节点错误地宣布死亡,并且它对此无能为力。
作为第三种场景,想象一个节点暂停执行一分钟。在此期间,不处理任何请求,也不发送任何响应。其他节点等待、重试、变得不耐烦,最终宣布该节点死亡并将其装上灵车。最后,暂停结束,节点的线程继续运行,仿佛什么都没发生过。其他节点惊讶地看到那个被认为已经死亡的节点突然从棺材中抬起头来,完全健康,并开始愉快地与旁观者聊天。起初,暂停的节点甚至没有意识到一分钟已经过去,它已经被宣布死亡——从它的角度看,自从上次与其他节点通信以来几乎没经过什么时间。
这些故事的寓意是,节点不一定能相信自己对情况的判断。分布式系统不能完全依赖单个节点,因为节点随时可能失效,可能导致系统卡住无法恢复。相反,许多分布式算法依赖于法定人数(即节点之间的投票;参见第 231 页的“使用法定人数进行读写”):决策需要来自多个节点的最少票数,以减少对任何单一节点的依赖。
这包括关于宣布节点死亡的决策。如果一组法定人数节点宣布另一个节点死亡,那么它必须被视为死亡,即使该节点仍然感觉自己非常活跃。单个节点必须遵守法定人数的决定并退出。最常见的是,法定人数是超过半数的绝对多数(尽管其他类型的法定人数也是可能的)。多数法定人数允许系统在少数节点故障时继续工作(对于三个节点,一个节点故障系统仍可工作;五个节点可以容忍两个节点故障;以此类推)。
9.1 分布式系统的麻烦
分布式锁与租约
分布式应用中的锁和租约容易被误用,是常见的bug来源 [84]。下面来看一个它们可能出错的典型案例。
在“进程暂停”(第366页)中,我们看到租约是一种带超时的锁,如果旧持有者停止响应(可能是因为崩溃、暂停时间过长或网络断开),租约可以分配给新持有者。当系统要求某些东西只能有一个时,可以使用租约。例如:
- 一个数据库分片只允许一个节点作为领导者,以避免脑裂(参见“处理节点故障”第204页)。
- 只允许一个事务或客户端更新某个特定资源或对象,以防止并发写入导致数据损坏。
- 在大数据处理作业中,只允许一个节点处理给定的输入文件,以避免多个节点重复执行相同的工作造成资源浪费。
值得仔细思考的是,如果多个节点同时认为自己持有租约(可能是因为进程暂停),会发生什么。在第三个例子中,后果仅仅是浪费一些计算资源,问题不大。但在前两个例子中,可能导致数据丢失或损坏,这就严重得多。
例如,图9-4展示了一个因锁实现不正确而导致数据损坏的bug。(这个bug并非理论上的;HBase曾经就存在这个问题 [85, 86]。)假设你想确保存储服务中的一个文件一次只能被一个客户端访问,因为如果多个客户端试图写入该文件,文件就会损坏。你尝试通过要求客户端在访问文件前从锁服务获取租约来实现这一点。这种锁服务通常使用共识算法实现,详见第10章。
这个问题是“进程暂停”(第366页)中讨论内容的一个实例。如果持有租约的客户端暂停时间过长,其租约就会过期。另一个客户端可以随后为同一文件获取租约并开始写入文件。当暂停的客户端恢复时,它(错误地)认为自己仍然持有有效租约,并继续写入文件。现在我们遇到了脑裂情况:两个客户端的写入冲突并损坏了文件。
图9-4. 分布式锁的错误实现:客户端1认为自己仍然持有有效租约,尽管租约已过期,从而损坏了存储中的文件。
图9-5展示了另一个导致类似后果的不同问题。在这个例子中,没有进程暂停,只有客户端1崩溃。就在客户端1崩溃之前,它向存储服务发送了一个写请求,但这个请求在网络中被延迟了很长时间(回顾“网络故障实践”第350页,数据包有时可能延迟一分钟或更久)。当写请求到达存储服务时,客户端1的租约已经超时,这允许客户端2获取租约并发出自己的写入。结果与图9-4类似,导致数据损坏。
图9-5. 前租约持有者的消息可能延迟很长时间,并在另一个节点接管租约后才到达。
围栏僵尸与延迟请求
僵尸(zombie)一词有时用于描述前租约持有者,它尚未发现自己已失去租约,仍在扮演当前租约持有者的角色。由于我们无法完全排除僵尸,因此必须确保它们不能以脑裂形式造成任何损害。这称为围栏僵尸(fencing off the zombie)。
、通过云提供商管理接口关闭虚拟机,甚至物理断电机器 [87]。这种方法有时被称为 爆头另一个节点(shoot the other node in the head, STONITH),但我们倾向于不使用如此暴力的术语。无论如何,这种方法并不特别有效:它无法防范图9-5中展示的大网络延迟;所有节点可能互相关闭 [19];等到僵尸被检测到并关闭时,可能已经为时已晚,数据可能已经损坏。
一个更健壮的围栏解决方案,既能防范僵尸又能防范延迟请求,如图9-6所示。
图9-6. 通过允许写入仅按递增的围栏令牌顺序进行,使对存储的访问安全。
假设每次锁服务授予锁或租约时,它也会返回一个围栏令牌(fencing token),这是一个每次授予锁时都会增加的数字(例如,由锁服务递增)。然后我们可以要求客户端每次向存储服务发送写请求时,都必须包含其当前的围栏令牌。
围栏令牌有其他几种名称。在Google的锁服务Chubby中,它们被称为 序列器(sequencers)[88];在Kafka中,被称为纪元号(epoch numbers)。在我们将于第10章讨论的共识算法中,选票编号(Paxos)或任期编号(Raft)起类似作用。
在图9-6中,客户端1获取了令牌为33的租约,但随后进入长时间暂停,租约过期。客户端2获取了令牌为34的租约(数字总是递增的)并向存储服务发送写请求,其中包含令牌。之后,客户端1恢复活动并向存储服务发送写入,包含其令牌值33。然而,存储服务记得已经处理过更高令牌号(34)的写入,因此拒绝了令牌33的请求。刚刚获取租约的客户端必须立即向存储服务进行一次写入,一旦该写入完成,所有僵尸都被围栏了。这些操作类似于我们在“悲观与乐观并发控制”(第318页)中看到的乐观并发控制(OCC)技术,但区别在于围栏是永久的,而并发控制失败可以重试。
如果使用ZooKeeper作为锁服务,可以使用事务ID zxid 或节点版本 cversion 作为围栏令牌 [85]。使用etcd时,修订号(revision number)与租约ID起类似作用 [89]。Hazelcast中的 FencedLock API 显式生成围栏令牌 [90]。
这种机制要求存储服务能够检查写入是否基于过期的令牌。或者,存储服务只需支持一种写入操作:仅当自当前客户端上次读取对象后,该对象未被其他客户端写入时,写入才成功,类似于原子CAS操作。例如,对象存储服务支持这种检查:Amazon S3称之为条件写入(conditional writes),Azure Blob Storage称之为条件标头(conditional headers),Google Cloud Storage称之为请求前提条件(request preconditions)。
带多个副本的围栏
如果你的客户端只需要写入一个支持条件写入的存储服务,那么锁服务有些多余 [91, 92],因为租约分配可以直接基于该存储服务实现 [93]。但是,一旦你有了围栏令牌,就可以将其用于多个服务或副本,并确保旧租约持有者在所有服务或副本上都被围栏。
例如,假设存储服务是一个无主复制键值存储,采用LWW冲突解决(参见“无主复制”第229页)。在这种系统中,客户端直接向每个副本发送写入,每个副本根据客户端分配的时间戳独立决定是否接受写入。
如图9-7所示,你可以将写入者的围栏令牌放在时间戳的最高有效位或数字中。这样你可以确保新租约持有者生成的任何时间戳都大于旧租约持有者生成的任何时间戳,即使旧租约持有者的写入发生在稍后。
图9-7. 在无主复制系统中使用围栏令牌:在LWW写入中,将围栏令牌编码为时间戳的最高有效位,确保新租约持有者的写入优先于旧租约持有者。
在图9-7中,客户端2的围栏令牌为34,因此它所有以34开头的…时间戳都大于客户端1生成的以33开头的任何时间戳。客户端2向一个仲裁(quorum)副本写入,但无法到达副本3。这意味着当僵尸客户端1稍后尝试写入时,其写入可能在副本3成功,尽管被副本1和2忽略。这不是问题,因为后续的仲裁读取会优先选择来自客户端2的、具有更大时间戳的写入,而读修复或反熵最终会覆盖客户端1写入的值。
第九章:分布式系统的麻烦
上下文提示
本部分接续上一段,讨论在无主复制数据库中使用隔离令牌保护写入,以及拜占庭故障、拜占庭将军问题、系统模型等内容。图中编号与原文一致。
图 9-7. 使用隔离令牌保护对无主复制数据库的写入
从这些例子可以看出,假设在任何时刻只有一个节点持有租约是不安全的。幸运的是,只要稍加小心,就可以使用隔离令牌来防止僵尸节点和延迟请求造成损害。
拜占庭故障
隔离令牌可以检测并阻止节点因疏忽而出现错误(例如,因为它尚未发现自己的租约已过期)。然而,如果节点有意破坏系统的保证,它只需发送带有伪造隔离令牌的消息即可轻松做到。
在本书中,我们假设节点不可靠但诚实。它们可能响应缓慢或永不响应(由于故障),其状态可能过时(由于垃圾回收暂停或网络延迟),但我们假设如果节点做出响应,它说的是“真话”——就其所知而言,它正在遵守协议规则。
如果节点有可能“撒谎”(发送任意的错误或损坏响应),分布式系统的问题就会变得困难得多——例如,一个节点可能在同一个选举中投出多次相互矛盾的选票。这种行为被称为拜占庭故障,在这种不可信的环境中达成共识的问题被称为拜占庭将军问题 [94]。
拜占庭将军问题
拜占庭将军问题是两军问题 [95] 的泛化。两军问题设想两位军队将军需要就一个作战计划达成一致。由于他们驻扎在不同地点,只能通过信使通信,而信使有时会延迟或丢失(就像网络中的数据包一样)。我们将在第十章讨论这个共识问题。
在拜占庭版本的问题中,有 n 位将军需要达成一致,而他们的努力因军队中的叛徒而受挫。大多数将军是忠诚的,因此发送真实的消息,但叛徒可能试图通过发送虚假或不实的消息来欺骗和迷惑其他将军。事先并不知道谁是叛徒。
名称来源
拜占庭(Byzantium)是古希腊城市,后来成为君士坦丁堡,即现在土耳其的伊斯坦布尔所在地。没有任何历史证据表明拜占庭的将军比其他地方的将军更容易搞阴谋诡计。实际上,这个名称源于“拜占庭式”一词,意指极度复杂、官僚、诡诈——早在计算机出现之前,这个词就已用于政治领域 [96]。Lamport 想选择一个不会冒犯任何读者的国籍,他被告知将其称为“阿尔巴尼亚将军问题”并不是一个好主意 [97]。
拜占庭容错的用途
如果一个系统即使部分节点发生故障且不遵守协议,或者恶意攻击者干扰网络,仍能继续正确运行,则该系统具有拜占庭容错性。这种关切在特定情况下是相关的。例如:
- 航空航天环境:计算机内存或 CPU 寄存器中的数据可能因辐射而损坏,导致其对其他节点做出任意不可预测的响应。由于系统故障的代价极其高昂(例如,飞机坠毁导致机上所有人丧生,或火箭与国际空间站相撞),飞行控制系统必须能容忍拜占庭故障 [98, 99]。
- 多方参与系统:某些参与者可能试图欺骗或欺诈他人。在这种情况下,节点简单地信任另一个节点的消息是不安全的,因为这些消息可能带有恶意意图。比特币和其他基于区块链的加密货币背后的共识机制,可以看作是一种让互不信任的各方就交易是否发生达成一致的方式,而无需依赖中央权威 [100]。
然而,在本书讨论的这类系统中,我们通常可以安全地假设不存在拜占庭故障。在数据中心里,所有节点都由你的组织控制(因此可以希望它们是可信的),辐射水平足够低,内存损坏不是主要问题(尽管轨道数据中心正在考虑中 [101])。多租户系统存在互不信任的租户,但它们通过防火墙、虚拟化和访问控制策略相互隔离,而不是使用拜占庭容错。实现拜占庭容错的协议相当昂贵 [102],而容错嵌入式系统依赖于硬件层面的支持 [98]。在大多数服务器端数据系统中,部署拜占庭容错解决方案的成本使其不切实际。
Web 应用程序确实需要预期来自受终端用户控制的客户端(例如 Web 浏览器)的任意恶意行为。这就是输入验证、清理和输出转义如此重要的原因——例如,防止 SQL 注入和跨站脚本攻击。然而,我们通常不在这里使用拜占庭容错协议,而是简单地将服务器作为判断客户端行为是否允许的权威。在点对点网络中,没有这样的中央权威,拜占庭容错就更为相关 [103, 104]。
软件错误与拜占庭容错
软件中的错误可被视为一种拜占庭故障,但如果你在所有节点上部署相同的软件,拜占庭容错算法也无法挽救你。大多数拜占庭容错算法要求超过三分之二的节点(超级多数)能够正确运行(例如,如果你有四个节点,最多只能有一个节点出现故障)。要将这种方法用于对抗错误,你必须有四个相同软件的独立实现,并希望特定错误只出现在四个实现之一中。
同样,如果协议能保护我们免受漏洞、安全破坏和恶意攻击,那将非常有吸引力。不幸的是,这也是不现实的。在大多数系统中,如果攻击者能攻破一个节点,他们很可能可以攻破所有节点,因为这些节点很可能运行着相同的软件。因此,传统的机制(认证、访问控制、加密、防火墙等)仍然是抵御攻击者的主要手段。
弱形式的撒谎
虽然我们假设节点通常是诚实的,但在软件中添加防范“撒谎”弱形式的机制可能是值得的——例如,由硬件问题、软件错误和错误配置导致的无效消息。这类保护机制并非完整的拜占庭容错,因为它们无法抵御顽固的对手,但仍然是迈向更好可靠性的简单而实用的步骤。例如:
- 由于硬件问题或操作系统、驱动程序、路由器等的错误,网络数据包有时确实会损坏。通常,损坏的数据包会被 TCP 和 UDP 内置的校验和捕获,但有时它们能逃脱检测 [105, 106, 107]。简单的措施通常足以防范此类损坏,例如在应用层协议中添加校验和。TLS 加密连接也能提供防损坏保护。
- 可公开访问的应用程序必须仔细清理任何用户输入——例如,通过转义某些字符以防止 SQL 注入攻击,检查值是否在合理范围内,以及限制字符串大小以防止通过大内存分配进行拒绝服务攻击。防火墙背后的内部服务或许可以放宽对输入的检查,但在协议解析器中进行基本检查仍然是个好主意 [105]。
- NTP 客户端可以配置多个服务器地址。同步时,客户端联系所有服务器,估计它们的误差,并检查大多数服务器是否同意一个时间范围。只要大多数服务器正常,报告错误时间的错误配置 NTP 服务器会被检测为异常值,并从同步中排除 [39]。使用多个服务器使 NTP 比仅使用单个服务器更健壮。
系统模型与现实
许多算法被设计用来解决分布式系统问题——例如,我们将在第十章中研究共识问题的解决方案。为了有用,这些算法需要容忍本章讨论的分布式系统的各种故障。
算法的编写方式不能过于依赖其运行的硬件和软件配置细节。这反过来要求我们以某种方式形式化我们预期系统中会发生的故障类型。我们通过定义系统模型来实现这一点,系统模型是一种描述算法假设的抽象。
关于时间假设,有三种常用的系统模型:
同步模型
同步模型假设网络延迟有界、进程暂停有界、时钟误差有界。这并不意味着时钟精确同步或网络延迟为零;它只是意味着你知道网络延迟、暂停和时钟漂移永远不会超过一个固定的上限 [108]。同步模型对于大多数实际系统来说不是一个现实的模型,因为(如本章所述)确实会发生无界延迟和暂停。
部分同步模型
部分同步意味着系统大多数时间表现得像同步系统,但有时网络延迟、进程暂停和时钟漂移会超过界限 [108]。这是许多系统的现实模型。大多数时候,
第9章:分布式系统的麻烦
系统模型
部分同步模型
部分同步(partial synchrony)是指一个系统在大多数时候表现得像一个同步系统,但有时会超过网络延迟、进程暂停和时钟漂移的界限[108]。这是许多系统的现实模型。大多数时候,网络和进程都表现得相当良好——否则我们将永远无法完成任何事情——但我们必须考虑到,任何时序假设都可能偶尔被打破。当这种情况发生时,网络延迟、暂停和时钟误差可能会变得任意大。
异步模型
在该模型中,算法不允许做出任何时序假设——事实上,它甚至没有时钟(因此不能使用超时)。有些算法可以针对异步模型设计,但该模型限制非常严格。
节点故障模型
除了时序问题,我们还需要考虑节点故障。一些常见的节点系统模型如下:
- 崩溃-停止故障:在崩溃-停止(或故障-停止)模型中,算法可以假设节点只能以一种方式失败——即崩溃[109]。节点可能在任何时刻突然停止响应,此后该节点将永远消失——它永远不会恢复。
- 崩溃-恢复故障:在崩溃-恢复模型中,我们假设节点可能随时崩溃,并在未知的时间后重新开始响应。假设节点具有稳定存储(即非易失性磁盘存储),该存储在崩溃期间得以保留,而内存状态则被认为丢失。
- 性能下降与部分功能:除了崩溃和重启,节点还可能变慢。它们可能仍能响应健康检查请求,但同时慢到无法完成任何实际工作。例如,千兆网络接口可能因驱动错误[110]突然降至1 Kb/s的吞吐量;处于内存压力下的进程可能将大部分时间花在垃圾收集上[111];磨损的SSD可能表现出不稳定的性能;硬件可能受到高温、松动的连接器、机械振动、电源问题、固件错误等影响[112]。这种情况被称为跛行节点(limping node)、灰色故障(gray failure)或缓慢故障(fail-slow)[113],其处理难度甚至高于干净故障的节点。另一个相关问题是,当一个进程停止执行其应该做的某些事情而其他方面继续工作时——例如,由于后台线程崩溃或死锁[114]。
- 拜占庭(任意)故障:节点可能做任何事情,包括试图欺骗和误导其他节点,如前一节所述。
对于真实系统的建模,带有崩溃-恢复故障的部分同步模型通常是最有用的。它允许无界网络延迟、进程暂停和缓慢节点。但分布式算法如何应对该模型?
算法的正确性定义
要定义算法正确意味着什么,我们可以描述其属性(properties)。例如,排序算法的输出具有如下属性:对于输出列表中任意两个不同元素,靠左的元素小于靠右的元素。这只是一种形式化定义列表已排序的方式——即排序列表的不变式。
类似地,我们可以写出我们期望分布式算法具备的属性,以定义其正确性。例如,如果我们为锁生成fencing token(见第374页“隔离僵尸和延迟请求”),我们可能要求算法具有以下属性:
- 唯一性:对
fencing token的两个请求不会返回相同的值。 - 单调序列:如果请求
x返回了令牌t_x,请求y返回了令牌t_y,且x在y开始之前完成,则t_x < t_y。 - 可用性:一个请求
fencing token且未崩溃的节点最终会收到响应。
在系统模型中,如果一个算法在该系统模型假设可能发生的所有情况下始终满足其属性,则该算法是正确的。然而,如果所有节点都崩溃,或者所有网络延迟突然变得无限长,那么任何算法都无法完成任何事。那么,即使在允许完全失败的系统模型中,我们如何还能做出有用的保证?
区分安全性与活跃性
为了澄清情况,有必要区分两类属性:安全性(safety)和活跃性(liveness)。在上述示例中,唯一性和单调序列是安全性属性,而可用性是活跃性属性。
这两类属性的区别是什么?一个提示是,活跃性属性在其定义中通常包含“最终”一词。(是的,你猜对了——最终一致性是一种活跃性属性[115]。)
安全性通常被非正式地定义为“坏事不会发生”,而活跃性则是“好事最终会发生”。然而,最好不要过度解读这些非正式定义,因为“好”和“坏”是价值判断,不适用于算法的精确定义。安全性和活跃性的实际定义更加精确[116]:
- 如果违反了安全性属性,我们可以指出违反发生在哪个特定时间点(例如,如果违反了唯一性属性,我们可以确定返回重复
fencing token的具体操作)。安全性属性被违反后,无法撤销——损害已经造成。 - 活跃性属性则相反。它在某个时间点可能不成立(例如,节点可能已经发送了请求但尚未收到响应),但总有可能在未来被满足(即收到响应)。
区分安全性和活跃性的一个优势是,它有助于我们应对困难的系统模型。对于分布式算法,通常要求安全性属性在系统模型的所有可能情况下始终成立[108]。即使所有节点崩溃或整个网络故障,算法也必须确保其不返回错误结果(即安全性属性仍被满足)。
然而,对于活跃性属性,我们可以做出一些例外说明——例如,我们可以说,仅当大多数节点未崩溃且网络最终从故障中恢复时,请求才需要收到响应。部分同步模型的定义要求系统最终回到同步状态——即任何网络中断时期只持续有限时间,然后被修复。
将系统模型映射到现实世界
安全性和活跃性属性以及系统模型对于推理分布式算法的正确性非常有用。然而,在实践中实现算法时,现实的混乱事实又会重新困扰我们,并且很明显,系统模型是对现实的简化抽象。
例如,崩溃-恢复模型中的算法通常假设稳定存储中的数据能在崩溃中存活。但是,如果磁盘上的数据因硬件错误或配置错误而损坏或擦除[117]怎么办?如果服务器出现固件错误,重启后无法识别其硬盘,即使硬盘正确连接到服务器[118]怎么办?
法定人数算法(见第231页“使用法定人数进行读写”)依赖于节点记住其声称已存储的数据。如果节点可能遭受失忆并忘记先前存储的数据,那将破坏法定人数条件,从而破坏算法的正确性。也许需要一种新的系统模型,其中我们假设稳定存储大多数情况下能在崩溃中存活,但有时也可能丢失。但那样模型变得更难推理。
算法的理论描述可以声明某些事情根本不会发生——在非拜占庭系统中,我们确实必须对可能和不可能发生的故障做出假设。然而,真实的实现可能仍然需要包含处理被认为不可能发生的事情的代码,即使这种处理最终只是printf("真倒霉")和exit(666)——即让人工操作员来清理混乱[119]。(这是计算机科学与软件工程之间的区别之一。)
这并非说理论性的抽象系统模型毫无价值——恰恰相反。它们对于将真实系统的复杂性提炼为一组可控的、能够推理的故障,从而理解问题并系统性地尝试解决它,具有难以置信的帮助。
形式化方法与随机化测试
我们如何知道一个算法满足所需的属性?由于并发、部分失效和网络延迟,存在大量潜在状态。我们需要保证属性在所有可能的状态下成立,并确保我们没有遗漏任何边界情况。
一种方法是通过数学描述算法,并使用证明技术来展示它在系统模型允许的所有情况下满足所需属性,从而对算法进行形式化验证(formal verification)。证明算法正确并不意味着它在真实系统上的实现总会表现正确,但这是非常好的第一步,因为理论分析能够揭示算法中可能在真实系统中长期隐藏的问题,只有在你的假设(例如关于时序)被异常情况打破时才会显露出来。
谨慎的做法是将理论分析与经验测试相结合,以验证实现是否按预期运行。像基于属性的测试(property-based testing)、模糊测试(fuzzing)和确定性模拟测试(deterministic simulation testing)等技术使用随机化来在广泛的情境下测试系统。亚马逊网络服务(AWS)、FoundationDB 和 TigerBeetle 等组织已成功地在他们的许多产品上结合使用了这些技术[120, 121, 122, 123]。
模型检查与规范语言
模型检查器(model checker)是帮助验证算法或系统是否按预期运行的工具。算法的规范用专门的语言编写,例如 TLA+、Gallina 或 FizzBee。这些语言使得更容易专注于算法的行为,而无需担心代码实现细节。模型检查器随后使用这些模型,通过系统地尝试所有可能发生的事情来验证不变式是否在所有算法状态中成立。
模型检查无法真正证明算法的不变式对每一个可能状态都成立,因为大多数现实世界算法具有无限的状态空间。对所有状态的真正验证需要形式化证明,这是可以做到的,但需要巨大的工作量。实际上,TLA+ 模型检查器(例如 TLC)可以通过限制状态空间的范围(例如,通过设置最大节点数或最大超时值)来探索算法所有可能轨迹的有限子集。尽管如此,模型检查仍然是发现算法错误的一种极其有效的方法。
作为使用正式规范语言的另一种选择,一些系统通过确定性的模拟测试(deterministic simulation testing)来测试其容错算法。在这种方法中,系统的实现是确定性的,并且可以精确控制网络消息的顺序和时间。这使得能够系统地探索大量可能的交错(interleaving),并模拟故障场景而不耗费大量实际时间。FoundationDB 就是一个成功使用确定性模拟测试的著名例子[121]。
关于模型检查的局限性
模型检查通常无法处理无限状态空间,但它通过探索有限但具有代表性的状态子集来发现错误。尽管有局限性,这种方法在实践中非常有效,已经发现了许多分布式算法中微妙的错误。
最后,值得强调的是,即使经过形式化验证的算法,当在现实硬件和网络环境中实现时,也可能因实现错误、硬件故障或系统模型未涵盖的假设而失败。然而,结合形式化方法、模型检查、随机化测试和健壮的错误处理,可以极大地提高分布式系统在实际中的可靠性。
第9章:分布式系统的麻烦
模型检测的局限性
模型检测实际上无法证明算法的所有不变量在所有可能状态下都成立,因为大多数真实世界算法具有无限状态空间。要真正验证所有状态,需要形式化证明,但这通常比运行模型检测器更困难。因此,模型检测器鼓励你将算法模型简化为可完全验证的近似版本,或者将执行限制在上界内(例如,设置最大可发送消息数)。任何仅发生在更长执行中的错误就不会被发现。
尽管如此,模型检测器在易用性与发现不明显错误的能力之间取得了良好平衡。CockroachDB、TiDB、Kafka 以及许多其他分布式系统都使用模型规格说明来发现和修复错误 [124, 125, 126]。例如,使用 TLA+,研究人员能够证明视图戳复制(VR)中由于算法描述散文的歧义性而可能导致的数据丢失 [127]。
按照设计,模型检测器并不运行你的实际代码,而是运行一个仅指定协议核心思想的简化模型。这使得系统地探索状态空间更加可行,但也带来了规格说明与实现可能不同步的风险 [128]。可以检查模型与真实实现是否具有等价行为,但这需要在真实实现中植入检测工具 [129]。
故障注入
许多错误是在机器和网络故障发生时被触发的。故障注入是一种有效(有时甚至令人害怕)的技术,用于验证系统在出现问题时是否按预期工作。其思想很简单:在运行系统的环境中注入故障,观察其行为。故障可以包括网络故障、机器崩溃、磁盘损坏、进程暂停——任何你能想象到的计算机问题。
故障注入测试通常在与系统将要运行的生产环境非常相似的环境中执行。有些团队甚至直接将故障注入到生产环境中。Netflix 通过其 Chaos Monkey 工具普及了这种方法 [130]。生产环境故障注入通常被称为混沌工程,我们在“可靠性与容错”(第43页)中讨论过。
要运行故障注入测试,首先部署待测系统,同时部署故障注入协调器和脚本。协调器负责决定注入什么故障以及何时执行。本地或远程脚本负责将故障注入单个节点或进程。注入脚本使用多种工具来触发故障。可以使用 Linux 的 kill 命令暂停或杀死 Linux 进程,使用 umount 卸载磁盘,通过防火墙设置中断网络连接。你可以在故障注入期间和之后检查系统行为,以确保一切按预期工作。
故障注入的复杂性
触发故障所需的众多工具使得编写故障注入测试变得繁琐。通常采用像 Jepsen 这样的故障注入框架来运行故障注入测试以简化过程。这类框架集成了多种操作系统以及许多预构建的故障注入器 [131]。Jepsen 在发现许多广泛使用系统中的关键错误方面非常有效 [132, 133]。
确定性仿真测试
另一种形式化技术,已成为模型检测和故障注入的流行补充,即确定性仿真测试(Deterministic Simulation Testing,DST)。它使用与模型检测器类似的状态空间探索过程,但测试的是你的实际代码,而不是模型。
在 DST 中,仿真自动运行大量随机化的系统执行。仿真期间的网络通信、I/O 和时钟时序全部被模拟对象替换,使模拟器能够控制事件发生的精确顺序,包括各种时序和故障场景。这使得模拟器能够探索比手动测试或故障注入多得多的情景。如果测试失败,可以重新运行,因为模拟器知道触发失败的确切操作顺序——这与故障注入不同,故障注入对系统的控制粒度没有这么细。
DST 要求模拟器能够控制所有非确定性来源,例如网络延迟或多线程代码中的线程调度。通常采用三种策略之一来实现代码的确定性:
应用级
有些系统从底层构建时就注重使代码易于确定性执行。例如,DST 领域的先驱之一 FoundationDB 是使用名为 Flow 的异步通信库构建的。Flow 为开发者提供了一个将确定性网络模拟注入系统的点 [134]。类似地,TigerBeetle 是一个具有一流 DST 支持的 OLTP 数据库。系统的状态被建模为状态机,所有变更都在单个事件循环内发生。当与诸如时钟之类的确定性模拟原语结合时,这种架构能够确定性运行 [135]。
运行时级
具有异步运行时和常用库的语言提供了引入确定性的插入点。使用单线程运行时强制所有异步代码顺序执行。例如,FrostDB 修补了 Go 的运行时以顺序执行 goroutine [136]。Rust 的 MadSim 库以类似方式工作。它提供了 Tokio 异步运行时 API、Amazon S3 库、Kafka Rust 库等的确定性实现;应用程序可以替换成确定性库和运行时,无需更改代码即可获得确定性测试执行。
机器级
不修补代码运行时,而是可以使整个机器具有确定性。这是一个精细的过程,需要机器对所有通常非确定性的调用都返回确定性响应。像 Antithesis 这样的工具通过构建一个自定义管理程序来实现这一点,该管理程序将通常非确定性的操作替换为确定性操作。从时钟到网络和存储,所有内容都需要被考虑在内。完成后,开发者可以在管理程序内的容器集合中运行整个分布式系统,从而获得完全确定性的分布式系统。
DST 提供了超越可重现性的几个优势。例如,Antithesis 尝试在发现不太常见的行为时将测试执行分支为多个子执行,从而探索应用代码中的许多路径。并且由于确定性测试通常使用模拟的时钟和网络调用,此类测试可以比挂钟时间运行得更快。例如,TigerBeetle 的时间抽象允许仿真模拟网络延迟和超时,而无需实际花费完整的时间来触发超时。这些技术使模拟器能够更快地探索更多代码路径。
确定性的力量
非确定性是我们在本章讨论的所有分布式系统挑战的核心:并发、网络延迟、进程暂停、时钟跳跃和崩溃都以不可预测的方式发生,每次系统运行都不相同。相反,如果你能使系统具有确定性,那将大大简化事情。实际上,使事物具有确定性是一个简单而强大的思想,在分布式系统设计中反复出现。除了确定性仿真测试,我们在前几章已经看到几种使用确定性的方式:
然而,使代码完全具备确定性需要谨慎。即使你已经移除了所有并发,并将 I/O、网络通信、时钟和随机数生成器替换为确定性模拟,非确定性的元素可能仍然存在。例如,在一些编程语言中,遍历哈希表元素时的顺序可能是非确定性的。是否遇到资源限制(内存分配失败、栈溢出)也是非确定性的。
总结
在本章中,我们讨论了分布式系统中可能出现的各种问题。例如:
- 每当你尝试通过网络发送数据包时,它可能丢失或任意延迟。同样,回复也可能丢失或延迟,因此如果你没有收到回复,你无法知道消息是否送达。
- 节点的时钟可能与其他节点显著不同步(尽管你尽了最大努力设置 NTP),它可能突然向前或向后跳跃,依赖它是危险的,因为你很可能没有一个好的时钟置信区间度量。
- 进程可能在执行的任何时刻暂停相当长的时间,被其他节点宣布死亡,然后再次复活而不知道它曾被暂停。
这种部分故障可能发生的事实是分布式系统的定义特征。每当软件试图与其他节点交互时,就有可能偶尔失败、随机变慢、或根本不响应(并最终超时)。在分布式系统中,我们试图在软件中构建对部分故障的容忍性,以便即使某些组成部分损坏,整个系统也能继续运行。
要容忍故障,第一步是检测它们,但这本身就很困难。大多数系统没有精确的机制来检测节点是否失效,因此大多数分布式算法依赖超时来确定远程节点是否仍然可用。然而,超时无法区分网络故障和节点故障,并且可变的网络延迟有时会导致节点被错误地怀疑已崩溃。处理跛行节点(它们正在响应但速度慢到无法做任何有用的事情)甚至更加困难。
一旦检测到故障,使系统容忍它也并非易事;机器之间没有全局变量、没有共享内存、没有公共知识或任何其他类型的共享状态 [83]。节点甚至无法就当前时间达成一致,更不用说任何更深刻的事情。信息从一个节点流向另一个节点的唯一方式是通过不可靠的网络发送。重大决策不能……
第九章:分布式系统的麻烦
机器之间没有共同知识或任何其他形式的共享状态 [83]。节点甚至无法就当前时间达成一致,更不用说其他更深奥的事情了。信息从一个节点流向另一个节点的唯一方式是将其通过不可靠的网络发送。重大决策不能安全地由单个节点做出,因此我们需要协议来争取其他节点的帮助,并试图获得法定人数的同意。
如果你习惯了在单台计算机的理想化数学完美环境中编写软件——在那里相同的操作总是确定性地返回相同的结果——那么迁移到分布式系统的混乱物理现实可能会有点令人震惊。反过来,分布式系统工程师通常会将那些可以在单台计算机上解决的问题视为琐碎的 [4],而实际上如今单台计算机能做的事情非常多。如果你能避免打开潘多拉魔盒,简单地将所有事情保持在单台机器上——例如,使用嵌入式存储引擎(参见第125页的“嵌入式存储引擎”)——通常这是值得做的。
然而,正如第19页的“分布式与单节点系统”中讨论的,可伸缩性并不是想要使用分布式系统的唯一原因。容错和低延迟(通过将数据地理上靠近用户放置)同样是重要的目标,而这些目标无法通过单个节点实现。分布式系统的力量在于,原则上它们可以永远运行而不中断服务,因为所有故障和维护都可以在节点级别处理。(在实践中,如果一个糟糕的配置变更被推广到所有节点,仍然会使分布式系统瘫痪。)
在本章中,我们还进行了一些探索,探讨网络、时钟和进程的不可靠性是否是不可避免的自然法则。我们看到事实并非如此:在网络上提供硬实时响应保证和有界延迟是可能的,但这样做代价非常高昂,并且会导致硬件资源利用率降低。大多数非安全关键系统选择廉价且不可靠的方案,而不是昂贵且可靠的。
本章一直在讨论问题,呈现出一种黯淡的前景。通过使用经过广泛测试的生产级分布式系统来管理这些问题,我们可以获得很多收益。在下一章中,我们将转向解决方案,讨论一些系统用于应对这些问题的算法。
参考文献
[1] Mark Cavage. “There’s Just No Getting Around It: You’re Building a Distributed System.” ACM Queue, volume 11, issue 4, pages 80–89, April 2013. doi:10.1145/2466486.2482856 – 无法回避:你正在构建一个分布式系统。
[2] Jay Kreps. “Getting Real About Distributed System Reliability.” blog.empathybox.com, March 2012. Archived at perma.cc/9B5Q-AEBW – 认真对待分布式系统的可靠性。
[3] Coda Hale. “You Can’t Sacrifice Partition Tolerance.” codahale.com, October 2010. Archived at perma.cc/6GJU-X4G5 – 你不能牺牲分区容错性。
[4] Jeff Hodges. “Notes on Distributed Systems for Young Bloods.” somethingsimilar.com, January 2013. Archived at perma.cc/B636-62CE – 给新手的分布式系统笔记。
[5] Van Jacobson. “Congestion Avoidance and Control.” At ACM Symposium on Communications Architectures and Protocols (SIGCOMM), August 1988. doi:10.1145/52324.52356 – 拥塞避免与控制。
[6] Bert Hubert. “The Ultimate SO_LINGER Page, or: Why Is My TCP Not Reliable.” blog.netherlabs.nl, January 2009. Archived at perma.cc/6HDX-L2RR – 终极SO_LINGER页面,或者:为什么我的TCP不可靠。
[7] Jerome H. Saltzer, David P. Reed, and David D. Clark. “End-To-End Arguments in System Design.” ACM Transactions on Computer Systems, volume 2, issue 4, pages 277–288, November 1984. doi:10.1145/357401.357402 – 系统设计中的端到端论证。
[8] Peter Bailis and Kyle Kingsbury. “The Network Is Reliable.” ACM Queue, volume 12, issue 7, pages 48–55, July 2014. doi:10.1145/2639988.2639988 – 网络是可靠的。
[9] Joshua B. Leners, Trinabh Gupta, Marcos K. Aguilera, and Michael Walfish. “Taming Uncertainty in Distributed Systems with Help from the Network.” At 10th European Conference on Computer Systems (EuroSys), April 2015. doi:10.1145/2741948.2741976 – 借助网络驯服分布式系统中的不确定性。
[10] Phillipa Gill, Navendu Jain, and Nachiappan Nagappan. “Understanding Network Failures in Data Centers: Measurement, Analysis, and Implications.” At ACM SIGCOMM Conference, August 2011. doi:10.1145/2018436.2018477 – 理解数据中心中的网络故障:测量、分析与启示。
[11] Urs Hölzle. “But recently a farmer had started grazing a herd of cows nearby. And whenever they stepped on the fiber link, they bent it enough to cause a blip.” x.com, May 2020. Archived at perma.cc/WX8X-ZZA5 – 但最近一位农夫开始在附近放牧牛群。每当它们踩到光纤链路时,就会将其弯曲到足以引起信号干扰。
[12] CBC News. “Hundreds Lose Internet Service in Northern B.C. After Beaver Chews Through Cable.” cbc.ca, April 2021. Archived at perma.cc/UW8C-H2MY – 不列颠哥伦比亚省北部数百人因海狸咬断电缆而断网。
[13] Will Oremus. “The Global Internet Is Being Attacked by Sharks, Google Confirms.” slate.com, August 2014. Archived at perma.cc/P6F3-C6YG – 谷歌确认全球互联网正遭受鲨鱼攻击。
[14] Jess Auerbach Jahajeeah. “Down to the Wire: The Ship Fixing Our Internet.” continent.substack.com, November 2023. Archived at perma.cc/DP7B-EQ7S – 最后的底线:修复我们互联网的船只。
[15] Santosh Janardhan. “More Details About the October 4 Outage.” engineering.fb.com, October 2021. Archived at perma.cc/WW89-VSXH – 关于10月4日宕机的更多细节。
[16] Tom Parfitt. “Georgian Woman Cuts off Web Access to Whole of Armenia.” theguardian.com, April 2011. Archived at perma.cc/KMC3-N3NZ – 格鲁吉亚妇女切断整个亚美尼亚的网络访问。
[17] Antonio Voce, Tural Ahmedzade and Ashley Kirk. “‘Shadow Fleets’ and Subaquatic Sabotage: Are Europe’s Undersea Internet Cables Under Attack?” theguardian.com, March 2025. Archived at perma.cc/HA7S-ZDBV – “影子舰队”与水下破坏:欧洲海底互联网电缆遭受攻击?
[18] Shengyun Liu, Paolo Viotti, Christian Cachin, Vivien Quéma, and Marko Vukolić. “XFT: Practical Fault Tolerance Beyond Crashes.” At 12th USENIX Symposium on Operating Systems Design and Implementation (OSDI), November 2016. – XFT:超越崩溃的实际容错。
[19] Mark Imbriaco. “Downtime Last Saturday.” github.blog, December 2012. Archived at perma.cc/M7X5-E8SQ – 上周六的停机。
[20] Tom Lianza and Chris Snook. “A Byzantine Failure in the Real World.” blog.cloudflare.com, November 2020. Archived at perma.cc/83EZ-ALCY – 现实世界中的拜占庭故障。
[21] Mohammed Alfatafta, Basil Alkhatib, Ahmed Alquraan, and Samer Al-Kiswany. “Toward a Generic Fault Tolerance Technique for Partial Network Partitioning.” At 14th USENIX Symposium on Operating Systems Design and Implementation (OSDI), November 2020. – 面向部分网络分区的通用容错技术。
[22] Marc A. Donges. “Re: bnx2 Cards Intermittantly Going Offline.” Message to Linux netdev mailing list, spinics.net, September 2012. Archived at perma.cc/TXP6-H8R3 – 回复:bnx2网卡间歇性离线。
[23] Troy Toman. “Inside a CODE RED: Network Edition.” signalvnoise.com, September 2020. Archived at perma.cc/BET6-FY25 – 深入CODE RED:网络版。
[24] Kyle Kingsbury. “Jepsen: Elasticsearch.” aphyr.com, June 2014. Archived at perma.cc/JK47-S89J – Jepsen:Elasticsearch。
[25] Salvatore Sanfilippo. “A Few Arguments About Redis Sentinel Properties and Fail Scenarios.” antirez.com, October 2014. Archived at perma.cc/8XEU-CLM8 – 关于Redis Sentinel属性和故障场景的一些论证。
[26] Nicolas Liochon. “CAP: If All You Have Is a Timeout, Everything Looks Like a Partition.” blog.thislongrun.com, May 2015. Archived at perma.cc/FS57-V2PZ – CAP:如果你只有超时,那么一切看起来都像分区。
[27] Matthew P. Grosvenor, Malte Schwarzkopf, Ionel Gog, Robert N. M. Watson, Andrew W. Moore, Steven Hand, and Jon Crowcroft. “Queues Don’t Matter When You Can JUMP Them!” At 12th USENIX Symposium on Networked Systems Design and Implementation (NSDI), May 2015. – 当你能够“跳跃”队列时,队列就不重要了!
[28] Theo Julienne. “Debugging Network Stalls on Kubernetes.” github.blog, November 2019. Archived at perma.cc/K9M8-XVGL – 调试Kubernetes上的网络停滞。
[29] Guohui Wang and T. S. Eugene Ng. “The Impact of Virtualization on Network Performance of Amazon EC2 Data Center.” At 29th IEEE International Conference on Computer Communications (INFOCOM), March 2010. doi:10.1109/INFCOM.2010.5461931 – 虚拟化对Amazon EC2数据中心网络性能的影响。
[30] Brandon Philips. “etcd: Distributed Locking and Service Discovery.” At Strange Loop, September 2014. – etcd:分布式锁与服务发现。
[31] Steve Newman. “A Systematic Look at EC2 I/O.” blog.scalyr.com, October 2012. Archived at perma.cc/FL4R-H2VE – 对EC2 I/O的系统性审视。
[32] Naohiro Hayashibara, Xavier Défago, Rami Yared, and Takuya Katayama. “The ϕ Accrual Failure Detector.” Japan Advanced Institute of Science and Technology, School of Information Science, Technical Report IS-RR-2004-010, May 2004. Archived at perma.cc/NSM2-TRYA – ϕ累加故障检测器。
[33] Jeffrey Wang. “Phi Accrual Failure Detector.” ternarysearch.blogspot.co.uk, August 2013. Archived at perma.cc/L452-AMLV – Phi累加故障检测器。
[34] Srinivasan Keshav. An Engineering Approach to Computer Networking: ATM Networks, the Internet, and the Telephone Network. Addison-Wesley Professional, 1997. ISBN: 9780201634426 – 计算机网络的一种工程方法:ATM网络、互联网与电话网络。
[35] Othmar Kyas. ATM Networks. International Thomson Publishing, 1995. ISBN: 9781850321286 – ATM网络。
[36] Jialin Li, Naveen Kr. Sharma, Dan R. K. Ports, and Steven D. Gribble. “Tales of the Tail: Hardware, OS, and Application-level Sources of Tail Latency.” At ACM Symposium on Cloud Computing (SOCC), November 2014. doi:10.1145/2670979.2670988 – 尾部延迟的故事:硬件、操作系统和应用层面的尾部延迟来源。
[37] Mellanox Technologies. “InfiniBand FAQ, Rev 1.3.” network.nvidia.com, December 2014. Archived at perma.cc/LQJ4-QZVK – InfiniBand常见问题解答,修订版1.3。
[38] Jose Renato Santos, Yoshio Turner, and G. (John) Janakiraman. “End-to-End Congestion Control for InfiniBand.” At 22nd Annual Joint Conference of the IEEE Computer and Communications Societies (INFOCOM), April 2003. Also published by HP Laboratories Palo Alto, Tech Report HPL-2002-359. doi:10.1109/INFCOM.2003.1208949 – InfiniBand的端到端拥塞控制。
[39] Ulrich Windl, David Dalton, Marc Martinec, and Dale R. Worley. “The NTP FAQ and HOWTO.” ntp.org, November 2006. Archived at archive.org – NTP常见问题解答与HOWTO。
[40] John Graham-Cumming. “How and Why the Leap Second Affected Cloudflare DNS.” blog.cloudflare.com, January 2017. Archived at archive.org – 闰秒如何以及为何影响Cloudflare DNS。
[41] David Holmes. “Inside the Hotspot VM: Clocks, Timers and Scheduling Events — Part I — Windows.” blogs.oracle.com, October 2006. Archived at archive.org – 深入Hotspot虚拟机:时钟、定时器与调度事件——第一部分——Windows。
[42] Joran Dirk Greef. “Three Clocks Are Better than One.” tigerbeetle.com, August 2021. Archived at perma.cc/5RXG-EU6B – 三个时钟胜过一个。
[43] Oliver Yang. “Pitfalls of TSC Usage.” oliveryang.net, September 2015. Archived at perma.cc/Z2QY-5FRA – TSC使用的陷阱。
[44] Steve Loughran. “Time on Multi-Core, Multi-Socket Servers.” steveloughran.blogspot.co.uk, September 2015. Archived at perma.cc/7M4S-D4U6 – 多核多插槽服务器上的时间。
[45] James C. Corbett, Jeffrey Dean, Michael Epstein, Andrew Fikes, Christopher Frost, JJ Furman, Sanjay Ghemawat, Andrey Gubarev, Christopher Heiser, Peter Hochschild, Wilson Hsieh, Sebastian Kanthak, Eugene Kogan, Hongyi Li, Alexander Lloyd, Sergey Melnik, David Mwaura, David Nagle, Sean Quinlan, Rajesh Rao, Lindsay Rolig, Yasushi Saito, Michal Szymaniak, Christopher Taylor, Ruth Wang, and Dale Woodford. “Spanner: Google’s Globally-Distributed Database.” At 10th USENIX Symposium on Operating Systems Design and Implementation (OSDI), October 2012. – Spanner:谷歌的全球分布式数据库。
Chapter 9: 分布式系统的麻烦
- [45][46] M. Caporaloni and R. Ambrosini. “How Closely Can a Personal Computer Clock Track the UTC Timescale Via the Internet?” European Journal of Physics, volume 23, issue 4, pages L17–L21, June 2012. doi:10.1088/0143-0807/23/4/103
[47] Nelson Minar. “A Survey of the NTP Network.” alumni.media.mit.edu, December 1999. Archived at perma.cc/EV76-7ZV3
[48] Viliam Holub. “Synchronizing Clocks in a Cassandra Cluster Pt. 1—The Problem.” blog.rapid7.com, March 2014. Archived at perma.cc/N3RV-5LNL
[49] Poul-Henning Kamp. “The One-Second War (What Time Will You Die?)” ACM Queue, volume 9, issue 4, pages 44–48, April 2011. doi:10.1145/1966989.1967009
[50] Nelson Minar. “Leap Second Crashes Half the Internet.” somebits.com, July 2012. Archived at perma.cc/2WB8-D6EU
[51] Christopher Pascoe. “Time, Technology and Leaping Seconds.” googleblog.blogspot.co.uk, September 2011. Archived at perma.cc/U2JL-7E74
[52] Mingxue Zhao and Jeff Barr. “Look Before You Leap—The Coming Leap Second and AWS.” aws.amazon.com, May 2015. Archived at perma.cc/KPE9-XMFM
[53] Darryl Veitch and Kanthaiah Vijayalayan. “Network Timing and the 2015 Leap Second.” At 17th International Conference on Passive and Active Measurement (PAM), April 2016. doi:10.1007/978-3-319-30505-9_29
[54] VMware, Inc. “Timekeeping in VMware Virtual Machines.” vmware.com, October 2008. Archived at perma.cc/HM5R-T5NF
[55] Victor Yodaiken. “Clock Synchronization in Finance and Beyond.” yodaiken.com, November 2017. Archived at perma.cc/9XZD-8ZZN
[56] Mustafa Emre Acer, Emily Stark, Adrienne Porter Felt, Sascha Fahl, Radhika Bhargava, Bhanu Dev, Matt Braithwaite, Ryan Sleevi, and Parisa Tabriz. “Where the Wild Warnings Are: Root Causes of Chrome HTTPS Certificate Errors.” At ACM SIGSAC Conference on Computer and Communications Security (CCS), October 2017. doi:10.1145/3133956.3134007
[57] European Securities and Markets Authority. “MiFID II / MiFIR: Regulatory Technical and Implementing Standards—Annex I.” esma.europa.eu, Report ESMA/2015/1464, September 2015. Archived at perma.cc/ZLX9-FGQ3
[58] Luke Bigum. “Solving MiFID II Clock Synchronisation with Minimum Spend (Part 1).” catach.blogspot.com, November 2015. Archived at perma.cc/4J5W-FNM4
[59] Oleg Obleukhov and Ahmad Byagowi. “How Precision Time Protocol Is Being Deployed at Meta.” engineering.fb.com, November 2022. Archived at perma.cc/29G6-UJNW
[60] John Wiseman. “GPSJAM: Daily Maps of GPS Interference.” gpsjam.org
[61] Josh Levinson, Julien Ridoux, and Chris Munns. “It’s About Time: Microsecond-Accurate Clocks on Amazon EC2 Instances.” aws.amazon.com, November 2023. Archived at perma.cc/56M6-5VMZ
[62] Kyle Kingsbury. “Jepsen: Cassandra.” aphyr.com, September 2013. Archived at perma.cc/4MBR-J96V
[63] John Daily. “Clocks Are Bad, or, Welcome to the Wonderful World of Distributed Systems.” riak.com, November 2013. Archived at perma.cc/4XB5-UCXY
[64] Marc Brooker. “It’s About Time!” brooker.co.za, November 2023. Archived at perma.cc/N6YK-DRPA
[65] Kyle Kingsbury. “The Trouble with Timestamps.” aphyr.com, October 2013. Archived at perma.cc/W3AM-5VAV
[66] Leslie Lamport. “Time, Clocks, and the Ordering of Events in a Distributed System.” Communications of the ACM, volume 21, issue 7, pages 558–565, July 1978. doi:10.1145/359545.359563
[67] Justin Sheehy. “There Is No Now: Problems with Simultaneity in Distributed Systems.” ACM Queue, volume 13, issue 3, pages 36–41, March 2015. doi:10.1145/2733108
[68] Murat Demirbas. “Spanner: Google’s Globally-Distributed Database.” muratbuffalo.blogspot.co.uk, July 2013. Archived at perma.cc/6VWR-C9WB
[69] Dahlia Malkhi and Jean-Philippe Martin. “Spanner’s Concurrency Control.” ACM SIGACT News, volume 44, issue 3, pages 73–77, September 2013. doi:10.1145/2527748.2527767
[70] Franck Pachot. “Achieving Precise Clock Synchronization on AWS.” yugabyte.com, December 2024. Archived at perma.cc/UYM6-RNBS
[71] Spencer Kimball. “Living Without Atomic Clocks: Where CockroachDB and Spanner Diverge.” cockroachlabs.com, January 2022. Archived at perma.cc/AWZ7-RXFT
[72] Murat Demirbas. “Use of Time in Distributed Databases (Part 4): Synchronized Clocks in Production Databases.” muratbuffalo.blogspot.com, January 2025. Archived at perma.cc/9WNX-Q9U3
[73] Cary G. Gray and David R. Cheriton. “Leases: An Efficient Fault-Tolerant Mechanism for Distributed File Cache Consistency.” At 12th ACM Symposium on Operating Systems Principles (SOSP), December 1989. doi:10.1145/74850.74870
[74] Daniel Sturman, Scott Delap, Max Ross, et al. “Roblox Return to Service.” corp.roblox.com, January 2022. Archived at perma.cc/8ALT-WAS4
[75] Todd Lipcon. “Avoiding Full GCs with MemStore-Local Allocation Buffers.” slideshare.net, February 2011. Archived at perma.cc/CH62-2EWJ
[76] Christopher Clark, Keir Fraser, Steven Hand, Jacob Gorm Hansen, Eric Jul, Christian Limpach, Ian Pratt, and Andrew Warfield. “Live Migration of Virtual Machines.” At 2nd USENIX Symposium on Symposium on Networked Systems Design & Implementation (NSDI), May 2005.
[77] Mike Shaver. “fsyncers and Curveballs.” shaver.off.net, May 2008. Archived at archive.org
[78] Zhenyun Zhuang and Cuong Tran. “Eliminating Large JVM GC Pauses Caused by Background IO Traffic.” engineering.linkedin.com, February 2016. Archived at perma.cc/ML2M-X9XT
[79] Martin Thompson. “Java Garbage Collection Distilled.” mechanical-sympathy.blogspot.co.uk, July 2013. Archived at perma.cc/DJT3-NQLQ
[80] David Terei and Amit Levy. “Blade: A Data Center Garbage Collector.” arXiv:1504.02578, April 2015.
[81] Martin Maas, Tim Harris, Krste Asanović, and John Kubiatowicz. “Trash Day: Coordinating Garbage Collection in Distributed Systems.” At 15th USENIX Workshop on Hot Topics in Operating Systems (HotOS), May 2015.
[82] Martin Fowler. “The LMAX Architecture.” martinfowler.com, July 2011. Archived at perma.cc/5AV4-N6RJ
[83] Joseph Y. Halpern and Yoram Moses. “Knowledge and Common Knowledge in a Distributed Environment.” Journal of the ACM (JACM), volume 37, issue 3, pages 549–587, July 1990. doi:10.1145/79147.79161
[84] Chuzhe Tang, Zhaoguo Wang, Xiaodong Zhang, Qianmian Yu, Binyu Zang, Haibing Guan, and Haibo Chen. “Ad Hoc Transactions in Web Applications: The Good, the Bad, and the Ugly.” At ACM International Conference on Management of Data (SIGMOD), June 2022. doi:10.1145/3514221.3526120
[85] Flavio P. Junqueira and Benjamin Reed. ZooKeeper: Distributed Process Coordination. O’Reilly Media, 2013. ISBN: 9781449361303
[86] Enis Söztutar. “HBase and HDFS: Understanding Filesystem Usage in HBase.” At HBaseCon, June 2013. Archived at perma.cc/4DXR-9P88
[87] SUSE LLC. “SUSE Linux Enterprise High Availability 15 SP6 Administration Guide, Section 12: Fencing and STONITH.” documentation.suse.com, March 2025. Archived at perma.cc/8LAR-EL9D
[88] Mike Burrows. “The Chubby Lock Service for Loosely-Coupled Distributed Systems.” At 7th USENIX Symposium on Operating System Design and Implementation (OSDI), November 2006.
[89] Kyle Kingsbury. “etcd 3.4.3.” jepsen.io, January 2020. Archived at perma.cc/2P3Y-MPWU
[90] Ensar Basri Kahveci. “Distributed Locks Are Dead; Long Live Distributed Locks!” hazelcast.com, April 2019. Archived at perma.cc/7FS5-LDXE
[91] Martin Kleppmann. “How to Do Distributed Locking.” martin.kleppmann.com, February 2016. Archived at perma.cc/Y24W-YQ5L
[92] Salvatore Sanfilippo. “Is Redlock Safe?” antirez.com, February 2016. Archived at perma.cc/B6GA-9Q6A
[93] Gunnar Morling. “Leader Election with S3 Conditional Writes.” morling.dev, August 2024. Archived at perma.cc/7V2N-J78Y
[94] Leslie Lamport, Robert Shostak, and Marshall Pease. “The Byzantine Generals Problem.” ACM Transactions on Programming Languages and Systems (TOPLAS), volume 4, issue 3, pages 382–401, July 1982. doi:10.1145/357172.357176
[95] Jim N. Gray. “Notes on Data Base Operating Systems.” In Operating Systems: An Advanced Course, Lecture Notes in Computer Science, volume 60, edited by R. Bayer, R. M. Graham, and G. Seegmüller, pages 393–481, Springer-Verlag, 1978. ISBN: 9783540087557. Archived at perma.cc/7S9M-2LZU
[96] Brian Palmer. “How Complicated Was the Byzantine Empire?” slate.com, October 2011. Archived at perma.cc/AN7X-FL3N
[97] Leslie Lamport. “My Writings.” lamport.azurewebsites.net, December 2014. Archived at perma.cc/5NNM-SQGR
[98] John Rushby. “Bus Architectures for Safety-Critical Embedded Systems.” At 1st International Workshop on Embedded Software (EMSOFT), October 2001. doi:10.1007/3-540-45449-7_22
[99] Jake Edge. “ELC: SpaceX Lessons Learned.” lwn.net, March 2013. Archived at perma.cc/AYX8-QP5X
[100] Shehar Bano, Alberto Sonnino, Mustafa Al-Bassam, Sarah Azouvi, Patrick McCorry, Sarah Meiklejohn, and George Danezis. “SoK: Consensus in the Age of Blockchains.” At 1st ACM Conference on Advances in Financial Technologies (AFT), October 2019. doi:10.1145/3318041.3355458
第9章 分布式系统的麻烦
[101] Ezra Feilden, Adi Oltean, and Philip Johnston. “Why We Should Train AI in Space.” White Paper, starcloud.com, September 2024. Archived at perma.cc/7Y3S-8UB6
[102] James Mickens. “The Saddest Moment.” USENIX ;login, May 2013. Archived at perma.cc/T7BZ-XCFR
[103] Martin Kleppmann and Heidi Howard. “Byzantine Eventual Consistency and the Fundamental Limits of Peer-to-Peer Databases.” arXiv:2012.00472, December 2020.
[104] Martin Kleppmann. “Making CRDTs Byzantine Fault Tolerant.” At 9th Workshop on Principles and Practice of Consistency for Distributed Data (PaPoC), April 2022. doi:10.1145/3517209.3524042
[105] Evan Gilman. “The Discovery of Apache ZooKeeper’s Poison Packet.” pagerduty.com, May 2015. Archived at perma.cc/RV6L-Y5CQ
[106] Jonathan Stone and Craig Partridge. “When the CRC and TCP Checksum Disagree.” At ACM Conference on Applications, Technologies, Architectures, and Protocols for Computer Communication (SIGCOMM), August 2000. doi:10.1145/347059.347561
[107] Evan Jones. “How Both TCP and Ethernet Checksums Fail.” evanjones.ca, October 2015. Archived at perma.cc/9T5V-B8X5
[108] Cynthia Dwork, Nancy Lynch, and Larry Stockmeyer. “Consensus in the Presence of Partial Synchrony.” Journal of the ACM, volume 35, issue 2, pages 288–323, April 1988. doi:10.1145/42282.42283
[109] Richard D. Schlichting and Fred B. Schneider. “Fail-Stop Processors: An Approach to Designing Fault-Tolerant Computing Systems.” ACM Transactions on Computer Systems (TOCS), volume 1, issue 3, pages 222–238, August 1983. doi:10.1145/357369.357371
[110] Thanh Do, Mingzhe Hao, Tanakorn Leesatapornwongsa, Tiratat Patana-anake, and Haryadi S. Gunawi. “Limplock: Understanding the Impact of Limpware on Scale-out Cloud Systems.” At 4th ACM Symposium on Cloud Computing (SoCC), October 2013. doi:10.1145/2523616.2523627
[111] Josh Snyder and Joseph Lynch. “Garbage Collecting Unhealthy JVMs, a Proactive Approach.” netflixtechblog.medium.com, November 2019. Archived at perma.cc/8BTA-N3YB
[112] Haryadi S. Gunawi, Riza O. Suminto, Russell Sears, Casey Golliher, Swaminathan Sundararaman, Xing Lin, Tim Emami, Weiguang Sheng, Nematollah Bidokhti, Caitie McCaffrey, Gary Grider, Parks M. Fields, Kevin Harms, Robert B. Ross, Andree Jacobson, Robert Ricci, Kirk Webb, Peter Alvaro, H. Birali Runesha, Mingzhe Hao, and Huaicheng Li. “Fail-Slow at Scale: Evidence of Hardware Performance Faults in Large Production Systems.” At 16th USENIX Conference on File and Storage Technologies, February 2018.
[113] Peng Huang, Chuanxiong Guo, Lidong Zhou, Jacob R. Lorch, Yingnong Dang, Murali Chintalapati, and Randolph Yao. “Gray Failure: The Achilles’ Heel of Cloud-Scale Systems.” At 16th Workshop on Hot Topics in Operating Systems (HotOS), May 2017. doi:10.1145/3102980.3103005
[114] Chang Lou, Peng Huang, and Scott Smith. “Understanding, Detecting and Localizing Partial Failures in Large System Software.” At 17th USENIX Symposium on Networked Systems Design and Implementation (NSDI), February 2020.
[115] Peter Bailis and Ali Ghodsi. “Eventual Consistency Today: Limitations, Extensions, and Beyond.” ACM Queue, volume 11, issue 3, pages 55–63, March 2013. doi:10.1145/2460276.2462076
[116] Bowen Alpern and Fred B. Schneider. “Defining Liveness.” Information Processing Letters, volume 21, issue 4, pages 181–185, October 1985. doi:10.1016/0020-0190(85)90056-0
[117] Flavio P. Junqueira. “Dude, Where’s My Metadata?” fpj.me, May 2015. Archived at perma.cc/D2EU-Y9S5
[118] Scott Sanders. “January 28th Incident Report.” github.com, February 2016. Archived at perma.cc/5GZR-88TV
[119] Jay Kreps. “A Few Notes on Kafka and Jepsen.” blog.empathybox.com, September 2013. Archived at perma.cc/XJ5C-F583
[120] Marc Brooker and Ankush Desai. “Systems Correctness Practices at AWS.” ACM Queue, volume 22, issue 6, pages 79–96, November/December 2024. doi:10.1145/3712057
[121] Andrey Satarin. “Testing Distributed Systems: Curated list of Resources on Testing Distributed Systems.” asatarin.github.io. Archived at perma.cc/U5V8-XP24
[122] Phil Eaton and Joran Dirk Greef. “We Put a Distributed Database in the Browser—And Made a Game of It!” tigerbeetle.com, June 2023. Archived at perma.cc/L7M7-X4HD
[123] Apple, Inc. and the FoundationDB project authors. “FoundationDB—Simulation and Testing.” apple.github.io. Archived at perma.cc/4C4L-AUH3
[124] Jack Vanlightly. “Verifying Kafka Transactions—Diary Entry 2—Writing an Initial TLA+ Spec.” jack-vanlightly.com, December 2024. Archived at perma.cc/NSQ8-MQ5N
[125] Siddon Tang. “From Chaos to Order—Tools and Techniques for Testing TiDB, A Distributed NewSQL Database.” pingcap.com, April 2018. Archived at perma.cc/5EJB-R29F
[126] Nathan VanBenschoten. “Parallel Commits: An Atomic Commit Protocol for Globally Distributed Transactions.” cockroachlabs.com, November 2019. Archived at perma.cc/5FZ7-QK6J
[127] Jack Vanlightly. “Paper: VR Revisited—State Transfer (Part 3).” jack-vanlightly.com, December 2022. Archived at perma.cc/KNK3-K6WS
[128] Hillel Wayne. “What If the Spec Doesn’t Match the Code?” buttondown.com, March 2024. Archived at perma.cc/8HEZ-KHER
[129] Lingzhi Ouyang, Xudong Sun, Ruize Tang, Yu Huang, Madhav Jivrajani, Xiaoxing Ma, Tianyin Xu. “Multi-Grained Specifications for Distributed System Model Checking and Verification.” At 20th European Conference on Computer Systems (EuroSys), March 2025. doi:10.1145/3689031.3696069
[130] Yury Izrailevsky and Ariel Tseitlin. “The Netflix Simian Army.” netflixtechblog.com, July, 2011. Archived at perma.cc/M3NY-FJW6
[131] Kyle Kingsbury. “Jepsen: On the Perils of Network Partitions.” aphyr.com, May, 2013. Archived at perma.cc/W98G-6HQP
[132] Kyle Kingsbury. Analyses. jepsen.io, 2024. Archived at perma.cc/8LDN-D2T8
[133] Rupak Majumdar and Filip Niksic. “Why Is Random Testing Effective for Partition Tolerance Bugs?” Proceedings of the ACM on Programming Languages (PACMPL), volume 2, issue POPL, article no. 46, December 2017. doi:10.1145/3158134
[134] FoundationDB project authors. “Simulation and Testing.” apple.github.io. Archived at perma.cc/NQ3L-PM4C
[135] Alex Kladov. “Simulation Testing for Liveness.” tigerbeetle.com, July 2023. Archived at perma.cc/RKD4-HGCR
[136] Alfonso Subiotto Marqués. “(Mostly) Deterministic Simulation Testing in Go.” polarsignals.com, May 2024. Archived at perma.cc/ULD6-TSA4
Summary
|
399
图片上下文
[Image 105 on Page 372]
图片内容无法获取,无法进行分析。
[Image 7963 on Page 372]
图片内容无法获取(显示为未支持的图像格式)。请重新提供可识别的图片,以便进行分析。
[Image 105 on Page 375]
图片未显示,无法分析其内容。
[Image 8069 on Page 378]
无法获取图片内容,请提供有效图片。
[Image 8252 on Page 387]
由于图片未能正确加载,无法分析其具体内容。请重新提供图片。
[Image 105 on Page 393]
图片内容不可见,无法进行分析。
[Image 8448 on Page 398]
无法分析图片,因为图片内容无法显示(显示为[Unsupported Image])。请提供图片的实际内容或描述。
[Image 8451 on Page 398]
图片无法加载,因此无法分析其具体内容。请提供可访问的图片或描述图片内容。
[Image 105 on Page 399]
图片不存在或无法显示。
[Image 8466 on Page 399]
图片无法显示,无法分析具体内容。
[Image 8496 on Page 401]
图片为空,无法分析。