实现Raft协议:Part 0 - 介绍

翻译自Eli Bendersky系列博客,已得到原做者受权。git

本文是系列文章中的序言,本系列文章旨在介绍Raft分布式一致性协议及其Go语言实现。文章的完整列表以下:github

Raft是一个相对较新的算法(2014),可是已经在业界取到了普遍的应用。最知名的案例应该就是Kubernetes,其中的分布式键值存储组件etcd就依赖了Raft协议。算法

本系列文章的写做目的,在于描述Raft协议的一个功能完备且通过严格测试的实现方式,并提供一些Raft工做方式的直观理解。这并非您学习Raft协议的惟一途径。我假定您至少读过Raft论文; 此外,也强烈建议您花时间仔细研究Raft网站上的资源——观看创做者的一两次演讲,鼓捣一下算法的可视化工具,浏览Ongaro的博士论文以学习更多细节,等等。数据库

不要期望能用一天时间就彻底掌握Raft协议。尽管Raft设计得比Paxos更易于理解,但Raft算法仍然是至关复杂的。 它要解决的问题(分布式一致性)是一个难题,所以解决方案天然也不会太简单。编程

复制状态机

分布式一致性算法能够认为是在解决跨服务器复制一个肯定性状态机的问题。这里的状态机一词能够用来表示任意服务。 毕竟,状态机是计算机科学的基础之一,并且一切事物均可以用状态机来表示。 数据库、文件服务器、锁服务器等均可以被看做是复杂的状态机。缓存

考虑用状态机来表明一些服务,有多个客户端能够链接到它,这些客户端会发出请求,并指望获得响应: 安全

Single state machine with two clients

只要运行状态机的服务器是稳定可靠的,这个系统就能够正常工做。若是服务器崩溃的话,咱们的服务也就不可用了,而这是不能接受的。一般,咱们系统的可靠性取决于其运行的服务器。服务器

提升服务可靠性的一种经常使用方法就是复制。 咱们能够在不一样的服务器上运行服务的多个实例。 这样就建立了一个服务器集群,这些服务器协同工做以提供服务,而且其中任何一台服务器的崩溃都不会致使服务中断。 服务器间相互隔离[1]能够摒除一些同时影响多台服务器的常见故障,从而进一步提升系统可靠性。网络

客户端不会再链接单个提供服务的机器,而是会链接整个集群。此外,构成该集群的服务副本之间必须相互通讯,以期正确地复制状态:并发

Replicated state machine with two clients

上图中的每一个状态机都是服务的一个副本。其思想就是,全部的状态机同步运行,从客户端请求中获取相同的输入,并执行相同的状态转换。这样就保证即便集群有一些服务器出现故障,也会返回相同的结果给客户端。Raft就是实现这个目的的一种算法。

如今正好澄清一些后文中会频繁使用的术语:

  • 服务(Service):是咱们将实现的分布式系统中的逻辑任务,好比说,一个键值数据库。
  • 服务器(Server)副本(Replica):在隔离的机器上运行的一个Raft服务实例,能够经过网络链接其它副本或者客户端。
  • 集群(Cluster):一组协做实现分布式服务的Raft服务器,典型的集群规模是3或5。

一致性模块和Raft日志

如今咱们来看一下上图展现的其中一个状态机。Raft做为一个通用的算法,并不关心服务是如何根据状态机实现的。它的目标是可靠、准确地记录并重现状态机接受的输入序列(Raft术语中也称为指令),给定初始状态和全部输入,就能够彻底准确地重放状态机。能够换个角度理解:若是咱们有相同状态机的两个独立的副本,而且从相同的起始状态开始向其发出一样的输入序列,那么两个副本最终会停留在相同的状态,而且会产生相同的输出。

这里是使用Raft的通常服务的结构:

Raft consensus module and log connected to state machine

