分布式系统的基础算法知识

原文链接:What We Talk About When We Talk About Distributed Systems(翻译:柳泉波)

我一直在学习有关分布式系统的知识,学习时间不算短了。老实说,只要你开始钻研分布式系统,知识点好像学不完似的,一个接一个。分布式系统领域的文献太多了,包括许多大学发表的论文,还有很多书籍可选。像我这样的绝对新手,很难决定应该阅读哪些论文或者购买哪些书籍。

同时,我还发现了几个博客作者,他们在博客中推荐这篇或者那篇论文,声称这是分布式系统工程师(先别纠结这个词的含义)应该必知必会的。于是,我的阅读列表越来越长:FLP, Zab, 时间, 分布式系统中的时钟和事件顺序Viewstamped Replication, Paxos, Chubby ,等等。问题是,很多时候他们没有说明为什么要阅读这篇或者那篇论文。为了满足好奇心,不分主次地学习所有知识,这种想法我挺喜欢。不过,我们还是应该确定不同内容的阅读优先级,毕竟,每天只有 24 小时呀。

除了有大量的论文和研究资料,分布式系统领域的书也很多。我买了挺多本。我翻看其中的章节,发现有些书的书名起得很有吸引力,貌似是我感兴趣的内容,实则不然,或者说,这些书的内容并没有涉及我想解决的问题。

在写这篇博客的同时,我仍然在持续学习分布式系统知识,所以请读者有点耐心,明白本文难免会存在错误。已经写好的内容,以后我会尽力做相应的扩充。

在这里,我想告诉大家,我已经在好几个会议上讲过这篇博客的内容,演讲稿:What We Talk About When We Talk About Distributed Systems

我在斯德哥尔摩 Erlang 用户大会上的演讲视频:What We Talk About When We Talk About Distributed Systems

文章目录

主要概念

确定分布式系统算法的分类,主要依据是搞清楚算法的各种属性。例如,定时模型、进程间通信类型和失效模型等等。

本文涉及的主要概念包括:

  • 定时模型(Timing Model)
  • 进程间通信(Interprocess Communication)
  • 失效模式(Failure Modes)
  • 失效检测器(Failure Detectors)
  • 领导人选举(Leader Election)
  • 共识(Consensus)
  • 法定人数(Quorums)
  • 分布式系统中的时间
  • 快速浏览 FLP
  • 结束语
  • 参考文献

 

定时模型

定时模型有三种:同步模型,异步模型和部分同步模型。

同步模型的使用最简单。所有组件在所谓的同步轮次(synchronous
round)中同时执行算法步骤。在同步模型中,消息送达的耗时是已知的,每个进程的速度也是确定的,即每个进程执行一个算法步骤的耗时是确定的。同步模型的主要问题是它不能很好地反映真实世界,与分布式系统的差别就更大了。在分布式系统中,我们向另外一个进程发送消息,肯定希望自己行大运,因为只有这样,才能保证消息一定发送到目标进程。好在我们可以利用同步模型获得理论结果,再把结果转换到其他模型。例如,在同步模型中时间是有保证的,如果一个问题在同步模型中都解决不了,那么在其他模型中,没有了时间的保证(例如,完美失效检测器就是这样一个模型),就更不可能解决这个问题了。

异步模型的使用相对复杂。在异步模型中,每个组件自行决定算法步骤的执行顺序,每一步的耗时也没有保证。虽然异步模型的描述很简单,也更接近真实世界,但是它仍然算不上正确地反映了真实世界。例如,在异步模型中,一个进程响应请求的时间有可能是无限长,但是在真实的项目中,一般会为请求设置一个超时限制,如果在此期间没有收到响应,将会中止请求并且报告异常。异步模型带来的一个难点是如何确认一个进程的活跃度条件(liveness condition)。在最著名的不可能性结果当中,其中一个就是有关异步模型的:“只要有一个进程可能会失效就不可能达成共识”(Impossibility of Consensus with one Faulty Process),也就是说,不可能区分下列两种情况:(1)进程失效了;(2)进程没有失效,但是它需要耗费无限长的时间去响应一条消息。

