第八章:分布式系统的麻烦

故障与部分失效

如果发生内部错误,我们宁愿电脑完全崩溃,而不是返回错误的结果,因为错误的结果很难处理。因为计算机隐藏了模糊不清的物理实现,并呈现出一个理想化的系统模型,并以数学一样的完美的方式运作。

在分布式系统中,尽管系统的其他部分工作正常,但系统的某些部分可能会以某种不可预知的方式被破坏。这被称为部分失效(partial failure)。难点在于部分失效是不确定性的(nonderterministic):如果你试图做任何涉及多个节点和网络的事情,它有时可能会工作,有时会出现不可预知的失败。

云计算与超级计算机

超级计算机更像是一个单节点计算机而不是分布式系统:通过让部分失败升级为完全失败来处理部分失败——如果系统的任何部分发生故障,只是让所有的东西都崩溃。

超级计算机通常由专用硬件构建而成,每个节点相当可靠,节点通过共享内存和远程直接内存访问(RDMA)进行通信。

大型数据中心网络通常基于IP和以太网,以闭合拓扑排列,以提供更高的二等分带宽。

如果系统可以容忍发生故障的节点,并继续保持整体工作状态,那么这对于操作和维护非常有用:例如,可以执行滚动升级,一次重新启动一个节点,而服务继续服务用户不中断。

在地理位置分散的部署中(保持数据在地理位置上接近用户以减少访问延迟),通信很可能通过互联网进行,与本地网络相比,通信速度缓慢且不可靠。

如果要使分布式系统工作,就必须接受部分故障的可能性,并在软件中建立容错机制

故障处理必须是软件设计的一部分,并且作为软件的运维,您需要知道在发生故障的情况下,软件可能会表现出怎样的行为

不可靠的网络

无共享系统:通过网络连接的一堆机器。网络是这些机器可以通信的唯一途径——我们假设每台机器都有自己的内存和磁盘,一台机器不能访问另一台机器的内存或磁盘(除了通过网络向服务器发出请求)。

无共享并不是构建系统的唯一方式,但它已经成为构建互联网服务的主要方式,其原因如下:相对便宜,因为它不需要特殊的硬件,可以利用商品化的云计算服务,通过跨多个地理分布的数据中心进行冗余可以实现高可靠性。

互联网和数据中心中的大多数内部网络都是异步分组网络(asynchronous packet networks):一个节点可以向另一个节点发送一个消息(一个数据包),但是网络不能保证它什么时候到达,或者是否到达。如果您发送请求并期待响应,则很多事情可能会出错。

可能会出现如下情况:

  1. 请求可能已经丢失(可能有人拔掉了网线)。
  2. 请求可能正在排队,稍后将交付(也许网络或收件人超载)。
  3. 远程节点可能已经失效(可能是崩溃或关机)。
  4. 远程节点可能暂时停止了响应,但稍后会再次响应。
  5. 远程节点可能已经处理了请求,但是网络上的响应已经丢失(可能是网络交换机配置错误)。
  6. 远程节点可能已经处理了请求,但是响应已经被延迟,并且稍后将被传递(可能是网络或者你自己的机器过载)。

如果发送请求并没有得到响应,则无法区分(a)请求是否丢失,(b)远程节点是否关闭,或(c)响应是否丢失

发送者甚至不能分辨数据包是否被发送:唯一的选择是让接收者发送响应消息,这可能会丢失或延迟

处理这个问题的通常方法是超时(Timeout):在一段时间之后放弃等待,并且认为响应不会到达。但是,当发生超时时,你仍然不知道远程节点是否收到了请求。

真实世界的网络故障

在一家中型数据中心进行的一项研究发现,每个月大约有12个网络故障,其中一半断开一台机器,一半断开整个机架。另一项研究测量了架顶式交换机,汇聚交换机和负载平衡器等组件的故障率。它发现添加冗余网络设备不会像您所希望的那样减少故障,因为它不能防范人为错误(例如,错误配置的交换机),这是造成中断的主要原因。

网络分区

当网络的一部分由于网络故障而被切断时,有时称为网络分区(network partition)网络断裂(netsplit)。通常会使用更一般的术语网络故障(network fault)

检测故障

