翻译自Eli Bendersky的系列博客,已得到原做者受权。git
本文是系列文章中的序言,本系列文章旨在介绍Raft分布式一致性协议及其Go语言实现。文章的完整列表以下:github
Raft是一个相对较新的算法(2014),可是已经在业界取到了普遍的应用。最知名的案例应该就是Kubernetes,其中的分布式键值存储组件etcd就依赖了Raft协议。算法
本系列文章的写做目的,在于描述Raft协议的一个功能完备且通过严格测试的实现方式,并提供一些Raft工做方式的直观理解。这并非您学习Raft协议的惟一途径。我假定您至少读过Raft论文; 此外,也强烈建议您花时间仔细研究Raft网站上的资源——观看创做者的一两次演讲,鼓捣一下算法的可视化工具,浏览Ongaro的博士论文以学习更多细节,等等。数据库
不要期望能用一天时间就彻底掌握Raft协议。尽管Raft设计得比Paxos更易于理解,但Raft算法仍然是至关复杂的。 它要解决的问题(分布式一致性)是一个难题,所以解决方案天然也不会太简单。编程
分布式一致性算法能够认为是在解决跨服务器复制一个肯定性状态机的问题。这里的状态机一词能够用来表示任意服务。 毕竟,状态机是计算机科学的基础之一,并且一切事物均可以用状态机来表示。 数据库、文件服务器、锁服务器等均可以被看做是复杂的状态机。缓存
考虑用状态机来表明一些服务,有多个客户端能够链接到它,这些客户端会发出请求,并指望获得响应: 安全
只要运行状态机的服务器是稳定可靠的,这个系统就能够正常工做。若是服务器崩溃的话,咱们的服务也就不可用了,而这是不能接受的。一般,咱们系统的可靠性取决于其运行的服务器。服务器
提升服务可靠性的一种经常使用方法就是复制。 咱们能够在不一样的服务器上运行服务的多个实例。 这样就建立了一个服务器集群,这些服务器协同工做以提供服务,而且其中任何一台服务器的崩溃都不会致使服务中断。 服务器间相互隔离[1]能够摒除一些同时影响多台服务器的常见故障,从而进一步提升系统可靠性。网络
客户端不会再链接单个提供服务的机器,而是会链接整个集群。此外,构成该集群的服务副本之间必须相互通讯,以期正确地复制状态:并发
上图中的每一个状态机都是服务的一个副本。其思想就是,全部的状态机同步运行,从客户端请求中获取相同的输入,并执行相同的状态转换。这样就保证即便集群有一些服务器出现故障,也会返回相同的结果给客户端。Raft就是实现这个目的的一种算法。
如今正好澄清一些后文中会频繁使用的术语:
如今咱们来看一下上图展现的其中一个状态机。Raft做为一个通用的算法,并不关心服务是如何根据状态机实现的。它的目标是可靠、准确地记录并重现状态机接受的输入序列(Raft术语中也称为指令),给定初始状态和全部输入,就能够彻底准确地重放状态机。能够换个角度理解:若是咱们有相同状态机的两个独立的副本,而且从相同的起始状态开始向其发出一样的输入序列,那么两个副本最终会停留在相同的状态,而且会产生相同的输出。
这里是使用Raft的通常服务的结构:
这些组件的详细描述以下:
Raft使用的是强领导模型,其中集群中的一个副本做为领导者,其它副本都做为追随者。领导者负责接受客户的请求,复制指令给追随者,并返回响应给客户端。
正常操做状况下,追随者的目的就是简单地复制领导者的日志。一旦领导者出现故障或者网络隔断,会有一个追随者接管领导权,所以服务仍然是可用的。
这个模型是有利有弊的。一个重要的优势就是简单,数据老是vong领导者流向追随者,并且只有领导者响应客户端的请求。这个设计使得Raft协议更容易被分析、测试和调试。缺点就是性能——由于集群中只有一个服务器与客户端进行交互,当客户端请求激增时这会变成系统的瓶颈。对于这个问题,答案一般是:Raft协议不适用于大流量服务。Raft协议更适用于那些以牺牲可用性为代价来保证一致性的低流量服务——咱们在容错部分会从新讨论这一点。
前面写过,“客户端不会再链接单个提供服务的机器,而是会链接整个集群”,这句话是什么含义呢?集群就是一组经过网络互连的服务器,因此你如何链接“整个集群”呢?
答案很简单:
第三点中提到的优化在多数状况下都不是必要的。一般来讲,在Raft环境中区分“正常运行”和“异常状况”是颇有用的。一个服务一般有99.9%的时间都是“正常运行”的,此时,客户端知道领导者是哪个,由于它们在第一次链接服务的时候就缓存了这些信息。故障场景下确定会形成混乱(下一节会讨论更多细节),可是也只是很短的时间。咱们在下一篇文章中也会详细介绍,Raft集群可以很快地从机器临时故障或网络分区问题中恢复——大多数状况下恢复间隔不到1秒钟。当新的领导者声明领导权以及客户端查找具体的领导者副本时,可能会出现短暂的不可用状态,可是以后集群会恢复到“正常运行模式”。
咱们来看一下三个Raft副本的示意图,此次不须要链接客户端:
在这个集群中,咱们能够预见什么类型的故障呢?
现代计算机中的每一个组件均可能会出现故障,可是为了方便讨论,咱们把Raft实例中运行的服务器看做一个原子单元。这样的话,咱们会面临两大类的故障:
从服务器A的角度来讲,其与服务器B之间相互通讯,对于服务器B的故障与A、B间的网络分区是没法区分的。这两种状况的表现是相同的——A接受不到任何B的信息及响应。可是,从系统的角度来讲,网络分区的影响更大,由于它们会同时影响多台服务器。在本系列的下一部分,咱们会讨论网络分区致使的一些复杂场景。
为了优雅地应对任意网络分区和服务器故障问题,Raft要求集群中的大多数服务器是正常启动的,并且在任意指定时刻均可觉得领导者所用。若是有3台服务器,Raft能够容许1台机器故障,对于5台服务器的集群,能够容许2台机器故障; 对于2N+1
台服务器,能够容许N
台服务器出现故障。
这就引出了CAP理论,其实际结论就是,当存在网络分区(实际应用中难以免的一部分)时,咱们必须仔细权衡可用性和一致性。
在这个权衡中,Raft坚决地站在一致性阵营。其设计理念就是防止集群可能达到不一致状态的状况,在这种状况下,不一样的客户端可能会获得不一样的响应。为此Raft牺牲了部分可用性。
我前面也简单提过,Raft不是为高吞吐量、细粒度的服务设计的。客户端的每个请求都会触发一系列工做——Raft副本间通讯,以期把指令复制到大多数服务并持久化;这些都发生在客户端获得回应以前。
举例来讲,你确定不会设计一个全部客户端请求都通过Raft的复制数据库,这样太慢了。Raft更适合于粗粒度的分布式原语——如实现锁服务器,为更高级别的协议选举领导者,在分布式系统中复制关键配置数据,等等。
本系列中介绍的Raft实现是用Go语言编写的。在我看来,Go语言有三大优点,也是本系列及通用的网络服务选择Go做为实现语言的缘由:
net/rpc
,这是一个足以应对此类任务的解决方案,能够快速使用并且不须要引入 (依赖)。感谢您能读到这里!若是您以为有哪些地方我能够写得更好的,请告诉我。尽管Raft在概念上可能看起来很简单,可是一旦咱们编码实现,仍是会遇到不少问题。本系列的后续部分将介绍关于Raft算法不一样方面的更多细节。
如今你应该已经准备好进入第1部分,咱们开始实现Raft吧。
本系列文章经过使用Golang实现Raft协议,不只直观解释了Raft协议中的一些难点,对于Go语言并发编程的学习也有很大的帮助。
本人在读完原博客以后以为受益不浅,在征求做者赞成以后,将本系列博客翻译为中文并分享给你们,但愿对Go或者Raft有兴趣的同窗都可以有所收获。
强烈建议读者在看完一篇文章以后,能够执行做者代码中的测试用例,对照测试输出日志巩固一下对Raft协议的理解。
我在学习过程当中,fork了原做者的代码,在原基础上添加了中文注释,也添加了测试用例的输出结果。对于不方便执行测试的读者,能够直接在其中查看测试输出日志。
需者自取,Github地址:github.com/GuoYaxiang/…
举例来讲,能够将它们放在不一样的机架中,或链接到不一样的电源,甚至放置在不一样的建筑物中。 大型公司提供的真正重要的服务一般是在全球范围内复制的,副本会分布在不一样的区域。 ↩︎