《Disigning Data-Intensive Applications》 Part II(5)

前面几章中一直在讲系统如何处理异常情况,比如说副本异常、同步延迟、事务的并发控制。只有我们了解到系统可能出现的问题,我们才有可能正确地进行处理。虽然说我们已经讨论了很多,但是真实场景还是可能会更糟。现在,我们更悲观点,认为“anything that can go wrong will go wrong”。

在这一章节中,我们会先看下网络、时钟相关的问题,然后讨论在哪些程度上这些问题可以被避免。

Faults and Partial Failures

我们在编写一个单机程序的时候,通常它表现出来的行为是可以预期的,要么work,要么不work。当然,有时候程序的问题,我们可能通过“万能的重启”来解决,当然,通常这都是软件的bug,只是我们不愿意投入更多的effort去解决它。

在计算机的设计中有确定性的选择:如果有内部错误发生,我们宁愿让计算机彻底地崩溃,而不是返回一个错误的结果(类似于软件中我们使用的assert)。但是在服务软件的设计中,我们有时候会避免去使用assert,而是向上层返回一些错误码。但是对于硬件而言,向上返回了个模糊的错误,软件会更加困惑 ,因为不知道如何去处理。

计算机设计中的这个选择,在我们编写分布式程序时是不合适的。我们的系统模型不再是理想的,而是必须面对物理世界的各种不靠谱。在分布式的系统中,我们可能会遇到系统中的某一部分是正常的,而另外一部分是有问题的。这通常被我们称之为“partial failure”。

Cloud Computing and Supercomputing

对于构建大规模的计算系统,有两个流派:

  • 一个流派的极端是high-performance computing,对于运行一些科学计算任务的系统,使用拥有上千cpu的超级计算机。
  • 另外一个流派的极端是cloud computing,云计算会由在多个数据中心的很多廉价的计算机通过网络连接。

HPC对于错误的处理逻辑更像是我们前面提到的计算机的设计,HPC会保留一些checkpoint,如果有节点失效,HPC会让整个系统停止运行,等待恢复之后然后从上一个checkpoint来恢复。在这本书中,我们会关注于提供互联网服务的系统,这些系统的设计抉择会和HPC很不相同:

  • 服务必须是永远在线的,停机维护是不可接受的。
  • 超级计算机的成本很高,通常会使用定制的硬件,节点的通信可能是通过共享内存和RDMA。而在云计算中,使用的硬件会是廉价机器,故障率也会高很多。
  • 网络环境是不一样的,大型的数据中心网络通常是基于IP或者以太网。
  • 系统的规模越大,组件异常的概率就会越高。到达一定规模之后,“something is always broken”,如果使用HPC一样的策略,系统很大部分时间可能都在做恢复,而不是做实际的事情。
  • 如果系统能够容忍失效节点,始终能够作为一个整体对外提供服务,这对于操作和维护是很有帮助的。
  • 对于在多个区域分布的系统,网络延迟可能会很高。而对于HPC而言,始终是假设所有的节点都是部署在一起的。

基于这些假设,就需要我们基于不可靠的组件来打造可靠的系统。期望错误是很少见的,然后简单地希望一切环境都完美地运行,这是很不明智的。所以我们在打造系统的时候,就必须考虑到很大范围可能出现的错误,然后有意地在测试系统中构造出这些错误。

在分布式系统中,坚持怀疑、悲观和偏执会取得最终的成功。

Unreliable Networks

在第二部分的讨论中,我们关注的分布式系统的类型是“shared-nothing systems”,网络是系统中的节点唯一的交互方式。
Shared-nothing不是唯一地构建系统的方式,但是是构建互联网服务的主流方式,因为这种方式不需要特殊的硬件,而且可以通过冗余来提供更高的可靠性。