许多系统需要自动检测故障节点。例如:

  • 负载平衡器需要停止向已死亡的节点转发请求(即从移出轮询列表(out of rotation))。
  • 在单主复制功能的分布式数据库中,如果主库失效,则需要将从库之一升级为新主库。

网络的不确定性使得很难判断一个节点是否工作:

  • 如果你可以到达运行节点的机器,但没有进程正在侦听目标端口(例如,因为进程崩溃),操作系统将通过发送FIN或RST来关闭并重用TCP连接。但是,如果节点在处理请求时发生崩溃,则无法知道远程节点实际处理了多少数据。
  • 如果节点进程崩溃(或被管理员杀死),但节点的操作系统仍在运行,则脚本可以通知其他节点有关该崩溃的信息,以便另一个节点可以快速接管,而无需等待超时到期。例如,HBase做这个。
  • 如果您有权访问数据中心网络交换机的管理界面,则可以查询它们以检测硬件级别的链路故障(例如,远程机器是否关闭电源)。如果您通过互联网连接,或者如果您处于共享数据中心而无法访问交换机,或者由于网络问题而无法访问管理界面,则排除此选项。
  • 如果路由器确认您尝试连接的IP地址不可用,则可能会使用ICMP目标不可达数据包回复您。但是,路由器不具备神奇的故障检测能力——它受到与网络其他参与者相同的限制。

超时与无穷的延迟

长时间的超时意味着长时间等待,直到一个节点被宣告死亡。短暂的超时可以更快地检测到故障,但是实际上它只是经历了暂时的减速而导致错误地宣布节点失效的风险更高。

过早地声明一个节点已经死了是有问题的:如果这个节点实际上是活着的,并且正在执行一些动作,而另一个节点接管,那么这个动作可能会最终执行两次。

当一个节点被宣告死亡时,它的职责需要转移到其他节点,这会给其他节点和网络带来额外的负担。如果系统已经处于高负荷状态,则过早宣告节点死亡会使问题更严重。可能发生,节点实际上并没有死亡,而是由于过载导致响应缓慢;将其负载转移到其他节点可能会导致级联失效(cascading failure)(在极端情况下,所有节点都宣告对方死亡,并且所有节点都停止工作)。

异步网络具有无限的延迟(即尽可能快地传送数据包,但数据包到达可能需要的时间没有上限),并且大多数服务器实现并不能保证它们可以在一定的最大时间内处理请求。对于故障检测,系统大部分时间快速运行是不够的:如果你的超时时间很短,往返时间只需要一个瞬时尖峰就可以使系统失衡

计算机网络上数据包延迟的可变性通常是由于排队【25】:

  • 如果多个不同的节点同时尝试将数据包发送到同一目的地,则网络交换机必须将它们排队并将它们逐个送入目标网络链路。在繁忙的网络链路上,数据包可能需要等待一段时间才能获得一个插槽(这称为网络连接)。如果传入的数据太多,交换机队列填满,数据包将被丢弃,因此需要重新发送数据包 - 即使网络运行良好。
  • 当数据包到达目标机器时,如果所有CPU内核当前都处于繁忙状态,则来自网络的传入请求将被操作系统排队,直到应用程序准备好处理它为止。根据机器上的负载,这可能需要一段任意的时间。
  • 在虚拟化环境中,正在运行的操作系统经常暂停几十毫秒,而另一个虚拟机使用CPU内核。在这段时间内,虚拟机不能从网络中消耗任何数据,所以传入的数据被虚拟机监视器 排队(缓冲),进一步增加了网络延迟的可变性。
  • TCP执行流量控制(flow control)(也称为拥塞避免(congestion avoidance)背压(backpressure)),其中节点限制自己的发送速率以避免网络链路或接收节点过载。这意味着在数据甚至进入网络之前,在发送者处需要进行额外的排队。

如果有多台机器将网络流量发送到同一目的地,则其交换机队列可能会被填满。在这里,端口1,2和4都试图发送数据包到端口3

如果TCP在某个超时时间内没有被确认,则认为数据包丢失,丢失的数据包将自动重新发送。尽管应用程序没有看到数据包丢失和重新传输,但它看到了延迟。

TCP与UDP

一些对延迟敏感的应用程序(如视频会议和IP语音(VoIP))使用UDP而不是TCP。这是在可靠性和和延迟可变性之间的折衷:由于UDP不执行流量控制并且不重传丢失的分组,所以避免了可变网络延迟的一些原因。