部分同步模型中,每个组件都了解一些关于定时的信息,它们要么使用近似同步的时钟,要么大致了解消息送达的耗时或者每个进程执行一个算法步骤的耗时。

Nancy Lynch 写的书《分布式算法》,就是按照上述三种定时模型组织内容的。

进程间通信

这部分讨论分布式系统的不同进程之间如何交换信息,包括两类:

  • 消息传递模型:进程间相互发送消息;
  • 共享内存模型:不同的进程读写共享的变量,实现数据的共享。

记住一点,我们可以做到:使用一种消息传递算法,构建一个分布式共享的内存对象。可读可写的寄存器就是这样一种实现,在很多分布式系统书籍中都可以见到这个例子。有些作者还使用队列和堆栈来描述一致性属性,例如,线性化(linearizabilty)。大家不要弄混“共享内存”的两种含义:第一种是指不同的进程读写同一个共享的变量,这是实现数据共享的一种方法;第二种是指在消息传递之上构建的共享内存抽象,刚刚已经讲过。

在理解基于消息传递模型的算法时,还必须弄清楚进程之间是哪一种链接(可以理解为进程之间的信道)。不同种类的链接抽象为算法提供的保证也不相同。例如,完美链接能够保证消息的可靠送达,不会重复发送消息;它保证消息会且只会一次送达。显然,现实世界并非如此。当算法设计者在设计尽量接近真实的模型时,他们会使用其他类型的链接抽象。请牢记,即使完美链接并非那么真实,它仍然有用。例如,如果我们能证明,即使链接是完美的我们也无法解决某个问题,那么所有的相关问题也将是不可解的。在讨论链接时,研究者通常会假定消息顺序是“先入先出”的, Zab 就是一个例子。

失效模式

我以前写过一篇文章“分布式系统中的失效模式”,不过仍然值得在这里说道说道。进程的失效模式是分布式系统模型的一个属性,它是对进程失效种类的假设。崩溃-结束(crash-stop)失效模式的假设是:进程一直是正确的,直到它发生崩溃;崩溃之后,这个进程不会被恢复。还有崩溃-恢复(crash-recovery)失效模式,进程崩溃后会被恢复。有些算法还会把进程恢复到崩溃之前的状态。要做到这一点,恢复后的进程要么从持久存储中读取以前的数据,要么与同一群组的其他进程通信,获取所需的数据。需要指出的是,在有些群组成员算法中,一个崩溃之后再恢复的进程并不等同于没崩溃之前的原始进程。二者是否等同,取决于这是一个动态群组还是一个固定群组。

还有一种失效模式叫做忽略失效模式(omission failure mode),它的假设是进程不能接收或者发送消息。忽略有两种:进程接收不到消息,或者进程发送不了消息。为什么要了解这个呢?想想这种情景,实现分布式缓存的一组进程,如果其中一个进程无法应答其他进程的请求,只要它能收到其他进程的请求,那么这个进程的状态就是最新的,它仍然可以响应来自客户的读请求。

一种更复杂的失效模式叫做拜占庭失效模式或者任意失效模式(Byzantine or arbitrary failures mode):进程有可能向同伴发送错误的信息;进程可能是冒充的;应答给其他进程的数据是正确的,但是篡改了本地数据库的内容,等等。

在设计一个分布式系统时,必须考虑清楚要应对的进程失效类别。Birman (参见《可靠的分布式系统指南》)认为,一般来说我们不需要应对拜占庭失效。他引用了在 Yahoo! 完成的工作,得出一个结论:崩溃失效比拜占庭失效更为常见。

失效检测器

有了定时模型和进程失效模式的假设,我们就可以构建报告系统状态的抽象——失效检测器,即检测某个进程是否已经崩溃,或者怀疑这个进程已经崩溃。完美失效检测器从不虚报进程失效。假定系统是崩溃-结束失效模式加上同步定时模型,我们只需使用超时就能实现一个完美失效检测器:要求进程定期向检测器报到,报到消息肯定能送达给失效检测器(同步模型能够保证这一点);如果报到消息没有如期送达,我们可以断定该进程已经崩溃。