互联网和很多数据中心内部的内部网络都是“asynchronous packet networks”。在这种网络中,一个节点可以给另外一个节点发送消息,但是网络不会保证这个消息何时会到达,或者会不会到达。如果你发送请求,然后等待回包,那么很多异常的情况都会发生:

发送者是无法确认一个消息投递成功的,唯一的选项就是接收者发送了一个回复。如果发送了一个消息,但是没有收到回复,发送者也是无法知道这中间发生了什么事情的。

常用的方法是设置一个超时时间,在一段时间之后就放弃等待回复,然后认为请求已经丢失了。当然,就算超时发生了,sender也没有一个确定处理方式,如果请求的处理是幂等的话,是可能可以进行无脑重试的。

Network Faults in Practice

有一些系统的研究表明,网络问题是非常常见的,即使是某个公司自己维护的数据中心这样非常可控的环境里面。

对于EC2这样的公有云环境,经常会发生一些环境问题,虽然私有的数据中心会更稳定一些,但是总体而言,没有人能够避免这样的网络问题。

应对网络问题并不说一定要容忍它们,而是需要你的程序能够正常地处理他们,并且能够保证网络恢复之后,程序能够恢复。

Detecting Faults

很多系统需要自动地探测出失效的节点:

  • 负载均衡的服务需要避免把请求转发到dead的节点
  • 使用single-leader复制的分布式数据库中,如果leader挂掉了,需要把某一个follower提升为leader

很不幸地是,网络的不确定性让我们比较难判定一个系统到底是不是活着的。在某些非常确定的情况下,可能可以得到非常确定的答复来说明节点确实失效了:

  • 如果请求到达某台节点,但是发现端口没有在listen了,操作系统会关闭或者拒绝tcp请求,回复一个RST或者FIN包
  • 如果节点上的进程挂掉了,那么这个节点上的脚本可以通知其它节点

关于远端节点没有挂掉的快速反馈是非常有用的,但是不能够依赖于这个反馈。即使TCP层拿到了某个请求的ack,应用层也可能在处理之前就挂掉了。如果要确定请求是否成功,必须从应用层拿到成功的回复。

相反地,如果有节点发生了,大部分情况下是没有任何回复的。

Timeouts and Unbounded Delays

如果timeout是唯一确定的探测失效的方式,那么,超时时间设置为多久比较好呢?很遗憾,这个问题并没有确定性的答案。

过早地判定一个节点挂掉是有问题的,如果一个节点实际上是活着的,而且正在执行某个操作(比如说发送邮件),另外一个节点接管了它的工作,那么就会再次执行这个操作,这显然是可能有问题的。

当一个节点被探测为失效后,它的工作就会被转移到其它节点,这就意味着其它节点会有更高的负载和网络流量。如果系统的负载本来就很高,那么可能就会发生级联的错误,极端情况下,整个系统的节点都会被探测为失效。

假设这里有一个虚构的系统,网络可以保证最大的延迟,每一个网络包要么可以在时间d内被投递,那么就是丢弃了,成功的投递时间不可能超过d。然后,可以假设一个没有失效的节点可以在时间r内处理完请求。

在这种情况下,可以保证每个成功处理时间在2d+r之内。那么2d+r看起来就是一个非常合理的超时时间了。很可惜的是,并没有系统能够提供这样的保证,首先,异步网络有延时是unbounded的;其次,很多服务器的实现不能够保证可能在某个时间范围内一定能够处理完请求。

Network congestion and queueing

在计算机网络中,网络包的延时通常和排队相关。TCP会自动地进行超时的重传,虽然应用层不会观察到这些,但是应用层会观察因此造成的延时增长。

在公有云和多租户的数据中心中,资源会被很多客户共享。如果有些客户在跑一些MapReduce之类的重吞吐的任务时,网络延时可能就会变高。在这样的环境中,需要动态地更改超时时间,这样才能够在过长的失效探测和过早的failover之间达到一个很好的折衷。使用Phi Accrual failure detector可以很好地达到这个目的,这个方法已经在akka和cassandra中得到了应用,TCP的重传超时时间也使用了类似的方法。