在公共云和多租户数据中心中,资源被许多客户共享:网络链接和交换机,甚至每个机器的网卡和CPU(在虚拟机上运行时)。批处理工作负载(如MapReduce)可能很容易使网络链接饱和。由于无法控制或了解其他客户对共享资源的使用情况,如果附近的某个人正在使用大量资源,则网络延迟可能会发生剧烈抖动。

测量延长的网络往返时间和多台机器的分布,以确定延迟的预期可变性。然后,考虑到应用程序的特性,可以确定故障检测延迟过早超时风险之间的适当折衷。

系统不是使用配置的常量超时时间,而是连续测量响应时间及其变化(抖动),并根据观察到的响应时间分布自动调整超时时间。这可以通过Phi Accrual故障检测器来完成,该检测器在例如Akka和Cassandra 中使用。 TCP超时重传机制也同样起作用。

同步网络 vs 异步网络

建立一个端到端的电路:在两个呼叫者之间的整个路线上为呼叫分配一个固定的,有保证的带宽量。这个电路会保持至通话结束。例如,ISDN网络以每秒4000帧的固定速率运行。呼叫建立时,每个帧内(每个方向)分配16位空间。因此,在通话期间,每一方都保证能够每250微秒发送一个精确的16位数据。

这种网络是同步的:即使数据经过多个路由器,也不会受到排队的影响,因为呼叫的16位空间已经在网络的下一跳中保留了下来。而且由于没有排队,网络的最大端到端延迟是固定的。我们称之为有限延迟(bounded delay)

我们不能简单地使网络延迟可预测吗?

以太网和IP是分组交换协议,不得不忍受排队的折磨,及其导致的网络无限延迟。这些协议没有电路的概念。

为什么数据中心网络和互联网使用分组交换?答案是,它们针对突发流量(bursty traffic) 进行了优化。

不可靠的时钟

网络上的每台机器都有自己的时钟,这是一个实际的硬件设备:通常是石英晶体振荡器。这些设备不是完全准确的,所以每台机器都有自己的时间概念,可能比其他机器稍快或更慢。可以在一定程度上同步时钟:最常用的机制是网络时间协议(NTP),它允许根据一组服务器报告的时间来调整计算机时钟。服务器则从更精确的时间源(如GPS接收机)获取时间。

单调钟与时钟

现代计算机至少有两种不同的时钟:时钟和单调钟。

时钟

时钟是您直观地了解时钟的依据:它根据某个日历(也称为挂钟时间(wall-clock time))返回当前日期和时间。例如,Linux上的clock_gettime(CLOCK_REALTIME)和Java中的System.currentTimeMillis()返回自epoch(1970年1月1日 午夜 UTC,格里高利历)以来的秒数(或毫秒),根据公历日历,不包括闰秒。

单调钟

单调钟适用于测量持续时间(时间间隔),例如超时或服务的响应时间:Linux上的clock_gettime(CLOCK_MONOTONIC),和Java中的System.nanoTime()都是单调时钟。他们保证总是前进的(而时钟可以及时跳回)。

如果NTP协议检测到计算机的本地石英钟比NTP服务器要更快或更慢,则可以调整单调钟向前走的频率(这称为偏移(skewing)时钟)。默认情况下,NTP允许时钟速率增加或减慢最高至0.05%,但NTP不能使单调时钟向前或向后跳转。单调时钟的分辨率通常相当好:在大多数系统中,它们能在几微秒或更短的时间内测量时间间隔

在分布式系统中,使用单调钟测量经过时间(elapsed time)(比如超时)通常很好。

时钟同步与准确性

单调钟不需要同步,但是时钟需要根据NTP服务器或其他外部时间源来设置才能有用。

计算机中的石英钟不够精确:它会漂移(drifts)(运行速度快于或慢于预期)。时钟漂移取决于机器的温度。

依赖同步时钟

时钟的问题在于:一天可能不会有精确的86,400秒,时钟可能会前后跳跃,而一个节点上的时间可能与另一个节点上的时间完全不同

不正确的时钟很容易被视而不见。如果一台机器的CPU出现故障或者网络配置错误,很可能根本无法工作,所以很快就会被注意和修复。另一方面,如果它的石英时钟有缺陷,或者它的NTP客户端配置错误,大部分事情似乎仍然可以正常工作,即使它的时钟逐渐偏离现实。