在实际的系统中,不能假定消息送达的耗时,也不能保证进程执行每个步骤的耗时。这时候,我们可以构建一个失效检测器 _p_ ,如果进程 _q_ 在超时限制 _N_ 毫秒内没有回答,那么检测器会报告进程 _q_ 可能已经崩溃。如果后来 _q_ 又开始应答了,那么 _p_ 将把 _q_ 从怀疑已经崩溃的名单中剔除。因为检测器不知道它与 _q_ 之间的实际网络延迟,又不想再把 _q_ 添加到怀疑名单中(事实已经证明 _q_ 没有崩溃),所以它会增大 _N_ 的取值,保证 _q_
有足够的报到时间。如果在某个时间点 _q_ 真的崩溃了,检测器首先把它列入怀疑名单,并将一直保留在那里(因为 _q_ 永远不会再报到了)。要了解这个算法,请阅读《可靠且安全的分布式程序设计指南》一书的“最终完美失效检测器”部分,讲得更好。

失效检测器通常有两个属性:完备性和精准性。最终完美失效检测器具有:

  • 强完备性:到最后,每一个崩溃的进程都会被每一个正常运行的进程永久地怀疑;
  • 最终强精准性:最终,没有任何正常运行的进程会被其他正常的进程所怀疑。

失效检测器是异步模型中解决共识问题的关键。在 FLP 论文中,提出了很多著名的不可能性结果。这篇论文指出,在异步的分布式系统中,如果进程有可能失效,那么就不可能达成共识。要达成共识,就必须为系统引入一个能够规避上述问题的失效检测器

领导人选举

与失效检测相反的一个问题是,如何判定一个进程没有崩溃,它因此能够正确地工作。网络中其他进程会信赖这个进程,把它当作能够协调分布式行动的领导人。像 Raft 或者 Zab 这样的协议就依赖领导人进程来协调行动。

协议中有一个领导人,意味着不同节点的地位变得不对称,非领导人节点将成为跟随者。领导人节点因此成为很多操作的瓶颈。选择什么协议,取决于我们要解决什么样的问题。有时,我们并不想使用需要领导人选举的协议。记住,大部分通过某种共识获得一致性的协议,都包含一个领导人进程和一组跟随者进程。这样的例子包括PaxosZab 或者 Raft

共识

共识(consensus 或 agreement)问题是由 Pease , Shostak 和 Lamport 在论文“在存在失效的情况下达成一致”首先提出来的。他们是这么描述的:

容错系统通常要求提供一种手段,使得独立的处理器或者进程能够达成某种精确的相互一致。例如,一个冗余系统的多个处理器可能需要定期同步它们的内部时钟。或者每个处理从某个时变的输入传感器读取的数值都有稍微不同,它们需要确定一个统一的值。

所以,共识是指独立的进程之间达成一致。针对某一个问题,这些进程提议了一些值,像它们的传感器的当前取值,然后根据这些值就共同的行动达成一致。例如,一辆轿车包含多个提供刹车温度水平信息的传感器。这些传感器的读数可能不同,因为它们的精度等不一样。但是汽车的防抱死制动系统需要它们达成一致,这样才能确定需要施加多少压力给刹车。这就是一个在我们日常生活中解决的共识问题。《容错的实时系统》这本书解释了在汽车行业的分布式系统中遇到的共识及其他问题。

实现某种形式共识的进程,它的作用是对外显露一个API ,包括提议和决定功能。在共识的开始阶段,进程提议一个值,然后根据系统中提议的所有值,决定一个最终的值。共识算法必须满足下列属性:终止(Termination)、合法性(Validity)、诚实(Integrity)和一致性(Agreement)。例如,对于常规共识,

  • 终止:每一个正确的进程最终都会决定一个值;
  • 合法性:如果某个进程最终决定取值是 _v_ ,那么这个 _v_ 必然是由某个进程提议的;
  • 诚实:没有进程会决定两次;
  • 一致性:两个正确的进程,它们的决定不会不同。

