1

之前介绍了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

Figure-1

在这样的一个架构下,通过多台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上。

Figure-2

此时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的简书


qc1iu
646 声望9 粉丝

☑码农