Synchronous Versus Asynchronous Networks

如果我们的网络能够有bounded delay,而且不会丢包,那我们构建起分布式系统会简单很多。为什么我们不能够从硬件层面上来解决这个问题呢。

实际上是有这样的网络的,比如说传统的“fixed-line telephone network”,这种网络是非常可靠的:延时的语音帧和丢弃的通话非常少见的。电话需要稳定的端到端的低延时和足够的带宽来传输语音帧。

这些是同步的网络。就算数据会经过多个路由器,数据都不需要进行排队,网络中的另外一跳已经为这次通话预留了16bits的空间了。正因为没有排队,所以这种网络的端到端的最大延迟是固定的,我们称之为bounded delay

Can we not simply make network delays predictable?

前面说的电话网络属于circuit-switched,而数据中心的网络使用的是packet switching的协议。数据中心的网络使用这种协议的目的是可以更好地支持bursty traffic。

如果我们需要通过circuit来传递一个文件,首先需要分配一个带宽,如果分配得太小,那么不好意思,文件会传得很慢;如果想分配得很大,那么可能会分配得很慢。相反地,TCP会根据可用的网络容量来动态地调整数据传输的速率。

有一些网络尝试构造一个混合的网络,同时支持circuit switching和packet switching,比如说ATM。InfiniBand也有些类似的特征:它在链路层上实现了端到端的流控,这样就减少了在网络中的排队,但是依然可能因为链接的阻塞导致延时。通过仔细地使用QoS和admission control,是可能在packet network上来模拟circuit的,然后提供统计上的bounded delay。

当然,这样的QoS在目前的多租房的数据中心和公有云中是不存在的。所以我们无法假设我们有bounded delay,所以对于超时时间,并没有正确的方法,需要我们根据实际情况进行实验决定。

Unreliable Clocks

应用程序在很多方面都依赖于时钟,比如:请求是否超时了;服务P99的响应时间是多少;服务的QPS是多少;用户在网站上停留了多久;文章是什么时间发表的;cache什么时候会失效;

上面的这样例子中,有些是说的duration,有些是说的points in time。在分布式系统中,对于时间的处理是很tricky的,因为通讯不是即时的,消息被接受到的时间总是比发送的时间要晚的,而且不知道要晚多久。

另外,网络中每个机器都会有自己的clock,而且通常是个硬件设备,quartz crystal oscillator。这些设备不是完美精确的,所以每台机器会有自己的时间概念,可能会比其它机器快或者慢。虽然可以通过NTP,使数据中心的机器通过某一组机器来进行调整,达到某些维度的一致。这一组机器会从一个更加精准的时间来源,比如说GPS接收器来拿到时间数据。

Monotonic Versus Time-of-Day Clocks

现代计算机至少会有两种不同的时钟:time-of-day时钟和monotonic时钟。虽然它们都是用来度量时间的,但是是出于不同的目的。

Time-of-day clocks

time-of-day clock更符合我们本能中对于时间的期望:返回基于某个日历(wall-clock time)的精确日期和时间。比如linux中的clock_gettime(CLOCK_REALTIME)和java中的System.currentTimeMillis()都是返回从epoch时间开始的秒数。这个时间通常是和NTP进行同步的。

Monotonic clocks

monotonic clock会被用来度量duration,比如说timeout和系统的响应时间,linux中的clock_gettime(CLOCK_MONOTONIC)和java中的System.nanoTime()都是这种时钟。这种时钟就和名称一样,只会向前。

这种时钟的绝对值是没有意义的,可能是计算机启动之后的纳秒数,也可能是其它的任意值。对于有多个CPU sockets的服务器,每个CPU可能都会有时钟,而且和其它CPU是不同步的。操作系统会来补偿这些不一致,然后对应用线程呈现单调的视图。对于这个单调性保持怀疑态度是明智的。