如果你使用需要同步时钟的软件,必须仔细监控所有机器之间的时钟偏移。时钟偏离其他时钟太远的节点应当被宣告死亡,并从集群中移除。这样的监控可以确保你在损失发生之前注意到破损的时钟。

有序事件的时间戳

依赖时钟,在多个节点上对事件进行排序

图 客户端B的写入比客户端A的写入要晚,但是B的写入具有较早的时间戳

客户端A在节点1上写入x = 1;写入被复制到节点3;客户端B在节点3上增加x(我们现在有x = 2);最后这两个写入都被复制到节点2。

当一个写入被复制到其他节点时,它会根据发生写入的节点上的时钟时钟标记一个时间戳。在这个例子中,时钟同步是非常好的:节点1和节点3之间的偏差小于3ms,这可能比你在实践中预期的更好。

但是时间戳却无法正确排列事件:写入x = 1的时间戳为42.004秒,但写入x = 2的时间戳为42.003秒,即使x = 2在稍后出现。当节点2接收到这两个事件时,会错误地推断出x = 1是最近的值,而丢弃写入x = 2。效果上表现为,客户端B的增量操作会丢失。

这种冲突解决策略被称为最后写入为准(LWW),它在多领导者复制和无领导者数据库(如Cassandra 和Riak )中被广泛使用。

有些实现会在客户端而不是服务器上生成时间戳,但这并不能改变LWW的基本问题:

  • 数据库写入可能会神秘地消失:具有滞后时钟的节点无法用快速时钟覆盖之前由节点写入的值,直到节点之间的时钟偏差过去。此方案可能导致一定数量的数据被悄悄丢弃,而未向应用报告任何错误。
  • LWW无法区分高频顺序写入(客户端B的增量操作一定发生在客户端A的写入之后)和真正并发写入(写入者意识不到其他写入者)。需要额外的因果关系跟踪机制(例如版本向量),以防止因果关系的冲突。
  • 两个节点可以独立生成具有相同时间戳的写入,特别是在时钟仅具有毫秒分辨率的情况下。为了解决这样的冲突,还需要一个额外的决胜值(tiebreaker)(可以简单地是一个大随机数),但这种方法也可能会导致违背因果关系。

通过保留最“最近”的值并放弃其他值来解决冲突是很诱惑人的,但是要注意,“最近”的定义取决于本地的时钟,这很可能是不正确的。

逻辑时钟是基于递增计数器而不是振荡石英晶体,对于排序事件来说是更安全的选择。逻辑时钟不测量一天中的时间或经过的秒数,而仅测量事件的相对顺序(无论一个事件发生在另一个事件之前还是之后)。相反,用来测量实际经过时间的时钟单调钟也被称为物理时钟。

时钟读数存在置信区间

将时钟读数视为一个时间点是没有意义的——它更像是一段时间范围:例如,一个系统可能以95%的置信度认为当前时间处于本分钟内的第10.3秒和10.5秒之间。

大多数系统不公开这种不确定性:例如,当调用clock_gettime()时,返回值不会告诉你时间戳的预期错误,所以你不知道其置信区间是5毫秒还是5年。

Spanner中的Google TrueTime API ,它明确地报告了本地时钟的置信区间。当获取当前时间时,会得到两个值:[最早,最晚],这是最早可能的时间戳和最晚可能的时间戳。

全局快照的同步时钟

快照隔离,是数据库中非常有用的功能,需要支持小型快速读写事务和大型长时间运行的只读事务,用于备份或分析。它允许只读事务看到特定时间点的处于一致状态的数据库,且不会锁定和干扰读写事务。

快照隔离最常见的实现需要单调递增的事务ID。如果写入比快照晚(即,写入具有比快照更大的事务ID),则该写入对于快照事务是不可见的。在单节点数据库上,一个简单的计数器就足以生成事务ID。

当数据库分布在许多机器上,全局单调递增的事务ID可能很难生成。

事务ID必须反映因果关系:如果事务B读取由事务A写入的值,则B必须具有比A更大的事务ID,否则快照就无法保持一致。在有大量的小规模、高频率的事务情景下,在分布式系统中创建事务ID成为一个站不住脚的瓶颈。

