1. 故障与部分失效
在我有限的经验中,我已经和很多东西打过交道:**单个数据中心(DC)**中长期存在的网络分区,配电单元 PDU 故障,交换机故障,整个机架意味重启,整个数据中心主干网络故障,整个数据中心的电源故障,以及一个低血糖的司机把他的福特皮卡撞在数据中心的 HVAC(加热、通风和空调)系统上。而且我甚至不是一个运维。
? —— 柯达黑尔
在一台计算机上编写一个程序时,它通常会以一种相当可以预测的方式允许:无论是工作还是不工作。单个计算机上的软件没有根本性不可靠的原因:当硬件正常工作时,相同的操作总是产生相同的结果—这是确定的。如果存在硬件问题,其后果通常是整个系统故障。装有良好软件的个人计算机通常要么功能完好,要么功能完全失效,而不是介于两者之间
而在分布式系统中,尽管系统的其他部分工作正常,但是系统的某些部分可能会以某种不可预知的方式被破坏。这被称为部分失效(partial failure)。难点在于部分失效是不确定的:如果你试图做任何涉及多个节点和网络的事情,它有时可能会工作,有时会出现不可预知的失败。
1.1 云计算与超级计算机
关于如何构建大型计算系统两种哲学:
- 高性能计算(HPC)领域。具有数千个 CPU 的超级计算机通常是计算密集型科学计算任务,比如天气预报或分子动力学(模拟原子和分子运动)。
- 云计算领域。通常与多租户数据中心,连接 IP 网络的商品计算机,弹性/按需资源分配以及计算计费等相关联。
注:传统企业数据中心位于这两个极端之间。
不同的哲学拥有不同的故障处理方式,超级计算机通过让部分失败升级为完全失败来处理部分失败,如果系统的任何部分发生故障,只是让所有东西都崩溃。
而在云服务中心的互联网系统与超级计算机有很多不同之处:
- 互联网程序都是「online」的,它需要能够随时以低延迟服务用户,服务不可以是不可以接受的(比如 美团、豆瓣…… 挂了)。相比之下,像天气模拟这样的离线工作可以停止并重新启动。
- 超级计算机由专门的硬件构成具有较低的故障率,而云服务中心的节点通常由普通机器构成,具有较高的故障率
- 大型数据中心网络通常基于 IP 和以太网,以闭合拓扑排序,以提供更高的二等分带宽。超级计算机通常使用专门的网络拓扑排序,例如多维网格和排序,这位具有已知通信模式的 HPC 工作负载提供了更好的性能。
- 系统越大,其组件之一就越有可能坏掉。一个具有成千上万个节点的系统中,有理由认为总是有一些东西是坏掉的。当采用简答的放弃组成时,大系统最终需要花费更多的时间从错误中恢复出来。
- 如果系统可以容忍发生故障的节点,并继续保持整体工作状态,那么这对于操作和维护非常有用,比如,可以执行滚动升级等。
- 在地理位置分散的部署中,通信很可能通过互联网进行,与本地网络相比,通信速度缓慢且不可靠。超级计算机通常假设它们所有的节点都靠近在一起。
从不可靠的组件构建可靠的系统
直观地看来,系统只能像其最不可靠的组件一样可靠。然而事实上,从不大可靠的潜在基础构建更可靠的系统是计算机领域的一个古老方案:
- 纠错码允许数字数据在网络通信信道上准确传输,比如无线网络上的无线电干扰
- 互联网协议(Internet Protocol, IP)不可靠:可能丢弃,延迟,复制或重排数据包。传输控制协议(Transmission Control IP, TCP)在互联网协议(IP)之上提供了更可靠的传输层:它确保丢失的数据被重新传输,消除重复,并且数据包被重新组装成它们被发送的顺序。
虽然这个策略比它底层部分更可靠,但它们的可靠性总是有限的:
- 纠错码可以处理少量的单比特错误,但无法处理信号被干扰淹没的情况
- TCP 可以隐藏数据包的丢失、重复和重新排序,但是不能消除网络中的延迟
2. 不可靠的网络
无共享并不是构建系统的唯一方式,但它已经成为构建互联网服务的主要方式,其主要原因:相对便宜,因为它不需要特殊的硬件,可以利用云计算服务,通过跨多个地理分布的数据中心进行冗余实现高可靠性。
但是此种方式构建的网络有一个相对明显的劣势——「网络不能确保发出去的包什么时候能到达,或者是否到达」
处理这个问题的通常方法是——超时:在一段时间之后放弃等待,并且认为响应不会到达。
注:即使客户端确认了超时,但是仍确保服务端是否收到了请求。比如,如果请求在某个地方排队,那么即使客户端已经放弃了该请求,仍然可能会将其发送给服务端
2.1 真实世界的网络故障
网络分区:当网络的一部分由于网络故障而被切断。
处理网络故障并不是容忍它们:如果网络是相当可靠的,一个有效的方法就是当网络遇到问题时,简单地向用户显示一条错误信息。
注:有意识的确认软件能够应对哪些网络问题,并确保系统能够从中恢复很重要。比如 Chaos Monkey (有意识地触发网络问题并测试系统响应)
2.2 检测故障
许多系统需要自动检测故障节点。例如:
- 负载均衡需要停止向已经死亡的节点转发请求
- 在单主复制功能的分布式数据库中,如果主库失效,则需要将从库之一升级为主库
不幸的是,网络的不确定性使得很难判断一个节点是否工作。在某些特定的情况下,可能会收到一些反馈,明确告诉你某些事情没有成功。节点的反馈很有用,但是不能指望它。即使 TCP 确认已经传送了一个数据包,应用程序在处理之前可能已经崩溃。如果你想确保一个请求是成功的,需要应用程序本身的正确响应。
2.3 超时与无穷的延迟
异步网络具有无限的延迟(即尽可能快地传送数据包,但数据包到达可能需要的时间没有上限),并且大多数服务器实现并不能保证他们可以在一定的最大时间内处理请求。
- 长时间的超时意味着长时间等待,直到一个节点被宣告死亡,这段时间内,用户不得不等待,或者看到错误的信息
- 短时间的超时可以更快地检测到故障,但有更高地风险误将一个节点宣告为失效,而该节点实际上只是暂时地变慢了。
2.3.1 网络拥塞和排队
在驾驶汽车时,由于交通拥塞,道路交通网络的通行时间往往有很大不同。同样,计算机网络上数据包延迟的可变性通常是由于排队:
- 多个不同节点尝试将数据包发送到同一目的地,则网络交换机必须将它们排队并逐个传入到目标网络链路。在繁忙的网络链路上,数据包可能需要等待一段时间才能获得一个插槽(这称为网络拥塞)。如果传入的数据太多,交换机队列填满,数据包将被丢弃,因此需要重新发送数据包——即使网络运行良好。
- 当数据包到达目标机器使,如果所有的 CPU 内核当前都处于繁忙状态,则来自网络的传入请求将被操作系统排队,直到应用程序准备好处理它为止。根据机器上的负载,这可能需要一段任意的时间。
- 在虚拟化环境中,正在运行的操作系统经常暂停几十毫秒,因为另一个虚拟机正在使用 CPU 内核。在这段时间内,虚拟机不能从网络中消耗任何数据,所有传入的数据被虚拟机监视器排队(缓冲),进一步增加了网络延迟的可变性。
- TCP 执行流量控制(flow control),其中节点会限制自己发送的速率以避免网络链路或接收节点过载。这意味着甚至在数据进入网络之前,在发送者处就进行额外的排队。
TCP 与 UDP
一些对延迟敏感的应用程序(如视频会议和 IP 语音)使用 UDP 而不是 TCP。这是在可靠性和延迟变化之间的折中:由于 UDP 不执行流量控制并且不重传丢失分组,所以避免了一些网络延迟变化的影响。但它仍然受切换队列和调度延迟的影响。
注:在延迟数据毫无价值的情况下, UDP 是一个不错的选择。
2.4 同步网络 vs 异步网络
拨打电话的网络是同步的,即使数据经过多个路由器,也不会受到排队的影响,因为呼叫的 16 位空间已经在网络的下一跳中保留了下来。由于没有排队,网络最大端到端延迟是固定的,称之为有限延迟。
注:当拨打电话时,会建立一个在两个呼叫者之间的整个路线上为呼叫分配一个固定的,有保证的带宽量。这个电路会保持至通过结束。
思考:不能简单地使网络延迟可预测吗?
答案是否定的,电话网络中的电路与 TCP 连接有很大不同:电路是固定数量的预留带宽,在电路建立时没有其他人可以使用,而 TCP 连接的数据包机会性地使用任何可用的网络带宽。
注:如果数据中心网络和互联网是电路交换网络,那么在建立电路使就可以建立一个受保证的最大往返时间。但是,以太网和 IP 是分组交换协议,不得不忍受排队的zhemo,及其导致的网络无限延迟。
思考:为什么数据中心网络和互联网使用分组交换?
答案:它们针对突发流量进行了优化。请求网页,发送电子邮件或传输文件没有任何特定带宽的要求——只希望它尽快完成。如果想通过电路传输文件,得预测一个带宽分配,如果猜的太低,传输速度会不必要的太慢,导致网络容量空闲。如果猜的太高,电路就无法建立。因此,将电路用于突发数据传输会浪费网络容量,并且使传输不必要的缓慢。 相比之下, TCP 动态调整数据传输速率以适应可用的网络容量
2.4.1 延迟和资源利用
互联网动态分配网络带带宽。发送者互相推诿和争夺,以让他们的数据包尽可能快地通过网络,并且网络交换机决定从一个时刻到另一个时刻发送哪个分组(即,带宽分配)。**这种方法有排队的缺点。但其优点是最大限度利用了电线。**电线固定成本,所以如果你更好地利用它,你通过电线发送的每个字节也就会更便宜。 CPU 也类似。
如果**资源是静态分区的(例如,专用硬件和专用带宽分配),则在某些环境中可以实现延迟保证。**但是,这是以降低利用率为代价的——换句话说,它是更昂贵的。另一方面,动态资源分配的多租户提供了更好的利用率,所以它具有可变延迟的缺点。
注:想要更低的延迟势必要投入更多的资源,也就是更多的钱,嗯,毕竟搭专线也是需要钱的。
3. 不可靠的时钟
在分布式系统中,时间是一件棘手的事情,因为通信不是即时的:消息通过网络从一台机器传送到另一台机器需要时间。收到消息的时间总是晚于发送的时间,但是由于网络中的可变延迟,并不能确切知道晚了多少时间。这导致在涉及多台机器发生很难确定事件发生的顺序。
比如,应用程序以下各种问题依赖于时钟来回答?
- 这个请求是否超时了?
- 这项服务的第 99 百分位响应时间是多少?
- 在过去五分钟内,该服务平均处理了多少个查询?
- 用户在我们的网站上花了多少时间?
- 这篇文章何时发布?
- 在什么时候发送提醒邮件?
- 这个缓存条目何时到期?
- 日志文件中此错误消息的时间戳是什么?
注:网络上每台机器都有自己的时钟,这是一个实际的硬件设备:通常是石英晶体振荡器。所以每台机器都有自己的时间概念,可能比其他机器稍快或慢。
3.1 单调钟和日历时钟
3.1.1 日历时钟
日历时钟是直观了解时钟的依据:它根据某个日历返回当前日期和时间。例如,Linux 上的 clock_gettime(CLOCK_REALTIME) 返回 UTC 时间 1970年1月1日午夜以来的秒数(或毫米)
注:如果本地时钟在 NTP 服务器之前太远,则它会被强制重置,看上去好像跳回了之前的时间点。
3.1.2 单调钟
在某个时间点检查单调钟的值,做一些逻辑,且稍后再次检查它。这两个值之间的差异告诉你两次检查之间经过了多长时间。
注:在分布式系统中,使用单调测量经过时间(比如超时)通常很好
3.2 时钟同步与准确性
日历时钟需要根据 NTP 服务器或其他外部时间源来设置才能用。但是,获取时钟的方法可能并不可靠和准确——硬件时钟和 NTP 可能会变换莫测。
- 计算机中的石英钟不够精确:它会漂移(drifts)(运行速度快于或慢于预期)。
- 如果计算机时钟与 NTP 服务器的时钟差别太大,可能会拒绝同步,或者本地时钟被强制重置。
- 某个节点被 NTP 服务器的防火墙意外阻塞,有可能会持续一段时间都没有人注意到。有证据表明,这在实践中确实发生过
- NTP 同步只能和网络延迟一样好,所以在拥有可变数据包延迟的拥塞网络上时, NTP 同步的准确性会受到限制。
- 一些 NTP 服务器是错误或者配置错误的,报告的时间可能相差几个小时。不过 NTP 客户端非常健壮,因为他们会查询多个服务器并忽略异常值。
- 闰秒导致一分钟可能有 59秒或61秒,这会打破一些在设计之初时未考虑闰秒的系统的时序假设。
- **在虚拟机中,硬件时钟被虚拟化,这对于需要精确计时的应用提出了额外的挑战。**当一个 CPU 核心在虚拟机之间共享时,每个虚拟机都会暂停几十毫秒,与此同时另一个虚拟机正在运行。从应用程序的角度看,这种停顿表现为时钟突然向前跳跃。
- 在没有完全控制权的设备中运行软件,则不应该完全信任该设备的硬件时钟。一些用户故意将其硬件时钟设置为不正确的日期和时间。比如:为了规避游戏中的时间限制,时钟可能会被设置到很远的过去或将来。
3.3 依赖同步时钟
3.3.1 有序事件的时间戳
在具有多个领导者复制的数据库中对时钟的危险使用例子:
- 客户端 A 在节点 1 上写入 x = 1;
- 写入被复制到节点 3;
- 客户端在节点 3 增加 x (此刻 x = 2)
- 最后两个写入都被复制到到节点 2
上图展示了时间戳无法正确排列事件的例子:写入 x = 1 的时间戳为 42.004s ,但写入 x = 2 的时间戳为 42.003s,即使 x = 2 为最后操作。但是当节点 2 接收到这两个事件时,会推断出 x = 1 是最近的值,而丢弃写入 x =2 ,效果上表现为,客户端 B 的增量操作会丢失。
注:使用逻辑时钟,即基于递增的计数器而不是振荡石英静态,对排序时间来说是更安全的选择。逻辑时钟不测量一天中的时间或经过的秒数,而仅测量事件的相对顺。
3.3.2 时钟读取存在置信区间
能够以微秒或甚至纳秒的精度读取机器的时钟。但即使可以得到如此细致的测量结果,这并不意味着这个值是准确的。即使每分钟与本地网络上的 NTP 服务器进行同步,几毫秒的时间漂移也很容易在不精确的石英时钟上发生。使用公共互联网上的 NTP 服务器,最好的准确度可能达到几十毫秒,但是当网络阻塞时,误差可能会超过 100 毫秒。
因此,将时钟读数视为一个时间点是没有意义的——它更像是一段时间范围:例如,一个系统可能以 95% 的置信度认为当前时间处于本分钟内的第 10.3s 和 10.5s 之间。
3.3.3 全局快照的同步时钟
快照隔离最常见的实现需要单调递增的事务 ID。如果写入具有比快照更大的事务 ID,则该写入对快照事务是不可见的。在单节点数据库上,一个简单的计数器就足以生成事务 ID。
但是当数据库分布在许多机器上,也许可能在多个数据中心中,由于需要协调,全局单调递增的事务 ID 会很难生成。事务 ID 必须反映因果关系:如果事务 B 读取事务 A 写入的值,则 B 必须具有比 A 更大的事务 ID,否则快照就无法保持一致。在有大量的小规模、高频率的事务场景下,分布式系统中创建事务 ID 成为一个难以处理的瓶颈。
是否可以使用同步时钟的时间戳作为事务 ID?
如果能够获得足够好的同步性,这种方式将具有很合适的属性:更晚的事务具有更大的时间戳。当然,问题在于时钟精度的不确定性。
注:spanner 以这种方式实现跨数据中心的快照隔离。为了确保事务时间戳反映因果关系,在提交读写事务之前,Spanner 会故意等待置信区间长度的时间。通过这样,它可以确保任何可能读取数据的事务处于足够晚的时间,因此它们的置信区间不会重叠。
3.4 进程暂停
在以下情况发生时,一个线程可能暂停很长时间:
- 许多编程语言运行时都有一个垃圾收集器(GC),偶尔需要停止所有正在运行的线程。
- 在虚拟化环境中,可以挂起虚拟机(暂停执行所有进程并将内存保存到磁盘)并恢复(恢复内存内容并继续执行)。这个暂停可以在进程的任意时间执行,并且可以持续任意时长。
- 在用户设备上(比如笔记本)上,执行也可能被暂停并随意恢复,例如当用户关闭笔记本电脑的盖子时。
- 操作系统上下文切换到另一个线程时,正在运行的线程可能在代码的任意点处暂停。
- 应用程序执行同步磁盘访问,则线程可能暂停,等待缓慢的磁盘 I/O 操作完成。在许多语言中,即使代码没有包含文件访问,磁盘访问也可能发生——比如, Java 类加载器在第一次使用时惰性加载类文件,这可能在程序执行过程中随时发生。I/O 暂停和 GC 暂停甚至可能同时发生。如果磁盘实际上是一个网络文件系统或网络块设备,I/O 延迟进一步收到网络延迟变化的影响。
- 如果操作系统配置为允许交换磁盘(页面交换),则简单的内存访问可能导致页面错误(page fault),要求将磁盘中页面装入内存。当这个缓慢的 I/O 操作发生时,线程暂停。
- 可以通过发送 SIGSTOP 信号来暂停 Unix 进程,例如在 Shell 中按下 Ctrl-Z。这个信号立即阻止进程继续执行更多的 CPU 周期,直到 SIGCONT 恢复为止。
所有这些事件可以随时「抢占」正在运行的线程,并在稍后的时间恢复运行,而线程甚至不会注意到这一点。这个问题类似于在单个机器上使多个线程代码线程安全:不能对时机做任何假设,因为随时可能发生上下文切换,或者并行运行。
注:当在一台机器上编写多线程代码使,有很多工具在确保线程安全:互斥量,信号量,原子计数器,无锁数据结构,阻塞队列等。不幸的是,这些工具并不能直接转化为分布式操作,因为分布式系统没有共享内存,只有通过不可靠网络发送消息。
3.4.1 响应时间保证
实时是真的吗?
在嵌入系统中,实时是指系统经过精心设计和测试,以满足所有情况下的特定时间保证。这个含义与 Web 上实时术语的模糊使用相反,后者描述了服务器将数据推送到客户端以及没有严格的响应时间限制的流处理。
在某些软件的运行环境要求很高,不能在特定时间内响应可能会导致严重的损失:控制飞机、火箭、机器人、汽车或其他物体的计算机必须对其传感器输入做出快速而可预测的响应。
对于大多数服务器端数据处理系统来说,实时保证是不经济或不合适的。因此,这些系统必须承受在非实时环境中运行的暂停和时钟不稳定性。
3.4.2 限制垃圾收集的影响
一个新兴的想法是将 GC 暂停视为一个节点的短暂计划中断,并在这个节点收集其垃圾的同时,让其他节点处理来自客户端的请求。如果运行时可以告警一个节点很快需要 GC 暂停,那么应用程序可以停止向该节点发送新请求,等待它完成处理未完成的请求,然后在没有请求正在进行时执行GC。这个技巧向客户端因此了 GC 暂停,并降低了响应时间的高百分比。
4. 知识、真相与谎言
在分布式系统的网络中的一个节点具有以下特征:
- 节点无法确切知道任何事情——它只能根据网络接收到的消息进行猜测。
- 节点只能通过交换消息来找出另一个节点所处的状态(存储了哪些数,是否正确运行等等)
- 如果远程节点没有响应,则无法知道它处于什么状态,因为节点可能是因为网络问题或是自身有问题
4.1 真相由多数所定义
分布式系统不能完全依赖单个节点,因为节点可能随时失效,可能会使系统卡死,无法恢复。所以许多分布式算法都依赖于法定人数,即在节点之间进行投票:决策需要来自多个节点的最小投票数,以减少对某个特定节点的依赖。
最常见的法定人数是超过一半的绝对多数。比如三个节点可以容忍单个节点故障,五个节点可以容忍双节点故障。
4.1.1 领导者和锁
通常情况下,一些东西在一个系统中只能有一个。例如:
- 数据库分区的领导者只能有一个节点,以避免「脑裂」
- 特定资源的锁或对象只允许一个事务/客户端持有,以防止同时写入或损坏
- 一个特定的用户名只能被一个用户所注册,因为用户名必须唯一标识一个用户
注:HBase 曾经有这个问题,需要客户端确保一个存储服务中的文件一次只能被一个客户端访问,如果多个客户端试图对此写入,则该文件将损坏。上图显示了由于「进程暂停」导致文件损坏的例子
4.1.2 防护令牌
假设每次锁定服务授予锁或者租约时,它还会返回一个防护令牌(fencing token),这个数字在每次授予锁定时都会增加。然后可以要求客户端每次向存储服务发送写入请求时,都必须包含当前的防护令牌。
- 客户端 1 以 33 的令牌获取租约,随后进入 GC 垃圾回收周期并且租约到期。
- 客户端 2 以 34 的令牌获取租约,随后将其写入请求发送到存储服务,包括令牌 34.
- 客户端 1 GC 结束后,将带有令牌 33 请求发送到服务器时,会被拒绝,因为它已经处理多一个更高的令牌。
注:这用使用机制要求资源本身在检查令牌方面发挥积极作用,通过拒绝使用旧的令牌。
4.2 拜占庭故障
拜占庭故障:在不信任的环境中达成共识的问题被称为拜占庭将军问题。
当一个系统的部分节点发生故障,不遵守协议、甚至恶意攻击、扰乱网络使仍能继续工作,称之为「拜占庭容错」,在特定场景下,这种担忧是很有意义的:
- 在航空航天中,计算机内存或 CPU 寄存器中的数据可能被辐射破坏,导致其以任意不可预测的方式响应其他节点。由于系统故障非常昂贵(例如,飞机撞毁和炸死船上所有人员,或火箭与国际空间站相撞),飞行控制系统必须容忍拜占庭故障
- 在多个参与组织的系统中,一些参与者可能会试图欺骗或欺骗他人。在这种情况下,节点仅仅信任另一个节点的消息是不安全的,因为他们可能是处于恶意的目的而被发送的。
注:在本书讨论的系统中,通常可以安全地假定没有拜占庭式的问题发生。
4.2.1 弱谎言形式
以下是向软件中添加防止「谎言」弱形式的机制:
- 由于硬件问题或操作系统、启动程序、路由器等中的错误,网络数据包有时会受到损坏。但是,损坏的数据包会被内建于 TCP 和 UDP的校验和所俘获,但有时他们也会逃脱检测。要对付这种破坏通常使用简单的方法就可以做到,例如应用程序级协议中的校验和
- 可公开访问的应用程序必须仔细清理来自用户的任何输入,例如检查值是否在合理的范围内,并限制字符串的大小以防止通过大内存分配的拒绝服务。
- NTP 客户端可以配置多个服务器地址。同步时,客户端连接多有的服务器,估计他们的误差,并检查大多数服务器是否对某个时间范围达成一致。只要大多数的服务器没有问题,一个配置错误的 NTP 服务器报告的时间会被当成特异值从同步中排除。
4.3 系统模型与实现
算法的编写方式不应该过分依赖于运行的硬件和软件配置的细节。这就要求我们以某种方式将在系统中发生的错误形式化。可以通过定义一个系统模型来做到这一点,这个模型是一个抽象,描述一个算法可以假设的事情。
关于时序假设,三种系统模式是常用的。
- 同步模型:假设网络延迟、进程暂停和时钟误差都是受限的。
- 部分同步模型:意味着一个系统在大多数情况下像一个同步系统一样运行,但有时候会超出网络延迟、进程暂停和时钟漂移的界限。
- 异步模型:在这个模型中个,一个算法不允许对时序做任何假设——事实上它甚至没有时钟。
关于节点失效,常见的系统模型:
- 崩溃—停止故障,算法假设一个节点只能以一种方式失效,即通过崩溃。这意味着节点可能在任意时刻突然停止响应,以后该节点永远消失——它永远不会回来。
- 崩溃—恢复故障,假设节点可能会在任何时候崩溃,但是也许会在未知的时间之后再次开始响应。在崩溃—恢复模型中,假设节点具有稳定的存储(即,非易失性磁盘存储)且会在崩溃中保留,而内存中的状态会丢失。
- 拜占庭(任意)故障:节点可以做任何事情,包括试图戏弄和欺骗其他节点。
注:对于真实系统的建模,具有「崩溃—停止故障的部分同步模型」通常是最有用的模型。
4.3.1 算法的正确性
为了算法是正确的,可以描述它的属性。比如,排序算法的输出具有如下特征:对于输出列表的任何两个不同的元素,左边的元素比右边的元素小。这只是定义对列表进行排序的一种形式方式。
同样,可以写下想要的分布式算法的属性来定义它的正确含义。比如,对于防护令牌,算法需要具有一下属性:
- 唯一性:没有两个防护令牌请求返回相同的值
- 单调序列:如果请求 x 返回令牌 t_x,请求 y 返回令牌 t_y,并且 x 在 y 开始之前已经完成,则 t_x < t_y
- 可用性:请求防护令牌并且不崩溃的节点,最终会收到响应。
注:如果一个系统模型中的算法总是满足它在所有我们假设可能发生的情况下的性质,那么这个算法是正确的。
4.3.2 安全性和活性
「唯一性和单调序列」是安全属性,而可用性是活性属性。安全性通常被非正式地定义为:没有坏事发生,而活性通常就类似:最终好事发生
- 安全属性被违反,违反行为不能被撤销——因为损失已经发生。
- 活性属性,在某个时间点不成立但是希望在未来能成立。
区分安全属性和活性属性的一个优点是可以帮助我们处理困难的系统模型。对于分布式算法,在系统模型的所有可能情况下,要求时钟保持安全属性是常见的。即所有节点崩溃,或整个网络出现故障,算法仍然必须确保它不会返回错误的结果。
4.3.3 将系统模型映射到现实世界
安全属性和活性属性以及系统模型对于推理分布式算法的正确性非常有用。然而,在实践中实施算法时,现实的混乱事实一般会让人抓狂,很明显系统模型是对现实的简化抽象。
例如
在崩溃—恢复模型中的算法通常假设稳定存储器中的数据在崩溃后可以幸存。但是
- 如果磁盘上的数据被破坏或者由于硬件错误或错误配置导致数据被清除,会发生什么情况?
- 如果服务器存在固件错误并且在重新启动时无法识别其硬盘驱动器,即使驱动器已正确连接到服务器,那又会发生什么情况?
法定人数算依赖节点来记住它声称存储的数据。但是
- 如果一个节点可能患有健忘症,忘记以前存储的数据,这会打破法定条件,从而破坏算法的正确性。
注:也许需要一个新的系统模型,在这个模型中,我们假设稳定的存储大多能在崩溃后幸存,但是也可能会丢失。但是这样的模型会更加难以推导。
算法的理论描述可以简单宣称一些事是不会发生的——在非拜占庭系统中,我们确实需要对可能发生或不可能发生的故障做出假设。
以上并不是说理论上抽象的系统模型是毫无价值的,恰恰相反,它们对于将实际系统的复杂性提取成一个个我们可以推导的可处理的错误类型是非常有帮助的,以便我们能够理解这个问题,并试图解决这个问题。我们可以证明算法是正确的,通过表明它们的属性在某个系统模型中总是成立的。
证明算法正确并不意味着它在真实系统上的实现必然总是正确的。但这迈出了很好的第一步,因为理论分享可以发现算法中的问题。理论分享与经验测试同样重要。
5. 总结
为了容忍错误,第一步是检测它们。大多数系统没有检测节点是否发生故障的准确机制。
- 大多数分布算法依靠超时来确定远程节点是否仍然可用。
- 超时无法区分网络失效和节点失效,并且可变的网络延迟有时会导致节点被错误地怀疑发生故障。
- 有时一个节点有可能处于降级状态,比如,由于驱动程序错误,千兆网卡可能突然下降到 1 Kb/s 的吞吐量。这类的「跛行」而不是死掉的节点可能比一个干净的失效节点更难处理。
一旦检测到故障,使系统容忍它们也很困难。
- 没有全局变量、没有共享内存、多个机器之间没有办法共享状态。
- 多个机器的节点对同一状态达成一致的时机无法预测。
- 数据从一个节点流向另一个节点的唯一的方法是通过不可靠的网络发送信息。
- 重大决策不能由单个节点完成,必须与其他节点商议决策,以期望达到法定人数的一致。
6. 写在最后
真是笔耕不辍的八月,出乎意料的总结好了第二篇。这个周末应该奖励一下自己,撒花儿
|