在分布式系统中,使用monotonic clock来度量duration是靠谱的。

Clock Synchronization and Accuracy

Monotonic clocks不需要进行同步,但是time-of-day clocks需要和NTP服务器或者其它外部的时间源进行同步来保证可用。不幸的是,我们获取正确时间的方法并不是那么精确。虽然有些机制,比如说GPS接收器、Precision Time Protocol,可以获取相应的精确性,但是,需要付出额外的努力和极强的专业性。

Relying on Synchronized Clocks

时钟的问题在于,虽然看起来简单而且易用,但是会有很多陷阱,比如:一天可能并不是精确地有86,400秒,time-of-day clocks可能会后退,某些节点上的时间会跟其它节点有很大的不同。

这一章节前面的内容我们提到了网络丢包和延迟,虽然网络大部分时间是表现正常的,但是软件必须要能够处理异常的情况。对于时钟来说是一样的,软件必须要能够处理时钟的异常。

所以,如果你的软件需要依赖于同步的时钟,那么就必须监控节点之前的时钟差异。

Timestamps for ordering events

下面的例子中描述了一个依赖于时钟的例子。假设这是一个leaderless的数据库,node 1和node 3都可以接受写请求;首先Client A向node 1写入x=1,同步到了node 3;Client B执行了加1的操作;例子里面,同步的请求都加了发起端的时间戳,但是node 3的时间比node 1要晚比较多,这样就导致同步到node 2时,node 2认为node 1的请求要晚到,于是node 2上的值就被更新为1了。这就造成了不一致。

对于LWW的冲突机制 ,使用逻辑时钟,而不是oscillating quartz crystal,会是对事件进行排序的一个很好的方法。

Clock readings have a confidence interval

即使每分种都和本地的NTP服务器进行同步,本地时间可能仍然会有数毫秒的差异;如果NTP服务器位于公共的网络上,那么这个差异可能是数十毫秒;如果有网络拥塞的话,那么可能是过百毫秒。但是系统的API并不会暴露这些不确定性给上层,比如说你通过clock_gettime()拿到一个时间,系统并不会告诉你真正的时间差异是在5毫秒之内或者是在5年之内。

Google的Spanner中的TrueTime API是个例外,它显式地返回本地时钟的一个确信的范围。当你需要获取当前时间时,你会得到两个数值:[earliest, latest],说明当前时间处于的范围。

Synchronized clocks for global snapshots

snapshot isolation最常见的实现是需要一个单调的事务ID,对于单节点的数据库,一个简单的计数器就可以用来生成事务ID。但是,当数据库分布在不同的机器上的时候,一个全局的、单调递增的事务ID因为需要协调是很难生成的。事务ID必须要能够反应因果性,如果事务B能够读到事务A写入的值,那么事务B就一定要比事务A的事务ID要大。如果事务的并发度很高,那么分布式系统中创建事务ID就会成为很大的瓶颈。

我们能否使用同步的time-of-day clocks作为事务ID呢?但是问题就在于时钟准确度的不确定性。

Spanner在数据中心之间使用下面的方法实现了snapshot isolation:使用前面说的TrueTime API返回的时间,也称之为confidence intervals,现在假设我们有A = [Aearliest, Alatest]和B = [Bearliest, Blatest]),如果A和B之间是没有overlap的,那么我们很容易判断它们的大小关系。

为了避免两个confidence intervals之间有overlap,Spanner在提交一个读写事务之前会刻意地等待confidence interval的长度,这样就可以保证任何读取到这个数据的事务都会在一个足够长的延后的时间,这样它们的confidence interval就不会有overlap了。举一个上面的例子,比如说A是一个读写事务,那么A的事务ID就是 [Aearliest, Alatest],那么A提交之后,Spanner就会等待Alatest-Aearliest的时间,这时候True API是不会返回时间的,那么这样等待完成之后的事务拿到的Bearlies就会比A要小了。所以Spanner需要保证这个length越小越好,为了这个目的,Google部署了GPS接收器和原子钟,来保证时钟同步在7ms以内。

