分布式系统的麻烦

2022/2/20 6:28:13

本文主要是介绍分布式系统的麻烦,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

数据密集型应用设计读书笔记第八章

分布式系统中有几大非常重要的麻烦,阻碍了人们的幻想。

使用分布式系统与在一台计算机上编写软件有着根本的区别,主要的区别在于,有许多新颖和刺激的方法可以使事情出错【1,2】。在这一章中,我们将了解实践中出现的问题,理解我们能够依赖,和不可以依赖的东西。下一章则探讨解决这些麻烦的一些算法。

故障与部分失效

当硬件确定稳定时,我们希望软件要么正确运行,要么崩溃中止,而不是返回一些错误的结果。因为这个结果是很难处理的。但是在分布式系统中可能很难如人所愿。在分布式系统中,尽管系统的其他部分工作正常,但系统的某些部分可能会以某种不可预知的方式被破坏。这被称为部分失效(partial failure)。难点在于部分失效是不确定性的(nonderterministic)

本章的重点放在互联网服务的系统上,要注意其与超级计算机这样的HPC架构有很多设计哲学上的不同。

超级计算机其实更像一个拥有很强计算机的单机服务器而不是分布式系统。

不可靠的网络

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

处理这个问题的通常方法是超时(Timeout)

如果超时是检测故障的唯一可靠方法,那么超时应该等待多久?不幸的是没有简单的答案。

超时时间过长,不利于系统快速的恢复正常服务的能力。

超时时间过短,则抗网络波动阻塞的能力就很弱,甚至可能导致每个节点都认为其它节点故障了,频繁的故障切换也很消耗时间。

所以研究分布式算法,大多时候都是先基于同步网络模型假设。异步网络具有无限的延迟。

在这种环境下,您只能通过实验方式选择超时:测量延长的网络往返时间和多台机器的分布,以确定延迟的预期可变性。然后,考虑到应用程序的特性,可以确定故障检测延迟过早超时风险之间的适当折衷。

更好的一种做法是,系统不是使用配置的常量超时时间,而是连续测量响应时间及其变化(抖动),并根据观察到的响应时间分布自动调整超时时间。

不可靠的时钟

在研究生的“高级操作系统”课程中学习过时钟的不可靠性。时钟分为物理时钟和逻辑时钟。

对计算机而言,物理时钟就是石英晶体振荡器。即使某一刻多个计算机的物理时钟的时刻是相同的,因为天然的误差,相互之间也会产生误差。可以在一定程度上同步时钟:最常用的机制是网络时间协议(NTP),它允许根据一组服务器报告的时间来调整计算机时钟【37】。或者是让服务器从更精确的时间源(如GPS接收机)获取时间。

基于物理时钟的分布式时间戳算法并没有理论上足够的安全性。

所谓的逻辑时钟(logic clock)【56,57】是基于递增计数器而不是振荡石英晶体,对于排序事件来说是更安全的选择,逻辑时钟不测量一天中的时间或经过的秒数,而仅测量事件的相对顺序。

那么有使用物理时钟来对事务排序的例子吗?
有,例如Spanner中的Google TrueTime API 【41】,它明确地报告了本地时钟的置信区间。当你询问当前时间时,你会得到两个值:[最早,最晚],这是最早可能的时间戳和最晚可能的时间戳。在不确定性估计的基础上,时钟知道当前的实际时间落在该区间内。
 Spanner以这种方式实现跨数据中心的快照隔离【59,60】。它使用TrueTime API报告的时钟置信区间,并基于以下观察结果:如果您有两个置信区间,每个置信区间包含最早和最晚可能的时间戳( A=[Aearliest,Alatest], B=[Bearliest,Blatest]),这两个区间不重叠(即:Aearliest<Alatest<Bearliest<Blatest)的话,那么B肯定发生在A之后——这是毫无疑问的。只有当区间重叠时,我们才不确定A和B发生的顺序。
 Spanner在提交读写事务时,会故意等待置信区间长度的时间,这样就能保证每个事务的置信区间不重叠。

暂停进程

因为垃圾收集器(GC),外部中断,虚拟机迁移等机制存在,分布式系统中的节点,必须假定其执行可能在任意时刻暂停相当长的时间,即使是在一个函数的中间。在暂停期间,世界的其它部分在继续运转,甚至可能因为该节点没有响应,而宣告暂停节点的死亡。最终暂停的节点可能会继续运行,在再次检查自己的时钟之前,甚至可能不会意识到自己进入了睡眠。

