之前介绍了viewservice,但是不能解决单点的问题。换句话说,如果viewserver crash, 那么整个系统就瘫痪了。
为了解决这个问题,一个可以想到的方法就是将单个的viewserver变为多个,而多个server保持同步。而多个server之间的同步问题,涉及到分布式系统中的一致性算法。本文介绍Paxos的概念及实现。
关于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
- PutAppend
- Get
在这样的一个架构下,通过多台server组成集群来避免单点问题。但是,如图所示的3台server必须保持同步。也就是说,如果Client向集群发送请求 Put("a", 1)
并成功,那么整个集群中任意一台server必须都含有数据("a", 1).
这里的Put("a", 1)
就是lamport论文中proposer提出的value
,这个稍后解释。对于数据库集群而言,所有的操作都是串行的,就像现在的数据库都提供日志功能一样,每个操作都会记录在日志当中,并而是有先后顺序的。如果我们的集群先后收到3个操作请求,分别为
- 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
是只读操作,因此没必要协商,也没必要将其写入状态表。