暂停进程

一个线程可能会暂停很长时间,这种情况发生的原因有很多种:

  • 许多编程语言运行时(如Java虚拟机)都有一个垃圾收集器(GC),偶尔需要停止所有正在运行的线程。这些“停止世界(stop-the-world)”GC暂停有时会持续几分钟!甚至像HotSpot JVM的CMS这样的所谓的“并行”垃圾收集器也不能完全与应用程序代码并行运行,它需要不时地停止世界。尽管通常可以通过改变分配模式或调整GC设置来减少暂停,但是如果我们想要提供健壮的保证,就必须假设最坏的情况发生。
  • 在虚拟化环境中,可以挂起(suspend)虚拟机(暂停执行所有进程并将内存内容保存到磁盘)并恢复(恢复内存内容并继续执行)。这个暂停可以在进程执行的任何时候发生,并且可以持续任意长的时间。这个功能有时用于虚拟机从一个主机到另一个主机的实时迁移,而不需要重新启动,在这种情况下,暂停的长度取决于进程写入内存的速率。
  • 在最终用户的设备(如笔记本电脑)上,执行也可能被暂停并随意恢复,例如当用户关闭笔记本电脑的盖子时。
  • 当操作系统上下文切换到另一个线程时,或者当管理程序切换到另一个虚拟机时(在虚拟机中运行时),当前正在运行的线程可以在代码中的任意点处暂停。在虚拟机的情况下,在其他虚拟机中花费的CPU时间被称为窃取时间(steal time)。如果机器处于沉重的负载下(即,如果等待运行的线程很长),暂停的线程再次运行可能需要一些时间。
  • 如果应用程序执行同步磁盘访问,则线程可能暂停,等待缓慢的磁盘I/O操作完成。在许多语言中,即使代码没有包含文件访问,磁盘访问也可能出乎意料地发生——例如,Java类加载器在第一次使用时惰性加载类文件,这可能在程序执行过程中随时发生。 I/O暂停和GC暂停甚至可能合谋组合它们的延迟。如果磁盘实际上是一个网络文件系统或网络块设备(如亚马逊的EBS),I/O延迟进一步受到网络延迟变化的影响。
  • 如果操作系统配置为允许交换到磁盘(分页),则简单的内存访问可能导致页面错误(page fault),要求将磁盘中的页面装入内存。当这个缓慢的I/O操作发生时,线程暂停。如果内存压力很高,则可能需要将不同的页面换出到磁盘。在极端情况下,操作系统可能花费大部分时间将页面交换到内存中,而实际上完成的工作很少(这被称为抖动(thrashing))。为了避免这个问题,通常在服务器机器上禁用页面调度(如果你宁愿干掉一个进程来释放内存,也不愿意冒抖动风险)。
  • 可以通过发送SIGSTOP信号来暂停Unix进程,例如通过在shell中按下Ctrl-Z。

所有这些事件都可以随时抢占(preempt)正在运行的线程,并在稍后的时间恢复运行,而线程甚至不会注意到这一点。

响应时间保证

在一些系统中,软件必须有一个特定的截止时间(deadline),如果截止时间不满足,可能会导致整个系统的故障。这就是所谓的硬实时(hard real-time)系统。

在系统中提供实时保证需要各级软件栈的支持:

  • 一个实时操作系统(RTOS),允许在指定的时间间隔内保证CPU时间的分配。库函数必须记录最坏情况下的执行时间;
  • 动态内存分配可能受到限制或完全不允许(实时垃圾收集器存在,但是应用程序仍然必须确保它不会给GC太多的负担);
  • 必须进行大量的测试和测量,以确保达到保证。

实时”与“高性能”不一样——事实上,实时系统可能具有较低的吞吐量,因为他们必须优先考虑及时响应高于一切。

对于大多数服务器端数据处理系统来说,实时保证是不经济或不合适的。因此,这些系统必须承受在非实时环境中运行的暂停和时钟不稳定性

限制垃圾收集的影响

将GC暂停视为一个节点的短暂计划中断,并让其他节点处理来自客户端的请求,同时一个节点正在收集其垃圾。如果运行时可以警告应用程序一个节点很快需要GC暂停,那么应用程序可以停止向该节点发送新的请求,等待它完成处理未完成的请求,然后在没有请求正在进行时执行GC。

