以前介绍了viewservice,可是不能解决单点的问题。换句话说,若是viewserver crash, 那么整个系统就瘫痪了。git
为了解决这个问题,一个能够想到的方法就是将单个的viewserver变为多个,而多个server保持同步。而多个server之间的同步问题,涉及到分布式系统中的一致性算法。本文介绍Paxos的概念及实现。github
关于Paxos算法,网上相关的文章不少,不过仍是建议看lamport的原文。原文经过不断增强约束,一步步的获得最后的结论,即:算法
For any v and n, if a proposal with value v and number n is issued,then there is a set S consisting of a majority of acceptors such that either (a) no acceptor in S has accepted any proposal numbered less than n, or (b) v is the value of the highest-numbered proposal among all proposals numbered less than n accepted by the acceptors in S .数据库
咱们以一个分布式的KV数据库为例,分析Paxos的应用场景。首先,假设数据库对外提供3种操做架构
Put负载均衡
PutAppendless
Get分布式
在这样的一个架构下,经过多台server组成集群来避免单点问题。可是,如图所示的3台server必须保持同步。也就是说,若是Client向集群发送请求 Put("a", 1)
并成功,那么整个集群中任意一台server必须都含有数据("a", 1).函数
这里的Put("a", 1)
就是lamport论文中proposer提出的value
,这个稍后解释。对于数据库集群而言,全部的操做都是串行的,就像如今的数据库都提供日志功能同样,每一个操做都会记录在日志当中,并而是有前后顺序的。若是咱们的集群前后收到3个操做请求,分别为spa
Put("a", 1)
Put("b", 2)
Put("c", 3)
那么在对于数据库集群而言,应该有这么一个表,相似为
+---+-------------+ |1 |Put("a", 1) | +---+-------------+ |2 |Put("b", 2) | +---+-------------+ |3 |Put("c", 3) | +---+-------------+
咱们暂时称它为状态表
。
这里的标号1,2,3就对应论文中的Sequence。
下面咱们就假设数据库集群是空的,即没有任何数据。客户端发送了上面3个请求,固然,这中间可能存在多个客户端同时发送请求的状况。咱们看看整个流程是怎么样的。
首先,假设Client1向集群发送了Put("a", 1)
, 这个请求虽然是发给集群的,但实际上最后确定会落实到某一个具体的server,这个过程可能经过负载均衡等方法获得,并非咱们关心的。咱们就假设这个请求发到了server1。这时server1是空的,因此他查询本身的状态表
,发现Seq1没有对应的操做,按理说他能够将今天的第一个操做肯定为Put("a", 1)
,但因为它是集群中的一份子,必需要协商一下。这个协商的过程,就是Paxos。他须要协商的问题是:
第1个操做是否能够为
Put("a", 1)
这里的协商过程咱们能够理解为一个函数, 函数的参数为操做的序号和操做,返回值为一个操做。
Op doPaxos(int seq, Op v){...}
这里因为只有一个Client提出的操做的请求,不存在竞争,因此协商很顺利,当server1调用这个协商函数的时候,返回的值就是它传入的值,此时咱们就能够认为集群中的3台server达成了一个共识,即集群的第一个操做为Put("a", 1)
。因而server1将本身的状态表改
+---+-------------+ |1 |Put("a", 1) | +---+-------------+
须要注意的是,在协商的过程当中,server2与server3也将本身的状态表改成上面的样子。这个状态表同步的过程就是论文中的Learning
阶段。
可见,Paxos做为一致性算法,在这里例子当中保证的一致性其实就是状态表的一致性。
有没有协商不顺利的状况呢? 固然, 有!
这里咱们假设有出现了两个客户端,Client2和Client3。接着用上面的例子,Client2向集群发送了请求Put("b", 2)
,假设这个请求最终到了server1上。同时, Client3向集群发送了请求Put("c", 3)
, 假设这个求情发到了server2上。
此时server1和server2的状态表是同样的,这一点是由Paxos保证的。他们的不一样之处在于:
server1的数据和server2不一样,即server1里面包含一条数据,就是刚刚Client1请求的Put("a", 1)
, 可是server2是空的。
server1的sequence为2, 可是server2的sequence仍然为1。缘由是server1已经作了一次Put操做,可是server2什么都没有作。
因为server1和server2的不一样,所以他们处理各自请求的方式也不一样。
先来看server2。server2从1开始遍历本身的状态表,发现1号操做非空,说明这是他遗漏的操做,因而server2也执行了操做Put("a", 1)
, 此时server2与server1的状态彻底同样了。在此以后,server2会向上面server1同样,调用doPaxos函数进行协商,即询问集群中的全部server我们的第2个操做可否为Put("c", 3)
doPaxos(2, v);
其中v为 Put("c", 3)
。
但就在此时,server1也调用了doPaxos进行协商,询问集群中的全部server第2个操做可否为Put("b", 2)
。
这里集群中两个server提出了不一样的议题(lamport的论文中也使用了这种说法)
,Paxos保证了最终会选出一个你们都赞成的议题。咱们这里假设server1的议题最终被经过了。因而,集群中全部server的状态表都更新为
+---+-------------+ |1 |Put("a", 1) | +---+-------------+ |2 |Put("b", 2) | +---+-------------+
而且server1向本身的database中插入("b", 2),并将本身的sequence更新为3.
而此时server2发现本身的状态表中sequence2有了对应的操做,但很惋惜不是本身提出的那个操做。这条信心代表集群其实已经作了2个操做了,本身想要提出的这个操做Put("c", 3)
已经不能做为集群的第2个操做了。因而server2对本身的database进行Put("b", 2)
操做, 而后将本身的sequence更新为3。再次调用doPaxos进行协商,试图将本身的操做做为集群的第3个操做,因而doPaxos的参数为
doPaxos(3, {Put("c", 3)})
此时因为只有一个提议,因此协商很顺利。此时集群中全部server的状态表都为
+---+-------------+ |1 |Put("a", 1) | +---+-------------+ |2 |Put("b", 2) | +---+-------------+ |3 |Put("c", 3) | +---+-------------+
其中,server1包含2条数据,server2包含3条数据,server3为空。
咱们假设此时Client1向集群发送了Get("a")
请求,并最终发到了server3。因为server3的sequence为, 因此他会从1开始,将本身状态表中的1,2,3号操做就执行一遍,直到到了sequence4时,发现状态表为空,因而进行查询操做,将结果返回Client。
因为Get
是只读操做,所以不必协商,也不必将其写入状态表。
示例代码:https://github.com/qc1iu/haze-kv/tree/master/src/kvpaxos
原文来自 qc1iu的简书