You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

127 lines
12 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 05 | Paxos算法如何在多个节点间确定某变量的值
你好,我是韩健。
提到分布式算法就不得不提Paxos算法在过去几十年里它基本上是分布式共识的代名词因为当前最常用的一批共识算法都是基于它改进的。比如Fast Paxos算法、Cheap Paxos算法、Raft算法等等。而很多同学都会在准确和系统理解Paxos算法上踩坑比如只知道它可以用来达成共识但不知道它是如何达成共识的。
这其实侧面说明了Paxos算法有一定的难度可分布式算法本身就很复杂Paxos算法自然也不会例外当然了除了这一点还跟兰伯特有关。
兰伯特提出的Paxos算法包含2个部分
* 一个是Basic Paxos算法描述的是多节点之间如何就某个值提案Value达成共识
* 另一个是Multi-Paxos思想描述的是执行多个Basic Paxos实例就一系列值达成共识。
可因为兰伯特提到的Multi-Paxos思想缺少代码实现的必要细节比如怎么选举领导者所以在理解上比较难。
为了让你理解Paxos算法接下来我会用2节课的时间分别以Basic Paxos和Multi-Paxos为核心带你了解Basic Paxos如何达成共识以及针对Basic Paxos的局限性Multi-Paxos又是如何改进的。今天咱们先来聊聊Basic Paxos。
在我看来Basic Paxos是Multi-Paxos思想的核心说白了Multi-Paxos就是多执行几次Basic Paxos。所以掌握它之后你能更好地理解后几讲基于Multi-Paxos思想的共识算法比如Raft算法还能掌握分布式共识算法的最核心内容当现在的算法不能满足业务需求进行权衡折中设计自己的算法。
**来看一道思考题。**
假设我们要实现一个分布式集群这个集群是由节点A、B、C组成提供只读KV存储服务。你应该知道创建只读变量的时候必须要对它进行赋值而且这个值后续没办法修改。因此一个节点创建只读变量后就不能再修改它了所以所有节点必须要先对只读变量的值达成共识然后所有节点再一起创建这个只读变量。
那么当有多个客户端比如客户端1、2访问这个系统试图创建同一个只读变量比如X客户端1试图创建值为3的X客户端2试图创建值为7的X这样要如何达成共识实现各节点上X值的一致呢带着这个问题我们进入今天的学习。
![](https://static001.geekbang.org/resource/image/93/67/93a9fa0a75c23971066dc791389b8567.jpg "图1")
在一些经典的算法中你会看到一些既形象又独有的概念比如二阶段提交协议中的协调者Basic Paxos算法也不例外。为了帮助人们更好地理解Basic Paxos算法兰伯特在讲解时也使用了一些独有而且比较重要的概念提案、准备Prepare请求、接受Accept请求、角色等等其中最重要的就是“角色”。因为角色是对Basic Paxos中最核心的三个功能的抽象比如由接受者Acceptor对提议的值进行投票并存储接受的值。
## 你需要了解的三种角色
在Basic Paxos中有提议者Proposer、接受者Acceptor、学习者Learner三种角色他们之间的关系如下
![](https://static001.geekbang.org/resource/image/77/42/77be9903f7cbe980e5a6e77412d2ad42.jpg "图2")
看着是不是有些复杂,其实并不难理解:
* **提议者Proposer**提议一个值用于投票表决。为了方便演示你可以把图1中的客户端1和2看作是提议者。但在绝大多数场景中集群中收到客户端请求的节点才是提议者图1 这个架构,是为了方便演示算法原理)。这样做的好处是,对业务代码没有入侵性,也就是说,我们不需要在业务代码中实现算法逻辑,就可以像使用数据库一样访问后端的数据。
* **接受者Acceptor**对每个提议的值进行投票并存储接受的值比如A、B、C三个节点。 一般来说,集群中的所有节点都在扮演接受者的角色,参与共识协商,并接受和存储数据。
讲到这儿你可能会有疑惑前面不是说接收客户端请求的节点是提议者吗这里怎么又是接受者呢这是因为一个节点或进程可以身兼多个角色。想象一下一个3节点的集群1个节点收到了请求那么该节点将作为提议者发起二阶段提交然后这个节点和另外2个节点一起作为接受者进行共识协商就像下图的样子
![](https://static001.geekbang.org/resource/image/3f/fe/3fed0fe5682f97f0a9249cf9519d09fe.jpg "图3")
* **学习者Learner**被告知投票的结果接受达成共识的值存储保存不参与投票的过程。一般来说学习者是数据备份节点比如“Master-Slave”模型中的Slave被动地接受数据容灾备份。
其实,这三种角色,在本质上代表的是三种功能:
* 提议者代表的是接入和协调功能,收到客户端请求后,发起二阶段提交,进行共识协商;
* 接受者代表投票协商和存储数据,对提议的值进行投票,并接受达成共识的值,存储保存;
* 学习者代表存储数据,不参与共识协商,只接受达成共识的值,存储保存。
因为一个完整的算法过程是由这三种角色对应的功能组成的所以理解这三种角色是你理解Basic Paxos如何就提议的值达成共识的基础。那么接下来咱们看看如何使用Basic Paxos达成共识解决开篇提到的那道思考题。
## 如何达成共识?
想象这样一个场景,现在疫情这么严重,每个村的路都封得差不多了,就你的村委会不作为,迟迟没有什么防疫的措施。你决定给村委会提交个提案,提一些防疫的建议,除了建议之外,为了和其他村民的提案做区分,你的提案还得包含一个提案编号,来起到唯一标识的作用。
与你的做法类似在Basic Paxos中兰伯特也使用提案代表一个提议。不过在提案中除了提案编号还包含了提议值。为了方便演示我使用\[n, v\]表示一个提案其中n为提案编号v 为提议值。
我想强调一下整个共识协商是分2个阶段进行的也就是我在03讲提到的二阶段提交。那么具体要如何协商呢
我们假设客户端1的提案编号为1客户端2的提案编号为5并假设节点A、B先收到来自客户端1的准备请求节点C先收到来自客户端2的准备请求。
### 准备Prepare阶段
先来看第一个阶段首先客户端1、2作为提议者分别向所有接受者发送包含提案编号的准备请求
![](https://static001.geekbang.org/resource/image/64/54/640219532d0fcdffc08dbd1b3b3f0454.jpg "图4")
**你要注意,在准备请求中是不需要指定提议的值的,只需要携带提案编号就可以了,这是很多同学容易产生误解的地方。**
接着当节点A、B收到提案编号为1的准备请求节点C收到提案编号为5的准备请求后将进行这样的处理
![](https://static001.geekbang.org/resource/image/5b/7a/5b6fcc5af76ad53e62c433e2589b6d7a.jpg "图5")
* 由于之前没有通过任何提案所以节点A、B将返回一个 “尚无提案”的响应。也就是说节点A和B在告诉提议者我之前没有通过任何提案呢并承诺以后不再响应提案编号小于等于1的准备请求不会通过编号小于1的提案。
* 节点C也是如此它将返回一个 “尚无提案”的响应并承诺以后不再响应提案编号小于等于5的准备请求不会通过编号小于5的提案。
另外当节点A、B收到提案编号为5的准备请求和节点C收到提案编号为1的准备请求的时候将进行这样的处理过程
![](https://static001.geekbang.org/resource/image/ec/24/ecf9a5872201e875a2e0417c32ec2d24.jpg "图6")
* 当节点A、B收到提案编号为5的准备请求的时候因为提案编号5大于它们之前响应的准备请求的提案编号1而且两个节点都没有通过任何提案所以它将返回一个 “尚无提案”的响应并承诺以后不再响应提案编号小于等于5的准备请求不会通过编号小于5的提案。
* 当节点C收到提案编号为1的准备请求的时候由于提案编号1小于它之前响应的准备请求的提案编号5所以丢弃该准备请求不做响应。
### 接受Accept阶段
第二个阶段也就是接受阶段首先客户端1、2在收到大多数节点的准备响应之后会分别发送接受请求
![](https://static001.geekbang.org/resource/image/70/89/70de602cb4b52de7545f05c5485deb89.jpg "图7")
* 当客户端1收到大多数的接受者节点A、B的准备响应后根据响应中提案编号最大的提案的值设置接受请求中的值。因为该值在来自节点A、B的准备响应中都为空也就是图5中的“尚无提案”所以就把自己的提议值3作为提案的值发送接受请求\[1, 3\]。
* 当客户端2收到大多数的接受者的准备响应后节点A、B和节点C根据响应中提案编号最大的提案的值来设置接受请求中的值。因为该值在来自节点A、B、C的准备响应中都为空也就是图5和图6中的“尚无提案”所以就把自己的提议值7作为提案的值发送接受请求\[5, 7\]。
当三个节点收到2个客户端的接受请求时会进行这样的处理
![](https://static001.geekbang.org/resource/image/f8/45/f836c40636d26826fc04a51a5945d545.jpg "图8")
* 当节点A、B、C收到接受请求\[1, 3\]的时候由于提案的提案编号1小于三个节点承诺能通过的提案的最小提案编号5所以提案\[1, 3\]将被拒绝。
* 当节点A、B、C收到接受请求\[5, 7\]的时候由于提案的提案编号5不小于三个节点承诺能通过的提案的最小提案编号5所以就通过提案\[5, 7\]也就是接受了值7三个节点就X值为7达成了共识。
讲到这儿我想补充一下,如果集群中有学习者,当接受者通过了一个提案时,就通知给所有的学习者。当学习者发现大多数的接受者都通过了某个提案,那么它也通过该提案,接受该提案的值。
通过上面的演示过程你可以看到最终各节点就X的值达成了共识。那么在这里我还想强调一下Basic Paxos的容错能力源自“大多数”的约定你可以这么理解当少于一半的节点出现故障的时候共识协商仍然在正常工作。
## 内容小结
本节课我主要带你了解了Basic Paxos的原理和一些特点我希望你明确这样几个重点。
1. 你可以看到Basic Paxos是通过二阶段提交的方式来达成共识的。二阶段提交是达成共识的常用方式如果你需要设计新的共识算法的时候也可以考虑这个方式。
2. 除了共识Basic Paxos还实现了容错在少于一半的节点出现故障时集群也能工作。它不像分布式事务算法那样必须要所有节点都同意后才提交操作因为“所有节点都同意”这个原则在出现节点故障的时候会导致整个集群不可用。也就是说“大多数节点都同意”的原则赋予了Basic Paxos容错的能力让它能够容忍少于一半的节点的故障。
3. 本质上而言,提案编号的大小代表着优先级,你可以这么理解,根据提案编号的大小,接受者保证**三个承诺**,具体来说:如果准备请求的提案编号,**小于等于**接受者已经响应的准备请求的提案编号,那么接受者将承诺不响应这个准备请求;如果接受请求中的提案的提案编号,**小于**接受者已经响应的准备请求的提案编号,那么接受者将承诺不通过这个提案;如果接受者之前有通过提案,那么接受者将承诺,会在准备请求的响应中,包含**已经通过的最大编号的提案信息**。
## 课堂思考
在示例中如果节点A、B已经通过了提案\[5, 7\]节点C未通过任何提案那么当客户端3提案编号为9时通过Basic Paxos执行“SET X = 6”最终三个节点上X值是多少呢为什么呢欢迎在留言区分享你的看法与我一同讨论。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。