这个想法的一个变种是只用垃圾收集器来处理短命对象(这些对象要快速收集),并定期在积累大量长寿对象(因此需要完整GC)之前重新启动进程。一次可以重新启动一个节点,在计划重新启动之前,流量可以从节点移开。

这些措施不能完全阻止垃圾回收暂停,但可以有效地减少它们对应用的影响。

真理由多数所定义

半断开(semi-disconnected)的节点:一个节点能够接收发送给它的所有消息,但是来自该节点的任何传出消息被丢弃或延迟。

节点不一定能相信自己对于情况的判断。分布式系统不能完全依赖单个节点,因为节点可能随时失效,可能会使系统卡死,无法恢复。相反,许多分布式算法都依赖于法定人数,即在节点之间进行投票:决策需要来自多个节点的最小投票数,以减少对于某个特定节点的依赖

最常见的法定人数是超过一半的绝对多数(尽管其他类型的法定人数也是可能的)。

领导者与锁定

通常情况下,一些东西在一个系统中只能有一个。例如:

  • 数据库分区的领导者只能有一个节点,以避免脑裂(split brain)
  • 特定资源的锁或对象只允许一个事务/客户端持有,以防同时写入和损坏。
  • 一个特定的用户名只能被一个用户所注册,因为用户名必须唯一标识一个用户。

在分布式系统中实现这一点需要注意:即使一个节点认为它是“天选者(the choosen one)”(分区的负责人,锁的持有者,成功获取用户名的用户的请求处理程序),但这并不一定意味着有法定人数的节点同意!一个节点可能以前是领导者,但是如果其他节点在此期间宣布它死亡(例如,由于网络中断或GC暂停),则它可能已被降级,且另一个领导者可能已经当选。

图 分布式锁的实现不正确:客户端1认为它仍然具有有效的租约,即使它已经过期,从而破坏了存储中的文件

如果持有租约的客户端暂停太久,它的租约将到期。另一个客户端可以获得同一文件的租约,并开始写入文件。当暂停的客户端回来时,它认为(不正确)它仍然有一个有效的租约,并继续写入文件。结果,客户的写入冲突和损坏的文件。

防护令牌

防护(fencing):当使用锁或租约来保护对某些资源的访问时,需要确保一个被误认为自己是“天选者”的节点不能中断系统的其它部分。

只允许以增加屏蔽令牌的顺序进行写操作,从而保证存储安全

假设每次锁定服务器授予锁或租约时,它还会返回一个防护令牌(fencing token),这个数字在每次授予锁定时都会增加。然后,可以要求客户端每次向存储服务发送写入请求时,都必须包含当前的屏蔽令牌。

客户端1以33的令牌获得租约,但随后进入一个长时间的停顿并且租约到期。客户端2以34的令牌(该数字总是增加)获取租约,然后将其写入请求发送到存储服务,包括34的令牌。稍后,客户端1恢复生机并将其写入存储服务,包括其令牌值33.但是,存储服务器会记住它已经处理了一个具有更高令牌编号(34)的写入,因此它会拒绝带有令牌33的请求。

如果将ZooKeeper用作锁定服务,则可将事务标识zxid或节点版本cversion用作屏蔽令牌。由于它们保证单调递增,因此它们具有所需的属性。

拜占庭故障

如果节点可能声称其实际上没有收到特定的消息。这种行为被称为拜占庭故障(Byzantine fault)在不信任的环境中达成共识的问题被称为拜占庭将军问题

当一个系统在部分节点发生故障、不遵守协议、甚至恶意攻击、扰乱网络时仍然能继续正确工作,称之为拜占庭容错(Byzantine fault-tolerant)。

在大多数系统中,如果攻击者可以渗透一个节点,那他们可能会渗透所有这些节点,因为它们可能运行相同的软件。因此传统机制(认证,访问控制,加密,防火墙等)仍然是攻击者的主要保护措施。