这些组件的详细描述以下:

  • 状态机与咱们前面所说的相同。它表明任意一种服务:在介绍Raft时经常使用的例子就是键值存储。
  • 日志(Log)是存储客户端发出的全部指令(输入)的位置。这些指令不会直接应用于状态机,相反,只有当它们被成功复制到大多数服务器时,Raft算法才会提交这些指令。此外。日志是持久的——它保存在抗系统崩溃的稳定存储中,而且在系统崩溃后能够用于重放状态机。
  • 一致性模块是Raft算法的核心。它会接受客户端的指令,确保它们保存在日志中,将指令复制到集群中的其它Raft副本中(上图中的绿色箭头),而且在肯定安全的时候将指令提交到状态机中。提交到状态机会将实际修改通知到客户端。

领导者和追随者

Raft使用的是强领导模型,其中集群中的一个副本做为领导者,其它副本都做为追随者。领导者负责接受客户的请求,复制指令给追随者,并返回响应给客户端。

正常操做状况下,追随者的目的就是简单地复制领导者的日志。一旦领导者出现故障或者网络隔断,会有一个追随者接管领导权,所以服务仍然是可用的。

这个模型是有利有弊的。一个重要的优势就是简单,数据老是vong领导者流向追随者,并且只有领导者响应客户端的请求。这个设计使得Raft协议更容易被分析、测试和调试。缺点就是性能——由于集群中只有一个服务器与客户端进行交互,当客户端请求激增时这会变成系统的瓶颈。对于这个问题,答案一般是:Raft协议不适用于大流量服务。Raft协议更适用于那些以牺牲可用性为代价来保证一致性的低流量服务——咱们在容错部分会从新讨论这一点。

客户端交互

前面写过,“客户端不会再链接单个提供服务的机器,而是会链接整个集群”,这句话是什么含义呢?集群就是一组经过网络互连的服务器,因此你如何链接“整个集群”呢?

答案很简单:

  • 在访问Raft集群时,客户端知道集群中副本的网络地址。至于它如何知道(例如经过某种服务发现机制)不在本文的讨论范围以内。
  • 客户端一开始会发请求到任意副本,若是这个副本是领导者,它会当即接受请求,并且客户端也会等待完整的响应,此后,客户端会记住这个副本是领导者,之后就没必要再次搜索领导者(除非遇到某些故障,如领导者崩溃)。
  • 若是副本表示本身不是领导者,客户端会尝试链接另外一个副本。这里能够进行优化,由追随者副本直接告诉客户端哪个副本是领导者。由于副本间是一直在相互通讯的,因此一般知道正确答案,这样能够节省客户端的猜想时间。
  • 还有一种状况下客户端会意识到本身链接的不是领导者,那就是它的请求在一段超时时间内没有提交成功。这可能意味着它链接的副本实际上不是领导者(即便它认为本身是)——它可能跟其它Raft服务器间出现了分割。当超时时间耗尽后,客户端会从新搜索其它的领导者。

第三点中提到的优化在多数状况下都不是必要的。一般来讲,在Raft环境中区分“正常运行”和“异常状况”是颇有用的。一个服务一般有99.9%的时间都是“正常运行”的,此时,客户端知道领导者是哪个,由于它们在第一次链接服务的时候就缓存了这些信息。故障场景下确定会形成混乱(下一节会讨论更多细节),可是也只是很短的时间。咱们在下一篇文章中也会详细介绍,Raft集群可以很快地从机器临时故障或网络分区问题中恢复——大多数状况下恢复间隔不到1秒钟。当新的领导者声明领导权以及客户端查找具体的领导者副本时,可能会出现短暂的不可用状态,可是以后集群会恢复到“正常运行模式”。

Raft容错机制和CAP理论

咱们来看一下三个Raft副本的示意图,此次不须要链接客户端:

Replicated state machine not showing clients

在这个集群中,咱们能够预见什么类型的故障呢?

现代计算机中的每一个组件均可能会出现故障,可是为了方便讨论,咱们把Raft实例中运行的服务器看做一个原子单元。这样的话,咱们会面临两大类的故障:

  1. 服务器崩溃,其中一个服务器在一段时间内中止响应全部的网络请求。崩溃的服务器一般会被重启,并在短暂的中断后从新上线。
  2. 网络分区,因为网络设备或传输介质问题,致使一个或多个服务器与其它服务器和/或客户端断开链接。