Process Pauses

现在来考虑分布式系统中另外一个使用时钟的危险的例子,假设我们现在有一个single leader的系统,只有leader能够接受写请求,那么一个节点怎么确定它是不是仍然是leader,怎么确定它能够安全地执行写操作呢?

一种方法是leader从其它节点拿到一个lease,比较像带有超时时间的锁。在某一个时间只有一个节点可能持有lease,节点会周期性地更新lease来保持leader的角色。当节点失效时,就会停止更新lease,这样其它节点就可以接管。

1
2
3
4
5
6
7
8
9
10
11
12
while (true) {
request = getIncomingRequest();
// Ensure that the lease always has at least 10 seconds remaining
if (lease.expiryTimeMillis - System.currentTimeMillis() < 10000) {
lease = lease.renew();
}
if (lease.isValid()) {
process(request);
}
}

这段代码有以下的问题:

  • 依赖于同步的时钟:lease的过期时间是由其它机器计算出来的,但是和本地的时钟进行的比较。
  • 通常来说,检查lease和后面的process(request)语句之前相关的时间会很短,看起来10s是足够了,但是如果发生异常情况,让程序的执行中断了10s以上呢,这个时候其它节点已经接管了leader的角色,这个节点仍然会继续处理请求。

是否会发生程序中断这么长时间的情况呢?很不幸的是,会:

  • GC可能会导致程序暂停
  • 虚拟机可能会被suspended

很多情况下都会使程序暂停然后恢复执行。在多线程的代码中,我们可以有很多方法来保证线程安全,但是在分布式环境中,我们并没有什么好的方法。

Response time guarantees

在编程语言和操作系统中,线程和进程可能会被暂停一段时间,但是如果做得好,可以没有必要暂停。在某些系统中,软件有一个确定的响应时间的deadline,如果没有在这个dealine之前返回,就会导致整个系统的失效,这种我们称之为强实时系统。

提供实时保证需要整个软件栈上的支持,比如说实时操作系统、提供确定时间的内存分配等等。正因为如此,所以开发实时系统是非常昂贵的,它们通常用于注重安全的嵌入式设备中。实时系统的吞吐一般都很低,因为他们需要把延时放在第一位。

Limiting the impact of garbage collection

一种观点是对待计划内的变更一样对待GC,当一个节点在做GC时,使用其它节点来处理请求。有一些延时非常敏感的金融交易系统使用了这种方法来避免长尾延时。这种方法的变种是gc只用来处理频繁释放的对象,在需要full GC之前重启节点。这些方法无法避免GC的卡顿,但是可以有效地降低GC对于应用的影响,虽然很low。

Knowledge, Truth, and Lies

这个章节到现在我们已经了解到了分布式系统和单节点的计算机的区别:没有共享内存、只能通过不可靠的网络来交换消息、系统可能会遇到partial failure、不可靠的时钟和进程暂停。

The Truth Is Defined by the Majority

假设网络现在有一个非对称的错误:一个节点能接受到发送给它的所有的消息,但是所有它发出的消息都被丢弃或者延时了。等到一段时间后,这个节点就被其它节点认为是失效了,并从集群中剔除掉。

当然,这个节点可能会意识到它发出的消息没有被其它节点回复,然后发现网络可能存在问题,但是仍然避免不了被从集群中剔除的命运。

上面这些例子告诉我们,一个节点不能够单纯相信自己对于环境的判断。一个分布式的系统不能够排它式地只依赖于某一个节点,应该这个节点可能随时失效,导致系统中止服务并且无法恢复。正因为如此,很多分布式的算法依赖于quorum,系统决策需要若干节点的投票,而不是依赖于某一个特定的节点。

The leader and the lock