弱谎言形式

  • 由于硬件问题或操作系统,驱动程序,路由器等中的错误,网络数据包有时会受到损坏。通常,内建于TCP和UDP中的校验和会俘获损坏的数据包,但有时它们会逃避检测 。简单的措施通常是采用充分的保护来防止这种破坏,例如应用程序级协议中的校验和。
  • 可公开访问的应用程序必须仔细清理来自用户的任何输入,例如检查值是否在合理的范围内,并限制字符串的大小以防止通过大内存分配拒绝服务。防火墙后面的内部服务可能能够在对输入进行较不严格的检查的情况下逃脱,但是一些基本的理智检查(例如,在协议解析中)是一个好主意。
  • NTP客户端可以配置多个服务器地址。同步时,客户端联系所有的服务器,估计它们的误差,并检查大多数服务器是否在对某个时间范围内达成一致。只要大多数的服务器没问题,一个配置错误的NTP服务器报告的时间会被当成特异值从同步中排除。使用多个服务器使NTP更健壮(比起只用单个服务器来)。

系统模型与现实

定时假设,三种系统模型:

同步模型

同步模型(synchronous model)假设网络延迟,进程暂停和和时钟误差都是有界限的。这并不意味着完全同步的时钟或零网络延迟;这只意味着你知道网络延迟,暂停和时钟漂移将永远不会超过某个固定的上限。

部分同步模型

部分同步(partial synchronous)意味着一个系统在大多数情况下像一个同步系统一样运行,但有时候会超出网络延迟,进程暂停和时钟漂移的界限。这是很多系统的现实模型:大多数情况下,网络和进程表现良好,否则我们永远无法完成任何事情。

异步模型

在这个模型中,一个算法不允许对时机做任何假设——事实上它甚至没有时钟(所以它不能使用超时)。

节点失效三种最常见的节点系统模型:

崩溃-停止故障

崩溃停止(crash-stop)模型中,算法可能会假设一个节点只能以一种方式失效,即通过崩溃。这意味着节点可能在任意时刻突然停止响应,此后该节点永远消失——它永远不会回来。

崩溃-恢复故障

我们假设节点可能会在任何时候崩溃,但也许会在未知的时间之后再次开始响应。在崩溃-恢复(crash-recovery)模型中,假设节点具有稳定的存储(即,非易失性磁盘存储)且会在崩溃中保留,而内存中的状态会丢失。

拜占庭(任意)故障

节点可以做(绝对意义上的)任何事情,包括试图戏弄和欺骗其他节点。

对于真实系统的建模,具有崩溃-恢复故障(crash-recovery)部分同步模型(partial synchronous)通常是最有用的模型。

算法的正确性

为了定义算法是正确的,我们可以描述它的属性

例如,排序算法的输出具有如下特性:对于输出列表中的任何两个不同的元素,左边的元素比右边的元素小。

安全性和活性

唯一性(uniqueness)单调序列(monotonic sequence)是安全属性,但可用性活性(liveness)属性。

活性属性通常在定义中通常包括“最终”一词。 最终一致性是一个活性属性。

安全性通常被非正式地定义为,没有坏事发生,而活性通常就类似:最终好事发生

安全性和活性的实际定义是精确的和数学的:

  • 如果安全属性被违反,我们可以指向一个特定的时间点 。违反安全属性后,违规行为不能撤销——损失已经发生。
  • 活性属性反过来:在某个时间点,它可能不成立,但总是希望在未来成立

将系统模型映射到现实世界

安全性和活性属性以及系统模型对于推理分布式算法的正确性非常有用。然而,现实的混乱事实再一次地让你咬牙切齿,很明显系统模型是对现实的简化抽象。

算法的理论描述可以简单宣称一些事在假设上是不会发生的——在非拜占庭式系统中。但实际上我们还是需要对可能发生和不可能发生的故障做出假设,真实世界的实现,仍然会包括处理“假设上不可能”情况的代码,即使代码可能就是printf("you sucks")exit(666)

抽象的系统模型对于将实际系统的复杂性降低到一个我们可以推理的可处理的错误是非常有帮助的,以便我们能够理解这个问题,并试图系统地解决这个问题。

证明算法正确并不意味着它在真实系统上的实现必然总是正确的。但这迈出了很好的第一步,因为理论分析可以发现算法中的问题,这种问题可能会在现实系统中长期潜伏,直到你的假设(例如,时间)因为不寻常的情况被打破。理论分析与经验测试同样重要。

posted on 2023-08-04 23:08  Mr.Tan&  阅读(48)  评论(0编辑  收藏  举报

导航