mirror of
https://github.com/DistSysCorp/ddia.git
synced 2024-12-25 12:20:22 +08:00
change _ to *
This commit is contained in:
parent
154b000d4b
commit
90b9bf1d4c
50
ch08.md
50
ch08.md
@ -21,7 +21,7 @@
|
||||
|
||||
在构建大型计算系统的选择上一个光谱:
|
||||
|
||||
1. 在光谱一侧,是**高性能计算**(HPC,_high-performance computing_)。使用上千个 CPU 构建的超级计算机,用于计算密集型工作,如天气预报、分子动力学模拟。
|
||||
1. 在光谱一侧,是**高性能计算**(HPC,*high-performance computing*)。使用上千个 CPU 构建的超级计算机,用于计算密集型工作,如天气预报、分子动力学模拟。
|
||||
2. 在光谱另一侧,是**云计算**(cloud computing)。云计算不是一个严谨的术语,而是一个偏口语化的形象指代。通常指将通用的廉价的计算资源,通过计算机网络收集起来进行池化,然后按需分配给多租户,并按实际用量进行计费。
|
||||
3. 传统的企业**自建的数据中心**位于光谱中间。
|
||||
|
||||
@ -30,8 +30,8 @@
|
||||
但在本章,我们将重点放到以网络连接的多机系统中,这样的系统与单机应用与诸多不同之处:
|
||||
|
||||
1. **在线离线**。互联网应用多为**在线**(online)服务,需要给用户提供随时可用、低延迟服务。在这种场景下,重启以恢复任务或者服务是不可接受的。但在离线任务中,如天气状况模拟。
|
||||
2. **专用通用**。超算多用**专用硬件**(_specialized hardware_)构建而成。组件本身很可靠,组件间通信也很稳定——多通过共享内存或 RDMA 的方式。与之相反,云服务多由通用机器组网而成,通过堆数量达到与超算相当的性能,经济但故障率高。
|
||||
3. **组网方式**。大型数据中心的网络通常基于 IP 和以太网,通常按 Clos 拓扑组网,以提供比较高的**对分带宽**(bisection bandwidth)。超算常用专用的网络拓扑,如多维网格、环面拓扑(_toruses_),能够为已知的 HPC 负载提供更好的性能。
|
||||
2. **专用通用**。超算多用**专用硬件**(*specialized hardware*)构建而成。组件本身很可靠,组件间通信也很稳定——多通过共享内存或 RDMA 的方式。与之相反,云服务多由通用机器组网而成,通过堆数量达到与超算相当的性能,经济但故障率高。
|
||||
3. **组网方式**。大型数据中心的网络通常基于 IP 和以太网,通常按 Clos 拓扑组网,以提供比较高的**对分带宽**(bisection bandwidth)。超算常用专用的网络拓扑,如多维网格、环面拓扑(*toruses*),能够为已知的 HPC 负载提供更好的性能。
|
||||
4. **故障常态化**。系统越是庞大,系统中有组件出错的概率便越高。在上千个节点组成的系统中,可以认为任何时刻,总有组件存在故障。在遇到故障时,如果在整个系统层面,仅简单选择放弃重试的策略,则系统可能不是在重试,就是在重试的路上,花在有效的工作时间少之又少。
|
||||
5. **容错**。当部分节点故障时,如果系统仍能作为一个整体而正常工作,将会对运维十分友好。如,对于滚动升级,虽然单个经历了重启,但是多个节点组成的系统渐次重启时,整体仍然能对外正常工作。在云上,如果某个虚拟机有点慢,我们可以销毁它,再拉起一台(如果故障节点是少数,期望会更快)。
|
||||
6. **本地异地**。多地部署的大型系统,多通过互联网通信(虽然也有专用网络),但总体来说,相对局域网更慢且易出错。相对的,我们对于超算有个基本预期——其多个节点都靠的很近。
|
||||
@ -155,11 +155,11 @@
|
||||
|
||||
如果我们的底层网络传输数据包时能够保证延迟上界、且不会丢包,那么基于此构建分布式系统将会容易的多。那为什么不在硬件层面解决相关问题让网络更可靠,从而让分布式软件免于关心这些复杂的细节呢?
|
||||
|
||||
为了回答这个问题,我们先来看一种历史产物——**固定电话网**(_fixed-line telephone network_,非 VOIP、非蜂窝网络)。在固话线路中,高延迟音频帧和意外断线都是非常罕见的。固话网会为每一次通话预留**稳定低延迟**和**充足的带宽**链路以传输语音。如果计算机网络中也采用类似的技术,生活不会很美好吗?
|
||||
为了回答这个问题,我们先来看一种历史产物——**固定电话网**(*fixed-line telephone network*,非 VOIP、非蜂窝网络)。在固话线路中,高延迟音频帧和意外断线都是非常罕见的。固话网会为每一次通话预留**稳定低延迟**和**充足的带宽**链路以传输语音。如果计算机网络中也采用类似的技术,生活不会很美好吗?
|
||||
|
||||
当你在固网内拨打电话时,会建立一条贯穿贯穿全链路的保证足量带宽的固定链路,我们称之为**电路**(circuit),该电路会保持到通话结束才释放。以 ISDN 网络为例,其每秒能容纳 4000 帧语音信号,当发起通话时,它会在每个方向为每帧数据分配 16 比特空间。因此,在整个通话期间,两端各自允许每 250 微秒(250us \* 4000 = 1s)发送 16 比特语音数据。
|
||||
|
||||
这种网络是**同步**(_synchronous_)的:尽管数据也会通过多个路由节点,但由于通信所需的资源(如上述 16 bit 空间)已经在下一跳中被提前预留出来了,因此这些数据帧不会面临排队问题。由于不存在排队,则端到端的最大延迟是固定的。我们也称此种网络为**有界网络**(bounded network)。
|
||||
这种网络是**同步**(*synchronous*)的:尽管数据也会通过多个路由节点,但由于通信所需的资源(如上述 16 bit 空间)已经在下一跳中被提前预留出来了,因此这些数据帧不会面临排队问题。由于不存在排队,则端到端的最大延迟是固定的。我们也称此种网络为**有界网络**(bounded network)。
|
||||
|
||||
### 计算机网络为什么不能同样稳定?
|
||||
|
||||
@ -170,9 +170,9 @@
|
||||
|
||||
应用层给到 TCP 的任意大小的数据,都会在尽可能短的时间内被发送给对端。如果一个 TCP 连接暂时空闲,则他不会占用任何网络带宽。相比之下,在打电话时即使不说话,电路所占带宽也得一直被预留。
|
||||
|
||||
如果数据中心和互联网使用**电路交换**(_circuit-switched_)网络,他们应该能够建立一条保证稳定最大延迟的数据链路。但是事实上,由于以太网和 IP 网采用**封包交换**协议(_packet-switched protocols_,常翻译为**分组交换**,但我老感觉它不太直观),没有电路的概念,只能在数据包传送的时候对其进行排队,也不得不忍受由此带来的无界延迟。
|
||||
如果数据中心和互联网使用**电路交换**(*circuit-switched*)网络,他们应该能够建立一条保证稳定最大延迟的数据链路。但是事实上,由于以太网和 IP 网采用**封包交换**协议(*packet-switched protocols*,常翻译为**分组交换**,但我老感觉它不太直观),没有电路的概念,只能在数据包传送的时候对其进行排队,也不得不忍受由此带来的无界延迟。
|
||||
|
||||
那为什么数据中心网络和互联网要使用封包交换协议呢?答曰,为了应对互联网中无处不在的**突发流量**(_bursty traffic_)。在电话电路中,音频传输所需带宽是固定的;但在互联网中,各种多媒体数据(如电子邮件、网页、文件)所需带宽却是差异极大且动态变化的,我们对他们的唯一要求就是传地尽可能快。
|
||||
那为什么数据中心网络和互联网要使用封包交换协议呢?答曰,为了应对互联网中无处不在的**突发流量**(*bursty traffic*)。在电话电路中,音频传输所需带宽是固定的;但在互联网中,各种多媒体数据(如电子邮件、网页、文件)所需带宽却是差异极大且动态变化的,我们对他们的唯一要求就是传地尽可能快。
|
||||
|
||||
设想你使用电路网络传输一个网页,你需要为它预留带宽,如果你预留过低,则传输速度会很慢;如果你预留过高,则可能电路都没法建立(带宽余量不够,就没法建立连接),如果建立了,也会浪费带宽。互联网数据的**丰富性**和**异构性**,让使用电路网络不太可能。
|
||||
|
||||
@ -190,7 +190,7 @@
|
||||
>
|
||||
> 类似的情形还发生在 CPU 的分时复用里。如果在多个线程间动态的共享每个 CPU,则一个线程使用 CPU 时,其他线程必须排队等待,且排队时间不确定。这种使用 CPU 的方式,比分配给每个线程固定的时间片要高效。类似的,使用虚拟化的方式共享同一台物理机,也会有更好的硬件利用率。
|
||||
>
|
||||
> 在资源静态分配的环境中,如专用的硬件、互斥的带宽分配,有界延迟能够被保证。但是,这种方式是以降低资源利用率为代价的,换句话说,更贵。反之,通过多租户方式动态的共享资源,更便宜,但代价是**不稳定的延迟**(_variable delays_)。**不稳定的延迟并非什么不可变的自然法则,而仅是一种代价和收益权衡的结果罢了**。
|
||||
> 在资源静态分配的环境中,如专用的硬件、互斥的带宽分配,有界延迟能够被保证。但是,这种方式是以降低资源利用率为代价的,换句话说,更贵。反之,通过多租户方式动态的共享资源,更便宜,但代价是**不稳定的延迟**(*variable delays*)。**不稳定的延迟并非什么不可变的自然法则,而仅是一种代价和收益权衡的结果罢了**。
|
||||
|
||||
# 不可靠的时钟
|
||||
|
||||
@ -241,7 +241,7 @@
|
||||
- 如果计算机时钟和 NTP 服务器相差太多,该计算机会拒绝同步或强制同步。如果强制同步,应用层在会在同步前后看到一个时钟的**跳变**。
|
||||
- 如果一个节点通过偶然的设置把 **NTP 服务器给墙**了,并且没有留意到这个问题,则可能会造成这段时间内时钟不同步,实践中确实发生过类似问题。
|
||||
- NTP 同步受限于网络的延迟,因此在延迟不稳定的**拥塞网络**中,其精度会受到影响。有实验表明通过互联网进行时钟同步,可以实现最小 35ms 的时钟误差。尽管网络中偶有将近 1s 延迟尖刺,但可以通过合理配置来忽略尖刺对应的同步。
|
||||
- 润秒的存在会导致一分钟可能有 59s 或者 61s,如果系统在做设计时没有考虑这种特殊情况,就有可能在运行时遇到问题,很多大型系统都因此而宕机。处理闰秒最好的办法是,让 NTP 服务器在一天中逐渐调整,**摊平闰秒**(也称为:**拖尾**,_smearing_),不过在实际中,NTP 服务器处理闰秒的行为不尽相同。
|
||||
- 润秒的存在会导致一分钟可能有 59s 或者 61s,如果系统在做设计时没有考虑这种特殊情况,就有可能在运行时遇到问题,很多大型系统都因此而宕机。处理闰秒最好的办法是,让 NTP 服务器在一天中逐渐调整,**摊平闰秒**(也称为:**拖尾**,*smearing*),不过在实际中,NTP 服务器处理闰秒的行为不尽相同。
|
||||
- 在虚拟机中,其**物理时钟是虚拟化**出来的,从而给运行其上并依赖精确计时的应用带来额外挑战。由于一个 CPU 内核是被多个 VM 所共享的,当一个 VM 运行时,其他 VM 就得让出内核几十毫秒。在 VM 恢复运行后,从应用代码的视角,其时钟就是毫无征兆的突然往前跳变了一段。
|
||||
- 如果你的软件将会运行在**不受控的设备**上,如智能手机或者嵌入式设备,则你不能完全相信设备系统时钟。因为用户可能会由于一些原因(比如绕开游戏时间限制),故意将其硬件时钟设置成一个错误的日期和时间,从而引起系统时钟的跳变。
|
||||
|
||||
@ -305,7 +305,7 @@
|
||||
|
||||
但不幸,大多数服务器的时钟系统 API 在给出时间点时,并不会一并给出对应的不确定区间。例如,你使用 `clock_gettime()` 系统调用获取时间戳时,返回值并不包括其置信区间,因此你无法知道这个时间点的误差是 5 毫秒还是 5 年。
|
||||
|
||||
一个有趣的反例是谷歌在 Spanner 系统中使用的 _TrueTime_ API,会显式的给出置信区间。当你向 TrueTime 系统询问当前时钟时,会得到两个值,或者说一个区间:`[earliest, latest]`,前者是最早可能的时间戳。后者是最迟可能的时间错。通过该不确定预估,我们可以确定准确时间点就在该时钟范围内。此时,区间的大小取决于,上一次同步过后本地石英钟的漂移多少。
|
||||
一个有趣的反例是谷歌在 Spanner 系统中使用的 *TrueTime* API,会显式的给出置信区间。当你向 TrueTime 系统询问当前时钟时,会得到两个值,或者说一个区间:`[earliest, latest]`,前者是最早可能的时间戳。后者是最迟可能的时间错。通过该不确定预估,我们可以确定准确时间点就在该时钟范围内。此时,区间的大小取决于,上一次同步过后本地石英钟的漂移多少。
|
||||
|
||||
### 用于快照的时钟同步
|
||||
|
||||
@ -317,7 +317,7 @@
|
||||
|
||||
那么,我们可以用机器的挂历时钟的时间戳作为事物的 ID 吗?如果我们能让系统中的多台机器时钟保持严格同步,则其可以满足要求:**后面的事务会具有较大的时间戳,即较大的事务 ID**。但现实中,由于时钟同步的不确定性,用这种方法产生事务 ID 是不太靠谱的。
|
||||
|
||||
但 Spanner 就使用了物理时钟实现了快照隔离,它是如何做到可用的呢?Spanner 在设计 TrueTime 的 API 时,让其返回一个**置信区间**,而非一个时间点,来代表一个**时间戳**。假如现在你有两个时间戳 A 和 B(_A_ = [*Aearliest*, *Alatest*] and _B_ = [*Bearliest*, *Blatest*]),且这两个时间戳对应的区间没有交集(例如,_Aearliest_ < _Alatest_ < _Bearliest_ < _Blatest_),则我们可以确信时间戳 B 发生于 A 之后。但如果两个区间有交集,我们则不能确定 A 和 B 的相对顺序。
|
||||
但 Spanner 就使用了物理时钟实现了快照隔离,它是如何做到可用的呢?Spanner 在设计 TrueTime 的 API 时,让其返回一个**置信区间**,而非一个时间点,来代表一个**时间戳**。假如现在你有两个时间戳 A 和 B(*A* = [*Aearliest*, *Alatest*] and *B* = [*Bearliest*, *Blatest*]),且这两个时间戳对应的区间没有交集(例如,*Aearliest* < *Alatest* < *Bearliest* < *Blatest*),则我们可以确信时间戳 B 发生于 A 之后。但如果两个区间有交集,我们则不能确定 A 和 B 的相对顺序。
|
||||
|
||||
为了保证这种时间戳能够用作事务 ID,相邻生成的两个时间戳最好要间隔一个置信区间,以保证其没有交集。为此,Spanner 在索要时间戳时(比如提交事务),会等待一个置信区间。因此置信区间越小,这种方案的性能也就越好。为此,谷歌在每个数据中心使用了专门的硬件做为时钟源,比如原子钟和 GPS 接收器,以保证时钟的置信区间不超过 7 ms。
|
||||
|
||||
@ -359,11 +359,11 @@ while (true) {
|
||||
- 一些编程语言的运行时(如 JVM),都会有垃圾回收器(GC)。垃圾回收器有时候会暂停所有运行中的线程(以进行垃圾回收),这个**全暂停**(stop-the-world)时间有时甚至能到达数分钟!即便号称可以并发 GC 的最新 GC 算法(如 Hotpot JVM 的 CMS 算法),也不能真正的和用户线程并行,仍会时不时的暂停,只不过这个时间缩短了很多。另外,我们也可通过修改内存分配模式和进行 GC 参数调优来进一步降低 GC 影响。但即便如此,如果我们想真正的提供足够鲁棒的程序,就不能对 GC 所造成的停顿时间做最坏假设。
|
||||
- 在**虚拟化环境**中,一个虚拟机可能会在任意时间点被**挂起**(suspended,暂停所有正在运行的进程,并将其上下文从内存中保存到磁盘)和**恢复**(resumed,将上下文恢复到内存中并且继续执行暂停的进程),挂起到恢复的间隔可能持续任意长时间。有时该功能也被称为虚机的**在线迁移**(live migration),此时虚机暂停的时间取决于上下文的迁移速率。
|
||||
- 哪怕像在笔记本这样的用户终端上,程序的运行也有可能被随时挂起和恢复。如,用户合上笔记本。
|
||||
- 当操作系统做上下文切换,将线程切走时;当**管控程序**(hypervisor)切到一个新的虚拟机时,当前正在执行的线程可能会停在代码中的任意位置。在虚拟机环境中,其他虚拟机占用 CPU 的时间也被称为**被窃时间**(_steal time_)。在物理机负载很重时,比如调度队列中有大量线程在等待时间片,某个被暂停的线程可能要好久才能重新执行。
|
||||
- 当操作系统做上下文切换,将线程切走时;当**管控程序**(hypervisor)切到一个新的虚拟机时,当前正在执行的线程可能会停在代码中的任意位置。在虚拟机环境中,其他虚拟机占用 CPU 的时间也被称为**被窃时间**(*steal time*)。在物理机负载很重时,比如调度队列中有大量线程在等待时间片,某个被暂停的线程可能要好久才能重新执行。
|
||||
- 如果操作系统配置了允许**换页**(swapping to disk, paging),则有时候一个简单的内存访问也可能引起缺页错误,这时我们就需要从磁盘中加载一个页到内存。在进行此 IO 时,线程多会挂起,让出 CPU。如果内存吃紧,缺页换页可能会非常频繁。在极端情况下,操作系统可能会将大部分时间都浪费在换页上,而非正经工作上(也被称为**颠簸**,thrashing)。为了避免此问题,服务器上的允许换页的配置项一般不打开。当然,你也可以点杀一部分进程来释放内存,避免换页,这就是 trade off 了。
|
||||
- 在 Unix 操作系统中,可以通过向进程发送 **SIGSTOP** 信号来让其暂停。如,用户对执行的进程在 Shell 中按下 Ctrl-Z。该信号会阻止进程再获取 CPU 的时间片,直到我们使用 SIGCONT 将其再度唤起。你自己的环境中可能不怎么使用 SIGSTOP,但是运维人员偶尔还是会用的。
|
||||
|
||||
所有上述情景都会在任意时刻**中断**(_preempt_)正在运行的线程,并在之后某个时刻将其重新唤醒,而线程本身对这个过程是不感知的。类似的情形还有单机多线程编程:你不能对多个线程代码的**相对执行顺序**有任何假设,因为上下文切换和并发执行可能会在任何时间以任何形式发生。
|
||||
所有上述情景都会在任意时刻**中断**(*preempt*)正在运行的线程,并在之后某个时刻将其重新唤醒,而线程本身对这个过程是不感知的。类似的情形还有单机多线程编程:你不能对多个线程代码的**相对执行顺序**有任何假设,因为上下文切换和并发执行可能会在任何时间以任何形式发生。
|
||||
|
||||
在单机我们有很多手段可以对多线程的执行进行协调,使之线程安全。如锁、信号量、原子计数器、无锁数据结构、阻塞队列等等。但不幸的是,分布式系统中我们没有对应的手段。因为在多机间不能**共享内存**,只能依靠**消息同步**,而且是要经过不可靠网络的消息!
|
||||
|
||||
@ -383,7 +383,7 @@ while (true) {
|
||||
|
||||
我们需要在全软件栈进行优化才能提供实时保证:
|
||||
|
||||
1. **在操作系统上**,需要能提供指定所需 CPU 时间片的实时操作系统(_real-time operating system_,RTOS)。
|
||||
1. **在操作系统上**,需要能提供指定所需 CPU 时间片的实时操作系统(*real-time operating system*,RTOS)。
|
||||
2. **在依赖库中**,所有的函数都需要注释其运行时间的上界。
|
||||
3. **在内存分配上**,要限制甚至禁止动态内存分配(会有实时 GC 器,但不会占用太多时间)。
|
||||
4. **在观测和测试上**,需要进行详尽的衡量和测试,以保证满足实时要求。
|
||||
@ -427,11 +427,11 @@ while (true) {
|
||||
|
||||
打个比方,这种情况就像一个噩梦:处于半连接的节点就像躺在棺材里被运向墓地,尽管他持续大喊:“我没有死”,但没有人能听到他的喊声,葬礼继续。
|
||||
|
||||
第二个场景,稍微不那么噩梦一些,这个处于半连接的节点意识到了他发出去的消息别人收不到,进而推测出应该是网络出了问题。但纵然如此,该节点仍然被标记为死亡,而它也没有办法做任何事情来改变,_但起码他自己能意识到这一点_。
|
||||
第二个场景,稍微不那么噩梦一些,这个处于半连接的节点意识到了他发出去的消息别人收不到,进而推测出应该是网络出了问题。但纵然如此,该节点仍然被标记为死亡,而它也没有办法做任何事情来改变,*但起码他自己能意识到这一点*。
|
||||
|
||||
第三个场景,假设一个节点经历了长时间的 GC,该节点上的所有线程都被中断长达一分钟,此时任何发到该节点的请求都无法被处理,自然也就无法收到答复。其他节点经过等待、重试、失掉耐心进而最终标记该节点死亡,然后将其送进棺材板。经过漫长的一分钟后,终于,GC 完成,所有线程被唤醒从中断处继续执行。**从该线程的角度来看,好像没有发生过任何事情**。但是其他节点惊讶地发现棺材板压不住了,该节点做起来了,恢复了健康,并且又开始跟旁边的人很开心的聊天了。
|
||||
|
||||
上述几个故事都表明,任何节点都没法**独自断言**其自身当前状态。一个分布式系统不能有单点依赖,因为单个节点可能在任意时刻故障,进而导致整个系统卡住,甚而不能恢复。因此,大部分分布式算法会基于一个**法定人数**(_quorum_),即让所有节点进行投票:**任何决策都需要达到法定人数才能生效,以避免对单节点的依赖**。
|
||||
上述几个故事都表明,任何节点都没法**独自断言**其自身当前状态。一个分布式系统不能有单点依赖,因为单个节点可能在任意时刻故障,进而导致整个系统卡住,甚而不能恢复。因此,大部分分布式算法会基于一个**法定人数**(*quorum*),即让所有节点进行投票:**任何决策都需要达到法定人数才能生效,以避免对单节点的依赖**。
|
||||
|
||||
其中,前面故事中的宣布某个节点死亡就是这样一种决策。如果有达到法定个数的节点宣布某节点死亡,那他就会被标记为死亡。即使他还活着,也不得不服从系统决策而出局。
|
||||
|
||||
@ -475,7 +475,7 @@ while (true) {
|
||||
|
||||
## 拜占庭错误
|
||||
|
||||
防护令牌只能检测并阻止**无意**(_inadvertently,如不知道自己租约过期了_)中犯错的客户端。但如果某个客户端节点存心想打破系统约定,可以通过**伪造**防护令牌来轻易做到。
|
||||
防护令牌只能检测并阻止**无意**(*inadvertently,如不知道自己租约过期了*)中犯错的客户端。但如果某个客户端节点存心想打破系统约定,可以通过**伪造**防护令牌来轻易做到。
|
||||
|
||||
在本书中我们假设所有参与系统的节点有可能**不可靠**(unreliable)、但一定是**诚实的**(honest):这些节点有可能反应较慢甚至没有响应(由于故障),他们的状态可能会过期(由于 GC 停顿或者网络延迟),但一旦节点响应,“说的都是真话”:**在其认知范围内,尽可能的遵守协议进行响应**。
|
||||
|
||||
@ -489,7 +489,7 @@ while (true) {
|
||||
>
|
||||
> 拜占庭是一个古希腊城市,后来罗马皇帝君士坦丁在此建立新都,称为“新罗马”,但后人普遍被以建立者之名称作君士坦丁堡,现在是土耳其的伊斯坦布尔。当然,没有任何历史证据表明拜占庭的将军比其他地方更多地使用阴谋诡计。相反,这个名字是来自于拜占庭本身,在计算机出现很久之前,拜占庭就有极度复杂、官僚主义、狡猾多变等含义。Lamport 想选一个不会冒犯任何读者的城市,比如,有人提醒他*阿尔巴尼亚将军问题*就不是一个好名字。
|
||||
|
||||
如果有一些节点发生故障且不遵守协议,或者恶意攻击者正在扰乱网络,一个系统仍能正确运行,则该系统是**拜占庭容错的**(_Byzantine fault-tolerant_)。举几个相关的场景例子:
|
||||
如果有一些节点发生故障且不遵守协议,或者恶意攻击者正在扰乱网络,一个系统仍能正确运行,则该系统是**拜占庭容错的**(*Byzantine fault-tolerant*)。举几个相关的场景例子:
|
||||
|
||||
- 在航天环境中,由于**高辐射环境**的存在,计算机内存或者寄存器中的数据可能会损坏,进而以任意不可预料的方式响应其他节点的请求。在这种场景下,系统故障代价会非常高昂(如:太空飞船坠毁并致使所有承载人员死亡,或者火箭装上国际空间站),因此**飞控系统**必须容忍拜占庭故障。
|
||||
- 在一个有**多方组织**参与的系统中,有些参与方可能会尝试作弊或者欺骗别人。在这种环境中,由于恶意消息发送方的存在,无脑的相信其他节点的消息是不安全的。如,类似比特币或者其他区块链的 p2p 网络,就是一种让没有互信基础的多方,在不依赖**中央权威**的情况下,就某个交易达成共识的一种方法。
|
||||
@ -518,21 +518,21 @@ Web 应用确实可能遇到由任意终端用户控制的客户端(如浏览
|
||||
|
||||
对于时间的假设,有三种系统模型很常用:
|
||||
|
||||
1. **同步模型(_synchronous model_)**。这种模型假设**网络延迟**、**进程停顿**和**时钟错误**都是**有界**的。但这不是说,时钟是完全同步的、网络完全没有延迟,只是说我们知道上述问题永远不会超过一个上界。但当然,这不是一个现实中的模型,因为在实践中,无界延迟和停顿都会实实在在的发生。
|
||||
1. **同步模型(synchronous model)**。这种模型假设**网络延迟**、**进程停顿**和**时钟错误**都是**有界**的。但这不是说,时钟是完全同步的、网络完全没有延迟,只是说我们知道上述问题永远不会超过一个上界。但当然,这不是一个现实中的模型,因为在实践中,无界延迟和停顿都会实实在在的发生。
|
||||
2. **半同步模型(partial synchronous)**。意思是在大多数情况下,网络延迟、进程停顿和时钟漂移都是有界的,只有偶尔,他们会超过界限。这是一种比较真实的模型,即在**大部分时间里**,系统中的网络和进程都表现良好,否则我们不可能完成任何事情。但与此同时,我们必须要记着,任何关于时限的假设都有可能被打破。且一旦出现出现异常现象,我们需要做好最坏的打算:网络延迟、进程停顿和时钟错误都有可能错得非常离谱。
|
||||
3. **异步模型(_Asynchronous model_)**。在这种模型里,算法不能对时间有任何假设,甚至时钟本身都有可能不存在(在这种情况下,超时间隔根本没有意义)。有些算法可能会针对这种场景进行设计,但很少很少。
|
||||
3. **异步模型(Asynchronous model)**。在这种模型里,算法不能对时间有任何假设,甚至时钟本身都有可能不存在(在这种情况下,超时间隔根本没有意义)。有些算法可能会针对这种场景进行设计,但很少很少。
|
||||
|
||||
除时间问题,我们还需要对节点故障进行抽象。针对节点,有三种最常用的系统模型:
|
||||
|
||||
1. **宕机停止故障(_Crash-stop faults_)**。节点只会通过崩溃的方式宕机,即某个时刻可能会突然宕机无响应,并且之后永远不会再上线。
|
||||
2. **宕机恢复故障(_Crash-recovery faults_)**。节点可能会在任意时刻宕机,但在宕机之后某个时刻会重新上线,但恢复所需时间我们是不知道的。在此模型中,我们假设节点的稳定存储中的数据在宕机前后不会丢失,但内存中的数据会丢失。
|
||||
3. **拜占庭(任意)故障(_Byzantine (arbitrary) faults_)**。我们不能对节点有任何假设,包括宕机和恢复时间,包括善意和恶意,前面小节已经详细讨论过了这种情形。
|
||||
1. **宕机停止故障(Crash-stop faults)**。节点只会通过崩溃的方式宕机,即某个时刻可能会突然宕机无响应,并且之后永远不会再上线。
|
||||
2. **宕机恢复故障(Crash-recovery faults)**。节点可能会在任意时刻宕机,但在宕机之后某个时刻会重新上线,但恢复所需时间我们是不知道的。在此模型中,我们假设节点的稳定存储中的数据在宕机前后不会丢失,但内存中的数据会丢失。
|
||||
3. **拜占庭(任意)故障(Byzantine (arbitrary) faults)**。我们不能对节点有任何假设,包括宕机和恢复时间,包括善意和恶意,前面小节已经详细讨论过了这种情形。
|
||||
|
||||
对于真实世界,**半同步模型**和宕机恢复故障是较为普遍的建模,那我们又要如何设计算法来应对这两种模型呢?
|
||||
|
||||
### 算法的正确性
|
||||
|
||||
我们可以通过描述算法**需要满足的性质**,来定义其正确性。举个例子,排序算法的输出满足特性:_任取结果列表中的两个元素,左边的都比右边的小_。这是一种简单的对列表有序的形式化定义。
|
||||
我们可以通过描述算法**需要满足的性质**,来定义其正确性。举个例子,排序算法的输出满足特性:*任取结果列表中的两个元素,左边的都比右边的小*。这是一种简单的对列表有序的形式化定义。
|
||||
|
||||
类似的,我们可以给出描述分布式算法的正确性的一些性质。如,我们想通过产生防护令牌的方式来上锁,则我们期望该算法具有以下性质:
|
||||
|
||||
@ -546,7 +546,7 @@ Web 应用确实可能遇到由任意终端用户控制的客户端(如浏览
|
||||
|
||||
为了进一步弄清状况,我们需要进一步区分**两类**不同的属性:**安全性**(safety)和**存活性**(liveness)。在上面的例子中,唯一性和单调有序性属于安全性,但可用性属于存活性。
|
||||
|
||||
那如何区分这两者呢?一个简单的方法是,在描述存活性的属性的定义里总会包含单词:“**最终**(_eventually_)”,对,我知道你想说什么,**最终一致性**(eventually)也是一个存活性属性。
|
||||
那如何区分这两者呢?一个简单的方法是,在描述存活性的属性的定义里总会包含单词:“**最终**(*eventually*)”,对,我知道你想说什么,**最终一致性**(eventually)也是一个存活性属性。
|
||||
|
||||
安全性,通俗的可以理解为**没有坏事发生**(nothing bad happens);而存活性可以理解为**好的事情最终发生了**(something good eventually happens),但也不要对这些非正式定义太过咬文嚼字,因为所谓的“好”和“坏”都是相对的。安全性和存活性的严格定义都是精确且数学化的:
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user