从服务器A的角度来讲,其与服务器B之间相互通讯,对于服务器B的故障与A、B间的网络分区是没法区分的。这两种状况的表现是相同的——A接受不到任何B的信息及响应。可是,从系统的角度来讲,网络分区的影响更大,由于它们会同时影响多台服务器。在本系列的下一部分,咱们会讨论网络分区致使的一些复杂场景。

为了优雅地应对任意网络分区和服务器故障问题,Raft要求集群中的大多数服务器是正常启动的,并且在任意指定时刻均可觉得领导者所用。若是有3台服务器,Raft能够容许1台机器故障,对于5台服务器的集群,能够容许2台机器故障; 对于2N+1台服务器,能够容许N台服务器出现故障。

这就引出了CAP理论,其实际结论就是,当存在网络分区(实际应用中难以免的一部分)时,咱们必须仔细权衡可用性一致性

在这个权衡中,Raft坚决地站在一致性阵营。其设计理念就是防止集群可能达到不一致状态的状况,在这种状况下,不一样的客户端可能会获得不一样的响应。为此Raft牺牲了部分可用性。

我前面也简单提过,Raft不是为高吞吐量、细粒度的服务设计的。客户端的每个请求都会触发一系列工做——Raft副本间通讯,以期把指令复制到大多数服务并持久化;这些都发生在客户端获得回应以前。

举例来讲,你确定不会设计一个全部客户端请求都通过Raft的复制数据库,这样太慢了。Raft更适合于粗粒度的分布式原语——如实现锁服务器,为更高级别的协议选举领导者,在分布式系统中复制关键配置数据,等等。

为何选Go

本系列中介绍的Raft实现是用Go语言编写的。在我看来,Go语言有三大优点,也是本系列及通用的网络服务选择Go做为实现语言的缘由:

  1. 并发 :Raft这类算法在本质上是彻底并行的,每一个副本要执行持续不断的操做(指令),为定时事件运行定时器,还必须响应其它副本和客户端的请求。我以前写过为何我认为Go是编写这类代码的理想语言
  2. 标准库:Go语言拥有一个强大的工业级标准库,能够轻松编写复杂的网络服务器,而不须要导入和学习任何第三方库。特别是在Raft中,须要面对的第一个问题就是“如何在副本之间发生消息?”,不少人会陷入设计协议和序列化的困境中,或者使用繁重的第三方库。Go语言中有net/rpc,这是一个足以应对此类任务的解决方案,能够快速使用并且不须要引入 (依赖)。
  3. 简单:即便不考虑编程语言,实现分布式一致性就已经足够复杂了。使用任何语言均可以写出清晰、简单的代码,可是在Go语言中,这是默认的习惯写法,这门语言在每一个可能的层面上都反对代码的复杂性。

下一步

感谢您能读到这里!若是您以为有哪些地方我能够写得更好的,请告诉我。尽管Raft在概念上可能看起来很简单,可是一旦咱们编码实现,仍是会遇到不少问题。本系列的后续部分将介绍关于Raft算法不一样方面的更多细节。

如今你应该已经准备好进入第1部分,咱们开始实现Raft吧。

译者注

本系列文章经过使用Golang实现Raft协议,不只直观解释了Raft协议中的一些难点,对于Go语言并发编程的学习也有很大的帮助。

本人在读完原博客以后以为受益不浅,在征求做者赞成以后,将本系列博客翻译为中文并分享给你们,但愿对Go或者Raft有兴趣的同窗都可以有所收获。

强烈建议读者在看完一篇文章以后,能够执行做者代码中的测试用例,对照测试输出日志巩固一下对Raft协议的理解。

我在学习过程当中,fork了原做者的代码,在原基础上添加了中文注释,也添加了测试用例的输出结果。对于不方便执行测试的读者,能够直接在其中查看测试输出日志。

需者自取,Github地址:github.com/GuoYaxiang/…


  1. 举例来讲,能够将它们放在不一样的机架中,或链接到不一样的电源,甚至放置在不一样的建筑物中。 大型公司提供的真正重要的服务一般是在全球范围内复制的,副本会分布在不一样的区域。 ↩︎

相关文章
相关标签/搜索