zookeeper raft poxas zab
CAP
CAP定理是分布式领域当中非常著名的定理,也是大家津津乐道的一个分布式定理。有些人这么理解CAP定理:
在分布式系统中,
- C代表一致性
- A代表可用性,
- P代表网络分区。
因为,分布式环境中,P不不可避免的,分布式系统要么选择一致性放弃可用性,要么选择可用性放弃一致性。
在一个shared-data的系统中,我们不可能同时获得以下3个特性:
- C:一致性,其实这里的一致性,是指线性一致性。
- A:可用性,是指系统中每一个non-failing的节点都可以在一个合理的时间里成功的完成读写操作。
- P: 容忍网络分区。
下面对CAP定理做几点说明。
- 1.因为CAP定理在描述的是一个shared-data的系统,网络分区是必然存在的,系统必须处理网络分区的情况。
- 2.定理中的一致性和可用性都是非常苛刻的条件。线性一致性是非常强的一致性模型,除了线性一致性模型,还有很多比线性一致性弱很多的一致性模型,如果你的系统采用了这样的相对较弱一致性模型,其实已经不是CAP定理的范畴了。你是可以实现出一个具有更弱的一致性模型并且同时满足A和P的系统出来。
(关于什么是线性一致性及其作用可以参考<线性一致性(Linearizability)是并发控制的基础>)
- 3.同理,A也是非常苛刻的,他强调了每一个节点都可以完成读写操作。所以Paxos虽然是线性一致性的,但是没满足这么苛刻的A的条件。所以也不适用CAP定理。
其实,CAP只是描述了你不可能实现出一种同时具有完美一致性和完美可用性的shared-data系统出来。这个定理基本上没有太多实用性的。目前已经有很多系统,实现了很好的一致性并且具有很好可用性。我们不能说这些现实的系统违反了CAP定理,而是CAP定理不适用于这些现实的系统。Paxos、Zookeeper、etcd就是这样的一个例子。另外,作为对立面的一个例子,Cassandra具有很好的可用性,并且也具有比线性一致性弱一些的一致性。
Sequential consistency
2017饿了么做异地多活,我的团队承担Zookeeper的异地多活改造。在此期间我听到2种不同的关于一致性的说法。一种说法是Zookeeper是最终一致性,因为由于多副本、以及保证大多数成功的Zab协议,当一个客户端进程写入一个新值,另外一个客户端进程不能保证马上就能读到这个值,但是能保证最终能读取到这个值。另外一种说法是Zookeeper的Zab协议类似于Paxos协议,并且提供了强一致性。每当我听到这2种说法,我都想上去纠正一下,“不对,Zookeeper是顺序一致性(Sequential consistency)”。但是解释起来太复杂了,需要一篇长文来说明。一直想写这篇文章说明这个说法,但是一直没写,饿了么的异地多活项目结束这么长时间了,终于挤一些时间把它写出来,和大家一起讨论一下。
从Zookeeper的文档中我们可以看到,Zookeeper文档中明确写明它的一致性是Sequential consistency。(参看 zookeeper的这个官方文档http://zookeeper.apache.org/doc/r3.4.9/zookeeperProgrammers.html#ch_zkGuarantees)
那么什么是Sequential consistency那?
Sequential consistency的是Lamport在1979年首次提出的。(参看他的这篇论文< How to make a multiprocessor computer that correctly executes multiprocess programs >)
论文中定义,当满足下面这个条件时就是sequential consistency:
the result of any execution is the same as if the operations of all the processors were executed in some sequential order, and the operations of each individual processor appear in this sequence in the order specified by its program.
这段英文定义很晦涩(这是Lamport大神的一向的风格,严谨但晦涩,Paxos协议也是如此),我第一次看到这段定义时的感觉就是:“这是什么鬼?”。为什么每个英文单词我都认识,但是怎么就是不知道他在说什么。第一次看到这句话和我有同感的小伙伴举个手。
本文后面我再把这段英文定义翻译成中文,现在我们先来看看这篇论文的标题和定义中出现的一个关键词,来说明一下sequential consistency的应用范围。论文的标题和这段定义中包含multiprocessor这个词,Multiprocessor是多核处理器的意思。从这个关键字上来看,sequential consistency是用来定义多核处理器和跑在多核处理器上的程序的一个特性。Lamport这篇论文的标题可以翻译成,“如何让具有多核处理器的计算机的正确执行多进程程序”,也就是说如果一个多核处理器具有sequential consistency的特性,这个多核处理器就可以正确的运行,后面我们来解释这个正确运行是什么意思(也就是本文后面讲到的Sequential consistency的作用)。从这个标题中我们还可以看出,Sequential consistency应该是个并发编程(concurrent programming)领域的概念。但是我们现在常常在分布式系统领域讨论Sequential consistency,比如本文主要要讨论Zookeeper(Zookeeper很明显是一个分布式系统)的一致性。实际上,多核处理器上的运行的多个程序,其实也是一种分布式系统(Lamport在他的这篇< Time, Clocks, and the Ordering of Events in a Distributed System >分布式系统的开山之作中也阐述了这个观点)。所以虽然Sequential consistency最早在并发编程中提出,但是它可以应用在分布式系统中,比如本文讨论的Zookeeper这种分布式存储存储系统。另外一个比较重要的Linearizability(线性一致性),也是在并发编程中最早提出的,目前也被广泛的应用在分布式系统领域中。
下面我们要来翻译上面那段晦涩的定义。做这段定义的翻译让我找到了上学时做阅读理解的感觉。我先不直接翻译,因为就算我把它翻译成中文,我估计很多人还是不明白是什么意思。还是会有那种感觉,为毛每个中文字我都懂,可还是不知道在说什么。
首先,我来解释一些个别的词。第一个,any execution,any execution是什么意思?你有多个程序(program)在多核处理器上运行,例如你有2个程序,第一个程序叫P1,它的代码如下:
P1_write(x);
P1_read(y);
1
2
第二个程序叫P2,代码如下:
P2_write(u);
P2_read(v);
1
2
从理论上来讲,2个程序运行在2个独立的处理器的核上,有多少种执行的可能那?我列举其中几种来举例说明。
第1种:
P1---write(x)--------read(y)--------
P2-----------write(u)-------read(v)-
1
2
第2种:
P1----------write(x)-read(y)--------
P2--write(u)----------------read(v)-
1
2
第3种:
P1---read(y)----------write(x)------
P2-----------write(u)---------read(v)-
1
2
我们有24中可能的执行顺序,也就是这4个操作任意的排列组合,也就是4!=24。类似第一种和第二种这样的可能性很好理解。为什么会出现像第3种这样的可能的执行那?那是因为就算是在同一个程序中,由于处理器会有多级的缓存,以及处理器中coherence的存在,虽然你的程序中是先write后read,在内存中真正生效的顺序,也有可能是先read后write。
其实还会出现类似下面这样的执行,2个操作在2个处理器上同时执行。
P1--write(x)-read(y)--------
P2--write(u)--------read(v)-
1
2
如果加上同时运行的这种情况,那就有更多种可能性。我的算数不好,这里我就不再继续算了,因为到底有多少个不重要,重要的是你知道有很多种可能性就可以了。那么定义中的”any execution”,就是指任意一种可能的执行,在定义中也可以理解为所有的这些可能的执行。
接下来还是不翻译定义,我们再来解释一个词–sequential order。什么叫sequential order?我们来翻一下英语词典(感觉更像是在做阅读理解了)。
sequential:连续的;相继的;有顺序的
order:命令;顺序;规则;[贸易] 定单
sequential order–有顺序的顺序,这个是什么鬼?
其实sequential是有一个接一个的意思,在处理器的这种上下文中,sequential就是指操作(operartion)一个接一个的执行,也就是顺序执行,并且没有重叠。Order是指经过一定的调整,让某样东西按照一定的规则变得有序。比如,在算法中的排序算法就是ordering,就是让数组这个东西按照从大到小的规则或则从小到大的规则变得有序。那么sequential order就是指让操作(operation)按照一个接一个这样的规则排列,并且没有重叠。
仍然说上面的例子,如果把4个操作,按一个接一个的规则排列,我们这时就可以得到4!的排列组合个可能的排列(order),仍然,到底有多少个不重要。
比如:
P1_write(x);P1_read(y);P2_write(u);P2_read(v);
P1_read(y);P1_write(x);P2_write(u);P2:read(v);
P2_write(u);P2_read(v);P1_read(y);P1:write(x);
1
2
3
我这里只列举其中3个,其他的大家可以自己排一下。
重点来了,其实sequential order就是让这4个操作一个接一个的顺序执行,并且没有重叠。注意这个排列不是真实的执行,真实的执行是any execution,这里说的是逻辑上的假设,这也就是为什么定义有一个as if。
做了这么多的铺垫,下面我们开始翻译定义中的第一句话:
任意一种可能的执行的效果和某一种所有的处理器上的操作按照顺序排列执行的效果是一样的。
注意,这里some在这里是某一的意思,不是一些,因为order是单数。(在做阅读理解)
这就话的意思就是说,一个处理器要满足这个条件,就要能够只允许满足这个条件的那些可能的执行存在,其他不满足的可能的执行都不会出现。
从第一句话中我们可以看出,一种多核处理器要想满足sequential consistency,那么多个程序在多个核运行效果”等同”于在一个核上顺序执行所有操作的效果是差不多的。如果这样的话,其实多核的威力基本就消失了。所以无论是从Lamport写这篇论文的1979,还是现在,没有任何一个现实的多核处理器,实现了sequential consistency。那么为什么Lamport大神提出这样一个不现实的概念那?(我要注意Lamport写这篇论文时,并没有把它引申到分布式系统领域,就是针对多核处理器,并发编程领域提出的)我们现在先不说,稍后在论述。
这里还要注意的一点是,在我的翻译里用了效果一词,但实际上英文原文定义中用的是result(结果)一词。那效果和结果有什么区别吗?我们解释一下什么叫执行结果?不管是任何真实的执行,还是某种经过顺序排序后的假设执行,程序会产生一定的结果,比如print出来的结果(result)。实际上定义中说的是结果一样。如果定义中用效果的话,那么这个定义就只是一个定性的定义,如果用结果的话,那这个定义就是一个定量的定义。定量的,也就是说是可以通过数学证明的。从这点我们就可以看出,大神就是不一样,任何理论都是可以通过数学证明是正确的。文章后面还会提到证明的事情,我们这里再卖个关子。
到这里,我们第一句定义的更准确翻译是:
任意一种可能的执行的结果和某一种所有的处理器上的操作按照顺序排列执行的结果是一样的。
这里我们还要注意一点的是,结果一样就意味着,如果有人真的要实现一种sequential consistency的多核处理器的话,因为要保证结果一样,所以他是有一定的空间来优化,而不会完全是一个核顺序执行的效果。但是估计这种优化也是非常有限的。
好了,我们终于把最难的第一话解释完了,大家可以松口气,第二句就非常简单了。我们还是先解释一个词再完整的翻译。这个词就是第二句中出现的sequence。我们刚刚解释过的sequential order是顺序排序(于就是按一个接一个排序),其实这是一个动作,动作会产生结果,它的结果产生了一个操作(operation)的队列。第二句中出现的sequence就是指这个操作(operation)的队列。
好,那第二句的翻译就是:
并且每个独立的处理器的操作都会按照程序指定的顺序出现在操作队列中。
也就是说如果程序里是先write(x);后read(y);那么只有满足这个顺序的操作队列是符合条件的。这样,我们刚刚说的很多可能的执行就少了很多,这里我也就不计算少了多少,还是那句话,数量不重要,反正是有,而且变少了。那么第二句意味这什么?意味着如果一个多核处理器实现了sequential consistency,那么这种多核处理器基本上就告别自(缓)行(存)车了。这里我还继续卖关子,连缓存这种最有效提高处理器性能的优化都没了,大神为什么要提出这个概念?
好了,到这里我们可以把2句翻译合起来,完整的看一下:
任意一种可能的执行的结果和某一种所有的处理器上的操作按照顺序排列执行的结果是一样的,并且每个独立的处理器的操作都会按照程序指定的顺序出现在操作队列中。
从这个定义中,我们可以看出,这个概念的核心就是sequential order,这也就是为什么Lamport老爷子,把这种一致性模型称之为sequential consistency。可以说这个命名是非常贴切的。不知道这种贴切对于以英语为母语的人来说是不是更好理解一些,应该不会出现”顺序的顺序是什么鬼”的这种情况。如果你看完这篇文章,也觉得sequential很贴切的话,那就说明我讲清楚了。
接下来我们举个具体的例子,再来说明一下。
execution A
P0 writex=1-------------------------------
P1 -------write x=2----------------------
P2 -----------------read x==1--read x==2
P3 -----------------read x==1--read x==2
sequetial order: P0_write x=1,P3_read x==1,P4_read x==1,P1_write x=2,P3_read x==2,P4_read x==2
execution B
P0 write=1-------------------------------
P1 -------write x=2----------------------
P2 -----------------read x==2--read x==1
P3 -----------------read x==2--read x==1
sequetial order: P1_write x=2,P3_read x==2,P4_read x==2,P0_write x=1,P3_read x==1,P4_read x==1
execution C
P0 write=1-------------------------------
P1 -------write x=2----------------------
P2 -----------------read x==1--read x==2
P3 -----------------read x==2--read x==1
sequetial order: 你找不出一个符合定义中2个条件的一种order。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
所以说如果一个多核处理器只允许execution A和B出现,不允许C出现,那么这个多核处理器就是sequetial consistency的。如果它允许C出现,那它就不是sequetial consistency。
到这里我们已经完整的讲完什么是sequetial consistency。但是,细心的朋友可能会问,如果你的program是的多线程的程序怎么办那?那么我们再把定义中最后的一个细节解释一下:program这个词。Program是指可以直接运行在处理器上的指令序列。这个并不是Pogram的严格定义,但是我要指出的是这个Program是在操作系统都没有的远古时代就存在的概念,这个定义中prgram就是指那个时代的program。这个Program里没有进程、线程的概念,这些概念都在有了操作系统之后才有的概念。因为没有操作系统,也没有内存空间的概念。不像是我们现在所说的程序(Program),不同的程序有自己独立的内存地址空间。我们这里,内存(memory)对于不同的Program来说是shared。另外,需要注意的是Program可以用来说明各种程序,不管你是操作系统内核,还是应用程序,都适用。
刚刚我们说了,sequential consistency虽然是针对并发编程的领域提出的,但实际上它是分布式领域的概念,特别是分布式存储系统。在< Distributed system: Principles and Paradigms >(作者Andrew S.Tanenbaum, Maarten Van Steen)这本书中,作者稍微修改了一下Lamport的定义,让这个定义更贴近分布式领域中的概念,我们来看一下作者是怎么改的:
The result of any execution is the same as if the (read and write) operations by all processes on the data store were executed in some sequential order and the operations of-each individual process appear in this sequence in the order specified by its program.
作者把processor换成了process,并且加了on the data store这个限定,在Lamport没有这个限定,其实默认指的是memory(内存)。Process就是指进程。以zookeeper为例,就是指访问zookeeper的应用进程。program也不是那么底层概念,也是基于操作系统的应用程序了。
好了,下面我该揭晓我上面卖的2个关子了。在Lamport的论文中,给出了一个小例子,如下:
process 1
a := 1;
if b = 0 then critical section:
a := 0
else ... fi
process 2
b := 1;
if a = 0 then critical section:
b := 0
else ... fi
1
2
3
4
5
6
7
8
9
10
11
Lamport在论文中说,如果一种多核处理器满足sequential consistency的条件,那么最多只有一个程序能够进入critical section。在论文中,Lamport老爷子并没有解释为什么最多只有一个程序能够进入critical section。而是把这个证明留给了论文的读者,就像我们常见的教科书中的课后习题一样,留给的读者。Lamport老爷子应该是认为这个证明太简单了,不应该花费它的笔墨来证明它。sequential consistency这篇论文只有不到2页A4纸,是我见过的最短的论文。这是Lamport老爷子一项的做事风格,Lamport的Paxos论文中,有很多细节,都是一笔带过的,给读者留下无尽的遐想(瞎想)。
假设现在我们已经证明这个是正确的(虽然我也没去证明一下,论文给出2个参考文献,用来证明这个),这个例子说明了什么那?你也许注意到了,这个例子没有用到任何锁,但是它实现了critical section,critical section是一种多线程synchronization 机制。如果多核处理器是sequential consistency的,那么你写的并发程序”天然就是正确的”。但是处理器的设计者为了最求性能,将保证程序正确的任务丢给程序开发者。只在硬件级别提供了一些fence、cas等指令,基于这些指令操作内核和语言基础库实现了各种synchronization机制,用来保证操作系统的正确性和应用程序的正确性。程序员必须小心谨慎的使用线程和这些synchronization机制,否则就会出各种意想不到的问题。如果你没有debug一个多线程bug连续加班2天,那说明你是大神。这些指令都是具有更高一致性级别,也就是linearizability(关于linearizability可以参看我的另外一篇文章<线性一致性(Linearizability)是并发控制的基础>),虽然一致性级别高,但只是个别指令的,处理器整体只是实现了比sequential consistency低很多的一致性级别。所以实现难度大大的降低了。虽然Lamport老爷子的sequential consistency的概念在concurrent programming领域中还没有实际意义,但是却给我们指出了程序员的天堂在哪里。在程序员的天堂里,没有多(车)线(来)程(车)编(往)程,只用写程序就行。你面试的时候不会再有人问你多线程编程,不会再问你各种锁。
在分布式领域中,sequential consistency更实际一些。zookeeper就实现了sequential consistency。同理,这应该也是可以证明的,但是目前还没发现有zookeeper社区有任何论文来证明这个。如果你已经明白上面解释的定义,你可以想清楚zookeeper是sequential consistency。欢迎大家一起来讨论。
实际上,zookeeper的一致性更复杂一些,Zookeeper的读操作是sequential consistency的,Zookeeper的写操作是linearizability的(关于linearizability可以参看我的另外一篇文章<线性一致性(Linearizability)是并发控制的基础>)。关于这个说法,Zookeeper的官方文档中没有写出来,但是在社区的邮件组有详细的讨论(邮件组的讨论参看,http://comments.gmane.org/gmane.comp.java.hadoop.zookeeper.user/5221 )。另外在这篇关于Zookeeper的论文中也有提到这个观点(这篇论文不是Zookeeper的主流论文,但是全面分析了Zookeeper的特性,以及Zookeeper跨机房方案,饿了么的Zookeeper异地多活改造也参考了这篇论文中的一些观点)。我们可以这么理解Zookeeper,从整体(read操作+write操作)上来说是sequential consistency,写操作实现了Linearizability。
通过简单的推理,我们可以得出Lamport论文中的小例子,在zookeeper中也是成立的。我们可以这样实现分布式锁。但zookeeper官方推荐的分布式实现方法并没有采用这个方式来实现,而是利用了Zookeeper的Linearizability特性实现了分布式锁(关于Zookeeper官方是如何实现分布式锁的,请参考我的这篇文章<Zookeeper实现分布式锁和选主>)。
为什么zookeeper要实现sequential consistency? Zookeeper最核心的功能是用来做coordination service,也就是用来做分布式锁服务,在分布式的环境下,zookeeper本身怎么做到”天然正确”?没有其他的synchronization机制保证zookeeper是正确的,所以只要zk实现了sc,那他自身就可以保证正确性,从而对外提供锁服务。
这一段有一个小的问题,锁的实现是基于写操作,底层是依赖 Linearizability。
Linearizability
在我的其他几篇文章里,已经多次提到线性一致性(Linearizability),那么到底线性一致性(Linearizability)是什么?线性一致性(Linearizability)有什么用处?
虽然,我们最常提到Linearizability是在讨论分布式系统的时候,但其实Linearizability是一个并发编程(concurrent programming)领域的概念。
线性一致性又叫做原子一致性。在并发编程中,如果一个操作对于系统的其他部分是不可中断的,那这个操作就可以称之为原子的,线性的,不可分割的。那这个操作就可以称之为具有线性一致性的特性。
原子操作具有”要么成功要么失败”的特性,就是说要么这个操作成功修改系统的状态,要么不会有任何作用。
操作系统的多线程编程就是一种并发编程。在做多线程编程时,并发控制是我们最需要考虑的一个事情,我们常常用各种锁来保护(shared-data)共享数据。那么操作系统是如何实现的这些锁那?在硬件层面,硬件会提供一些具有线性一致性的指令,比如atomic read-write,atomic swap,test-and-set,操作系统基于这些指令建立mutual exclusion、 semaphores 等锁结构。
在分布式领域中,我们也会说线性一致性,例如Zookeeper是线性一致性的,再比如分布式领域著名的CAP定理中的C,也是指线性一致性。(关于这一点请参看<从Paxos不违反CAP来解释什么是CAP定理>)虽然我们在讨论分布式系统的线性一致性,但其实我们是将一个分布式系统看成一个整体,分布式系统的多个客户端同时访问操作这个分布式系统。这种场景和在多线程编程中,多个线程访问操作同一个内存是类似的。所以这也是一个并发编程的场景。类似于多线程编程中,我们需要锁机制来控制多个线程的并发访问,在分布式编程中也需要控制多个进程的并发访问,也需要一种锁机制,那么就要用Zookeeper这样就有线性一致性的系统来充当协调者(coordinator),来实现分布式锁。
从多线程编程和分布式编程中,我们可以看出,线性一致性是实现并发控制的原语(Primitive),在这个原语的基础上可以实现各种锁机制来控制并发访问。
所以说,线性一致性(Linearizability)是并发编程中(包括多线程编程和分布式编程)并发控制的基础。
如果你的系统具有线性一致性,那么你的系统可以用来做分布式锁服务,从系统本身来讲,具有很好的并发控制能力。
另外,Linearizability可以得出客户端视角的一个特性,只要一个value被写入,那么后续的client做read操作时,一定能够读到这个新值。(在< Tunable Consistency不能让Cassandra成为CP系统>这边文章讨论中就用到了这个特性)
raft
对于这个简化后的问题,有许多解决方案,第一个被证明的共识算法是 Paxos,由拜占庭将军问题的作者 Leslie Lamport 在1990年提出,最初以论文难懂而出名,后来这哥们在2001重新发了一篇简单版的论文 Paxos Made Simple,然而还是挺难懂的。
因为 Paxos 难懂,难实现,所以斯坦福大学的教授在2014年发表了新的分布式协议 Raft。与 Paxos 相比,Raft 有着基本相同运行效率,但是更容易理解,也更容易被用在系统开发上。
针对简化版拜占庭将军问题,Raft 解决方案类比
我们还是用拜占庭将军的例子来帮助理解 Raft。
假设将军中没有叛军,信使的信息可靠但有可能被暗杀的情况下,将军们如何达成一致性决定?
Raft 的解决方案大概可以理解成 先在所有将军中选出一个大将军,所有的决定由大将军来做。选举环节:比如说现在一共有3个将军 A, B, C,每个将军都有一个随机时间的倒计时器,倒计时一结束,这个将军就会把自己当成大将军候选人,然后派信使去问其他几个将军,能不能选我为总将军?假设现在将军A倒计时结束了,他派信使传递选举投票的信息给将军B和C,如果将军B和C还没把自己当成候选人(倒计时还没有结束),并且没有把选举票投给其他,他们把票投给将军A,信使在回到将军A时,将军A知道自己收到了足够的票数,成为了大将军。在这之后,是否要进攻就由大将军决定,然后派信使去通知另外两个将军,如果在一段时间后还没有收到回复(可能信使被暗杀),那就再重派一个信使,直到收到回复。
故事先讲到这里,希望不做技术方面的朋友可以大概能理解 Raft 的原理,下面从比较技术的角度讲讲 Raft 的原理。
1. Raft 节点状态
从拜占庭将军的故事映射到分布式系统上,每个将军相当于一个分布式网络节点,每个节点有三种状态:Follower,Candidate,Leader,状态之间是互相转换的,可以参考下图,具体的后面说。
![](http://upload-images.jianshu.io/upload_images/2736397-458eb385e8ccc1c6.png?imageMogr2/auto-orient/strip|imageView2/2/w/605/format/webp)
每个节点上都有一个倒计时器 (Election Timeout),时间随机在 150ms 到 300ms 之间。有几种情况会重设 Timeout:
- 收到选举的请求
- 收到 Leader 的 Heartbeat (后面会讲到)
在 Raft 运行过程中,最主要进行两个活动:
- 选主 Leader Election
- 复制日志 Log Replication
三个状态 Follower Candidate Leader
2. 选主 Leader Election
2.1 正常情况下选主
![](http://upload-images.jianshu.io/upload_images/2736397-63072559e6b9d35f.png?imageMogr2/auto-orient/strip|imageView2/2/w/671/format/webp)
假设现在有如图5个节点,5个节点一开始的状态都是 Follower。
![](http://upload-images.jianshu.io/upload_images/2736397-c639092cc6cd0804.png?imageMogr2/auto-orient/strip|imageView2/2/w/670/format/webp)
在一个节点倒计时结束 (Timeout) 后,这个节点的状态变成 Candidate 开始选举,它给其他几个节点发送选举请求 (RequestVote)
![](http://upload-images.jianshu.io/upload_images/2736397-1ad7ee7ae8fff9cb.png?imageMogr2/auto-orient/strip|imageView2/2/w/668/format/webp)
其他四个节点都返回成功,这个节点的状态由 Candidate 变成了 Leader,并在每个一小段时间后,就给所有的 Follower 发送一个 Heartbeat 以保持所有节点的状态,Follower 收到 Leader 的 Heartbeat 后重设 Timeout。
这是最简单的选主情况,只要有超过一半的节点投支持票了,Candidate 才会被选举为 Leader,5个节点的情况下,3个节点 (包括 Candidate 本身) 投了支持就行。
2.2 Leader 出故障情况下的选主
![](http://upload-images.jianshu.io/upload_images/2736397-b7cf7a276aa5bf96.png?imageMogr2/auto-orient/strip|imageView2/2/w/680/format/webp)
一开始已经有一个 Leader,所有节点正常运行。
![](http://upload-images.jianshu.io/upload_images/2736397-25775188b6b66321.png?imageMogr2/auto-orient/strip|imageView2/2/w/680/format/webp)
Leader 出故障挂掉了,其他四个 Follower 将进行重新选主。
![](http://upload-images.jianshu.io/upload_images/2736397-b24776c5b473ce7a.png?imageMogr2/auto-orient/strip|imageView2/2/w/670/format/webp)
![](http://upload-images.jianshu.io/upload_images/2736397-b0c6f7d0350db3d2.png?imageMogr2/auto-orient/strip|imageView2/2/w/674/format/webp)
![](http://upload-images.jianshu.io/upload_images/2736397-d3e2c1b65e0cd570.png?imageMogr2/auto-orient/strip|imageView2/2/w/670/format/webp)
4个节点的选主过程和5个节点的类似,在选出一个新的 Leader 后,原来的 Leader 恢复了又重新加入了,这个时候怎么处理?在 Raft 里,第几轮选举是有记录的,重新加入的 Leader 是第一轮选举 (Term 1) 选出来的,而现在的 Leader 则是 Term 2,所有原来的 Leader 会自觉降级为 Follower
![](http://upload-images.jianshu.io/upload_images/2736397-249223e23550d8eb.png?imageMogr2/auto-orient/strip|imageView2/2/w/676/format/webp)
2.3 多个 Candidate 情况下的选主
![](http://upload-images.jianshu.io/upload_images/2736397-7e8c1550477c6f38.png?imageMogr2/auto-orient/strip|imageView2/2/w/644/format/webp)
假设一开始有4个节点,都还是 Follower。
![](http://upload-images.jianshu.io/upload_images/2736397-235369e90df6c4dd.png?imageMogr2/auto-orient/strip|imageView2/2/w/648/format/webp)
有两个 Follower 同时 Timeout,都变成了 Candidate 开始选举,分别给一个 Follower 发送了投票请求。
![](http://upload-images.jianshu.io/upload_images/2736397-8a96dd1604c08fc5.png?imageMogr2/auto-orient/strip|imageView2/2/w/660/format/webp)
两个 Follower 分别返回了ok,这时两个 Candidate 都只有2票,要3票才能被选成 Leader。
![](http://upload-images.jianshu.io/upload_images/2736397-7844d9465c816ada.png?imageMogr2/auto-orient/strip|imageView2/2/w/654/format/webp)
两个 Candidate 会分别给另外一个还没有给自己投票的 Follower 发送投票请求。
![](http://upload-images.jianshu.io/upload_images/2736397-8424138e1c39373d.png?imageMogr2/auto-orient/strip|imageView2/2/w/648/format/webp)
但是因为 Follower 在这一轮选举中,都已经投完票了,所以都拒绝了他们的请求。所以在 Term 2 没有 Leader 被选出来。
![](http://upload-images.jianshu.io/upload_images/2736397-f487f6e9c0cddb70.png?imageMogr2/auto-orient/strip|imageView2/2/w/648/format/webp)
这时,两个节点的状态是 Candidate,两个是 Follower,但是他们的倒计时器仍然在运行,最先 Timeout 的那个节点会进行发起新一轮 Term 3 的投票。
![](http://upload-images.jianshu.io/upload_images/2736397-25086b76d62d09b1.png?imageMogr2/auto-orient/strip|imageView2/2/w/650/format/webp)
两个 Follower 在 Term 3 还没投过票,所以返回 OK,这时 Candidate 一共有三票,被选为了 Leader。
![](http://upload-images.jianshu.io/upload_images/2736397-fd55d81e86324c40.png?imageMogr2/auto-orient/strip|imageView2/2/w/642/format/webp)
如果 Leader Heartbeat 的时间晚于另外一个 Candidate timeout 的时间,另外一个 Candidate 仍然会发送选举请求。
![](http://upload-images.jianshu.io/upload_images/2736397-cdd1f533e0bbb33a.png?imageMogr2/auto-orient/strip|imageView2/2/w/654/format/webp)
两个 Follower 已经投完票了,拒绝了这个 Candidate 的投票请求。
![](http://upload-images.jianshu.io/upload_images/2736397-10176f4f4d60f401.png?imageMogr2/auto-orient/strip|imageView2/2/w/650/format/webp)
Leader 进行 Heartbeat, Candidate 收到后状态自动转为 Follower,完成选主。
以上是 Raft 最重要活动之一选主的介绍,以及在不同情况下如何进行选主。
3. 复制日志 Log Replication
3.1 正常情况下复制日志
Raft 在实际应用场景中的一致性更多的是体现在不同节点之间的数据一致性,客户端发送请求到任何一个节点都能收到一致的返回,当一个节点出故障后,其他节点仍然能以已有的数据正常进行。在选主之后的复制日志就是为了达到这个目的。
![](http://upload-images.jianshu.io/upload_images/2736397-2615f4223329848d.png?imageMogr2/auto-orient/strip|imageView2/2/w/664/format/webp)
一开始,Leader 和 两个 Follower 都没有任何数据。
![](http://upload-images.jianshu.io/upload_images/2736397-33453ff94de067d1.png?imageMogr2/auto-orient/strip|imageView2/2/w/668/format/webp)
客户端发送请求给 Leader,储存数据 “sally”,Leader 先将数据写在本地日志,这时候数据还是 Uncommitted (还没最终确认,红色表示)
![](http://upload-images.jianshu.io/upload_images/2736397-1251c82292264ef0.png?imageMogr2/auto-orient/strip|imageView2/2/w/664/format/webp)
Leader 给两个 Follower 发送 AppendEntries 请求,数据在 Follower 上没有冲突,则将数据暂时写在本地日志,Follower 的数据也还是 Uncommitted。
![](http://upload-images.jianshu.io/upload_images/2736397-8e4fe60b92e5f4dd.png?imageMogr2/auto-orient/strip|imageView2/2/w/666/format/webp)
Follower 将数据写到本地后,返回 OK。Leader 收到后成功返回,只要收到的成功的返回数量超过半数 (包含Leader),Leader 将数据 “sally” 的状态改成 Committed。( 这个时候 Leader 就可以返回给客户端了)
![](http://upload-images.jianshu.io/upload_images/2736397-59b9c16018de6a8f.png?imageMogr2/auto-orient/strip|imageView2/2/w/674/format/webp)
Leader 再次给 Follower 发送 AppendEntries 请求,收到请求后,Follower 将本地日志里 Uncommitted 数据改成 Committed。这样就完成了一整个复制日志的过程,三个节点的数据是一致的,
3.2 Network Partition 情况下进行复制日志
在 Network Partition 的情况下,部分节点之间没办法互相通信,Raft 也能保证在这种情况下数据的一致性。
![](http://upload-images.jianshu.io/upload_images/2736397-2d0423a3a1466d99.png?imageMogr2/auto-orient/strip|imageView2/2/w/718/format/webp)
一开始有 5 个节点处于同一网络状态下。
![](http://upload-images.jianshu.io/upload_images/2736397-9eb9ee3f7e900ac0.png?imageMogr2/auto-orient/strip|imageView2/2/w/718/format/webp)
Network Partition 将节点分成两边,一边有两个节点,一边三个节点。
![](http://upload-images.jianshu.io/upload_images/2736397-ee03f4d0b8cebfaa.png?imageMogr2/auto-orient/strip|imageView2/2/w/700/format/webp)
两个节点这边已经有 Leader 了,来自客户端的数据 “bob” 通过 Leader 同步到 Follower。
![](http://upload-images.jianshu.io/upload_images/2736397-89370d272ddf1a5f.png?imageMogr2/auto-orient/strip|imageView2/2/w/718/format/webp)
因为只有两个节点,少于3个节点,所以 “bob” 的状态仍是 Uncommitted。所以在这里,服务器会返回错误给客户端
![](http://upload-images.jianshu.io/upload_images/2736397-3ade6c4d64aea90f.png?imageMogr2/auto-orient/strip|imageView2/2/w/726/format/webp)
另外一个 Partition 有三个节点,进行重新选主。客户端数据 “tom” 发到新的 Leader,通过和上节网络状态下相似的过程,同步到另外两个 Follower。
![](http://upload-images.jianshu.io/upload_images/2736397-c684006e9f9ecc63.png?imageMogr2/auto-orient/strip|imageView2/2/w/720/format/webp)
![](http://upload-images.jianshu.io/upload_images/2736397-ca63d0cef1b5702a.png?imageMogr2/auto-orient/strip|imageView2/2/w/696/format/webp)
![](http://upload-images.jianshu.io/upload_images/2736397-5e11229719b600ee.png?imageMogr2/auto-orient/strip|imageView2/2/w/712/format/webp)
因为这个 Partition 有3个节点,超过半数,所以数据 “tom” 都 Commit 了。
![](http://upload-images.jianshu.io/upload_images/2736397-cd0f310dbf16f12f.png?imageMogr2/auto-orient/strip|imageView2/2/w/716/format/webp)
网络状态恢复,5个节点再次处于同一个网络状态下。但是这里出现了数据冲突 “bob" 和 “tom"
![](http://upload-images.jianshu.io/upload_images/2736397-da5f3690cb880c78.png?imageMogr2/auto-orient/strip|imageView2/2/w/708/format/webp)
三个节点的 Leader 广播 AppendEntries
![](http://upload-images.jianshu.io/upload_images/2736397-938d63d89875df5a.png?imageMogr2/auto-orient/strip|imageView2/2/w/720/format/webp)
两个节点 Partition 的 Leader 自动降级为 Follower,因为这个 Partition 的数据 “bob” 没有 Commit,返回给客户端的是错误,客户端知道请求没有成功,所以 Follower 在收到 AppendEntries 请求时,可以把 “bob“ 删除,然后同步 ”tom”,通过这么一个过程,就完成了在 Network Partition 情况下的复制日志,保证了数据的一致性。
![](http://upload-images.jianshu.io/upload_images/2736397-c97a5fcc7b75a398.png?imageMogr2/auto-orient/strip|imageView2/2/w/744/format/webp)
小总结
Raft 是能够实现分布式系统强一致性的算法,每个系统节点有三种状态 Follower,Candidate,Leader。实现 Raft 算法两个最重要的事是:选主和复制日志
参考链接:
Raft 官网:https://raft.github.io/
Raft 原理动画 (推荐看看):http://thesecretlivesofdata.com/raft/
Raft 算法解析图片来源:http://www.infoq.com/cn/articles/coreos-analyse-etcd
Paxos
Paxos有2种含义,广义上来讲,是指一系列协议的统称,比如cheap Paxos <Cheap Paxos>(2004), fast paxos <Fast Paxos >(2005),Disk Paxos 和Byzantine Paxos<The ABCD’s of Paxos> (2001);狭义上来讲,是指Leslie Lamport在其论文<The Part-Time Parliament >(1989)中提出的协议。
那Paxos协议是什么那?可以看到很多文章在介绍Paxos时,都将其介绍为“是一种强一致性协议”,我觉得这么说不够准确。那么Paxos究竟是什么那?我们来看看几篇Paxos经典的论文是如何定义Paxos的。Leslie Lamport在自己另外一篇论文<Paxos Made Simple> (2001)里这么说的:The Paxos algorithm for implementing a fault-tolerant distributed system
has been regarded as difficult to understand, perhaps because the original
presentation was Greek to many readers. 我们的重点在前半句,至于要理解后半句,需要了解Paxos的产生历史,Paxos的历史是计算机历史中最有趣的历史之一,这里就不八卦了,有兴趣的同学可以自行google.
另外一篇论文<Paxos made code>这样描述Paxos:
The PAXOS algorithm for solving consensus is used to implement a fault-tolerant
Atomic Broadcast.
这篇论文中<Paxos Made Live - An Engineering Perspective>这样描述:
We used the Paxos algorithm (“Paxos”) as the base for a framework that implements a fault-tolerant
log.
从这3句描述Paxos的句子中可以发现,都共同提到了一个词:fault-tolerant。Fault-tolerant就是Paxos的核心。通常Paxos被用在数据写入多个副本的场景,Paxos可以保证在容忍少量节点(n/2)挂掉的情况下仍然可以保证数据最终被成功写入所有副本。假设我们不用Paxos,自己来实现副本写入的逻辑,我们同步写所有的副本,当所有副本都返回成功后,再通知用户这条数据写入成功,这种实现可以达到写入多个副本的目的,但是这种方式无法容忍节点挂掉。Paxos的核心就是在容忍节点挂掉的情况下,保证数据最终写入所有副本。所以说Paxos的核心是fault-tolerant,在任何一个Paxos的定义中都没有提到一致性,所以说一致性不是Paxos关注的点。(Paxos是否保证强一致性这里就不讨论了)
从上面的3句话中我们还能看出2个点,第一个点是Paxos解决什么问题:consensus。什么是consensus,有人把它翻译成一致性(也许这就是为什么Paxos被有些人误解为”是一种强一致性协议”的原因吧,错误的翻译了这个词),其实不准确, consensus应该翻译成共识。共识也就是说多个进程在分布式的条件下,针对一个值达成共识。后面会再解释consensus。
第二个点就是Paxos可以用来实现Atomic Broadcast,或者log(也可以说状态机)。后面也会再解释Atomic Broadcast和状态机。
总结一下Paxos是什么:
核心:fault-tolerant
解决的问题:consensus
应用在:Atomic Broadcast和状态机
场景:数据写入多个副本
接下来,说一下Paxos具体是什么?Leslie Lamport的论文中的Paxos协议由2个部分组成,一个是basic Paxos,一个是multi Paxos。协议中定义了4中角色:client, proposer, acceptor, learner 。这里要特别指出的是learner。了解Zookeeper的人都知道,Zookeeper所使用的Zab协议和Paxos类似(有人说Zab是Paxos的变种,个人觉得2者差别很大)。Zookeeper中有3种角色,leader,follower,observer,在Zookeeper中observer角色其实是可用可无,但是在Paxos中learner角色是必须的。个人曾经受上面说法的影响,认为learner类似oberserver的所起的功能,导致很长时间无法正确理解Paxos协议的细节。
Basic Paxos是一种consensus算法。consensus像上面所说的是用来让多个进程针对一个值达成共识的,而且这个共识一旦达成就不可更改。这里我们先不展开说明达成共识是怎样的一个过程。我们这里假设你已经理解了这个过程。这个过程可以单独再写篇文章来分析。这个达成共识过程就是Atomic Broadcast要完成功能。Zab其实就是从这个角度出发,将自己叫作原子广播协议(Zookeeper Atomic Broadcast)。
那么我们针对一个值达成共识有什么用?这就要来说multi Paxos。我们把独立的一个这样达成共识的过程成之为一个实例(instance)。那么我们反复运行这个过程,就可以形成一系列的共识,也就是一系列的实例。那么这个反复运行的过程就是multi Paxos。一系列的实例,就是一系列确定下来的值,这一系列的值可以看做一个日志流,而且是复制到所有节点上的日志流。Raft就是从这个角度出发,这样定义自己”Raft is a consensus algorithm for managing a replicated log”<In Search of an Understandable Consensus Algorithm>。(个人觉得Raft和Paxos的细节差别也很大)。接下来我们就可以基于这个日志实现状态机(state machine)。这个状态机可以用来实现可靠的存储系统,在存储系统中的一个节点写入一个值或者修改一个值,我们将这个变更写入日志,做为日志的一条记录,也就是一个Paxos的一个实例,日志被复制到其他节点,其他节点按照相同的顺序重做日志,那么就会得到和主节点完全相同的状态。从而实现了一个fault-tolerant的存储系统。所以说存储系统引入类似Paxos这样的协议是提高了系统的可靠性。
最后再总结一下Paxos是什么:
核心:fault-tolerant
解决的问题:consensus
应用在:Atomic Broadcast和状态机
场景:数据写入多个副本
————————————————
版权声明:本文为CSDN博主「cadem」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/cadem/java/article/details/77073864
作者:闭眼卖布
链接:https://www.jianshu.com/p/8e4bbe7e276c
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
版权声明:本文为CSDN博主「cadem」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/cadem/java/article/details/79932574
版权声明:本文为CSDN博主「cadem」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/cadem/java/article/details/80359270
版权声明:本文为CSDN博主「cadem」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/cadem/java/article/details/79933005