过程暂停的负面影响可以在不诉诸昂贵的实时调度保证的情况下得到缓解。

例如垃圾回收器, 一个新兴的想法是将GC暂停视为一个节点的短暂计划中断。那么当该节点需要进行垃圾回收时,需要提前告诉应用程序这件事,将请求转移到其它节点上。

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

 

知识、真相与谎言

根据上面的说法,一个节点,很有可能错误地认为自己没有被暂停过,自己还是主节点。它应该在与其它多数节点的交流中意识到这个问题。所以一个分布式系统应该听信大多数节点地认知,而非单节点(即使它被选举出来,也不一定做的就是对的,这可能是因为知识的局限性,也有可能是撒谎)。

虽然书中没提到,分布式算法根据容错能力,分为崩溃容错(CFT)和拜占庭容错(BFT)

前者容忍不超过二之一的节点出现问题。这是因为“过半投票”的存在。即对任何重要决策,需要过半票数才能被承认,如果连一半的正常工作节点数都不能超过,这个系统机制就无法运转。

后者,则认为不能超过三分之一。设节点总数是n,其中作恶节点有f,那么剩下的正确节点为n - f,意味着只要收到n - f个消息就能做出决定(所以后面要对f做出限定条件),但是这n - f个消息有可能由f个是由作恶节点(作恶节点也可以什么都不干)冒充的,那么正确的消息就是n - f - f(最恶劣的情况下)个,为了多数一致,正确消息必须占多数,也就是n - f - f > f但是节点必须是整数个,所以n最少是3f+1个。

领导者和锁

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

  • 数据库分区的领导者只能有一个节点,以避免脑裂(split brain)(参阅“处理节点宕机”)。

  • 特定资源的锁或对象只允许一个事务/客户端持有,以防同时写入和损坏。

  • 一个特定的用户名只能被一个用户所注册,因为用户名必须唯一标识一个用户。

分布式系统中可能出现这样的错误。例如,一个客户端A获取了一个特定资源的锁,这时候发生了进程暂停。此时其它服务节点都认定这个客户端超时了并允许另一个客户端B获得了这个锁。当客户端A醒来后,认为自己仍旧持有锁,此时两个客户端都会很自然地往资源里写数据,从而发生错误。

解决方法:每次获取锁的时候,获得一个令牌,这个令牌的有效性是由资源数据库来识别的。服务节点会在帮助客户端获取锁后给予这个令牌,当客户端想在这个资源上进行操作时,请求中就需要持有这个令牌,令牌有效性由资源所在的服务器识别。

拜占庭故障

如果节点故意搞破坏,不发送数据,或者发送错误的数据。那么如何达成一致?(即让诚实的节点能够达成统一的操作)

系统模型与现实

介绍分布式算法的前提,需要介绍网络环境模型

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

部分同步(partial synchronous)意味着一个系统在大多数情况下像一个同步系统一样运行,但有时候会超出网络延迟,进程暂停和时钟漂移的界限【88】。这是很多系统的现实模型:大多数情况下,网络和进程表现良好,否则我们永远无法完成任何事情,但是我们必须承认,在任何时刻假设都存在偶然被破坏的事实。发生这种情况时,网络延迟,暂停和时钟错误可能会变得相当大。

异步模型。在这个模型中,一个算法不允许对时机做任何假设——事实上它甚至没有时钟(所以它不能使用超时)。一些算法被设计为可用于异步模型,但非常受限。

进一步来说,除了时间问题,我们还要考虑节点失效

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

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

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

最后,基于以上模型设计出一个算法前,要定义算法的正确性,也就是希望它实现的目标所必须具备的一些属性,例如可用性( 发送请求并且不会崩溃的节点,最终会收到响应),唯一性(不同的请求能获得不同的令牌),单调序列(后一个请求获得的令牌一定更新)等等。

安全性和活性的区别安全性(safety)和活性(liveness)

这两种性质有什么区别?一个试金石就是,活性属性通常在定义中通常包括“最终”一词。 (是的,你猜对了——最终一致性是一个活性属性【89】。)

在刚刚给出的例子中,唯一性(uniqueness)单调序列(monotonic sequence)是安全属性,但可用性活性(liveness)属性。

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

 

注意:证明算法正确并不意味着它在真实系统上的实现必然总是正确的(实际实现可能有许多困难)。但是一个基于理论模型的算法,对于将实际系统的复杂性降低到一个我们可以推理的可处理的错误是非常有帮助的,以便我们能够理解这个问题,并试图系统地解决这个问题。



这篇关于分布式系统的麻烦的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程