更多细节,请参考上面提到的原始论文。还有一些很棒的参考书:

 

法定人数

法定人数(Quorum)是设计容错分布式系统的工具,它是能表征系统特征的进程的交集,其中某些进程有可能失效。

举个例子,假设某个算法遵循崩溃失效模式,由 _N_ 个进程组成。只要大多数进程成功地应用了某个操作(例如,写入数据库),就可以说已经有了进程的法定人数。只要只有少数进程崩溃,即不超过 _N/2-1_ 个进程崩溃,那么大多数进程仍然知晓应用到系统的最后操作。例如, Raft
在提交日志到系统时就用到了由大多数进程构成的法定人数。领导人向跟随者发出日志复制请求,只要有一半的服务器有应答,领导人立即把这一日志项应用到它的状态机。领导人加上一半服务器,已经构成了大多数。这样做的好处是, Raft 不必等待所有的服务器都应答日志复制的 RPC 请求之后才开始复制操作。

再举一个例子,限定每次只能有一个进程可以访问共享的资源。保卫资源的进程组成了集合 _S_ 。当进程 _p_ 要访问资源时,它首先要向 _S_ 中的大多数进程征求许可,大多数进程授权它可以访问资源。现在,系统中另外一个进程 _q_ 也要访问这个资源,但是它永远获得不了大多数保卫进程的许可和授权。只有当进程 _p_ 释放资源后,进程 _q_ 才有可能访问这个资源。更多细节,见论文
“法定人数系统的负载、容量和可用性”

法定人数并不总是指进程的大多数。有时,为了保证操作的成功,需要更多的进程形成法定人数,例如,由 _N_ 个可能出现拜占庭失效的进程构成的进程组。此时,如果 _f_ 表示可容忍的最多失效进程数,那么法定人数将是大于 _(N + f) / 2_ 。参见 《可靠且安全的分布式程序设计导论》

如果你对这个话题感兴趣,有一本专门讲分布式系统法定人数的书:

《法定人数系统及其在存储和共识中的应用》

分布式系统中的时间

理解时间及其后果,是分布式系统最大的问题之一。在日常生活中,我们都习惯了事件一个接一个发生,按照完美定义的“在……之前发生”(happend before)的顺序。如果是一系列分布式进程,它们交换消息和访问资源都是并发的。我们该如何判断事件的先后发生顺序?要回答此类问题,不同的进程必须共享一个同步时钟,还要准确地知道通过网络交换信息的耗时、 CPU 调度任务的耗时等等。显然,在现实系统中不可能做到这一点。

有一篇讨论上述问题的影响深远的论文,标题是“时间,时钟和分布式系统中的事件顺序”。这篇论文引入了逻辑时钟的概念。逻辑时钟是为系统中每个事件分配一个数字的方法。这些数字与实际的时间无关,而是与分布式系统节点对事件的处理有关。

逻辑时钟算法有很多种,像向量时钟区间树时钟

我推荐一篇讨论分布式系统时间的文章, Justin Sheehy 写的“没有现在”,很有意思的讨论。

我认为,时间及其在分布式系统中引起的问题,是需要理解的关键概念。我们必须摒弃同时性(simultaneity)的想法。同时性的想法源自“绝对知识”的老观念,我们以前认为绝对知识是可以获得的。物理定律告诉我们,即使是光也需要时间才能从一个地方达到另外一个地方,这样,当光到达我们的眼睛时,我们的大脑会处理光传递的信息。这是一种旧的世界观。Umberto Eco
《发明敌人》这本书的“绝对和相对”一章中讨论了上述想法。

快速浏览 FLP

最后,我们快速浏览只要有一个进程有可能失效,就无法达成分布式共识这篇论文,把刚才学到的有关分布式系统的各种概念关联起来。