通常来说,系统需要某些唯一性:

  • 数据库的分区只能有一个leader
  • 只有一个事务或者客户端能够持有某一个资源的锁,防止其它并发的写入破坏它
  • 只有某一个用户能够申请到某一个用户名

在分布式系统中实现这些唯一性的时候需要特别注意,即使某一个节点确认了一个唯一性,并不代表集群中quorum的节点都同意这个唯一性。

如果在其它节点认为某个节点失效之后,这个节点仍然认为自己持有了某些资源,就可能会对整个系统造成破坏。下图的例子在Hbase中真实发生过,Client 1获取到了某个锁之后,准备去写文件,但是写文件之前执行了一次长时间的GC。在GC的过程中,锁过期了,过期之后Client 2获取到了锁,执行了写文件操作。这时候Client 1恢复了,认为自己仍然持有锁,然后继续写文件,这时候就会破坏文件了。

Fencing tokens

为了解决上面提到的这个问题,我们可以引入下面的方法:当lock service分配lock或者lease的时候,都会同时返回一个fencing token。每次客户端发送写请求到storage service的时候,都需要带上当前的fencing token。就像下图描述中一样进行操作,就会避免掉之前的问题:

如果使用ZooKeeper来作为锁服务,那么我们可能使用 zxid或者node的版本号cversion来作为fencing token。注意到这个机制中需要资源本身能够来检查这些token,并且拒绝掉比较老的token的写入。

在服务端来检查token看起来不太好,但是服务端做好对滥用的客户端的防御,看起来是非常有必要的。

Byzantine Faults

Fencing tokens可以探测并阻止一个节点非故意的错误行为,但是如果一个节点就是要有意地破坏系统时,它可以很容易地伪造一个token。在这本书中,我们假设所有的节点都是不可靠但是诚实的:回复可能会丢弃或者延时,但是只有回复了,那么回复的就一定是truth。

如果节点会回复错误的信息,那么分布式系统的问题会变得复杂很多。这样的行为我们通常称为Byzantine fault,在不可靠的环境中达到一致的问题也被称为Byzantine Generals Problem。

如果一个系统在某些节点出故障不遵守协议的规定、或者在网络中有蓄意攻击时仍然能够正常运作,那么这个系统就是能够容忍Byzantine fault的。这个特定在某些场景下是非常重要的:

  • 在航空环境下,计算机内存或者CPU寄存器中的数据可能会受到辐射影响,导致会回复其它节点一些不可预期的内容。由于这种场景下系统失效的代价实在是太高,所以飞行控制系统必须是能够容忍Byzantine fault的。
  • 某些系统由多个参与的组织构成,某些参与者可能会诈取其它人。这种情况下,就不能够简单地相信某一个节点的消息。bitcoin和其它的区块链都属于这种情况。

本书中我们讨论的系统,都是假设没有Byzantine faults的。对于大多数服务端程序而言,部署容忍Byzantine faults的解决方案是不现实的。对于Web应用,我们相信server的权威性,让server来判断哪些client行为是被允许的。但是在p2p网络中,并没有这样的中心决策节点,所以容忍Byzantine faults就变得非常重要了。

Weak forms of lying

虽然我们假设所有节点都是诚实的,但是我们在软件中增加防御性的代码还是非常有价值的。这些措施并不能够容忍Byzantine faults,但是可以提高系统的可靠性:

  • 网络包可能会因为在硬件问题或者操作系统驱动的问题损坏。这种情况我们可能借助于应用层的校验码来处理。
  • 一个公开访问的应用必须仔细地审查用户的每一个输入。一个内部的服务也需要对于输入的数据做一些基本的审查。
  • NTP客户端必须配置多个server的地址。

System Model and Reality

很多算法被设计成可以解决分布式系统问题的。为了有用,这些算法就需要能够容忍我们在这一章提到的那些问题。

算法需要保证不会过度地依赖硬件的细节和软件的配置,这就要求我们把系统中可以发生的错误形式化。所以,我们需要定义一个system model,这是算法的假设的抽象。

对于timing的假设,通常用到的是3种假设:

  • Synchronous model。同步模型假设网络中的延迟bounded、进程的暂停时间是bounded、时钟的错误是bounded。这种模型太过于理想了。
  • Partially synchronous model。系统大部分时间和Synchronous model一样,但是有时候会超出bound的限制,这比较接近于现实中的情况了。
  • Asynchronous model。在这个模型中,算法不允许做任何关于timing的假设,这种模型太严格了。

关于节点失效也有3种模型:

  • Crash-stop faults。节点可能会突然在某一时刻停止响应,这之后就寿终正寝了。
  • Crash-recovery faults。节点可能在某一时刻crash,然后在某一个不确定的时间之后重新开始响应。
  • Byzantine (arbitrary) faults。节点可能会有任意的响应。

对于实现的系统,最常见的是Partially synchronous model和Crash-recovery faults。

Correctness of an algorithm

正确的分布式算法需要具备如下的属性:

  • Uniqueness
  • Monotonic sequence
  • Availability

Safety and liveness

以上属性可以分为两种类型:safety和liveness。上面提到的属性中,Uniqueness和Monotonic sequence是safety属性、Availability是liveness属性。简单来说,safety属性就是nothing bad happens,而liveness属性是something good eventually happens。

  • 如果违反了safety属性,我们可以在某一时间点指出它被破坏了。safety属性被违反是不可逆的。
  • liveness属性就是另外一种方式,这个属性可能在某一时刻没有被满足,但是最终总是会得到满足的。

对于分布式算法,safety属性始终要满足。也就是说即使所有的节点都失效,整体网络都失效了,算法也需要保证能够满足safety属性。

对于liveness属性,我们可以设置一些限制:比如一个请求只有大多数节点没有crash的时候才需要收到回复,而且只有当网络从故障中恢复过来的时候。partially synchronous model的定义需要系统最终恢复到synchronous的状态,也就是说,任何网络中断都会在有限的时间内被修复。

Mapping system models to the real world

safety和liveness属性、system models对于验证分布式算法的正确性是很有帮助的。在实现这些算法的过程中,现实的混乱会让你很头疼,因为system models是对于现实比较简单的抽象。

Quorum算法依赖于节点会记录它声称已经存储下来的数据。如果一个节点失忆了,忘掉了它之间存储的数据,那么就会破坏quorum的条件,从而破坏整个算法的正确性。

算法的理论描述可以声称某些情况不会发生。但是,在实际的实现当中,仍然需要添加code来处理那些我们假设不会发生的错误,比如说用assert/abort之类的语句。当然,这并不是说理论的、抽象的system models是没有价值的,恰恰相反,理论的模型把现实系统的复杂性抽象成算法能够管理的错误集,然后系统性地解决他们。

证明一个算法是正确的,并不意味着它在真实系统中的实现就是正确的。但是实现理论上的算法是非常好的第一步,因为理论分析可以发现可能在真实系统中隐藏了很久的问题,这些问题可能在特殊场景被触发,然后让你无比惊讶。理论的分析和经验测试都是非常重要的。

Summary

在这一章中,我们讨论了分布式系统中可能发生的各种问题,这样的partial failures是分布式系统的典型特征。如果需要做到故障容忍,首先需要做到的是探测到故障,但即使是这个,也是非常困难的。即使故障被探测到了,让系统来处理这些故障也是不太容易的。现在单个的计算机已经很优秀了,如果能够避免打开分布式系统这个潘多拉魔盒,那还是非常值得的。当然,扩展性、故障容灾和低延迟都是分布式系统可能带来的优势,这些是单机无法提供的。

在这一章中,我们只是抛出了问题,下一章节中,我们会讨论这些问题应该如何解决。