在摘要的开头,作者写道:

共识问题涉及的是由一组进程组成的异步系统,其中一些进程是不可靠的。

在异步系统中,对处理速度或者消息送达时间不做任何假设,其中有些进程可能会崩溃。

这种说法有一个问题。在通常的技术术语中,异步可能是指处理请求的方式。以 RPC 为例,进程 _p_ 向进程 _q_ 发送一个异步请求;在进程 _q_ 处理请求的同时, _p_ 会继续做别的事情——也就是说, _p_ 不会为了等待应答而阻塞自己的运行。你看,同样是叫异步,在这里的含义与在分布式系统文献中的含义完全不同。不了解这一点,你很难完全理解 FLP 论文的第一句话。

接下来,作者写道:

本文提出一个令人惊奇的结果:系统中哪怕是只有一个进程可能会失效,就完全不可能存在异步共识协议。我们假设系统中不存在拜占庭失效,消息系统也是可靠的——所有消息都会被正确地一次送达。

由此可见,这篇论文讨论的系统只存在崩溃-停止(或者说失效-停止)的失效模式,(除了不存在拜占庭模式),也不存在忽略失效模式,因为消息系统是可靠的。

最后,作者还添加下面一条约束:

不假定进程能够检测出另外一个进程是否死亡。也就是说,一个进程无法区分另外一个进程的两种状态:死亡(完全停止运行)或者运行得很慢。

这就是说,本文讨论的系统中不存在失效检测器。

总结一下, FLP 不可能性结果适用于具有下列特征的异步系统:崩溃-停止失效模式;可靠的消息系统;不存在失效检测器。不了解各种分布式系统模型的理论,我们就可能漏掉很多细节,我们的理解甚至与作者的原意相去甚远。

想要更详细地了解 FLP 不可能性,请看博客文章: FLP 简要梳理

Marcos Aguilera 写了一篇有意思的论文“共识研究中的那些坑:误解与问题”,文中讨论了 FLP 作为分布式系统的一个不可能性结果,它的含义究竟是什么(剧透警告:这里说的不可能性与停机问题中的不可能性不是一回事儿)。

结束语

如你所知,分布式系统的学习需要时间投入。分布式系统是一个非常庞大的议题,每个子领域都已经有非常多的研究。与此同时,分布式系统的实现和验证又是非常复杂的,有很多容易犯错的细微之处,处理不好的话,我们实现的分布式系统就会在意想不到的情况下无法正常地工作。

如果不能正确地理解分布式系统的底层理论,下列问题甚至更多问题就会发生:选择错误的法定人数,结果导致新的复制算法丢失关键数据;选择非常保守的法定人数,导致应用程序运行变慢,从而满足不了向用户承诺的服务约定;我们要解决的问题本来根本没必要使用共识,用最终一致性就行,(但是我们却选择使用共识);错误地假设系统的定时模型;使用了不符合底层系统属性的失效检测器;我们想优化像 Raft
这样的算法,于是去掉了貌似不相关的一个小步骤,结果却破坏了算法的安全性保证。

好,我明白了,我不想重新发明分布式系统轮子,但是面对如此众多的问题和文献,我该从哪里着手呢?在本文开头,我指出随机地阅读论文没有什么用处。就像上面提到的 FLP 论文,不了解各种定时模型,你就没办法理解论文的第一句话。因此,我推荐下列入门书籍:

《分布式算法》,Nancy Lynch 著。这本书就好像是分布式系统的圣经,内容涵盖上面提及的各种模型,包括每种模型涉及的算法。

《可靠且安全的分布式程序设计指南》,Christian Cachin 等人著。这本书不光导论写得非常棒,还涵盖了很多种共识算法。这本书还有个优点,到处可见解释算法的伪代码。

还有很多书。我觉得这两本非常适合入门使用。

参考文献

如果你还想深入研究,请看本文的参考文献列表:

本文是全系列中第2 / 3篇:分布式系统设计

打赏作者
提交看法

抢沙发

还没有评论,你可以来抢沙发