mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-17 14:43:42 +08:00
mod
This commit is contained in:
133
极客时间专栏/分布式协议与算法实战/协议和算法篇/05 | Paxos算法(一):如何在多个节点间确定某变量的值?.md
Normal file
133
极客时间专栏/分布式协议与算法实战/协议和算法篇/05 | Paxos算法(一):如何在多个节点间确定某变量的值?.md
Normal file
@@ -0,0 +1,133 @@
|
||||
<audio id="audio" title="05 | Paxos算法(一):如何在多个节点间确定某变量的值?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e0/87/e07ac4e74d80a5aa041ee12fd35fdf87.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
提到分布式算法,就不得不提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值的一致呢?带着这个问题,我们进入今天的学习。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/93/67/93a9fa0a75c23971066dc791389b8567.jpg" alt="" title="图1">
|
||||
|
||||
在一些经典的算法中,你会看到一些既形象又独有的概念(比如二阶段提交协议中的协调者),Basic Paxos算法也不例外。为了帮助人们更好地理解Basic Paxos算法,兰伯特在讲解时,也使用了一些独有而且比较重要的概念,提案、准备(Prepare)请求、接受(Accept)请求、角色等等,其中最重要的就是“角色”。因为角色是对Basic Paxos中最核心的三个功能的抽象,比如,由接受者(Acceptor)对提议的值进行投票,并存储接受的值。
|
||||
|
||||
## 你需要了解的三种角色
|
||||
|
||||
在Basic Paxos中,有提议者(Proposer)、接受者(Acceptor)、学习者(Learner)三种角色,他们之间的关系如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/77/42/77be9903f7cbe980e5a6e77412d2ad42.jpg" alt="" title="图2">
|
||||
|
||||
看着是不是有些复杂,其实并不难理解:
|
||||
|
||||
<li>
|
||||
**提议者(Proposer)**:提议一个值,用于投票表决。为了方便演示,你可以把图1中的客户端1和2看作是提议者。但在绝大多数场景中,集群中收到客户端请求的节点,才是提议者(图1 这个架构,是为了方便演示算法原理)。这样做的好处是,对业务代码没有入侵性,也就是说,我们不需要在业务代码中实现算法逻辑,就可以像使用数据库一样访问后端的数据。
|
||||
</li>
|
||||
<li>
|
||||
**接受者(Acceptor)**:对每个提议的值进行投票,并存储接受的值,比如A、B、C三个节点。 一般来说,集群中的所有节点都在扮演接受者的角色,参与共识协商,并接受和存储数据。
|
||||
</li>
|
||||
|
||||
讲到这儿,你可能会有疑惑:前面不是说接收客户端请求的节点是提议者吗?这里怎么又是接受者呢?这是因为一个节点(或进程)可以身兼多个角色。想象一下,一个3节点的集群,1个节点收到了请求,那么该节点将作为提议者发起二阶段提交,然后这个节点和另外2个节点一起作为接受者进行共识协商,就像下图的样子:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3f/fe/3fed0fe5682f97f0a9249cf9519d09fe.jpg" alt="" title="图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作为提议者,分别向所有接受者发送包含提案编号的准备请求:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/64/54/640219532d0fcdffc08dbd1b3b3f0454.jpg" alt="" title="图4">
|
||||
|
||||
**你要注意,在准备请求中是不需要指定提议的值的,只需要携带提案编号就可以了,这是很多同学容易产生误解的地方。**
|
||||
|
||||
接着,当节点A、B收到提案编号为1的准备请求,节点C收到提案编号为5的准备请求后,将进行这样的处理:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5b/7a/5b6fcc5af76ad53e62c433e2589b6d7a.jpg" alt="" title="图5">
|
||||
|
||||
- 由于之前没有通过任何提案,所以节点A、B将返回一个 “尚无提案”的响应。也就是说节点A和B在告诉提议者,我之前没有通过任何提案呢,并承诺以后不再响应提案编号小于等于1的准备请求,不会通过编号小于1的提案。
|
||||
- 节点C也是如此,它将返回一个 “尚无提案”的响应,并承诺以后不再响应提案编号小于等于5的准备请求,不会通过编号小于5的提案。
|
||||
|
||||
另外,当节点A、B收到提案编号为5的准备请求,和节点C收到提案编号为1的准备请求的时候,将进行这样的处理过程:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ec/24/ecf9a5872201e875a2e0417c32ec2d24.jpg" alt="" title="图6">
|
||||
|
||||
- 当节点A、B收到提案编号为5的准备请求的时候,因为提案编号5大于它们之前响应的准备请求的提案编号1,而且两个节点都没有通过任何提案,所以它将返回一个 “尚无提案”的响应,并承诺以后不再响应提案编号小于等于5的准备请求,不会通过编号小于5的提案。
|
||||
- 当节点C收到提案编号为1的准备请求的时候,由于提案编号1小于它之前响应的准备请求的提案编号5,所以丢弃该准备请求,不做响应。
|
||||
|
||||
### 接受(Accept)阶段
|
||||
|
||||
第二个阶段也就是接受阶段,首先客户端1、2在收到大多数节点的准备响应之后,会分别发送接受请求:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/70/89/70de602cb4b52de7545f05c5485deb89.jpg" alt="" title="图7">
|
||||
|
||||
<li>
|
||||
当客户端1收到大多数的接受者(节点A、B)的准备响应后,根据响应中提案编号最大的提案的值,设置接受请求中的值。因为该值在来自节点A、B的准备响应中都为空(也就是图5中的“尚无提案”),所以就把自己的提议值3作为提案的值,发送接受请求[1, 3]。
|
||||
</li>
|
||||
<li>
|
||||
当客户端2收到大多数的接受者的准备响应后(节点A、B和节点C),根据响应中提案编号最大的提案的值,来设置接受请求中的值。因为该值在来自节点A、B、C的准备响应中都为空(也就是图5和图6中的“尚无提案”),所以就把自己的提议值7作为提案的值,发送接受请求[5, 7]。
|
||||
</li>
|
||||
|
||||
当三个节点收到2个客户端的接受请求时,会进行这样的处理:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f8/45/f836c40636d26826fc04a51a5945d545.jpg" alt="" title="图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的原理和一些特点,我希望你明确这样几个重点。
|
||||
|
||||
<li>
|
||||
你可以看到,Basic Paxos是通过二阶段提交的方式来达成共识的。二阶段提交是达成共识的常用方式,如果你需要设计新的共识算法的时候,也可以考虑这个方式。
|
||||
</li>
|
||||
<li>
|
||||
除了共识,Basic Paxos还实现了容错,在少于一半的节点出现故障时,集群也能工作。它不像分布式事务算法那样,必须要所有节点都同意后才提交操作,因为“所有节点都同意”这个原则,在出现节点故障的时候会导致整个集群不可用。也就是说,“大多数节点都同意”的原则,赋予了Basic Paxos容错的能力,让它能够容忍少于一半的节点的故障。
|
||||
</li>
|
||||
<li>
|
||||
本质上而言,提案编号的大小代表着优先级,你可以这么理解,根据提案编号的大小,接受者保证**三个承诺**,具体来说:如果准备请求的提案编号,**小于等于**接受者已经响应的准备请求的提案编号,那么接受者将承诺不响应这个准备请求;如果接受请求中的提案的提案编号,**小于**接受者已经响应的准备请求的提案编号,那么接受者将承诺不通过这个提案;如果接受者之前有通过提案,那么接受者将承诺,会在准备请求的响应中,包含**已经通过的最大编号的提案信息**。
|
||||
</li>
|
||||
|
||||
## 课堂思考
|
||||
|
||||
在示例中,如果节点A、B已经通过了提案[5, 7],节点C未通过任何提案,那么当客户端3提案编号为9时,通过Basic Paxos执行“SET X = 6”,最终三个节点上X值是多少呢?为什么呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
@@ -0,0 +1,101 @@
|
||||
<audio id="audio" title="06 | Paxos算法(二):Multi-Paxos不是一个算法,而是统称" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3c/ad/3c7ac8d8ed55027165e5a266a01c8ead.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
经过上节课的学习,你应该知道,Basic Paxos只能就单个值(Value)达成共识,一旦遇到为一系列的值实现共识的时候,它就不管用了。虽然兰伯特提到可以通过多次执行Basic Paxos实例(比如每接收到一个值时,就执行一次Basic Paxos算法)实现一系列值的共识。但是,很多同学读完论文后,应该还是两眼摸黑,虽然每个英文单词都能读懂,但还是不理解兰伯特提到的Multi-Paxos,为什么Multi-Paxos这么难理解呢?
|
||||
|
||||
在我看来,兰伯特并没有把Multi-Paxos讲清楚,只是介绍了大概的思想,缺少算法过程的细节和编程所必须的细节(比如缺少选举领导者的细节)。这也就导致每个人实现的Multi-Paxos都不一样。不过从本质上看,大家都是在兰伯特提到的Multi-Paxos思想上补充细节,设计自己的Multi-Paxos算法,然后实现它(比如Chubby的Multi-Paxos实现、Raft算法等)。
|
||||
|
||||
所以在这里,我补充一下:**兰伯特提到的Multi-Paxos是一种思想,不是算法。而Multi-Paxos算法是一个统称,它是指基于Multi-Paxos思想,通过多个Basic Paxos实例实现一系列值的共识的算法(比如Chubby的Multi-Paxos实现、Raft算法等)。** 这一点尤其需要你注意。
|
||||
|
||||
为了帮你掌握Multi-Paxos思想,我会先带你了解,对于Multi-Paxos兰伯特是如何思考的,也就是说,如何解决Basic Paxos的痛点问题;然后我再以Chubby的Multi-Paxos实现为例,具体讲解一下。为啥选它呢?因为Chubby的Multi-Paxos实现,代表了Multi-Paxos思想在生产环境中的真正落地,它将一种思想变成了代码实现。
|
||||
|
||||
## 兰伯特关于Multi-Paxos的思考
|
||||
|
||||
熟悉Basic Paxos的同学(可以回顾一下[05讲](https://time.geekbang.org/column/article/201700?utm_source=geektimeweb&utm_medium=pc&utm_term=pc_interstitial_143))可能还记得,Basic Paxos是通过二阶段提交来达成共识的。在第一阶段,也就是准备阶段,接收到大多数准备响应的提议者,才能发起接受请求进入第二阶段(也就是接受阶段):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/aa/e0/aafabff1fe2a26523e9815805ccca6e0.jpg" alt="">
|
||||
|
||||
而如果我们直接通过多次执行Basic Paxos实例,来实现一系列值的共识,就会存在这样几个问题:
|
||||
|
||||
<li>
|
||||
如果多个提议者同时提交提案,可能出现因为提案编号冲突,在准备阶段没有提议者接收到大多数准备响应,协商失败,需要重新协商。你想象一下,一个5节点的集群,如果3个节点作为提议者同时提案,就可能发生因为没有提议者接收大多数响应(比如1个提议者接收到1个准备响应,另外2个提议者分别接收到2个准备响应)而准备失败,需要重新协商。
|
||||
</li>
|
||||
<li>
|
||||
2轮RPC通讯(准备阶段和接受阶段)往返消息多、耗性能、延迟大。你要知道,分布式系统的运行是建立在RPC通讯的基础之上的,因此,延迟一直是分布式系统的痛点,是需要我们在开发分布式系统时认真考虑和优化的。
|
||||
</li>
|
||||
|
||||
那么如何解决上面的2个问题呢?可以通过引入领导者和优化Basic Paxos执行来解决,咱们首先聊一聊领导者。
|
||||
|
||||
### 领导者(Leader)
|
||||
|
||||
我们可以通过引入领导者节点,也就是说,领导者节点作为唯一提议者,这样就不存在多个提议者同时提交提案的情况,也就不存在提案冲突的情况了:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/af/f6/af3d6a291d960ace59a88898abb74ef6.jpg" alt="">
|
||||
|
||||
在这里,我补充一点:**在论文中,兰伯特没有说如何选举领导者,需要我们在实现Multi-Paxos算法的时候自己实现。** 比如在Chubby中,主节点(也就是领导者节点)是通过执行Basic Paxos算法,进行投票选举产生的。
|
||||
|
||||
那么,如何解决第二个问题,也就是如何优化Basic Paxos执行呢?
|
||||
|
||||
### 优化Basic Paxos执行
|
||||
|
||||
我们可以采用“当领导者处于稳定状态时,省掉准备阶段,直接进入接受阶段”这个优化机制,优化Basic Paxos执行。也就是说,领导者节点上,序列中的命令是最新的,不再需要通过准备请求来发现之前被大多数节点通过的提案,领导者可以独立指定提案中的值。这时,领导者在提交命令时,可以省掉准备阶段,直接进入到接受阶段:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3c/54/3cd72a4a138fe1cde52aedd1b897f954.jpg" alt="">
|
||||
|
||||
你看,和重复执行Basic Paxos相比,Multi-Paxos引入领导者节点之后,因为只有领导者节点一个提议者,只有它说了算,所以就不存在提案冲突。另外,当主节点处于稳定状态时,就省掉准备阶段,直接进入接受阶段,所以在很大程度上减少了往返的消息数,提升了性能,降低了延迟。
|
||||
|
||||
讲到这儿,你可能会问了:在实际系统中,该如何实现Multi-Paxos呢?接下来,我以Chubby的Multi-Paxos实现为例,具体讲解一下。
|
||||
|
||||
## Chubby的Multi-Paxos实现
|
||||
|
||||
既然兰伯特只是大概的介绍了Multi-Paxos思想,那么Chubby是如何补充细节,实现Multi-Paxos算法的呢?
|
||||
|
||||
首先,它通过引入主节点,实现了兰伯特提到的领导者(Leader)节点的特性。也就是说,主节点作为唯一提议者,这样就不存在多个提议者同时提交提案的情况,也就不存在提案冲突的情况了。
|
||||
|
||||
另外,在Chubby中,主节点是通过执行Basic Paxos算法,进行投票选举产生的,并且在运行过程中,主节点会通过不断续租的方式来延长租期(Lease)。比如在实际场景中,几天内都是同一个节点作为主节点。如果主节点故障了,那么其他的节点又会投票选举出新的主节点,也就是说主节点是一直存在的,而且是唯一的。
|
||||
|
||||
其次,在Chubby中实现了兰伯特提到的,“当领导者处于稳定状态时,省掉准备阶段,直接进入接受阶段”这个优化机制。
|
||||
|
||||
最后,在Chubby中,实现了成员变更(Group membership),以此保证节点变更的时候集群的平稳运行。
|
||||
|
||||
最后,我想补充一点:**在Chubby中,为了实现了强一致性,读操作也只能在主节点上执行。** 也就是说,只要数据写入成功,之后所有的客户端读到的数据都是一致的。具体的过程,就是下面的样子。
|
||||
|
||||
- 所有的读请求和写请求都由主节点来处理。当主节点从客户端接收到写请求后,作为提议者,执行Basic Paxos实例,将数据发送给所有的节点,并且在大多数的服务器接受了这个写请求之后,再响应给客户端成功:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7e/b9/7e2c2e194d5a0fda5594c5e4e2d9ecb9.jpg" alt="">
|
||||
|
||||
- 当主节点接收到读请求后,处理就比较简单了,主节点只需要查询本地数据,然后返回给客户端就可以了:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/07/64/07501bb8d9015af3fb34cf856fe3ec64.jpg" alt="">
|
||||
|
||||
Chubby的Multi-Paxos实现,尽管是一个闭源的实现,但这是Multi-Paxos思想在实际场景中的真正落地,Chubby团队不仅编程实现了理论,还探索了如何补充细节。其中的思考和设计非常具有参考价值,不仅能帮助我们理解Multi-Paxos思想,还能帮助我们理解其他的Multi-Paxos算法(比如Raft算法)。
|
||||
|
||||
## 内容小结
|
||||
|
||||
本节课我主要带你了解了Basic Paxos的局限,以及Chubby的Multi-Paxos实现。我希望你明确的重点如下:
|
||||
|
||||
<li>
|
||||
兰伯特提到的Multi-Paxos是一种思想,不是算法,而且还缺少算法过程的细节和编程所必须的细节,比如如何选举领导者等,这也就导致了每个人实现的Multi-Paxos都不一样。而Multi-Paxos算法是一个统称,它是指基于Multi-Paxos思想,通过多个Basic Paxos实例实现一系列数据的共识的算法(比如Chubby的Multi-Paxos实现、Raft算法等)。
|
||||
</li>
|
||||
<li>
|
||||
Chubby实现了主节点(也就是兰伯特提到的领导者),也实现了兰伯特提到的 **“当领导者处于稳定状态时,省掉准备阶段,直接进入接受阶段”** 这个优化机制,省掉Basic Paxos的准备阶段,提升了数据的提交效率,但是所有写请求都在主节点处理,限制了集群处理写请求的并发能力,约等于单机。
|
||||
</li>
|
||||
<li>
|
||||
因为在Chubby的Multi-Paxos实现中,也约定了“大多数原则”,也就是说,只要大多数节点正常运行时,集群就能正常工作,所以Chubby能容错(n - 1)/2个节点的故障。
|
||||
</li>
|
||||
<li>
|
||||
本质上而言,“当领导者处于稳定状态时,省掉准备阶段,直接进入接受阶段”这个优化机制,是通过减少非必须的协商步骤来提升性能的。这种方法非常常用,也很有效。比如,Google设计的QUIC协议,是通过减少TCP、TLS的协商步骤,优化HTTPS性能。**我希望你能掌握这种性能优化思路,后续在需要时,可以通过减少非必须的步骤,优化系统性能。**
|
||||
</li>
|
||||
|
||||
最后,我想说的是,我个人比较喜欢Paxos算法(兰伯特的Basic Paxos和Multi-Paxos),虽然Multi-Paxos缺失算法细节,但这反而给我们提供了思考空间,让我们可以反复思考和考据缺失的细节,比如在Multi-Paxos中到底需不需要选举领导者,再比如如何实现提案编号等等。
|
||||
|
||||
但我想强调,Basic Paxos是经过证明的,而Multi-Paxos是一种思想,缺失实现算法的必须编程细节,这就导致,Multi-Paxos的最终算法实现,是建立在一个未经证明的基础之上的,正确性是个问号。
|
||||
|
||||
**与此同时,实现Multi-Paxos算法,最大的挑战是如何证明它是正确的。** 比如Chubby的作者做了大量的测试,和运行一致性检测脚本,验证和观察系统的健壮性。在实际使用时,我不推荐你设计和实现新的Multi-Paxos算法,而是建议优先考虑Raft算法,因为Raft的正确性是经过证明的。当Raft算法不能满足需求时,你再考虑实现和优化Multi-Paxos算法。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
既然,我提了Chubby只能在主节点上执行读操作,那么在最后,我给你留了一个思考题,这个设计有什么局限呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
174
极客时间专栏/分布式协议与算法实战/协议和算法篇/07 | Raft算法(一):如何选举领导者?.md
Normal file
174
极客时间专栏/分布式协议与算法实战/协议和算法篇/07 | Raft算法(一):如何选举领导者?.md
Normal file
@@ -0,0 +1,174 @@
|
||||
<audio id="audio" title="07 | Raft算法(一):如何选举领导者?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f1/2f/f1de532312e957e347343ff836a1ff2f.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
通过前两节课,我带你打卡了Paxos算法,今天我想和你聊聊最常用的共识算法,Raft算法。
|
||||
|
||||
Raft算法属于Multi-Paxos算法,它是在兰伯特Multi-Paxos思想的基础上,做了一些简化和限制,比如增加了日志必须是连续的,只支持领导者、跟随者和候选人三种状态,在理解和算法实现上都相对容易许多。
|
||||
|
||||
**除此之外,Raft算法是现在分布式系统开发首选的共识算法。**绝大多数选用Paxos算法的系统(比如Cubby、Spanner)都是在Raft算法发布前开发的,当时没得选;而全新的系统大多选择了Raft算法(比如Etcd、Consul、CockroachDB)。
|
||||
|
||||
对你来说,掌握这个算法,可以得心应手地处理绝大部分场景的容错和一致性需求,比如分布式配置系统、分布式NoSQL存储等等,轻松突破系统的单机限制。
|
||||
|
||||
**如果要用一句话概括Raft算法,我觉得是这样的:从本质上说,Raft算法是通过一切以领导者为准的方式,实现一系列值的共识和各节点日志的一致。**这句话比较抽象,我来做个比喻,领导者就是Raft算法中的霸道总裁,通过霸道的“一切以我为准”的方式,决定了日志中命令的值,也实现了各节点日志的一致。
|
||||
|
||||
我会用三讲的时间,分别以领导者选举、日志复制、成员变更为核心,讲解Raft算法的原理,在实战篇中,会带你进一步剖析Raft算法的实现,介绍基于Raft算法的分布式系统开发实战。那么我希望从原理到实战,在帮助你掌握分布式系统架构设计技巧和开发实战能力的同时,加深你对Raft算法的理解。
|
||||
|
||||
在课程开始之前,我们先来看一道思考题。
|
||||
|
||||
假设我们有一个由节点A、B、C组成的Raft集群(如图所示),因为Raft算法一切以领导者为准,所以如果集群中出现了多个领导者,就会出现不知道谁来做主的问题。在这样一个有多个节点的集群中,在节点故障、分区错误等异常情况下,Raft算法如何保证在同一个时间,集群中只有一个领导者呢?带着这个问题,我们正式进入今天的学习。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b9/33/b99084301cf7b944af0490aa0297ff33.jpg" alt="">
|
||||
|
||||
既然要选举领导者,那要从哪些成员中选举呢?除了领导者,Raft算法还支持哪些成员身份呢?这部分内容是你需要掌握的,最基础的背景知识。
|
||||
|
||||
## 有哪些成员身份?
|
||||
|
||||
成员身份,又叫做服务器节点状态,**Raft算法支持领导者(Leader)、跟随者(Follower)和候选人(Candidate) 3种状态。**为了方便讲解,我们使用不同的图形表示不同的状态。在任何时候,每一个服务器节点都处于这3个状态中的1个。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/28/03/280eff8a48c50fc3566ddbde9a97ee03.jpg" alt="">
|
||||
|
||||
<li>
|
||||
跟随者:就相当于普通群众,默默地接收和处理来自领导者的消息,当等待领导者心跳信息超时的时候,就主动站出来,推荐自己当候选人。
|
||||
</li>
|
||||
<li>
|
||||
候选人:候选人将向其他节点发送请求投票(RequestVote)RPC消息,通知其他节点来投票,如果赢得了大多数选票,就晋升当领导者。
|
||||
</li>
|
||||
<li>
|
||||
领导者:蛮不讲理的霸道总裁,一切以我为准,平常的主要工作内容就是3部分,处理写请求、管理日志复制和不断地发送心跳信息,通知其他节点“我是领导者,我还活着,你们现在不要发起新的选举,找个新领导者来替代我。”
|
||||
</li>
|
||||
|
||||
**需要你注意的是,Raft算法是强领导者模型,集群中只能有一个“霸道总裁”。**
|
||||
|
||||
## 选举领导者的过程
|
||||
|
||||
那么这三个成员是怎么选出来领导者的呢?为了方便你理解,我以图例的形式演示一个典型的领导者选举过程。
|
||||
|
||||
首先,在初始状态下,集群中所有的节点都是跟随者的状态。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5b/a2/5b391fd6cb9ed54ba77b0b96efed75a2.jpg" alt="">
|
||||
|
||||
Raft算法实现了随机超时时间的特性。也就是说,每个节点等待领导者节点心跳信息的超时时间间隔是随机的。通过上面的图片你可以看到,集群中没有领导者,而节点A的等待超时时间最小(150ms),它会最先因为没有等到领导者的心跳信息,发生超时。
|
||||
|
||||
这个时候,节点A就增加自己的任期编号,并推举自己为候选人,先给自己投上一张选票,然后向其他节点发送请求投票RPC消息,请它们选举自己为领导者。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/aa/9c/aac5704d69f142ead5e92d33f893a69c.jpg" alt="">
|
||||
|
||||
如果其他节点接收到候选人A的请求投票RPC消息,在编号为1的这届任期内,也还没有进行过投票,那么它将把选票投给节点A,并增加自己的任期编号。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a4/95/a4bb6d1fa7c8c48106a4cf040b7b1095.jpg" alt="">
|
||||
|
||||
如果候选人在选举超时时间内赢得了大多数的选票,那么它就会成为本届任期内新的领导者。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ff/2c/ffaa3f6e9e87d6cea2a3bfc29647e22c.jpg" alt="">
|
||||
|
||||
节点A当选领导者后,他将周期性地发送心跳消息,通知其他服务器我是领导者,阻止跟随者发起新的选举,篡权。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0a/91/0a626f52c2e2a147c59c862b148be691.jpg" alt="">
|
||||
|
||||
讲到这儿,你是不是发现领导者选举很容易理解?与现实中的议会选举也蛮类似?当然,你可能还是对一些细节产生一些疑问:
|
||||
|
||||
- 节点间是如何通讯的呢?
|
||||
- 什么是任期呢?
|
||||
- 选举有哪些规则?
|
||||
- 随机超时时间又是什么?
|
||||
|
||||
## 选举过程四连问
|
||||
|
||||
老话说,细节是魔鬼。这些细节也是很多同学在学习Raft算法的时候比较难掌握的,所以我认为有必要具体分析一下。咱们一步步来,先来看第一个问题。
|
||||
|
||||
#### 节点间如何通讯?
|
||||
|
||||
在Raft算法中,服务器节点间的沟通联络采用的是远程过程调用(RPC),在领导者选举中,需要用到这样两类的RPC:
|
||||
|
||||
1.请求投票(RequestVote)RPC,是由候选人在选举期间发起,通知各节点进行投票;
|
||||
|
||||
2.日志复制(AppendEntries)RPC,是由领导者发起,用来复制日志和提供心跳消息。
|
||||
|
||||
我想强调的是,日志复制RPC只能由领导者发起,这是实现强领导者模型的关键之一,希望你能注意这一点,后续能更好地理解日志复制,理解日志的一致是怎么实现的。
|
||||
|
||||
#### 什么是任期?
|
||||
|
||||
我们知道,议会选举中的领导者是有任期的,领导者任命到期后,要重新开会再次选举。Raft算法中的领导者也是有任期的,每个任期由单调递增的数字(任期编号)标识,比如节点A的任期编号是1。任期编号是随着选举的举行而变化的,这是在说下面几点。
|
||||
|
||||
<li>
|
||||
跟随者在等待领导者心跳信息超时后,推举自己为候选人时,会增加自己的任期号,比如节点A的当前任期编号为0,那么在推举自己为候选人时,会将自己的任期编号增加为1。
|
||||
</li>
|
||||
<li>
|
||||
如果一个服务器节点,发现自己的任期编号比其他节点小,那么它会更新自己的编号到较大的编号值。比如节点B的任期编号是0,当收到来自节点A的请求投票RPC消息时,因为消息中包含了节点A的任期编号,且编号为1,那么节点B将把自己的任期编号更新为1。
|
||||
</li>
|
||||
|
||||
我想强调的是,与现实议会选举中的领导者的任期不同,Raft算法中的任期不只是时间段,而且任期编号的大小,会影响领导者选举和请求的处理。
|
||||
|
||||
<li>
|
||||
在Raft算法中约定,如果一个候选人或者领导者,发现自己的任期编号比其他节点小,那么它会立即恢复成跟随者状态。比如分区错误恢复后,任期编号为3的领导者节点B,收到来自新领导者的,包含任期编号为4的心跳消息,那么节点B将立即恢复成跟随者状态。
|
||||
</li>
|
||||
<li>
|
||||
还约定如果一个节点接收到一个包含较小的任期编号值的请求,那么它会直接拒绝这个请求。比如节点C的任期编号为4,收到包含任期编号为3的请求投票RPC消息,那么它将拒绝这个消息。
|
||||
</li>
|
||||
|
||||
在这里,你可以看到,Raft算法中的任期比议会选举中的任期要复杂。同样,在Raft算法中,选举规则的内容也会比较多。
|
||||
|
||||
#### 选举有哪些规则
|
||||
|
||||
在议会选举中,比成员的身份、领导者的任期还要重要的就是选举的规则,比如一人一票、弹劾制度等。“无规矩不成方圆”,在Raft算法中,也约定了选举规则,主要有这样几点。
|
||||
|
||||
<li>
|
||||
领导者周期性地向所有跟随者发送心跳消息(即不包含日志项的日志复制RPC消息),通知大家我是领导者,阻止跟随者发起新的选举。
|
||||
</li>
|
||||
<li>
|
||||
如果在指定时间内,跟随者没有接收到来自领导者的消息,那么它就认为当前没有领导者,推举自己为候选人,发起领导者选举。
|
||||
</li>
|
||||
<li>
|
||||
在一次选举中,赢得大多数选票的候选人,将晋升为领导者。
|
||||
</li>
|
||||
<li>
|
||||
在一个任期内,领导者一直都会是领导者,直到它自身出现问题(比如宕机),或者因为网络延迟,其他节点发起一轮新的选举。
|
||||
</li>
|
||||
<li>
|
||||
在一次选举中,每一个服务器节点最多会对一个任期编号投出一张选票,并且按照“先来先服务”的原则进行投票。比如节点C的任期编号为3,先收到了1个包含任期编号为4的投票请求(来自节点A),然后又收到了1个包含任期编号为4的投票请求(来自节点B)。那么节点C将会把唯一一张选票投给节点A,当再收到节点B的投票请求RPC消息时,对于编号为4的任期,已没有选票可投了。
|
||||
</li>
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/33/84/3373232d5c10813c7fc87f2fd4a12d84.jpg" alt="">
|
||||
|
||||
1. 日志完整性高的跟随者(也就是最后一条日志项对应的任期编号值更大,索引号更大),拒绝投票给日志完整性低的候选人。比如节点B的任期编号为3,节点C的任期编号是4,节点B的最后一条日志项对应的任期编号为3,而节点C为2,那么当节点C请求节点B投票给自己时,节点B将拒绝投票。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/99/6d/9932935b415e37c2ca758ab99b34f66d.jpg" alt="">
|
||||
|
||||
我想强调的是,选举是跟随者发起的,推举自己为候选人;大多数选票是指集群成员半数以上的选票;大多数选票规则的目标,是为了保证在一个给定的任期内最多只有一个领导者。
|
||||
|
||||
其实在选举中,除了选举规则外,我们还需要避免一些会导致选举失败的情况,比如同一任期内,多个候选人同时发起选举,导致选票被瓜分,选举失败。那么在Raft算法中,如何避免这个问题呢?答案就是随机超时时间。
|
||||
|
||||
#### 如何理解随机超时时间
|
||||
|
||||
在议会选举中,常出现未达到指定票数,选举无效,需要重新选举的情况。在Raft算法的选举中,也存在类似的问题,那它是如何处理选举无效的问题呢?
|
||||
|
||||
其实,Raft算法巧妙地使用随机选举超时时间的方法,把超时时间都分散开来,在大多数情况下只有一个服务器节点先发起选举,而不是同时发起选举,这样就能减少因选票瓜分导致选举失败的情况。
|
||||
|
||||
我想强调的是,**在Raft算法中,随机超时时间是有2种含义的,这里是很多同学容易理解出错的地方,需要你注意一下:**
|
||||
|
||||
1.跟随者等待领导者心跳信息超时的时间间隔,是随机的;
|
||||
|
||||
2.如果候选人在一个随机时间间隔内,没有赢得过半票数,那么选举无效了,然后候选人发起新一轮的选举,也就是说,等待选举超时的时间间隔,是随机的。
|
||||
|
||||
## 内容小结
|
||||
|
||||
以上就是本节课的全部内容了,本节课我主要带你了解了Raft算法的特点、领导者选举等。我希望你明确这样几个重点。
|
||||
|
||||
<li>
|
||||
Raft算法和兰伯特的Multi-Paxos不同之处,主要有2点。首先,在Raft中,不是所有节点都能当选领导者,只有日志较完整的节点(也就是日志完整度不比半数节点低的节点),才能当选领导者;其次,在Raft中,日志必须是连续的。
|
||||
</li>
|
||||
<li>
|
||||
Raft算法通过任期、领导者心跳消息、随机选举超时时间、先来先服务的投票原则、大多数选票原则等,保证了一个任期只有一位领导,也极大地减少了选举失败的情况。
|
||||
</li>
|
||||
<li>
|
||||
本质上,Raft算法以领导者为中心,选举出的领导者,以“一切以我为准”的方式,达成值的共识,和实现各节点日志的一致。
|
||||
</li>
|
||||
|
||||
在本讲,我们使用Raft算法在集群中选出了领导者节点A,那么选完领导者之后,领导者需要处理来自客户的写请求,并通过日志复制实现各节点日志的一致(下节课我会重点带你了解这一部分内容)。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
既然我提到,Raft算法实现了“一切以我为准”的强领导者模型,那么你不妨思考,这个设计有什么限制和局限呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
117
极客时间专栏/分布式协议与算法实战/协议和算法篇/08 | Raft算法(二):如何复制日志?.md
Normal file
117
极客时间专栏/分布式协议与算法实战/协议和算法篇/08 | Raft算法(二):如何复制日志?.md
Normal file
@@ -0,0 +1,117 @@
|
||||
<audio id="audio" title="08 | Raft算法(二):如何复制日志?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5c/9b/5c959035da4ff3a7fa190c2f0126de9b.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
通过上一讲的学习,你应该知道Raft除了能实现一系列值的共识之外,还能实现各节点日志的一致,不过你也许会有这样的疑惑:“什么是日志呢?它和我的业务数据有什么关系呢?”
|
||||
|
||||
想象一下,一个木筏(Raft)是由多根整齐一致的原木(Log)组成的,而原木又是由木质材料组成,所以你可以认为日志是由多条日志项(Log entry)组成的,如果把日志比喻成原木,那么日志项就是木质材料。
|
||||
|
||||
在Raft算法中,副本数据是以日志的形式存在的,领导者接收到来自客户端写请求后,处理写请求的过程就是一个复制和应用(Apply)日志项到状态机的过程。
|
||||
|
||||
那Raft是如何复制日志的呢?又如何实现日志的一致的呢?这些内容是Raft中非常核心的内容,也是我今天讲解的重点,我希望你不懂就问,多在留言区提出你的想法。首先,咱们先来理解日志,这是你掌握如何复制日志、实现日志一致的基础。
|
||||
|
||||
## 如何理解日志?
|
||||
|
||||
刚刚我提到,副本数据是以日志的形式存在的,日志是由日志项组成,日志项究竟是什么样子呢?
|
||||
|
||||
其实,日志项是一种数据格式,它主要包含用户指定的数据,也就是指令(Command),还包含一些附加信息,比如索引值(Log index)、任期编号(Term)。那你该怎么理解这些信息呢?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d5/6d/d5c7b0b95b4289c10c9e0817c71f036d.jpg" alt="">
|
||||
|
||||
- 指令:一条由客户端请求指定的、状态机需要执行的指令。你可以将指令理解成客户端指定的数据。
|
||||
- 索引值:日志项对应的整数索引值。它其实就是用来标识日志项的,是一个连续的、单调递增的整数号码。
|
||||
- 任期编号:创建这条日志项的领导者的任期编号。
|
||||
|
||||
从图中你可以看到,一届领导者任期,往往有多条日志项。而且日志项的索引值是连续的,这一点你需要注意。
|
||||
|
||||
讲到这儿你可能会问:不是说Raft实现了各节点间日志的一致吗?那为什么图中4个跟随者的日志都不一样呢?日志是怎么复制的呢?又该如何实现日志的一致呢?别着急,接下来咱们就来解决这几个问题。先来说说如何复制日志。
|
||||
|
||||
## 如何复制日志?
|
||||
|
||||
你可以把Raft的日志复制理解成一个优化后的二阶段提交(将二阶段优化成了一阶段),减少了一半的往返消息,也就是降低了一半的消息延迟。那日志复制的具体过程是什么呢?
|
||||
|
||||
首先,领导者进入第一阶段,通过日志复制(AppendEntries)RPC消息,将日志项复制到集群其他节点上。
|
||||
|
||||
接着,如果领导者接收到大多数的“复制成功”响应后,它将日志项应用到它的状态机,并返回成功给客户端。如果领导者没有接收到大多数的“复制成功”响应,那么就返回错误给客户端。
|
||||
|
||||
学到这里,有同学可能有这样的疑问了,领导者将日志项应用到它的状态机,怎么没通知跟随者应用日志项呢?
|
||||
|
||||
这是Raft中的一个优化,领导者不直接发送消息通知其他节点应用指定日志项。因为领导者的日志复制RPC消息或心跳消息,包含了当前最大的,将会被提交(Commit)的日志项索引值。所以通过日志复制RPC消息或心跳消息,跟随者就可以知道领导者的日志提交位置信息。
|
||||
|
||||
因此,当其他节点接受领导者的心跳消息,或者新的日志复制RPC消息后,就会将这条日志项应用到它的状态机。而这个优化,降低了处理客户端请求的延迟,将二阶段提交优化为了一段提交,降低了一半的消息延迟。
|
||||
|
||||
为了帮你理解,我画了一张过程图,然后再带你走一遍这个过程,这样你可以更加全面地掌握日志复制。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b8/29/b863dc8546a78c272c965d6e05afde29.jpg" alt="">
|
||||
|
||||
<li>
|
||||
接收到客户端请求后,领导者基于客户端请求中的指令,创建一个新日志项,并附加到本地日志中。
|
||||
</li>
|
||||
<li>
|
||||
领导者通过日志复制RPC,将新的日志项复制到其他的服务器。
|
||||
</li>
|
||||
<li>
|
||||
当领导者将日志项,成功复制到大多数的服务器上的时候,领导者会将这条日志项应用到它的状态机中。
|
||||
</li>
|
||||
<li>
|
||||
领导者将执行的结果返回给客户端。
|
||||
</li>
|
||||
<li>
|
||||
当跟随者接收到心跳信息,或者新的日志复制RPC消息后,如果跟随者发现领导者已经提交了某条日志项,而它还没应用,那么跟随者就将这条日志项应用到本地的状态机中。
|
||||
</li>
|
||||
|
||||
不过,这是一个理想状态下的日志复制过程。在实际环境中,复制日志的时候,你可能会遇到进程崩溃、服务器宕机等问题,这些问题会导致日志不一致。那么在这种情况下,Raft算法是如何处理不一致日志,实现日志的一致的呢?
|
||||
|
||||
## 如何实现日志的一致?
|
||||
|
||||
在Raft算法中,领导者通过强制跟随者直接复制自己的日志项,处理不一致日志。也就是说,Raft是通过以领导者的日志为准,来实现各节点日志的一致的。具体有2个步骤。
|
||||
|
||||
- 首先,领导者通过日志复制RPC的一致性检查,找到跟随者节点上,与自己相同日志项的最大索引值。也就是说,这个索引值之前的日志,领导者和跟随者是一致的,之后的日志是不一致的了。
|
||||
- 然后,领导者强制跟随者更新覆盖的不一致日志项,实现日志的一致。
|
||||
|
||||
我带你详细地走一遍这个过程(为了方便演示,我们引入2个新变量)。
|
||||
|
||||
- PrevLogEntry:表示当前要复制的日志项,前面一条日志项的索引值。比如在图中,如果领导者将索引值为8的日志项发送给跟随者,那么此时PrevLogEntry值为7。
|
||||
- PrevLogTerm:表示当前要复制的日志项,前面一条日志项的任期编号,比如在图中,如果领导者将索引值为8的日志项发送给跟随者,那么此时PrevLogTerm值为4。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e5/f4/e5b5a644c5a0878d26bc4a4a0448c3f4.jpg" alt="">
|
||||
|
||||
<li>
|
||||
领导者通过日志复制RPC消息,发送当前最新日志项到跟随者(为了演示方便,假设当前需要复制的日志项是最新的),这个消息的PrevLogEntry值为7,PrevLogTerm值为4。
|
||||
</li>
|
||||
<li>
|
||||
如果跟随者在它的日志中,找不到与PrevLogEntry值为7、PrevLogTerm值为4的日志项,也就是说它的日志和领导者的不一致了,那么跟随者就会拒绝接收新的日志项,并返回失败信息给领导者。
|
||||
</li>
|
||||
<li>
|
||||
这时,领导者会递减要复制的日志项的索引值,并发送新的日志项到跟随者,这个消息的PrevLogEntry值为6,PrevLogTerm值为3。
|
||||
</li>
|
||||
<li>
|
||||
如果跟随者在它的日志中,找到了PrevLogEntry值为6、PrevLogTerm值为3的日志项,那么日志复制RPC返回成功,这样一来,领导者就知道在PrevLogEntry值为6、PrevLogTerm值为3的位置,跟随者的日志项与自己相同。
|
||||
</li>
|
||||
<li>
|
||||
领导者通过日志复制RPC,复制并更新覆盖该索引值之后的日志项(也就是不一致的日志项),最终实现了集群各节点日志的一致。
|
||||
</li>
|
||||
|
||||
从上面步骤中你可以看到,领导者通过日志复制RPC一致性检查,找到跟随者节点上与自己相同日志项的最大索引值,然后复制并更新覆盖该索引值之后的日志项,实现了各节点日志的一致。需要你注意的是,跟随者中的不一致日志项会被领导者的日志覆盖,而且领导者从来不会覆盖或者删除自己的日志。
|
||||
|
||||
## 内容小结
|
||||
|
||||
本节课我主要带你了解了在Raft中什么是日志、如何复制日志、以及如何处理不一致日志等内容。我希望你明确这样几个重点。
|
||||
|
||||
<li>
|
||||
在Raft中,副本数据是以日志的形式存在的,其中日志项中的指令表示用户指定的数据。
|
||||
</li>
|
||||
<li>
|
||||
兰伯特的Multi-Paxos不要求日志是连续的,但在Raft中日志必须是连续的。而且在Raft中,日志不仅是数据的载体,日志的完整性还影响领导者选举的结果。也就是说,日志完整性最高的节点才能当选领导者。
|
||||
</li>
|
||||
<li>
|
||||
Raft是通过以领导者的日志为准,来实现日志的一致的。
|
||||
</li>
|
||||
|
||||
学完本节课你可以看到,值的共识和日志的一致都是由领导者决定的,领导者的唯一性很重要,那么如果我们需要对集群进行扩容或缩容,比如将3节点集群扩容为5节点集群,这时候是可能同时出现两个领导者的。这是为什么呢?在Raft中,又是如何解决这个问题的呢?我会在下一讲带你了解。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
我提到,领导者接收到大多数的“复制成功”响应后,就会将日志应用到它自己的状态机,然后返回“成功”响应客户端。如果此时有个节点不在“大多数”中,也就是说它接收日志项失败,那么在这种情况下,Raft会如何处理实现日志的一致呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
131
极客时间专栏/分布式协议与算法实战/协议和算法篇/09 | Raft算法(三):如何解决成员变更的问题?.md
Normal file
131
极客时间专栏/分布式协议与算法实战/协议和算法篇/09 | Raft算法(三):如何解决成员变更的问题?.md
Normal file
@@ -0,0 +1,131 @@
|
||||
<audio id="audio" title="09 | Raft算法(三):如何解决成员变更的问题?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5e/20/5ed0f35e141e9a641feed80d7ed4cc20.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
在日常工作中,你可能会遇到服务器故障的情况,这时你就需要替换集群中的服务器。如果遇到需要改变数据副本数的情况,则需要增加或移除集群中的服务器。总的来说,在日常工作中,集群中的服务器数量是会发生变化的。
|
||||
|
||||
讲到这儿,也许你会问:“老韩,Raft是共识算法,对集群成员进行变更时(比如增加2台服务器),会不会因为集群分裂,出现2个领导者呢?”
|
||||
|
||||
在我看来,的确会出现这个问题,因为Raft的领导者选举,建立在“大多数”的基础之上,那么当成员变更时,集群成员发生了变化,就可能同时存在新旧配置的2个“大多数”,出现2个领导者,破坏了Raft集群的领导者唯一性,影响了集群的运行。
|
||||
|
||||
而关于成员变更,不仅是Raft算法中比较难理解的一部分,非常重要,也是Raft算法中唯一被优化和改进的部分。比如,最初实现成员变更的是联合共识(Joint Consensus),但这个方法实现起来难,后来Raft的作者就提出了一种改进后的方法,单节点变更(single-server changes)。
|
||||
|
||||
为了帮你掌握这块内容,今天我除了带你了解成员变更问题的本质之外,还会讲一下如何通过单节点变更的方法,解决成员变更的问题。学完本讲内容之后,你不仅能理解成员变更的问题和单节点变更的原理,也能更好地理解Raft源码实现,掌握解决成员变更问题的方法。
|
||||
|
||||
在开始今天内容之前,我先介绍一下“配置”这个词儿。因为常听到有同学说,自己不理解配置(Configuration)的含义,从而不知道如何理解论文中的成员变更。
|
||||
|
||||
的确,配置是成员变更中一个非常重要的概念,我建议你这么理解:它就是在说集群是哪些节点组成的,是集群各节点地址信息的集合。比如节点A、B、C组成的集群,那么集群的配置就是[A, B, C]集合。
|
||||
|
||||
理解了这一点之后,咱们先来看一道思考题。
|
||||
|
||||
假设我们有一个由节点A、B、C组成的Raft集群,现在我们需要增加数据副本数,增加2个副本(也就是增加2台服务器),扩展为由节点A、B、C、D、E, 5个节点组成的新集群:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/85/04/853b678cb8a088ce1bc9f91fc62bde04.jpg" alt="">
|
||||
|
||||
那么Raft算法是如何保障在集群配置变更时,集群能稳定运行,不出现2个领导者呢?带着这个问题,我们正式进入今天的学习。
|
||||
|
||||
老话说得好,“认识问题,才能解决问题”。为了帮你更好地理解单节点变更的方法,我们先来看一看,成员变更时,到底会出现什么样的问题?
|
||||
|
||||
## 成员变更的问题
|
||||
|
||||
在我看来,在集群中进行成员变更的最大风险是,可能会同时出现2个领导者。比如在进行成员变更时,节点A、B和C之间发生了分区错误,节点A、B组成旧配置中的“大多数”,也就是变更前的3节点集群中的“大多数”,那么这时的领导者(节点A)依旧是领导者。
|
||||
|
||||
另一方面,节点C和新节点D、E组成了新配置的“大多数”,也就是变更后的5节点集群中的“大多数”,它们可能会选举出新的领导者(比如节点C)。那么这时,就出现了同时存在2个领导者的情况。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/82/9e/827a4616e65633015c1f77f3425b1a9e.jpg" alt="">
|
||||
|
||||
如果出现了2个领导者,那么就违背了“领导者的唯一性”的原则,进而影响到集群的稳定运行。你要如何解决这个问题呢?也许有的同学想到了一个解决方法。
|
||||
|
||||
因为我们在启动集群时,配置是固定的,不存在成员变更,在这种情况下,Raft的领导者选举能保证只有一个领导者。也就是说,这时不会出现多个领导者的问题,那我可以先将集群关闭再启动新集群啊。也就是先把节点A、B、C组成的集群关闭,然后再启动节点A、B、C、D、E组成的新集群。
|
||||
|
||||
**在我看来,这个方法不可行。** 为什么呢?因为你每次变更都要重启集群,意味着在集群变更期间服务不可用,肯定不行啊,太影响用户体验了。想象一下,你正在玩王者荣耀,时不时弹出一个对话框通知你:系统升级,游戏暂停3分钟。这体验糟糕不糟糕?
|
||||
|
||||
既然这种方法影响用户体验,根本行不通,那到底怎样解决成员变更的问题呢?**最常用的方法就是单节点变更。**
|
||||
|
||||
## 如何通过单节点变更解决成员变更的问题?
|
||||
|
||||
单节点变更,就是通过一次变更一个节点实现成员变更。如果需要变更多个节点,那你需要执行多次单节点变更。比如将3节点集群扩容为5节点集群,这时你需要执行2次单节点变更,先将3节点集群变更为4节点集群,然后再将4节点集群变更为5节点集群,就像下图的样子。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7e/55/7e2b1caf3c68c7900d6a7f71e7a3a855.jpg" alt="">
|
||||
|
||||
现在,让我们回到开篇的思考题,看看如何用单节点变更的方法,解决这个问题。为了演示方便,我们假设节点A是领导者:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/25/40/25cabfbad4627ec4c39b8d32a567d440.jpg" alt="">
|
||||
|
||||
目前的集群配置为[A, B, C],我们先向集群中加入节点D,这意味着新配置为[A, B, C, D]。成员变更,是通过这么两步实现的:
|
||||
|
||||
- 第一步,领导者(节点A)向新节点(节点D)同步数据;
|
||||
- 第二步,领导者(节点A)将新配置[A, B, C, D]作为一个日志项,复制到新配置中所有节点(节点A、B、C、D)上,然后将新配置的日志项应用(Apply)到本地状态机,完成单节点变更。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7f/07/7f687461706f3b226d79a55b618e4c07.jpg" alt="">
|
||||
|
||||
在变更完成后,现在的集群配置就是[A, B, C, D],我们再向集群中加入节点E,也就是说,新配置为[A, B, C, D, E]。成员变更的步骤和上面类似:
|
||||
|
||||
- 第一步,领导者(节点A)向新节点(节点E)同步数据;
|
||||
- 第二步,领导者(节点A)将新配置[A, B, C, D, E]作为一个日志项,复制到新配置中的所有节点(A、B、C、D、E)上,然后再将新配置的日志项应用到本地状态机,完成单节点变更。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7d/43/7d3b5da84db682359ab82579fdd2e243.jpg" alt="">
|
||||
|
||||
这样一来,我们就通过一次变更一个节点的方式,完成了成员变更,保证了集群中始终只有一个领导者,而且集群也在稳定运行,持续提供服务。
|
||||
|
||||
我想说的是,在正常情况下,**不管旧的集群配置是怎么组成的,旧配置的“大多数”和新配置的“大多数”都会有一个节点是重叠的。** 也就是说,不会同时存在旧配置和新配置2个“大多数”:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5f/b8/5fe7c8d90857737d7314263eae2166b8.jpg" alt=""><img src="https://static001.geekbang.org/resource/image/4a/27/4a00b7e1b89922cd9f785c6f153aca27.jpg" alt="">
|
||||
|
||||
从上图中你可以看到,不管集群是偶数节点,还是奇数节点,不管是增加节点,还是移除节点,新旧配置的“大多数”都会存在重叠(图中的橙色节点)。
|
||||
|
||||
需要你注意的是,在分区错误、节点故障等情况下,如果我们并发执行单节点变更,那么就可能出现一次单节点变更尚未完成,新的单节点变更又在执行,导致集群出现2个领导者的情况。
|
||||
|
||||
如果你遇到这种情况,可以在领导者启动时,创建一个NO_OP日志项(也就是空日志项),只有当领导者将NO_OP日志项应用后,再执行成员变更请求。这个解决办法,你记住就可以了,可以自己在课后试着研究下。具体的实现,可参考Hashicorp Raft的源码,也就是runLeader()函数中:
|
||||
|
||||
```
|
||||
noop := &logFuture{
|
||||
log: Log{
|
||||
Type: LogNoop,
|
||||
},
|
||||
}
|
||||
r.dispatchLogs([]*logFuture{noop})
|
||||
|
||||
```
|
||||
|
||||
当然,有的同学会好奇“联合共识”,在我看来,因为它难以实现,很少被Raft实现采用。比如,除了Logcabin外,未见到其他常用Raft实现采用了它,所以这里我就不多说了。如果你有兴趣,可以自己去阅读论文,加深了解。
|
||||
|
||||
## 内容小结
|
||||
|
||||
以上就是本节课的全部内容了,本节课我主要带你了解了成员变更的问题和单节点变更的方法,我希望你明确这样几个重点。
|
||||
|
||||
<li>
|
||||
成员变更的问题,主要在于进行成员变更时,可能存在新旧配置的2个“大多数”,导致集群中同时出现两个领导者,破坏了Raft的领导者的唯一性原则,影响了集群的稳定运行。
|
||||
</li>
|
||||
<li>
|
||||
单节点变更是利用“一次变更一个节点,不会同时存在旧配置和新配置2个‘大多数’”的特性,实现成员变更。
|
||||
</li>
|
||||
<li>
|
||||
因为联合共识实现起来复杂,不好实现,所以绝大多数Raft算法的实现,采用的都是单节点变更的方法(比如Etcd、Hashicorp Raft)。其中,Hashicorp Raft单节点变更的实现,是由Raft算法的作者迭戈·安加罗(Diego Ongaro)设计的,很有参考价值。
|
||||
</li>
|
||||
|
||||
除此之外,考虑到本节课是Raft算法的最后一讲,所以在这里,我想多说几句,帮助你更好地理解Raft算法。
|
||||
|
||||
有很多同学把Raft当成一致性算法,其实Raft不是一致性算法而是共识算法,是一个Multi-Paxos算法,实现的是如何就一系列值达成共识。并且,Raft能容忍少数节点的故障。虽然Raft算法能实现强一致性,也就是线性一致性(Linearizability),但需要客户端协议的配合。在实际场景中,我们一般需要根据场景特点,在一致性强度和实现复杂度之间进行权衡。比如Consul实现了三种一致性模型。
|
||||
|
||||
<li>
|
||||
default:客户端访问领导者节点执行读操作,领导者确认自己处于稳定状态时(在leader leasing时间内),返回本地数据给客户端,否则返回错误给客户端。在这种情况下,客户端是可能读到旧数据的,比如此时发生了网络分区错误,新领导者已经更新过数据,但因为网络故障,旧领导者未更新数据也未退位,仍处于稳定状态。
|
||||
</li>
|
||||
<li>
|
||||
consistent:客户端访问领导者节点执行读操作,领导者在和大多数节点确认自己仍是领导者之后返回本地数据给客户端,否则返回错误给客户端。在这种情况下,客户端读到的都是最新数据。
|
||||
</li>
|
||||
<li>
|
||||
stale:从任意节点读数据,不局限于领导者节点,客户端可能会读到旧数据。
|
||||
</li>
|
||||
|
||||
一般而言,在实际工程中,Consul的consistent就够用了,可以不用线性一致性,只要能保证写操作完成后,每次读都能读到最新值就可以了。比如为了实现冥等操作,我们使用一个编号(ID)来唯一标记一个操作,并使用一个状态字段(nil/done)来标记操作是否已经执行,那么只要我们能保证设置了ID对应状态值为done后,能立即和一直读到最新状态值就可以了,也就通过防止操作的重复执行,实现了冥等性。
|
||||
|
||||
总的来说,Raft算法能很好地处理绝大部分场景的一致性问题,我推荐你在设计分布式系统时,优先考虑Raft算法,当Raft算法不能满足现有场景需求时,再去调研其他共识算法。
|
||||
|
||||
比如我负责过多个QQ后台的海量服务分布式系统,其中配置中心、名字服务以及时序数据库的META节点,采用了Raft算法。在设计时序数据库的DATA节点一致性时,基于水平扩展、性能和数据完整性等考虑,就没采用Raft算法,而是采用了Quorum NWR、失败重传、反熵等机制。这样安排不仅满足了业务的需求,还通过尽可能采用最终一致性方案的方式,实现系统的高性能,降低了成本。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
在最后,我给你留了一个思考题,强领导者模型会限制集群的写性能,那你想想看,有什么办法能突破Raft集群的写性能瓶颈呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
150
极客时间专栏/分布式协议与算法实战/协议和算法篇/10 | 一致哈希算法:如何分群,突破集群的“领导者”限制?.md
Normal file
150
极客时间专栏/分布式协议与算法实战/协议和算法篇/10 | 一致哈希算法:如何分群,突破集群的“领导者”限制?.md
Normal file
@@ -0,0 +1,150 @@
|
||||
<audio id="audio" title="10 | 一致哈希算法:如何分群,突破集群的“领导者”限制?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/79/b4/79779af5a6ce3738519669a000eb40b4.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
学完前面几讲后,有些同学可能有这样的疑问:如果我们通过Raft算法实现了KV存储,虽然领导者模型简化了算法实现和共识协商,但写请求只能限制在领导者节点上处理,导致了集群的接入性能约等于单机,那么随着业务发展,集群的性能可能就扛不住了,会造成系统过载和服务不可用,这时该怎么办呢?
|
||||
|
||||
其实这是一个非常常见的问题。在我看来,这时我们就要通过分集群,突破单集群的性能限制了。
|
||||
|
||||
说到这儿,有同学可能会说了,分集群还不简单吗?加个Proxy层,由Proxy层处理来自客户端的读写请求,接收到读写请求后,通过对Key做哈希找到对应的集群就可以了啊。
|
||||
|
||||
是的,哈希算法的确是个办法,但它有个明显的缺点:当需要变更集群数时(比如从2个集群扩展为3个集群),这时大部分的数据都需要迁移,重新映射,数据的迁移成本是非常高的。那么如何解决哈希算法,数据迁移成本高的痛点呢?答案就是一致哈希(Consistent Hashing)。
|
||||
|
||||
为了帮你更好地理解如何通过哈希寻址实现KV存储的分集群,我除了会带你了解哈希算法寻址问题的本质之外,还会讲一下一致哈希是如何解决哈希算法数据迁移成本高这个痛点,以及如何实现数据访问的冷热相对均匀。
|
||||
|
||||
对你来说,学完本讲内容之后,不仅能理解一致哈希的原理,还能掌握通过一致哈希实现数据访问冷热均匀的实战能力。
|
||||
|
||||
老规矩,在正式开始学习之前,我们先看一道思考题。
|
||||
|
||||
假设我们有一个由A、B、C三个节点组成(为了方便演示,我使用节点来替代集群)的KV服务,每个节点存放不同的KV数据:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/16/9f/1651257c6ff0194960b6b2f07f51e99f.jpg" alt="" title="图1">
|
||||
|
||||
那么,使用哈希算法实现哈希寻址时,到底有哪些问题呢?带着这个问题,让我们开始今天的内容吧。
|
||||
|
||||
## 使用哈希算法有什么问题?
|
||||
|
||||
通过哈希算法,每个key都可以寻址到对应的服务器,比如,查询key是key-01,计算公式为hash(key-01) % 3 ,经过计算寻址到了编号为1的服务器节点A(就像图2的样子)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c6/5a/c6f440a9e77973c3591394d0d896ed5a.jpg" alt="" title="图2">
|
||||
|
||||
但如果服务器数量发生变化,基于新的服务器数量来执行哈希算法的时候,就会出现路由寻址失败的情况,Proxy无法找到之前寻址到的那个服务器节点,这是为什么呢?
|
||||
|
||||
想象一下,假如3个节点不能满足业务需要了,这时我们增加了一个节点,节点的数量从3变化为4,那么之前的hash(key-01) % 3 = 1,就变成了hash(key-01) % 4 = X,因为取模运算发生了变化,所以这个X大概率不是1(可能X为2),这时你再查询,就会找不到数据了,因为key-01对应的数据,存储在节点A上,而不是节点B:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/27/13/270362d08b0ea77437abcf032634b713.jpg" alt="" title="图3">
|
||||
|
||||
同样的道理,如果我们需要下线1个服务器节点(也就是缩容),也会存在类似的可能查询不到数据的问题。
|
||||
|
||||
而解决这个问题的办法,在于我们要迁移数据,基于新的计算公式hash(key-01) % 4 ,来重新对数据和节点做映射。需要你注意的是,数据的迁移成本是非常高的。
|
||||
|
||||
为了便于你理解,我举个例子,对于1000万key的3节点KV存储,如果我们增加1个节点,变为4节点集群,则需要迁移75%的数据。
|
||||
|
||||
```
|
||||
$ go run ./hash.go -keys 10000000 -nodes 3 -new-nodes 4
|
||||
74.999980%
|
||||
|
||||
```
|
||||
|
||||
**从示例代码的输出,你可以看到,迁移成本是非常高昂的,这在实际生产环境中也是无法想象的。**
|
||||
|
||||
那我们如何通过一致哈希解决这个问题呢?
|
||||
|
||||
## 如何使用一致哈希实现哈希寻址?
|
||||
|
||||
一致哈希算法也用了取模运算,但与哈希算法不同的是,哈希算法是对节点的数量进行取模运算,而一致哈希算法是对2^32进行取模运算。你可以想象下,一致哈希算法,将整个哈希值空间组织成一个虚拟的圆环,也就是哈希环:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a1/89/a15f17c6951dd1e195d5142f5087ef89.jpg" alt="" title="图4">
|
||||
|
||||
从图4中你可以看到,哈希环的空间是按顺时针方向组织的,圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推,2、3、4、5、6……直到2^32-1,也就是说0点左侧的第一个点代表2^32-1。
|
||||
|
||||
在一致哈希中,你可以通过执行哈希算法(为了演示方便,假设哈希算法函数为“c-hash()”),将节点映射到哈希环上,比如选择节点的主机名作为参数执行c-hash(),那么每个节点就能确定其在哈希环上的位置了:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3c/f5/3cb21a553580afbc840b68d4c6b128f5.jpg" alt="" title="图5">
|
||||
|
||||
当需要对指定key的值进行读写的时候,你可以通过下面2步进行寻址:
|
||||
|
||||
- 首先,将key作为参数执行c-hash()计算哈希值,并确定此key在环上的位置;
|
||||
- 然后,从这个位置沿着哈希环顺时针“行走”,遇到的第一节点就是key对应的节点。
|
||||
|
||||
为了帮助你更好地理解如何通过一致哈希进行寻址,我举个例子。假设key-01、key-02、key-03 三个key,经过哈希算法c-hash()计算后,在哈希环上的位置就像图6的样子:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/00/3a/00e85e7abdc1dc0488af348b76ba9c3a.jpg" alt="" title="图6">
|
||||
|
||||
那么根据一致哈希算法,key-01将寻址到节点A,key-02将寻址到节点B,key-03将寻址到节点C。讲到这儿,你可能会问:“老韩,那一致哈希是如何避免哈希算法的问题呢?”
|
||||
|
||||
别着急,接下来我分别以增加节点和移除节点为例,具体说一说一致哈希是如何避免上面的问题的。假设,现在有一个节点故障了(比如节点C):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/68/2e/68a09f7bc604040302fb2d3f102b422e.jpg" alt="" title="图7">
|
||||
|
||||
你可以看到,key-01和key-02不会受到影响,只有key-03的寻址被重定位到A。一般来说,在一致哈希算法中,如果某个节点宕机不可用了,那么受影响的数据仅仅是,会寻址到此节点和前一节点之间的数据。比如当节点C宕机了,受影响的数据是会寻址到节点B和节点C之间的数据(例如key-03),寻址到其他哈希环空间的数据(例如key-01),不会受到影响。
|
||||
|
||||
那如果此时集群不能满足业务的需求,需要扩容一个节点(也就是增加一个节点,比如D):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/91/d9/913e4709c226dae2bec0500b90d597d9.jpg" alt="" title="图8">
|
||||
|
||||
你可以看到,key-01、key-02不受影响,只有key-03的寻址被重定位到新节点D。一般而言,在一致哈希算法中,如果增加一个节点,受影响的数据仅仅是,会寻址到新节点和前一节点之间的数据,其它数据也不会受到影响。
|
||||
|
||||
让我们一起来看一个例子。使用一致哈希的话,对于1000万key的3节点KV存储,如果我们增加1个节点,变为4节点集群,只需要迁移24.3%的数据:
|
||||
|
||||
```
|
||||
$ go run ./consistent-hash.go -keys 10000000 -nodes 3 -new-nodes 4
|
||||
24.301550%
|
||||
|
||||
```
|
||||
|
||||
**你看,使用了一致哈希后,我们需要迁移的数据量仅为使用哈希算法时的三分之一,是不是大大提升效率了呢?**
|
||||
|
||||
总的来说,使用了一致哈希算法后,扩容或缩容的时候,都只需要重定位环空间中的一小部分数据。**也就是说,一致哈希算法具有较好的容错性和可扩展性。**
|
||||
|
||||
**需要你注意的是,在哈希寻址中常出现这样的问题:**客户端访问请求集中在少数的节点上,出现了有些机器高负载,有些机器低负载的情况,那么在一致哈希中,有什么办法能让数据访问分布的比较均匀呢?答案就是虚拟节点。
|
||||
|
||||
在一致哈希中,如果节点太少,容易因为节点分布不均匀造成数据访问的冷热不均,也就是说大多数访问请求都会集中少量几个节点上:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d0/14/d044611092965188e28cd2daf8336814.jpg" alt="" title="图9">
|
||||
|
||||
你能从图中看到,虽然有3个节点,但访问请求主要集中的节点A上。**那如何通过虚拟节点解决冷热不均的问题呢?**
|
||||
|
||||
其实,就是对每一个服务器节点计算多个哈希值,在每个计算结果位置上,都放置一个虚拟节点,并将虚拟节点映射到实际节点。比如,可以在主机名的后面增加编号,分别计算 “Node-A-01”“Node-A-02”“Node-B-01”“Node-B-02”“Node-C-01”“Node-C-02”的哈希值,于是形成6个虚拟节点:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/75/d4/75527ae8011c8311dfb29c4b8ac005d4.jpg" alt="" title="图10">
|
||||
|
||||
你可以从图中看到,增加了节点后,节点在哈希环上的分布就相对均匀了。这时,如果有访问请求寻址到“Node-A-01”这个虚拟节点,将被重定位到节点A。你看,这样我们就解决了冷热不均的问题。
|
||||
|
||||
最后我想说的是,可能有同学已经发现了,当节点数越多的时候,使用哈希算法时,需要迁移的数据就越多,使用一致哈希时,需要迁移的数据就越少:
|
||||
|
||||
```
|
||||
$ go run ./hash.go -keys 10000000 -nodes 3 -new-nodes 4
|
||||
74.999980%
|
||||
$ go run ./hash.go -keys 10000000 -nodes 10 -new-nodes 11
|
||||
90.909000%
|
||||
|
||||
$ go run ./consistent-hash.go -keys 10000000 -nodes 3 -new-nodes 4
|
||||
24.301550%
|
||||
$ go run ./consistent-hash.go -keys 10000000 -nodes 10 -new-nodes 11
|
||||
6.479330%
|
||||
|
||||
```
|
||||
|
||||
从示例代码的输出中你可以看到,当我们向10个节点集群中增加节点时,**如果使用了哈希算法,需要迁移高达90.91%的数据,使用一致哈希的话,只需要迁移6.48%的数据。**
|
||||
|
||||
我希望你能注意到这个规律,使用一致哈希实现哈希寻址时,可以通过增加节点数降低节点宕机对整个集群的影响,以及故障恢复时需要迁移的数据量。后续在需要时,你可以通过增加节点数来提升系统的容灾能力和故障恢复效率。
|
||||
|
||||
## 内容小结
|
||||
|
||||
以上就是本节课的全部内容了,本节课我主要带你了解了哈希算法的缺点、一致哈希的原理等内容。我希望你明确这样几个重点。
|
||||
|
||||
<li>
|
||||
一致哈希是一种特殊的哈希算法,在使用一致哈希算法后,节点增减变化时只影响到部分数据的路由寻址,也就是说我们只要迁移部分数据,就能实现集群的稳定了。
|
||||
</li>
|
||||
<li>
|
||||
当节点数较少时,可能会出现节点在哈希环上分布不均匀的情况。这样每个节点实际占据环上的区间大小不一,最终导致业务对节点的访问冷热不均。需要你注意的是,这个问题可以通过引入更多的虚拟节点来解决。
|
||||
</li>
|
||||
|
||||
最后我想说的是,一致哈希本质上是一种路由寻址算法,适合简单的路由寻址场景。比如在KV存储系统内部,它的特点是简单,不需要维护路由信息。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
Raft集群具有容错能力,能容忍少数的节点故障,那么在多个Raft集群组成的KV系统中,如何设计一致哈希,实现当某个集群的领导者节点出现故障,并选举出新的领导者后,整个系统还能稳定运行呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
110
极客时间专栏/分布式协议与算法实战/协议和算法篇/11 | Gossip协议:流言蜚语,原来也可以实现一致性.md
Normal file
110
极客时间专栏/分布式协议与算法实战/协议和算法篇/11 | Gossip协议:流言蜚语,原来也可以实现一致性.md
Normal file
@@ -0,0 +1,110 @@
|
||||
<audio id="audio" title="11 | Gossip协议:流言蜚语,原来也可以实现一致性" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/29/2a/290df07bd96fb72b274fe0f06b72a92a.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
有一部分同学的业务在可用性上比较敏感,比如监控主机和业务运行的告警系统。这个时候,相信你希望自己的系统能在极端情况下(比如集群中只有一个节点在运行)也能运行。回忆了二阶段提交协议和Raft算法之后,你发现它们都需要全部节点或者大多数节点正常运行,才能稳定运行,那么它们就不适合了。而根据Base理论,你需要实现最终一致性,怎么样才能实现最终一致性呢?
|
||||
|
||||
在我看来,你可以通过Gossip协议实现这个目标。
|
||||
|
||||
Gossip协议,顾名思义,就像流言蜚语一样,利用一种随机、带有传染性的方式,将信息传播到整个网络中,并在一定时间内,使得系统内的所有节点数据一致。对你来说,掌握这个协议不仅能很好地理解这种最常用的,实现最终一致性的算法,也能在后续工作中得心应手地实现数据的最终一致性。
|
||||
|
||||
为了帮你彻底吃透Gossip协议,掌握实现最终一致性的实战能力,我会先带你了解Gossip三板斧,因为这是Gossip协议的核心内容,也是实现最终一致性的常用三种方法。然后以实际系统为例,带你了解在实际系统中是如何实现反熵的。接下来,就让我们开始今天的内容吧。
|
||||
|
||||
## Gossip的三板斧
|
||||
|
||||
Gossip的三板斧分别是:直接邮寄(Direct Mail)、反熵(Anti-entropy)和谣言传播(Rumor mongering)。
|
||||
|
||||
直接邮寄:就是直接发送更新数据,当数据发送失败时,将数据缓存下来,然后重传。从图中你可以看到,节点A直接将更新数据发送给了节点B、D。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/40/6e/40890515407ae099b317ebb52342e56e.jpg" alt="" title="图1">
|
||||
|
||||
在这里我想补充一点,直接邮寄虽然实现起来比较容易,数据同步也很及时,但可能会因为缓存队列满了而丢数据。也就是说,只采用直接邮寄是无法实现最终一致性的,这一点我希望你能注意到。
|
||||
|
||||
那如何实现最终一致性呢?答案就是反熵。本质上,反熵是一种通过异步修复实现最终一致性的方法(关于异步修复,你可以回顾一下[04讲](https://time.geekbang.org/column/article/200717))。常见的最终一致性系统(比如Cassandra),都实现了反熵功能。
|
||||
|
||||
反熵指的是集群中的节点,每隔段时间就随机选择某个其他节点,然后通过互相交换自己的所有数据来消除两者之间的差异,实现数据的最终一致性:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d9/e8/d97c70ab65d83cc3de7e1be009d225e8.jpg" alt="" title="图2">
|
||||
|
||||
从图2中你可以看到,节点A通过反熵的方式,修复了节点D中缺失的数据。那具体怎么实现的呢?
|
||||
|
||||
**其实,在实现反熵的时候,主要有推、拉和推拉三种方式。**我将以修复下图中,2个数据副本的不一致为例,具体带你了解一下。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/89/c2/894f2b082cda09dc1dc555a67f4accc2.jpg" alt="" title="图3">
|
||||
|
||||
推方式,就是将自己的所有副本数据,推给对方,修复对方副本中的熵:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/49/a1/499f02776c205428849d362f5f6d52a1.jpg" alt="" title="图4">
|
||||
|
||||
拉方式,就是拉取对方的所有副本数据,修复自己副本中的熵:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9a/cd/9a9e4fee6ba67e3455e38774022645cd.jpg" alt="" title="图5">
|
||||
|
||||
理解了推和拉之后,推拉这个方式就很好理解了,这个方式就是同时修复自己副本和对方副本中的熵:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/74/aa/745431244e0ca531b32d0b9821d1a8aa.jpg" alt="" title="图6">
|
||||
|
||||
也许有很多同学,会觉得反熵是一个很奇怪的名词。其实,你可以这么来理解,反熵中的熵是指混乱程度,反熵就是指消除不同节点中数据的差异,提升节点间数据的相似度,降低熵值。
|
||||
|
||||
另外需要你注意的是,因为反熵需要节点两两交换和比对自己所有的数据,执行反熵时通讯成本会很高,所以我不建议你在实际场景中频繁执行反熵,并且可以通过引入校验和(Checksum)等机制,降低需要对比的数据量和通讯消息等。
|
||||
|
||||
虽然反熵很实用,但是执行反熵时,相关的节点都是已知的,而且节点数量不能太多,如果是一个动态变化或节点数比较多的分布式环境(比如在DevOps环境中检测节点故障,并动态维护集群节点状态),这时反熵就不适用了。**那么当你面临这个情况要怎样实现最终一致性呢?答案就是谣言传播。**
|
||||
|
||||
谣言传播,广泛地散播谣言,它指的是当一个节点有了新数据后,这个节点变成活跃状态,并周期性地联系其他节点向其发送新数据,直到所有的节点都存储了该新数据:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ea/63/ea8e882c825d8c45832300358f8eb863.jpg" alt="" title="图7">
|
||||
|
||||
从图中你可以看到,节点A向节点B、D发送新数据,节点B收到新数据后,变成活跃节点,然后节点B向节点C、D发送新数据。其实,谣言传播非常具有传染性,它适合动态变化的分布式系统。
|
||||
|
||||
## 如何使用Anti-entropy实现最终一致
|
||||
|
||||
在分布式存储系统中,实现数据副本最终一致性,最常用的方法就是反熵了。为了帮你彻底理解和掌握在实际环境中实现反熵的方法,我想以自研InfluxDB的反熵实现为例,具体带你了解一下。
|
||||
|
||||
在自研InfluxDB中,一份数据副本是由多个分片组成的,也就是实现了数据分片,三节点三副本的集群,就像下图的样子:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8b/27/8bdbde17ab2b252ea50d9851dcafa127.jpg" alt="" title="图8">
|
||||
|
||||
反熵的目标是确保每个DATA节点拥有元信息指定的分片,而且不同节点上,同一分片组中的分片都没有差异。比如说,节点A要拥有分片Shard1和Shard2,而且,节点A的Shard1和Shard2,与节点B、C中的Shard1和Shard2,是一样的。
|
||||
|
||||
那么,在DATA节点上,存在哪些数据缺失的情况呢?也就说,我们需要解决哪些问题呢?
|
||||
|
||||
我们将数据缺失,分为这样2种情况。
|
||||
|
||||
- 缺失分片:也就是说,在某个节点上整个分片都丢失了。
|
||||
<li>节点之间的分片不一致:也就是说,节点上分片都存在,但里面的数据不一样,有数据丢失的情况发生。<br>
|
||||
第一种情况修复起来不复杂,我们只需要将分片数据,通过RPC通讯,从其他节点上拷贝过来就可以了:</li>
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/50/50/50401f9ce72cd56a86a84cf259330d50.jpg" alt="" title="图9">
|
||||
|
||||
你需要注意的是第二种情况,因为第二种情况修复起来要复杂一些。我们需要设计一个闭环的流程,按照一个顺序修复,执行完流程后,也就是实现了一致性了。具体是怎么设计的呢?
|
||||
|
||||
它是按照一定顺序来修复节点的数据差异,先随机选择一个节点,然后循环修复,每个节点生成自己节点有、下一个节点没有的差异数据,发送给下一个节点,进行修复(为了方便演示,假设Shard1、Shard2在各节点上是不一致的):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/39/7a/39f87f2298d8b0a303dda3b0d15c677a.jpg" alt="" title="图10">
|
||||
|
||||
从图中你可以看到,数据修复的起始节点为节点A,数据修复是按照顺时针顺序,循环修复的。需要你注意的是,最后节点A又对节点B的数据执行了一次数据修复操作,因为只有这样,节点C有、节点B缺失的差异数据,才会同步到节点B上。
|
||||
|
||||
学到这里你可以看到,在实现反熵时,实现细节和最初算法的约定有些不同。比如,不是一个节点不断随机选择另一个节点,来修复副本上的熵,而是设计了一个闭环的流程,一次修复所有节点的副本数据不一致。
|
||||
|
||||
为什么这么设计呢?因为我们希望能在一个确定的时间范围内实现数据副本的最终一致性,而不是基于随机性的概率,在一个不确定的时间范围内实现数据副本的最终一致性。
|
||||
|
||||
这样做能减少数据不一致对监控视图影响的时长。而我希望你能注意到,技术是要活学活用的,要能根据场景特点权衡妥协,设计出最适合这个场景的系统功能。**最后需要你注意的是,因为反熵需要做一致性对比,很消耗系统性能,所以建议你将是否启用反熵功能、执行一致性检测的时间间隔等,做成可配置的,能在不同场景中按需使用。**
|
||||
|
||||
## 内容小结
|
||||
|
||||
以上就是本节课的全部内容了,本节课我主要带你了解了Gossip协议、如何在实际系统中实现反熵等。我希望你明确这样几个重点:
|
||||
|
||||
<li>
|
||||
作为一种异步修复、实现最终一致性的协议,反熵在存储组件中应用广泛,比如Dynamo、InfluxDB、Cassandra,我希望你能彻底掌握反熵的实现方法,在后续工作中,需要实现最终一致性时,优先考虑反熵。
|
||||
</li>
|
||||
<li>
|
||||
因为谣言传播具有传染性,一个节点传给了另一个节点,另一个节点又将充当传播者,传染给其他节点,所以非常适合动态变化的分布式系统,比如Cassandra采用这种方式动态管理集群节点状态。
|
||||
</li>
|
||||
|
||||
在实际场景中,实现数据副本的最终一致性时,一般而言,直接邮寄的方式是一定要实现的,因为不需要做一致性对比,只是通过发送更新数据或缓存重传,来修复数据的不一致,性能损耗低。在存储组件中,节点都是已知的,一般采用反熵修复数据副本的一致性。当集群节点是变化的,或者集群节点数比较多时,这时要采用谣言传播的方式,同步更新数据,实现最终一致。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
既然使用反熵实现最终一致性时,需要通过一致性检测发现数据副本的差异,如果每次做一致性检测时都做数据对比的话,肯定是比较消耗性能的,那有什么办法降低一致性检测时的性能消耗呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
@@ -0,0 +1,96 @@
|
||||
<audio id="audio" title="12 | Quorum NWR算法:想要灵活地自定义一致性,没问题!" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/80/f8/80943614595a3650312b883be58d6cf8.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
不知道你在工作中有没有遇到这样的事儿:你开发实现了一套AP型的分布式系统(我在[04讲](https://time.geekbang.org/column/article/200717)提到了AP型系统的特点,你可以回顾一下),实现了最终一致性。业务也接入了,运行正常,一起看起来都那么美好。
|
||||
|
||||
可是,突然有同事说,我们要拉这几个业务的数据做实时分析,希望数据写入成功后,就能立即读取到新数据,也就是要实现强一致性([Werner Vogels提出的客户端侧一致性模型](https://www.allthingsdistributed.com/2008/12/eventually_consistent.html),不是指线性一致性),数据更改后,要保证用户能立即查询到。这时你该怎么办呢?首先你要明确最终一致性和强一致性有什么区别。
|
||||
|
||||
- 强一致性能保证写操作完成后,任何后续访问都能读到更新后的值;
|
||||
- 最终一致性只能保证如果对某个对象没有新的写操作了,最终所有后续访问都能读到相同的最近更新的值。也就是说,写操作完成后,后续访问可能会读到旧数据。
|
||||
|
||||
其实,在我看来,为了一个临时的需求,我们重新开发一套系统,或者迁移数据到新系统,肯定是不合适的。因为工作量比较大,而且耗时也长,而我建议你通过Quorum NWR解决这个问题。
|
||||
|
||||
也就是说,在原有系统上开发实现一个新功能,就可以满足业务同学的需求了。因为通过Quorum NWR,你可以自定义一致性级别,通过临时调整写入或者查询的方式,当W + R > N时,就可以实现强一致性了。
|
||||
|
||||
其实,在AP型分布式系统中(比如Dynamo、Cassandra、InfluxDB企业版的DATA节点集群),Quorum NWR是通常都会实现的一个功能,很常用。对你来说,掌握Quorum NWR,不仅是掌握一种常用的实现一致性的方法,更重要的是,后续用户可以根据业务的特点,灵活地指定一致性级别。
|
||||
|
||||
为了帮你掌握Quorum NWR,除了带你了解它的原理外,我还会以InfluxDB企业版的实现为例,带你看一下它在实际场景中的实现,这样你可以在理解原理的基础上,掌握Quorum NWR的实战技巧。
|
||||
|
||||
首先,你需要了解Quorum NWR中的三个要素,N、W、R。因为它们是Quorum NWR的核心内容,我们就是通过组合这三个要素,实现自定义一致性级别的。
|
||||
|
||||
## Quorum NWR的三要素
|
||||
|
||||
N表示副本数,又叫做复制因子(Replication Factor)。也就是说,N表示集群中同一份数据有多少个副本,就像下图的样子:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8a/bb/8a582c39e4795429a986955a6a1c9ebb.jpg" alt="" title="图1">
|
||||
|
||||
从图中你可以看到,在这个三节点的集群中,DATA-1有2个副本,DATA-2有3个副本,DATA-3有1个副本。也就是说,副本数可以不等于节点数,不同的数据可以有不同的副本数。
|
||||
|
||||
需要你注意的是,在实现Quorum NWR的时候,你需要实现自定义副本的功能。也就是说,用户可以自定义指定数据的副本数,比如,用户可以指定DATA-1具有2个副本,DATA-2具有3个副本,就像图中的样子。
|
||||
|
||||
当我们指定了副本后,就可以对副本数据进行读写操作了。那么这么多副本,你要如何执行读写操作呢?先来看一看写操作,也就是W。
|
||||
|
||||
W,又称写一致性级别(Write Consistency Level),表示成功完成W个副本更新,才完成写操作:<br>
|
||||
<img src="https://static001.geekbang.org/resource/image/1b/7b/1b175952d815d40de45c0d0aba99ac7b.jpg" alt="" title="图2">
|
||||
|
||||
从图中你可以看到,DATA-2的写副本数为2,也就说,对DATA-2执行写操作时,完成了2个副本的更新(比如节点A、C),才完成写操作。
|
||||
|
||||
那么有的同学会问了,DATA-2有3个数据副本,完成了2副本的更新,就完成了写操作,那么如何实现强一致性呢?如果读到了第三个数据副本(比如节点B),不就可能无法读到更新后的值了吗?别急,我讲完如何执行读操作后,你就明白了。
|
||||
|
||||
R,又称读一致性级别(Read Consistency Level),表示读取一个数据对象时需要读R个副本。你可以这么理解,读取指定数据时,要读R副本,然后返回R个副本中最新的那份数据:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5b/5c/5b634d40032cceeffcbc66c3e177735c.jpg" alt="" title="图3">
|
||||
|
||||
从图中你可以看到,DATA-2的读副本数为2。也就是说,客户端读取DATA-2的数据时,需要读取2个副本中的数据,然后返回最新的那份数据。
|
||||
|
||||
这里需要你注意的是,无论客户端如何执行读操作,哪怕它访问的是写操作未强制更新副本数据的节点(比如节点B),但因为W(2) + R(2) > N(3),也就是说,访问节点B,执行读操作时,因为要读2份数据副本,所以除了节点B上的DATA-2,还会读取节点A或节点C上的DATA-2,就像上图的样子(比如节点C上的DATA-2),而节点A和节点C的DATA-2数据副本是强制更新成功的。这个时候,返回给客户端肯定是最新的那份数据。
|
||||
|
||||
你看,通过设置R为2,即使读到前面问题中的第三份副本数据(比如节点B),也能返回更新后的那份数据,实现强一致性了。
|
||||
|
||||
除此之外,关于NWR需要你注意的是,N、W、R值的不同组合,会产生不同的一致性效果,具体来说,有这么两种效果:
|
||||
|
||||
- 当W + R > N的时候,对于客户端来讲,整个系统能保证强一致性,一定能返回更新后的那份数据。
|
||||
- 当W + R <= N的时候,对于客户端来讲,整个系统只能保证最终一致性,可能会返回旧数据。
|
||||
|
||||
你可以看到,Quorum NWR的原理并不复杂,也相对比较容易理解,但在这里,我想强调一下,掌握它的关键在于如何根据不同的场景特点灵活地实现Quorum NWR,所以接下来,我带你具体问题具体分析,以InfluxDB企业版为例讲解一下。
|
||||
|
||||
## 如何实现Quorum NWR?
|
||||
|
||||
在InfluxDB企业版中,可以在创建保留策略时,设置指定数据库(Database)对应的副本数,具体的命令,就像下面的样子:
|
||||
|
||||
- create retention policy “rp_one_day” on “telegraf” duration 1d replication 3
|
||||
|
||||
通过replication参数,指定了数据库telegraf对应的副本数为3。
|
||||
|
||||
需要你注意的,在InfluxDB企业版中,副本数不能超过节点数据。你可以这么理解,多副本的意义在于冗余备份,如果副本数超过节点数,就意味着在一个节点上会存在多个副本,那么这时冗余备份的意义就不大了。比如机器故障时,节点上的多个副本是同时被影响的。
|
||||
|
||||
InfluxDB企业版,支持“any、one、quorum、all”4种写一致性级别,具体的含义是这样的。
|
||||
|
||||
- any:任何一个节点写入成功后,或者接收节点已将数据写入Hinted-handoff缓存(也就是写其他节点失败后,本地节点上缓存写失败数据的队列)后,就会返回成功给客户端。
|
||||
- one:任何一个节点写入成功后,立即返回成功给客户端,不包括成功写入到Hinted-handoff缓存。
|
||||
- quorum:当大多数节点写入成功后,就会返回成功给客户端。此选项仅在副本数大于2时才有意义,否则等效于all。
|
||||
- all:仅在所有节点都写入成功后,返回成功。
|
||||
|
||||
我想强调一下,对时序数据库而言,读操作常会拉取大量数据,查询性能是挑战,是必须要考虑优化的,因此,在InfluxDB企业版中,不支持读一致性级别,只支持写一致性级别。另外,我们可以通过设置写一致性级别为all,来实现强一致性。
|
||||
|
||||
你看,如果我们像InfluxDB企业版这样,实现了Quorum NWR,那么在业务临时需要实现强一致性时,就可以通过设置写一致性级别为all,来实现了。
|
||||
|
||||
## 内容小结
|
||||
|
||||
以上就是本节课的全部内容了,本节课我主要带你了解了Quorum NWR的原理、InfluxDB企业版的Quorum NWR实现。我希望你明确这样几个重点。
|
||||
|
||||
<li>
|
||||
一般而言,不推荐副本数超过当前的节点数,因为当副本数据超过节点数时,就会出现同一个节点存在多个副本的情况。当这个节点故障时,上面的多个副本就都受到影响了。
|
||||
</li>
|
||||
<li>
|
||||
当W + R > N时,可以实现强一致性。另外,如何设置N、W、R值,取决于我们想优化哪方面的性能。比如,N决定了副本的冗余备份能力;如果设置W = N,读性能比较好;如果设置R = N,写性能比较好;如果设置W = (N + 1) / 2、R = (N + 1) / 2,容错能力比较好,能容忍少数节点(也就是(N - 1) / 2)的故障。
|
||||
</li>
|
||||
|
||||
最后,我想说的是,Quorum NWR是非常实用的一个算法,能有效弥补AP型系统缺乏强一致性的痛点,给业务提供了按需选择一致性级别的灵活度,建议你的开发实现AP型系统时,也实现Quorum NWR。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
我提到实现Quorum NWR时,需要实现自定义副本的能力,那么,一般设置几个副本就可以了,为什么呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
119
极客时间专栏/分布式协议与算法实战/协议和算法篇/13 | PBFT算法:有人作恶,如何达成共识?.md
Normal file
119
极客时间专栏/分布式协议与算法实战/协议和算法篇/13 | PBFT算法:有人作恶,如何达成共识?.md
Normal file
@@ -0,0 +1,119 @@
|
||||
<audio id="audio" title="13 | PBFT算法:有人作恶,如何达成共识?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2b/43/2b265eec87732419c6ebda3e4f8c8e43.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
学完了[01讲](https://time.geekbang.org/column/article/195662)的拜占庭将军问题之后,有同学在留言中表达了自己的思考和困惑:口信消息型拜占庭问题之解在实际项目中是如何落地的呢?先给这位同学点个赞,很棒!你能在学习的同时思考落地实战。
|
||||
|
||||
不过事实上,它很难在实际项目落地,因为口信消息型拜占庭问题之解是一个非常理论化的算法,没有和实际场景结合,也没有考虑如何在实际场景中落地和实现。
|
||||
|
||||
比如,它实现的是在拜占庭错误场景下,忠将们如何在叛徒干扰时,就一致行动达成共识。但是它并不关心结果是什么,这会出现一种情况:现在适合进攻,但将军们达成的最终共识却是撤退。
|
||||
|
||||
很显然,这不是我们想要的结果。因为在实际场景中,我们需要就提议的一系列值(而不是单值),即使在拜占庭错误发生的时候也能被达成共识。那你要怎么做呢?答案就是掌握PBFT算法。
|
||||
|
||||
PBFT算法非常实用,是一种能在实际场景中落地的拜占庭容错算法,它在区块链中应用广泛(比如Hyperledger Sawtooth、Zilliqa)。为了帮助你更好地理解PBFT算法,在今天的内容中,我除了带你了解PBFT达成共识的原理之外,还会介绍口信消息型拜占庭问题之解的局限。相信学习完本讲内容后,你不仅能理解PBFT达成共识的基本原理,还能理解算法背后的演化和改进。
|
||||
|
||||
老规矩,在开始今天的学习之前,咱们先看一道思考题:
|
||||
|
||||
假设苏秦再一次带队抗秦,这一天,苏秦和4个国家的4位将军赵、魏、韩、楚商量军机要事,结果刚商量完没多久苏秦就接到了情报,情报上写道:联军中可能存在一个叛徒。这时,苏秦要如何下发作战指令,保证忠将们正确、一致地执行下发的作战指令,而不是被叛徒干扰呢?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/24/d3/2493047e33459cfa85843dd194ddced3.jpg" alt="">
|
||||
|
||||
带着这个问题,我们正式进入今天的学习。
|
||||
|
||||
首先,咱们先来研究一下,为什么口信消息型拜占庭问题之解很难在实际场景中落地,除了我在开篇提到的非常理论化,没有和实际的需求结合之外,还有其他的原因么?
|
||||
|
||||
其实,这些问题是后续众多拜占庭容错算法在努力改进和解决的,理解了这些问题,能帮助你更好地理解后来的拜占庭容错算法(包括PBFT算法)。
|
||||
|
||||
## 口信消息型拜占庭问题之解的局限
|
||||
|
||||
我想说的是,这个算法有个非常致命的缺陷。如果将军数为n、叛将数为 f,那么算法需要递归协商 f+1轮,消息复杂度为O(n ^ (f + 1)),消息数量指数级暴增。你可以想象一下,如果叛将数为64,消息数已经远远超过**int64**所能表示的了,这是无法想象的,肯定不行啊。
|
||||
|
||||
另外,尽管对于签名消息,不管叛将数(比如f)是多少,经过f + 1轮的协商,忠将们都能达成一致的作战指令,但是这个算法同样存在“理论化”和“消息数指数级暴增”的痛点。
|
||||
|
||||
讲到这儿,你肯定明白为什么这个算法很难在实际场景中落地了。可技术是不断发展的,算法也是在解决实际场景问题中不断改进的。那么PBFT算法的原理是什么呢?为什么它能在实际场景中落地呢?
|
||||
|
||||
## PBFT是如何达成共识的?
|
||||
|
||||
我们先来看看如何通过PBFT算法,解决苏秦面临的共识问题。先假设苏秦制定的作战指令是进攻,而楚是叛徒(为了演示方便):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8a/37/8a6fe551e5b99a28e0fed8105ed5cc37.jpg" alt="" title="图1">
|
||||
|
||||
需要你注意的是,所有的消息都是签名消息,也就是说,消息发送者的身份和消息内容都是无法伪造和篡改的(比如,楚无法伪造一个假装来自赵的消息)。
|
||||
|
||||
首先,苏秦联系赵,向赵发送包含作战指令“进攻”的请求(就像下图的样子)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5d/76/5da99fcab9c99b92351e05aca9a9a976.jpg" alt="" title="图2">
|
||||
|
||||
当赵接收到苏秦的请求之后,会执行三阶段协议(Three-phase protocol)。
|
||||
|
||||
- 赵将进入预准备(Pre-prepare)阶段,构造包含作战指令的预准备消息,并广播给其他将军(魏、韩、楚)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/40/2f/40669f5c4bcaffbac446475251f1fa2f.jpg" alt="" title="图3">
|
||||
|
||||
那么在这里,我想问你一个问题:魏、韩、楚,收到消息后,能直接执行指令吗?
|
||||
|
||||
答案是不能,因为他们不能确认自己接收到指令和其他人接收到的指令是相同的。比如,赵可能是叛徒,赵收到了2个指令,分别是“进攻”和“准备30天的粮草”,然后他给魏发送的是“进攻”,给韩、楚发送的是“准备30天粮草”,这样就会出现无法一致行动的情况。那么他们具体怎么办呢?我接着说一下。
|
||||
|
||||
- 接收到预准备消息之后,魏、韩、楚将进入准备(Prepare)阶段,并分别广播包含作战指令的准备消息给其他将军。比如,魏广播准备消息给赵、韩、楚(如图所示)。为了方便演示,我们假设叛徒楚想通过不发送消息,来干扰共识协商(你能看到,图中的楚是没有发送消息的)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/12/43/12063907d531486261c42691ebc52c43.jpg" alt="" title="图4">
|
||||
|
||||
然后,当某个将军收到2f个一致的包含作战指令的准备消息后,会进入提交(Commit)阶段(这里的2f包括自己,其中f为叛徒数,在我的演示中是1)。在这里,我也给你提一个问题:这个时候该将军(比如魏)可以直接执行指令吗?
|
||||
|
||||
答案还是不能,因为魏不能确认赵、韩、楚是否收到了2f 个一致的包含作战指令的准备消息。也就是说,魏这时无法确认赵、韩、楚是否准备好了执行作战指令。那么怎么办呢?别着急,咱们继续往下看。
|
||||
|
||||
- 进入提交阶段后,各将军分别广播提交消息给其他将军,也就是告诉其他将军,我已经准备好了,可以执行指令了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8a/b4/8a0f34d9098d361f114f91db8c4b1cb4.jpg" alt="" title="图5">
|
||||
|
||||
- 最后,当某个将军收到2f + 1个验证通过的提交消息后(包括自己,其中f为叛徒数,在我的演示中为1),也就是说,大部分的将军们已经达成共识,这时可以执行作战指令了,那么该将军将执行苏秦的作战指令,执行完毕后发送执行成功的消息给苏秦。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c7/4a/c7e8f7152487f65ba14569c50f08254a.jpg" alt="" title="图6">
|
||||
|
||||
最后,当苏秦收到f+1个相同的响应(Reply)消息时,说明各位将军们已经就作战指令达成了共识,并执行了作战指令(其中f为叛徒数,在我的演示中为1)。
|
||||
|
||||
你看,经过了三轮协商,是不是就指定的作战指令达成了共识,并执行了作战指令了呢?
|
||||
|
||||
在这里,苏秦采用的就是**简化版的PBFT算法**。在这个算法中:
|
||||
|
||||
- 你可以将赵、魏、韩、楚理解为分布式系统的四个节点,其中赵是主节点(Primary),魏、韩、楚是备份节点(Backup);
|
||||
- 将苏秦理解为业务,也就是客户端;
|
||||
- 将消息理解为网络消息;
|
||||
- 将作战指令“进攻”,理解成客户端提议的值,也就是希望被各节点达成共识,并提交给状态机的值。
|
||||
|
||||
**在这里我想说的是,** PBFT算法是通过签名(或消息认证码MAC)约束恶意节点的行为,也就是说,每个节点都可以通过验证消息签名确认消息的发送来源,一个节点无法伪造另外一个节点的消息。最终,基于大多数原则(2f + 1)实现共识的。
|
||||
|
||||
需要你注意的是,最终的共识是否达成,客户端是会做判断的,如果客户端在指定时间内未收到请求对应的f + 1相同响应,就认为集群出故障了,共识未达成,客户端会重新发送请求。
|
||||
|
||||
另外需要你注意的是,PBFT算法通过视图变更(View Change)的方式,来处理主节点作恶,当发现主节点在作恶时,会以“轮流上岗”方式,推举新的主节点。
|
||||
|
||||
最后我想说的是,尽管PBFT算法相比口信消息型拜占庭之解已经有了很大的优化,将消息复杂度从O(n ^ (f + 1))降低为O(n ^ 2),能在实际场景中落地,并解决实际的共识问题。但PBFT还是需要比较多的消息。比如在13节点集群中(f为4)。
|
||||
|
||||
- 请求消息:1
|
||||
- 预准备消息:3f = 12
|
||||
- 准备消息:3f * (3f - f) = 96
|
||||
- 提交消息:(3f - f + 1) * (3f + 1)= 117
|
||||
- 回复消息:3f - 1 = 11
|
||||
|
||||
也就是说,一次共识协商需要237个消息,你看,消息数还是蛮多的,所以我推荐你,在中小型分布式系统中使用PBFT算法。
|
||||
|
||||
## 内容小结
|
||||
|
||||
以上就是本节课的全部内容了,本节课我主要带你了解了口信消息型拜占庭问题之解的局限和PBFT的原理,我希望你明确这样几个重点。
|
||||
|
||||
<li>
|
||||
不管口信消息型拜占庭问题之解,还是签名消息型拜占庭问题之解,都是非常理论化的,未考虑实际场景的需求,而且协商成本非常高,指数级的消息复杂度是很难在实际场景中落地,和解决实际场景问题的。
|
||||
</li>
|
||||
<li>
|
||||
PBFT算法是通过签名(或消息认证码MAC)约束恶意节点的行为,采用三阶段协议,基于大多数原则达成共识的。另外,与口信消息型拜占庭问题之解(以及签名消息型拜占庭问题之解)不同的是,PBFT算法实现的是一系列值的共识,而不是单值的共识。
|
||||
</li>
|
||||
|
||||
最后,我想说的是,相比Raft算法完全不适应有人作恶的场景,PBFT算法能容忍(n - 1)/3个恶意节点(也可以是故障节点)。另外,相比PoW算法,PBFT的优点是不消耗算力,所以在日常实践中,PBFT比较适用于相对“可信”的场景中,比如联盟链。
|
||||
|
||||
需要你注意的是,PBFT算法与Raft算法类似,也存在一个“领导者”(就是主节点),同样,集群的性能也受限于“领导者”。另外,O(n ^ 2)的消息复杂度,以及随着消息数的增加,网络时延对系统运行的影响也会越大,这些都限制了运行PBFT算法的分布式系统的规模,也决定了PBFT算法适用于中小型分布式系统。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
当客户端在收到了f + 1个结果,就认为共识达成了,那么为什么这个值不能小于f + 1呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
117
极客时间专栏/分布式协议与算法实战/协议和算法篇/14 | PoW算法:有办法黑比特币吗?.md
Normal file
117
极客时间专栏/分布式协议与算法实战/协议和算法篇/14 | PoW算法:有办法黑比特币吗?.md
Normal file
@@ -0,0 +1,117 @@
|
||||
<audio id="audio" title="14 | PoW算法:有办法黑比特币吗?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/48/fe/48644f2a34a6f42e7485ad182f68f7fe.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
谈起比特币,你应该再熟悉不过了,比特币是基于区块链实现的,而区块链运行在因特网上,这就存在有人试图作恶的情况。学完[01讲](https://time.geekbang.org/column/article/195662)和[13讲](https://time.geekbang.org/column/article/209450)之后,有些同学可能已经发现了,口信消息型拜占庭问题之解、PBFT算法虽然能防止坏人作恶,但只能防止少数的坏人作恶,也就是(n - 1) / 3个坏人 (其中n为节点数)。可如果区块链也只能防止一定比例的坏人作恶,那就麻烦了,因为坏人可以不断增加节点数,轻松突破(n - 1) / 3的限制。
|
||||
|
||||
那区块链是如何改进这个问题的呢?答案就是PoW算法。
|
||||
|
||||
在我看来,区块链通过工作量证明(Proof of Work)增加了坏人作恶的成本,以此防止坏人作恶。比如,如果坏人要发起51%攻击,需要控制现网51%的算力,成本是非常高昂的。为啥呢?因为根据Cryptoslate 估算,对比特币进行 51% 算力攻击需要上百亿人民币!
|
||||
|
||||
那么为了帮你更好地理解和掌握PoW算法,我会详细讲解它的原理和51%攻击的本质。希望让你在理解PoW算法的同时,也了解PoW算法的局限。
|
||||
|
||||
首先我来说说PoW的原理,换句话说,就是PoW是如何运行的。
|
||||
|
||||
## 如何理解工作量证明?
|
||||
|
||||
什么是工作量证明(Proof Of Work,简称PoW)呢?你可以这么理解:就是一份证明,用来确认你做过一定量的工作。比如,你的大学毕业证书就是一份工作量证明,证明你通过4年的努力完成了相关课程的学习。
|
||||
|
||||
那么回到计算机世界,具体来说就是,客户端需要做一定难度的工作才能得出一个结果,验证方却很容易通过结果来检查出客户端是不是做了相应的工作。
|
||||
|
||||
比如小李来BAT面试,说自己的编程能力很强,那么他需要做一定难度的工作(比如做个编程题)。根据做题结果,面试官可以判断他是否适合这个岗位。你看,小李做个编程题,面试官核验做题结果,这就是一个现实版的工作量证明。
|
||||
|
||||
具体的工作量证明过程,就像下图中的样子:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/83/59/837a01a9d5adef33caa9eeea69143b59.jpg" alt="">
|
||||
|
||||
请求方做了一些运算,解决了某个问题,然后把运算结果发送给验证方,进行核验,验证方根据运算结果,就能判断请求方是否做了相关的工作。
|
||||
|
||||
需要你注意的是,这个算法具有不对称性,也就是说,工作对于请求方是有难度的,对于验证方则是比较简单的,易于验证的。
|
||||
|
||||
既然工作量证明是通过指定的结果,来证明自己做过了一定量的工作。那么在区块链的PoW算法中需要做哪些工作呢?答案是哈希运算。
|
||||
|
||||
区块链是通过执行哈希运算,然后通过运算后的结果值,证明自己做过了相关工作。为了帮你更好地理解哈希运算,在介绍哈希运算之前,咱们先来聊一聊哈希函数。
|
||||
|
||||
哈希函数(Hash Function),也叫散列函数。就是说,你输入一个任意长度的字符串,哈希函数会计算出一个长度相同的哈希值。假设我们对任意长度字符串(比如"geektime")执行SHA256哈希运算,就会得到一个32字节的哈希值,就像下面的样子:
|
||||
|
||||
```
|
||||
$ echo -n "geektime" | sha256sum
|
||||
bb2f0f297fe9d3b8669b6b4cec3bff99b9de596c46af2e4c4a504cfe1372dc52 -
|
||||
|
||||
```
|
||||
|
||||
那我们如何通过哈希函数进行哈希运算,从而证明工作量呢?为了帮你理解这部分内容,我举个具体的例子。
|
||||
|
||||
我们给出的工作量要求是,基于一个基本的字符串(比如"geektime"),你可以在这个字符串后面添加一个整数值,然后对变更后(添加整数值)的字符串进行SHA256哈希运算,如果运算后得到的哈希值(16进制形式)是以"0000"开头的,就验证通过。为了达到这个工作量证明的目标,我们需要不停地递增整数值,一个一个试,对得到的新字符串进行 SHA256 哈希运算。
|
||||
|
||||
按照这个规则,我们需要经过35024次计算,才能找到恰好前4位为0的哈希值。
|
||||
|
||||
```
|
||||
"geektime0" => 01f28c5df06ef0a575fd0e529be9a6f73b1290794762de014ec84182081e118e
|
||||
"geektime1" => a2567c06fdb5775cb1e3ce17b72754cf146fcc6da75c8f1d87d7ab6a1b8c4523
|
||||
...
|
||||
"geektime35022" =>
|
||||
8afc85049a9e92fe0b6c98b02b27c09fb869fbfe273d0ab84ad8c5ac17b8627e
|
||||
"geektime35023" =>
|
||||
0000ec5927ba10ea45a6822dcc205050ae74ae1ad2d9d41e978e1ec9762dc404
|
||||
|
||||
```
|
||||
|
||||
通过这个示例你可以看到,工作量证明是通过执行哈希运算,经过一段时间的计算后,得到符合条件的哈希值。也就是说,可以通过这个哈希值,来证明我们的工作量。
|
||||
|
||||
关于这个规则,我也想多说几句,这个规则不是固定的,在实际场景中,你可以根据场景特点,制定不同的规则,比如,你可以试试分别运行多少次,才能找到恰好前3位和前5位为0的哈希值。
|
||||
|
||||
现在,你对工作量证明的原理应该有一定的了解了,那么有同学肯定好奇了,在区块链中是如何实现工作量证明的呢?
|
||||
|
||||
## 区块链如何实现PoW算法的?
|
||||
|
||||
区块链也是通过SHA256来执行哈希运算的,通过计算出符合指定条件的哈希值,来证明工作量的。因为在区块链中,PoW算法是基于区块链中的区块信息,进行哈希运算的,所以我先带你回顾一下区块链的相关知识。
|
||||
|
||||
区块链的区块,是由区块头、区块体2部分组成的,就像下图中的样子。
|
||||
|
||||
- 区块头(Block Head):区块头主要由上一个区块的哈希值、区块体的哈希值、4字节的随机数(nonce)等组成的。
|
||||
- 区块体(Block Body):区块包含的交易数据,其中的第一笔交易是Coinbase交易,这是一笔激励矿工的特殊交易。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a0/1d/a0d4cfff95de26e64d85189dd47eee1d.jpg" alt="">
|
||||
|
||||
我想说的是,拥有80字节固定长度的区块头,就是用于区块链工作量证明的哈希运算中输入字符串,而且通过双重SHA256哈希运算(也就是对SHA256哈希运算的结果,再执行一次哈希运算),计算出的哈希值,只有小于目标值(target),才是有效的,否则哈希值是无效的,必须重算。
|
||||
|
||||
学到这儿你可以看到,在区块链中是通过对区块头执行SHA256哈希运算,得到小于目标值的哈希值,来证明自己的工作量的。
|
||||
|
||||
计算出符合条件的哈希值后,矿工就会把这个信息广播给集群中所有其他节点,其他节点验证通过后,会将这个区块加入到自己的区块链中,最终形成一串区块链,就像下图的样子:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5a/9b/5afbe81873d69fc9dcd986d11c5c369b.jpg" alt="">
|
||||
|
||||
最后,我想说的是,算力越强,系统大概率会越先计算出这个哈希值。这也就意味着,如果坏人们掌握了51%的算力,就可以发起51%攻击,比如,实现双花(Double Spending),也就是说,同一份钱花2次。
|
||||
|
||||
具体说的话,就是攻击者掌握了较多的算力,能挖掘一条比原链更长的攻击链,并将攻击链向全网广播,这时呢,按照约定,节点将接受更长的链,也就是攻击链,丢弃原链。就像下图的样子:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/15/5a/15ae8837e42aaef0f8ee2e4227bf115a.jpg" alt="">
|
||||
|
||||
需要你注意的是,即使攻击者只有30%的算力,他也有可能连续计算出多个区块的哈希值,挖掘出更长的攻击链,发动攻击; 另外,即使攻击者拥有51%的算力,他也有可能半天无法计算出一个区块的哈希值,也就是攻击失败。也就是说,能否计算出符合条件的哈希值,有一定的概率性,但长久来看,攻击者攻击成功的概率等同于攻击者算力的权重。
|
||||
|
||||
## 内容小结
|
||||
|
||||
以上就是本节课的全部内容了,本节课我主要带你了解了PoW算法的原理,和51%攻击,我希望你明确这样几个重点。
|
||||
|
||||
<li>
|
||||
在比特币的区块链中,PoW算法,是通过SHA256进行哈希运算,计算出符合指定条件的哈希值,来证明工作量的。
|
||||
</li>
|
||||
<li>
|
||||
51%攻击,本质是因为比特币的区块链约定了“最长链胜出,其它节点在这条链基础上扩展”,攻击者可以通过优势算力实现对最长链的争夺。
|
||||
</li>
|
||||
<li>
|
||||
除了通过PoW算法,增加坏人作恶的成本,比特币还通过“挖矿得币”奖励好人,最终保持了整个系统的运行稳定。
|
||||
</li>
|
||||
|
||||
因为本讲是拜占庭容错算法的最后一讲,我想多说几句:学完了01讲的同学,应该还记得,我们提到Raft算法是非拜占庭容错算法。那么如果我们把Raft算法用于拜占庭场景中,会怎么样呢?
|
||||
|
||||
比如,在比特币中,我们采用了Raft算法实现共识,而不是基于PoW算法的区块链,那么,就会出现这样的情况,当恶意节点当选为领导者后,他可以不断地告诉其他节点,这些比特币都是我的,按照Raft的约定,其他节点也就只能接受这种情况,谁让恶意节点是领导者呢?**最终就会出现,所有的比特币都被恶意节点盗走的情况**,完全乱套了。
|
||||
|
||||
另外我想说的是,因为拜占庭容错算法(比如PoW算法、PBFT算法),能容忍一定比例的作恶行为,所以它在相对开放的场景中应用广泛,比如公链、联盟链。非拜占庭容错算法(比如Raft)无法对作恶行为进行容错,主要用于封闭、绝对可信的场景中,比如私链、公司内网的DevOps环境。我希望你能准确理解2类算法之间的差异,根据场景特点,选择合适的算法,保障业务高效、稳定的运行。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
既然,我提了如何通过计算得到"0000"开头的哈希值,来做实现工作量证明,那么你不妨思考下,如果约定是更多“0”开头的哈希值,比如“00000000”,工作量是增加了还是减少了,为什么呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
160
极客时间专栏/分布式协议与算法实战/协议和算法篇/15 | ZAB协议:如何实现操作的顺序性?.md
Normal file
160
极客时间专栏/分布式协议与算法实战/协议和算法篇/15 | ZAB协议:如何实现操作的顺序性?.md
Normal file
@@ -0,0 +1,160 @@
|
||||
<audio id="audio" title="15 | ZAB协议:如何实现操作的顺序性?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4f/0c/4f4de8e08226dcd22cff8a332cf6900c.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
很多同学应该使用过ZooKeeper,它是一个开源的分布式协调服务,比如你可以用它进行配置管理、名字服务等等。在ZooKeeper中,数据是以节点的形式存储的。如果你要用ZooKeeper做配置管理,那么就需要在里面创建指定配置,假设创建节点"/geekbang"和"/geekbang/time",步骤如下:
|
||||
|
||||
```
|
||||
[zk: 192.168.0.10:2181(CONNECTED) 0] create /geekbang 123
|
||||
Created /geekbang
|
||||
[zk: 192.168.0.10:2181(CONNECTED) 1] create /geekbang/time 456
|
||||
Created /geekbang/time
|
||||
|
||||
```
|
||||
|
||||
我们分别创建了配置"/geekbang"和"/geekbang/time",对应的值分别为123和456。那么在这里我提个问题:你觉得在ZooKeeper中,能用兰伯特的Multi-Paxos实现各节点数据的共识和一致吗?
|
||||
|
||||
当然不行。因为兰伯特的Multi-Paxos,虽然能保证达成共识后的值不再改变,但它不关心达成共识的值是什么,也无法保证各值(也就是操作)的顺序性。而这就是Zookeeper没有采用Multi-Paxos的原因,又是ZAB协议着力解决的,也是你理解ZAB协议的关键。
|
||||
|
||||
那么为了帮你更好地理解这个协议,接下来,我将分别以如何实现操作的顺序性、领导者选举、故障恢复、处理读写请求为例,具体讲解一下。希望你能在全面理解ZAB协议的同时,加深对Paxos算法的理解。
|
||||
|
||||
今天这节课,我会从ZAB协议的最核心设计目标(如何实现操作的顺序性)出发,带你了解它的基础原理。
|
||||
|
||||
老规矩,在开始今天的内容之前,我们先来看一道思考题:
|
||||
|
||||
假如节点A、B、C组成的一个分布式集群,我们要设计一个算法,来保证指令(比如X、Y)执行的顺序性,比如,指令X在指令Y之前执行,那么我们该如何设计这个算法呢?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/55/c9/55dc6f6bf822db027858b8b4fdb89cc9.jpg" alt="" title="图1">
|
||||
|
||||
带着这个问题,我们进入今天的内容。
|
||||
|
||||
## 为什么Multi-Paxos无法保证操作顺序性?
|
||||
|
||||
刚刚我提到“Multi-Paxos无法保证操作的顺序性”。为了让你真正理解这个问题,我举个具体的例子演示一下(为了演示方便,我们假设当前所有节点上的被选定指令,最大序号都为100,那么新提议的指令对应的序号就会是101)。
|
||||
|
||||
首先节点A是领导者,提案编号为1,提议了指令X、Y,对应的序号分别为101和102,但是因为网络故障,指令只成功复制到了节点A。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3e/f3/3ed203904d35eaf0bd5162262c1b4ef3.jpg" alt="" title="图2">
|
||||
|
||||
假设这时节点A故障了,新当选的领导者为节点B。节点B当选领导者后,需要先作为学习者了解目前已被选定的指令。节点B学习之后,发现当前被选定指令的最大序号为100(因为节点A故障了,它被选定指令的最大序号102,无法被节点B发现),那么它可以从序号101开始提议新的指令。这时它接收到客户端请求,并提议了指令Z,指令Z被成功复制到节点B、C。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/77/12/77f94aa5dec044ffc1a61547cb10f112.jpg" alt="" title="图3">
|
||||
|
||||
假设这时节点B故障了,节点A恢复了,选举出领导者C后,节点B故障也恢复了。节点C当选领导者后,需要先作为学习者了解目前已被选定的指令,这时它执行Basic Paxos的准备阶段,就会发现之前选定的值(比如Z、Y),然后发送接受请求,最终在序号101、102处达成共识的指令是Z、Y。就像下图的样子。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e6/c4/e6c27d879025ef6943a5985da1351bc4.jpg" alt="" title="图4">
|
||||
|
||||
在这里,你可以看到,原本预期的指令是X、Y,最后变成了Z、Y。讲到这儿,你应该可以知道,为什么用Multi-Paxos不能达到我们想要的结果了吧?
|
||||
|
||||
这个过程,其实很明显的验证了“Multi-Paxos虽然能保证达成共识后的值不再改变,但它不关心达成共识的值是什么。”
|
||||
|
||||
那么咱们接着回到开篇的问题,假设在ZooKeeper中直接使用了兰伯特的Multi-Paxos,这时创建节点"/geekbang"和"/geekbang/time",那么就可能出现,系统先创建了节点"/geekbang/time",这样肯定就出错了:
|
||||
|
||||
```
|
||||
[zk: 192.168.0.10:2181(CONNECTED) 0] create /geekbang/time 456
|
||||
Node does not exist: /geekbang/time
|
||||
|
||||
```
|
||||
|
||||
因为创建节点"/geekbang/time"时,找不到节点"/geekbang",所以就会创建失败。
|
||||
|
||||
在这里我多说几句,除了Multi-Paxos,兰伯特还有很多关于分布式的理论,这些理论都很经典(比如拜占庭将军问题),但也因为太早了,与实际场景结合的不多,所以后续的众多算法是在这个基础之上做了大量的改进(比如,PBFT、Raft等)。关于这一点,我在13讲也强调过,你需要注意一下。
|
||||
|
||||
另外我再延伸一下,其实在[ZAB论](https://www.semanticscholar.org/paper/Zab:-High-performance-broadcast-for-primary-backup-Junqueira-Reed/b02c6b00bd5dbdbd951fddb00b906c82fa80f0b3)文中,关于Paxos问题(Figure 1 )的分析是有争议的。因为ZooKeeper当时应该考虑的是Multi-Paxos,而不是有多个提议者的Basic Paxos。而在Multi-Paxos中,领导者作为唯一提议者,是不存在同时多个提议者的情况。也就是说,Paxos(更确切的说是Multi-Paxos)无法保证操作的顺序性的问题是存在的,但原因不是ZAB论文中演示的原因,本质上是因为Multi-Paxos实现的是一系列值的共识,不关心最终达成共识的值是什么,不关心各值的顺序,就像我们在上面演示的过程那样。
|
||||
|
||||
那既然Multi-Paxos不行,ZooKeeper怎么实现操作的顺序性的呢?答案是它实现了ZAB协议。
|
||||
|
||||
你可能会说了:Raft可以实现操作的顺序性啊,为什么ZooKeeper不用Raft呢?这个问题其实比较简单,因为Raft出来的比较晚,直到2013年才正式提出,在2007年开发ZooKeeper的时候,还没有Raft呢。
|
||||
|
||||
## ZAB是如何实现操作的顺序性的?
|
||||
|
||||
如果用一句话解释ZAB协议到底是什么,我觉得它是:能保证操作顺序性的,基于主备模式的原子广播协议。
|
||||
|
||||
接下来,我还是以X、Y指令为例具体演示一下,帮助你更好理解为什么ZAB能实现操作的顺序性(为了演示方便,我们假设节点A为主节点,节点B、C为备份节点)。
|
||||
|
||||
首先,需要你注意的是,在ZAB中,写操作必须在主节点(比如节点A)上执行。如果客户端访问的节点是备份节点(比如节点B),它会将写请求转发给主节点。如图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/77/6f/770c39b4ea339799bc3ca4a0b0d8266f.jpg" alt="" title="图5">
|
||||
|
||||
接着,当主节点接收到写请求后,它会基于写请求中的指令(也就是X,Y),来创建一个提案(Proposal),并使用一个唯一的ID来标识这个提案。这里我说的唯一的ID就是指事务标识符(Transaction ID,也就是zxid),就像下图的样子。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d0/9b/d0063fa9275ce0a114ace27db326d19b.jpg" alt="" title="图6">
|
||||
|
||||
从图中你可以看到,X、Y对应的事务标识符分别为<1, 1>和<1, 2>,这两个标识符是什么含义呢?
|
||||
|
||||
你可以这么理解,事务标识符是64位的long型变量,有任期编号epoch和计数器counter两部分组成(为了形象和方便理解,我把epoch翻译成任期编号),格式为<epoch, counter>,高32位为任期编号,低32位为计数器:
|
||||
|
||||
<li>
|
||||
任期编号,就是创建提案时领导者的任期编号,需要你注意的是,当新领导者当选时,任期编号递增,计数器被设置为零。比如,前领导者的任期编号为1,那么新领导者对应的任期编号将为2。
|
||||
</li>
|
||||
<li>
|
||||
计数器,就是具体标识提案的整数,需要你注意的是,每次领导者创建新的提案时,计数器将递增。比如,前一个提案对应的计数器值为1,那么新的提案对应的计数器值将为2。
|
||||
</li>
|
||||
|
||||
为什么要设计的这么复杂呢?因为事务标识符必须按照顺序、唯一标识一个提案,也就是说,事务标识符必须是唯一的、递增的。
|
||||
|
||||
在创建完提案之后,主节点会基于TCP协议,并按照顺序将提案广播到其他节点。这样就能保证先发送的消息,会先被收到,保证了消息接收的顺序性。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e5/96/e525af146900c892e0c002affa77d496.jpg" alt="" title="图7">
|
||||
|
||||
你看这张图,X一定在Y之前到达节点B、C。
|
||||
|
||||
然后,当主节点接收到指定提案的“大多数”的确认响应后,该提案将处于提交状态(Committed),主节点会通知备份节点提交该提案。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1d/19/1d3950b6d91845789cce1f0569969419.jpg" alt="" title="图8">
|
||||
|
||||
在这里,需要你注意的是,主节点提交提案是有顺序性的。主节点根据事务标识符大小,按照顺序提交提案,如果前一个提案未提交,此时主节点是不会提交后一个提案的。也就是说,指令X一定会在指令Y之前提交。
|
||||
|
||||
最后,主节点返回执行成功的响应给节点B,节点B再转发给客户端。**你看,这样我们就实现了操作的顺序性,保证了指令X一定在指令Y之前执行。**
|
||||
|
||||
最后我想补充的是,当写操作执行完后,接下来你可能需要执行读操作了。你需要注意,为了提升读并发能力,Zookeeper提供的是最终一致性,也就是读操作可以在任何节点上执行,客户端会读到旧数据:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d4/1c/d405381e5fad12730149baa4fae63e1c.jpg" alt="" title="图9">
|
||||
|
||||
如果客户端必须要读到最新数据,怎么办呢?Zookeeper提供了一个解决办法,那就是sync命令。你可以在执行读操作前,先执行sync命令,这样客户端就能读到最新数据了:
|
||||
|
||||
```
|
||||
[zk: 192.168.0.10:2181(CONNECTED) 2] sync /geekbang/time
|
||||
[zk: 192.168.0.10:2181(CONNECTED) 3] Sync returned 0
|
||||
[zk: 192.168.0.10:2181(CONNECTED) 3] get /geekbang/time
|
||||
456
|
||||
cZxid = 0x100000005
|
||||
ctime = Mon Apr 20 21:19:28 HKT 2020
|
||||
mZxid = 0x100000005
|
||||
mtime = Mon Apr 20 21:19:28 HKT 2020
|
||||
pZxid = 0x100000005
|
||||
cversion = 0
|
||||
dataVersion = 0
|
||||
aclVersion = 0
|
||||
ephemeralOwner = 0x0
|
||||
dataLength = 3
|
||||
numChildren = 0
|
||||
[zk: 192.168.0.10:2181(CONNECTED) 4]
|
||||
|
||||
```
|
||||
|
||||
## 内容小结
|
||||
|
||||
本节课我主要带你了解了为什么Multi-Paxos无法实现操作的顺序性,以及ZAB协议如何保证操作的顺序性。我希望你明确这样几个重点。
|
||||
|
||||
1.兰伯特的Multi-Paxos只考虑了如何实现共识,也就是如何就一系列值达成共识,未考虑如何实现各值(也就是操作)的顺序性。
|
||||
|
||||
2.ZAB是通过“一切以领导者为准”的强领导者模型和严格按照顺序处理、提交提案,来实现操作的顺序性的。
|
||||
|
||||
那么说到ZAB,很多同学可能有这样的疑问:为什么ZAB作者宣称[ZAB不是Paxos算法,](https://cwiki.apache.org/confluence/display/ZOOKEEPER/Zab+vs.+Paxos)但又有很多资料提到ZAB是Multi-Paxos算法呢?到底该怎么理解呢?
|
||||
|
||||
我的看法是,你可以把它理解为Multi-Paxos算法。因为技术是发展的,概念的内涵也在变化。Raft算法(主备、强领导者模型)与ZAB协议非常类似,它是作为共识算法和Multi-Paxos算法提出的。当它被广泛接受和认可后,共识算法的内涵也就丰富和发展了,不仅能实现一系列值的共识,还能保证值的顺序性。同样,Multi-Paxos算法不仅指代多次执行Basic Paxos的算法,还指代主备、强领导者模型的共识算法。
|
||||
|
||||
当然了,在学习技术过程中,我们不可避免的会遇到有歧义、有争议的信息,就像在11讲留言区中,有同学提到“从网上搜了搜相关资料,发现大部分资料将谣言传播等同于Gossip协议,也有把反熵等同于Gossip协议的,感到很迷惑”。
|
||||
|
||||
这就需要我们不仅要在平时工作和学习中,认真、全面的学习理论,掌握概念的内涵,还要能“包容”和“发展”着理解技术。
|
||||
|
||||
最后,在本节课我们了解了ZAB协议的最核心设计目标(如何实现操作的顺序性),那么既然“所有数据都是以主节点的数据为准的”,主节点(也就是领导者)那么重要,那当它崩溃了,该怎么处理呢?下节课,我会重点带你了解这部分内容。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
既然我提到在ZAB协议中,主节点是基于TCP协议来广播消息的,并保证了消息接收的顺序性。那么你不妨想想,如果ZAB采用的是UDP协议,能保证消息接收的顺序性吗?为什么呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这节课让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
|
||||
编辑角:目前课程已经结束了,为了交付更好的内容,《分布式协议与算法实战》于2020年4.26日启动迭代计划,15讲为迭代版本,后面也会对ZAB协议进行更细致化的讨论,敬请期待!
|
||||
109
极客时间专栏/分布式协议与算法实战/协议和算法篇/加餐 | MySQL XA是如何实现分布式事务的?.md
Normal file
109
极客时间专栏/分布式协议与算法实战/协议和算法篇/加餐 | MySQL XA是如何实现分布式事务的?.md
Normal file
@@ -0,0 +1,109 @@
|
||||
<audio id="audio" title="加餐 | MySQL XA是如何实现分布式事务的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/bd/03/bd95de5183ba578c7dc5dc5525cc4b03.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
相信很多同学都知道MySQL支持单机事务,那么在分布式系统中,涉及多个节点,MySQL又是怎么实现分布式事务的呢?
|
||||
|
||||
这个和我最近遇到的问题很类似,我现在负责的一个业务系统,需要接收来自外部的指令,然后访问多个内部其他系统来执行指令,但执行完指令后,我需要同时更新多个内部MySQL数据库中的值(比如MySQL数据库A、B、C)。
|
||||
|
||||
但又因为业务敏感,系统必须处于一个一致性状态(也就是说,MySQL数据库A、B、C中的值要么同时更新成功,要么全部不更新)。不然的话,会出现有的系统显示指令执行成功了,有的系统显示指令尚未被执行,导致多部门对指令执行结果理解混乱。
|
||||
|
||||
那么我当时是如何实现多个MySQL数据库更新的一致性呢?答案就是采用MySQL XA。
|
||||
|
||||
在我看来,MySQL通过支持XA规范的二阶段提交协议,不仅实现了多个MySQL数据库操作的事务,还能实现MySQL、Oracle、SQL Server等支持XA规范的数据库操作的事务。
|
||||
|
||||
对你来说,理解MySQL XA,不仅能理解数据层分布式事务的原理,还能在实际系统中更加深刻的理解二阶段提交协议,这样一来,当你在实际工作中,遇到多个MySQL数据库的事务需求时,就知道如何通过MySQL XA来处理了。
|
||||
|
||||
老规矩,咱们先来看一道思考题。
|
||||
|
||||
假设两个数据库A、B(位于不同的服务器节点上),我们需要实现多个数据库更新(比如,UPDATE executed_table SET status = true WHERE id=100)和插入操作(比如,INSERT into operation_table SET id = 100, op = ‘get-cdn-log’)的事务,那么在MySQL中如何实现呢?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/92/e4/92164eafa6604e334b22d245d1543ce4.jpg" alt="">
|
||||
|
||||
带着这个问题,我们进入今天的学习。不过因为MySQL通过XA规范实现分布式事务的,所以你有必要先来了解一下XA规范。
|
||||
|
||||
## 什么是XA?
|
||||
|
||||
提到XA规范,就不得不说DTP模型( Distributed Transaction Processing),因为XA规范约定的是DTP模型中2个模块(事务管理器和资源管理器)的通讯方式,那DTP,就是分布式事务处理,就像下图的样子:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a0/93/a08794d4a09101fdc0789496a50db193.jpg" alt="">
|
||||
|
||||
为了帮助你更好的理解DTP模型,我来解释一下DTP各模块的作用。
|
||||
|
||||
- AP:应用程序(Aplication Program),一般指事务的发起者(比如数据库客户端或者访问数据库的程序),定义事务对应的操作(比如更新操作UPDATE executed_table SET status = true WHERE id=100)。
|
||||
- RM:资源管理器(Resource Manager),管理共享资源,并提供访问接口,供外部程序来访问共享资源,比如数据库,另外RM还应该具有事务提交或回滚的能力。
|
||||
- TM:事务管理器(Transaction Manager),TM是分布式事务的协调者。TM与每个RM进行通信,协调并完成事务的处理。
|
||||
|
||||
你是不是觉得这个架构看起来很复杂?其实在我看来,你可以这么理解这个架构:应用程序访问、使用资源管理器的资源,并通过事务管理器的事务接口(TX interface)定义需要执行的事务操作,然后事务管理器和资源管理器会基于XA规范,执行二阶段提交协议。
|
||||
|
||||
那么,XA规范是什么样子的呢?它约定了事务管理器和资源管理器之间双向通讯的接口规范,并实现了二阶段提交协议:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4e/ed/4e6d8e0104c4b22e58c5a400323e94ed.jpg" alt="">
|
||||
|
||||
为了帮你更好地理解这个过程,咱们一起走下流程,加深下印象:
|
||||
|
||||
1. AP(应用程序)联系TM(事务管理器)发起全局事务;
|
||||
1. TM调用ax_open()建立与资源管理器的会话;
|
||||
1. TM调用xa_start()标记事务分支(Transaction branch)的开头;
|
||||
1. AP访问RM(资源管理器),并定义具体事务分支的操作,比如更新一条数据记录(UPDATE executed_table SET status = true WHERE id=100)和插入一条数据记录(INSERT into operation_table SET id = 100, op = ‘get-cdn-log’);
|
||||
1. TM调用xa_end()标记事务分支的结尾;
|
||||
1. TM调用xa_prepare()通知RM做好事务分支提交的准备工作,比如锁定相关资源,也就是执行二阶段提交协议的提交请求阶段;
|
||||
1. TM调用xa_commit()通知RM提交事务分支(xa_rollback()通知RM回滚事务),也就是执行二阶段提交协议的提交执行阶段;
|
||||
1. TM调用xa_close()关闭与RM的会话。
|
||||
|
||||
整个过程,也许有些复杂,不过你可以这么理解:**xa_start()和xa_end()在准备和标记事务分支的内容,然后调用xa_prepare()和xa_commit()(或者xa_rollback())执行二阶段提交协议,实现操作的原子性。**在这里需要你注意的是,这些接口需要按照一定顺序执行,比如xa_start()必须要在xa_end()之前执行。
|
||||
|
||||
另外我想说的是,事务管理器对资源管理器调用的xa_start()和xa_end()这对组合,一般用于标记事务分支(就像上面的更新一条数据记录和插入一条数据记录)的开头和结尾。在这里,你需要注意的是:
|
||||
|
||||
- 对于同一个资源管理器,根据全局事务的要求,可以前后执行多个操作组合,比如,先标记一个插入操作,然后再标记一个更新操作。
|
||||
- 事务管理器只是标记事务,并不执行事务,最终是由应用程序通知资源管理器来执行事务操作的。
|
||||
|
||||
另外,XA规范还约定了如何向事务管理器注册和取消资源管理器的API接口(也就是ax_reg()和ax_unreg()接口)。在这里需要你注意的是,这两个接口是ax_开头的,而不是像xa_start()那样是xa_开头的,这是很容易误解的点,我希望你能注意到。
|
||||
|
||||
那么讲了这么多,我们该如何通过MySQL XA实现分布式事务呢?
|
||||
|
||||
## 如何通过MySQL XA实现分布式事务呢?
|
||||
|
||||
首先,你需要创建一个唯一的事务ID(比如xid),来唯一标识事务,并调用“XA START”和“XA END”来定义事务分支对应的操作(比如INSERT into operation_table SET id = 100, op = ‘get-cdn-log’)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/76/ca/76c1110506c7409c748f20e17ea23bca.jpg" alt="">
|
||||
|
||||
接着,你需要执行“XA PREPARE”命令,来执行二阶段提交协议的提交请求阶段。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/28/30/285e440ff3bee6b6c74eeaaa2b37c430.jpg" alt="">
|
||||
|
||||
最后,你需要调用“XA COMMIT”来提交事务(或者“XA ROLLBACK”回滚事务)。这样你就实现了全局事务的一致性了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/16/93/169ae090f2b55c6e520ecf7424c0f293.jpg" alt="">
|
||||
|
||||
从上面的流程中,你可以看到,客户端在扮演事务管理器的角色,而MySQL数据库就在扮演资源管理器的角色。另外,你要注意,上面流程中的xid必须是唯一值。
|
||||
|
||||
另外,我想补充的是,如果你要开启MySQL的XA功能,必须设置存储引擎为 InnoDB,也就是说,在MySQL中,只有InnoDB引擎支持XA规范。
|
||||
|
||||
当然了,可能有些同学对MySQL XA有这样的疑问,能否将“XA END”和“XA PREPARE”合并到一起呢?**答案是不能,因为在“XA END”之后,是可以直接执行“XA COMMIT”的,也就是一阶段提交(比如当共享资源变更只涉及到一个RM时)。**
|
||||
|
||||
最后,我强调一下,MySQL XA性能不高,适合在并发性能要求不高的场景中使用,而我之所以需要采用MySQL XA实现分布式事务,不仅因为整个系统对并发性能要求不高,还因为底层架构是多个第三方的,没法改造。
|
||||
|
||||
## 内容小结
|
||||
|
||||
本节课我主要带你了解了XA规范,以及如何使用MySQL XA实现分布式事务。我希望你明确这样几个重点。
|
||||
|
||||
1.XA规范是个标准的规范,也就是说,无论是否是相同的数据库,只要这些数据库(比如MySQL、Oracle、SQL Server)支持XA规范,那么它们就能实现分布式事务,也就是能保证全局事务的一致性。
|
||||
|
||||
2.相比商业数据库对XA规范的支持,MySQL XA性能不高,所以,我不推荐你在高并发的性能至上场景中,使用MySQL XA。
|
||||
|
||||
3.在实际开发中,为了降低单点压力,通常会根据业务情况进行分表分库,将表分布在不同的库中,那么,在这种情况下,如果后续需要保证全局事务的一致性时,也需要实现分布式事务。
|
||||
|
||||
最后我想说的是,尽管XA规范保证了全局事务的一致性,实现成本较低,而且包括MySQL在内的主流数据库都已经支持,但因为XA规范是基于二阶段提交协议实现的,所以它也存在二阶段提交协议的局限,比如:
|
||||
|
||||
首先,XA规范存在单点问题,也就是说,因为事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在第一阶段已经完成了,在第二阶段正准备提交的时候,事务管理器宕机了,相关的资源会被锁定,无法访问。
|
||||
|
||||
其次,XA规范存在资源锁定的问题,也就是说,在进入准备阶段后,资源管理器中的资源将处于锁定状态,直到提交完成或者回滚完成。
|
||||
|
||||
不过,虽然MySQL XA能实现数据层的分布式事务,但在我现在负责的这套业务系统中,还面临这样的问题:那就是在接收到外部的指令后,我需要访问多个内部系统,执行指令约定的操作,而且,我必须保证指令执行的原子性,也就是说,要么全部成功,要么全部失败,那么我应该怎么做呢?答案是TCC,这也是我在下一讲想和你聊一聊的。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
既然我提到了我通过MySQL XA解决了数据库操作的一致性问题,而MySQL XA性能不高,适用于对并发性能要求不高的场景中。那么你不妨想想,在MySQL XA不能满足并发需求时,如何重新设计底层数据系统,来避免采用分布式事务呢?为什么呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这节课让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
104
极客时间专栏/分布式协议与算法实战/协议和算法篇/加餐 | PBFT算法:如何替换作恶的领导者?.md
Normal file
104
极客时间专栏/分布式协议与算法实战/协议和算法篇/加餐 | PBFT算法:如何替换作恶的领导者?.md
Normal file
@@ -0,0 +1,104 @@
|
||||
<audio id="audio" title="加餐 | PBFT算法:如何替换作恶的领导者?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/cb/2d/cba877891dad215a4300896e0f383e2d.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
上一讲,我们了解到,PBFT可以防止备份节点作恶,因为这个算法是主节点和备份节点组成的,那你想象一下,如果主节点作恶(比如主节点接收到了客户端的请求,但就是默不作声,不执行三阶段协议),这时无论正常节点数有多少,备份节点肯定没办法达成共识,整个集群都没办法正常运行。这么大的问题,你该怎么解决呢?
|
||||
|
||||
答案是视图变更(View Change),也就是通过领导者选举,选举出新的主节点,并替换掉作恶的主节点。(其中的“视图”你可以理解为领导者任期的,不同的视图值对应不同的主节点。比如,视图值为1时,主节点为A;视图值为2时,主节点为B。)
|
||||
|
||||
对于领导者模型算法而言,不管是非拜占庭容错算法(比如Raft),还是拜占庭容错算法(比如PBFT),领导者选举都是它们实现容错能力非常重要的一环。比如,对Raft而言,领导者选举实现了领导者节点的容错能力,避免了因领导者节点故障导致整个集群不可用。而对PBFT而言,视图变更,除了能解决主节点故障导致的集群不可用之外,还能解决主节点是恶意节点的问题。
|
||||
|
||||
对你来说,理解视图变更,可以理解拜占庭容错算法如何处理领导者故障和作恶。这样一样,从07讲到13讲(非拜占庭容错场景到拜占庭容错场景),你就能更全面地理解领导者选举的原理,和能解决的问题了,这样当你后续熟悉其他领导者选举算法,或设计自己的领导者选举算法时,也能更加的得心应手了。
|
||||
|
||||
既然领导者选举这么重要,那么PBFT到底是如何实现视图变更的呢?带着这样的疑问,我们进入今天的内容。
|
||||
|
||||
## 主节点作恶会出现什么问题?
|
||||
|
||||
在PBFT中,主节点作恶有这么几种情况,比如:
|
||||
|
||||
- 我开篇提到的,主节点接收到客户端请求后,它不做任何处理,也就是默不作声;
|
||||
- 主节点接收到客户端请求后,给不同的预准备请求分配不同的序号;
|
||||
- 再或者,主节点只给部分节点发送预准备消息。
|
||||
|
||||
需要你注意的是,不管出现哪种情况,共识都是无法达成的,也就是说,**如果恶意节点当选了主节点,此时无论忠诚节点数多少,忠诚节点们将都无法达成共识。**
|
||||
|
||||
而这种情况肯定是无法接受的,这就需要我们在发现主节点可能在作恶时,设计一个机制,将作恶的主节点替换掉,并保证最终只有忠诚的节点在担任主节点。这样,PFBT才能保证当节点数为3f + 1(其中f为恶意节点数)时,忠诚的节点们能就客户端提议的指令达成共识,并执行一致的指令。
|
||||
|
||||
那么,在PBFT中,视图变更是如何选举出新的主节点,并替换掉作恶的主节点的呢?答案你肯定知道了,那就是视图变更。
|
||||
|
||||
## 如何替换作恶的主节点?
|
||||
|
||||
在我看来,视图变更是保证PBFT算法能稳定运行的关键,当系统运行异常时,客户端或备份节点触发系统的视图变更,通过“轮流上岗”的方式:**(v + 1) mod |R|,其中v为当前视图的值,|R|为节点数**选出下一个视图的主节点,最终选出一个忠诚、稳定运行新主节点,并保证了共识的达成。
|
||||
|
||||
为了帮你更好地理解视图变更的原理,我继续以苏秦为例(这次,咱们把叛将楚当作是“大元帅”,让它扮演主节点的角色)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d7/d2/d73b976fc3c0d9bc7c1b82d94f11a9d2.jpg" alt="" title="图1">
|
||||
|
||||
首先,苏秦联系楚,向楚发送包含作战指令“进攻”的请求。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f2/be/f22058b2d209978d3488f57375e448be.jpg" alt="" title="图2">
|
||||
|
||||
当楚接收到苏秦的请求之后,为了达到破坏作战计划的目的,它默不作声,内心想:我就是不执行三阶段协议(Three-phase protocol),不执行你的指令,也不通知其他将军执行你的指令,你能把我怎么办?
|
||||
|
||||
结果,苏秦等到花都谢了,还是没办法接收到2个相同的响应(Reply)消息。都过了约定的时间了,苏秦在想,也许各位将军们出什么问题了。
|
||||
|
||||
这时苏秦会直接给各位将军发送作战指令。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2e/35/2e55e095c723a54d22bb9830f4029435.jpg" alt="" title="图3">
|
||||
|
||||
当赵、魏、韩接收到来自的苏秦的作战指令时,它们会将作战指令分别发送给楚,并等待一段时间,如果在这段时间内,仍未接收到来自楚的预准备消息,那么它们就认为楚可能已经叛变了,就发起视图变更(采用“轮流上岗”的方式选出新的大元帅,比如赵),并向集群所有节点发送视图变更消息(view-change message)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/39/93/398fd2a7b42f79f7bb0f20a2a2d7ba93.jpg" alt="" title="图4">
|
||||
|
||||
当赵接收到2个视图变更消息后,它就发送新视图消息(new-view message)给其他将军,告诉大家,我是大元帅了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8e/3c/8ecde229a9c3715346cadc9ff862ce3c.jpg" alt="" title="图5">
|
||||
|
||||
当其他将军接收到新视图消息后,就认为选出了新的大元帅。然后,忠诚的将军们就可以一致地执行来自苏秦的作战指令了。
|
||||
|
||||
你看,叛变的大元帅,就这样被发现和替换掉了,而最终大元帅一定是忠诚的。
|
||||
|
||||
回到计算机的世界中,如何理解呢?与13讲一样,在这里我就不啰嗦了。不过为了帮你更全面地理解视图变更,我想补充几点:
|
||||
|
||||
首先,当一个备份节点,在定时器超时触发了视图变更后,它将暂时停止接收和处理,除了检查点(CHECKPOINT) 、视图变更、新视图之外的消息。你可以这么理解,这个节点认为现在集群处于异常状态,不能再处理客户端请求相关的消息了。
|
||||
|
||||
其次,除了演示中的情况,会触发备份节点进行视图变更,下面几种情况也会触发视图变更,比如:
|
||||
|
||||
- 备份节点发送了准备消息后,在约定的时间内未接收到来自其他节点的2f个相同的准备消息。
|
||||
- 备份节点发送了提交消息后,在约定的时间内未接收到来自其他节点的2f个相同的提交消息。
|
||||
- 备份节点接收到异常消息,比如视图值、序号和已接受的消息相同,但内容摘要不同。
|
||||
|
||||
也就是说,视图变更除了能解决主节点故障和作恶的问题,还能避免备份节点长时间阻塞等待客户端请求被执行。
|
||||
|
||||
最后,需要你注意的是,了解Raft的同学应该知道,领导者的选举和日志提交,都是由集群的节点来完成的。但在PBFT中,客户端参与了拜占庭容错的实现,比如,客户端实现定时器,等待接收来自备份节点的响应,并且如果等待超时,发送请求给所有节点,我希望你能注意到这点。
|
||||
|
||||
## 内容小结
|
||||
|
||||
本节课我主要带你了解了PBFT是如何替换作恶的领导者的。我希望你明确这样几个重点。
|
||||
|
||||
1.客户端通过等待f+1个相同响应消息超时,来发现主节点可能在作恶,此时客户端发送客户端请求给所有集群节点,从而触发可能的视图变更。
|
||||
|
||||
2.与Raft在领导者选举期间服务不可用类似,在视图变更时,PBFT集群也是无法提供服务的。
|
||||
|
||||
因为本讲是PBFT算法的最后一讲,所以我想多说几句。
|
||||
|
||||
首先,在一般情况下,每个节点都需要持久化保存状态数据(比如准备消息),以便在后面使用。但随着系统运行,数据就会越来越多,最终肯定会出现存储空间不足的情况。那么,怎么解决这个问题?
|
||||
|
||||
答案是检查点(checkpoint)机制。PBFT实现了检查点,来定时清理节点本地缓存的但已经不再需要的历史数据(比如预准备消息、准备消息和提交消息),节省了本地的存储空间,并不会影响系统的运行。
|
||||
|
||||
其次,我们都知道基于数字签名的加解密,是非常消耗性能,这也是为什么在一些对加解密要求高的场景中,大家常直接在硬件中实现加解密,比如IPSEC VPN。如果在PBFT中,所有消息都是签名消息,那么肯定非常消耗性能,会极大制约PBFT算法的落地场景。那么,有什么办法优化这个问题呢?
|
||||
|
||||
答案是将数字签名和消息验证码(MAC)混合着使用。具体来说就是,在PBFT中,只有视图变更消息和新视图消息采用了签名消息,其他消息采用的是消息验证码,这样一来,就节省了大量的加解密的性能开销。
|
||||
|
||||
最后,PBFT是一个能在实际场景中落地的拜占庭容错算法,它和区块链也结合紧密,具体来说的话,有这么几种应用。
|
||||
|
||||
- 相对可信、有许可限制的联盟链,比如Hyperledger Sawtooth。
|
||||
- 与其他拜占庭容错算法结合起来,落地公有链。比如Zilliqa,将POW算法和PBFT结合起来,实现公有链的共识协商。具体来说,POW算法作为认证,证明节点不是“坏人”,PBFT来实现共识。针对PBFT消息数过多、不适应大型分布式系统的痛点,Zilliqa实现了分片(Sharding)技术。
|
||||
|
||||
另外,也有团队因为PBFT消息数过多、不适应大型分布式系统的痛点,放弃使用PBFT,通过法律来约束“节点作恶”的行为,比如IBM的Hyperledger Fabric。那么我想说的是,技术是发展的,适合的才是最好的,所以,我建议你根据场景的可信度,来决定是否采用PBFT算法,是否改进和优化PBFT算法。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
既然我提到在PBFT中,PBFT是通过视图变更来选举出新的主节点的。那么你不妨想想,集群是在视图变更时,能否继续处理来自客户端的写请求呢?为什么呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这节课让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
119
极客时间专栏/分布式协议与算法实战/协议和算法篇/加餐 | TCC如何实现指令执行的原子性?.md
Normal file
119
极客时间专栏/分布式协议与算法实战/协议和算法篇/加餐 | TCC如何实现指令执行的原子性?.md
Normal file
@@ -0,0 +1,119 @@
|
||||
<audio id="audio" title="加餐 | TCC如何实现指令执行的原子性?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f8/58/f80byyac12e279ac38044ec6ca05af58.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
在上一讲我提到,虽然MySQL XA能实现数据层的分布式事务,解决多个MySQL操作的事务问题,但我现在负责的这套业务系统还面临别的问题:在接收到外部的指令后,我需要访问多个内部系统,执行指令约定的操作,而且,还必须保证指令执行的原子性(也就是事务要么全部成功,要么全部失败)。
|
||||
|
||||
那么我是如何实现指令执行的原子性呢?答案是TCC。
|
||||
|
||||
在我看来,上一讲中,基于二阶段提交协议的XA规范,实现的是数据层面操作的事务,而TCC能实现业务层面操作的事务。
|
||||
|
||||
对你来说,理解了二阶段提交协议和TCC后,你可以从数据层面到业务层面,更加全面理解如何实现分布式事务了,这样一来,当你在日常工作中,需要实现操作的原子性或者系统状态的一致性时,就知道该如何处理了。
|
||||
|
||||
那么为了帮助你更好地理解TCC,咱们还是先来看一道思考题。
|
||||
|
||||
我以如何实现订票系统为例,假设现在要实现一个企鹅订票系统,给内部员工提供机票订购服务,但在实现订票系统时,我们需要考虑这样的情况:
|
||||
|
||||
我想从深圳飞北京,但这时没有直达的机票,要先定深圳航空的航班,从深圳去上海,然后再定上海航空的航班,从上海去北京。
|
||||
|
||||
因为我的目的地是北京,所以如果只有一张机票订购成功,肯定是不行的,这个系统必须保障2个订票操作的事务要么全部成功,要么全部不成功。那么该如何实现2个订票操作的事务呢?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7c/3b/7ca6e1def38b2ba6873711d8f283dc3b.jpg" alt="">
|
||||
|
||||
带着这个问题,我们进入今天的学习,先来了解一下什么是TCC。
|
||||
|
||||
## 什么是TCC?
|
||||
|
||||
在[04讲](https://time.geekbang.org/column/article/200717),我们介绍了TCC,你如果对TCC不熟悉,或者忘记了,那么可以回过头复习一下。在这里,我只想补充一点,那就是:你可以对比二阶段提交协议来理解,TCC包含的预留、确认或撤销这 2 个阶段,比如:
|
||||
|
||||
<li>
|
||||
Try是指预留,它和二阶段提交协议中,提交请求阶段的操作类似,具体来说就是,系统会将需要确认的资源预留、锁定,确保确认操作一定能执行成功。
|
||||
</li>
|
||||
<li>
|
||||
Confirm是指确认,它呢和二阶段提交协议中,提交执行阶段的操作类似,具体是指,系统将最终执行的操作。
|
||||
</li>
|
||||
<li>
|
||||
Cancel是指撤销,比较像二阶段提交协议中的回滚操作,具体指系统将撤销之前预留的资源,也就是撤销已执行的预留操作对系统产生的影响。
|
||||
</li>
|
||||
|
||||
在我看来,二阶段提交协议和TCC的目标,都是为了实现分布式事务,这也就决定了它们“英雄所见略同”,在思想上是类似的,但我再次强调一下,这两个算法解决的问题场景是不同的,一个是数据层面,一个是业务层面,这就决定了它们在细节实现是不同的。所以接下来,我们就一起看看TCC的细节。
|
||||
|
||||
为了更好地演示TCC的原理,我们假设深圳航空、上海航空分别为订票系统提供了以下3个接口:机票预留接口、确认接口和撤销接口。
|
||||
|
||||
那么这时,订票系统可以这样来实现操作的事务:
|
||||
|
||||
首先,订票系统调用2个航空公司的机票预留接口,向2个航空公司申请机票预留。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e1/74/e149ed696a360b1761c6571a2a96d374.jpg" alt="">
|
||||
|
||||
如果两个机票都预留成功,那么订票系统将执行确认操作,也就是订购机票。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8e/3e/8ea845eeb2032e7af2876ace81605a3e.jpg" alt="">
|
||||
|
||||
但如果此时有机票没有预留成功(比如深圳航空的从深圳到上海的机票),那这时该怎么办呢?这时订票系统就需要通过撤销接口来撤销订票请求。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/52/c0/52f65a9202f3619dfeac1ba0e2acf8c0.jpg" alt="">
|
||||
|
||||
你看这样,我们就实现了订票操作的事务了。TCC是不是也很容易理解呢?答案是肯定的,那它难在哪儿呢?
|
||||
|
||||
在我看来,TCC的难点不在于理解TCC的原理,而在于如何根据实际场景特点来实现预留、确认、撤销三个操作。所以,为帮助你更深刻的理解TCC三操作的实现要点,我将以一个实际项目具体说一说。
|
||||
|
||||
## 如何通过TCC指令执行的原子性?
|
||||
|
||||
我在一开始提到,当我接收到外部指令时,需要实现操作1、2、3,其中任何一个操作失败,我都需要暂停指令执行,将系统恢复到操作未执行状态,然后再重试。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ea/07/eab5cfdf9a5d622f29ced94a4790c207.jpg" alt="">
|
||||
|
||||
其中,操作1、2、3的含义具体如下。
|
||||
|
||||
- 操作1:生成指定URL页面对应的图片,并持久化存储。
|
||||
- 操作2:调用内部系统1的接口,禁用指定域名的访问权限。
|
||||
- 操作3:通过MySQL XA更新多个数据库的数据记录。
|
||||
|
||||
那么我是如何使用TCC来解决这个问题的呢?答案是我在实现每个操作时,都会分别实现相应的预留、确认、撤销三操作。
|
||||
|
||||
首先,因为操作1是生成指定URL页面对应的图片,我是这么实现TCC三操作的。
|
||||
|
||||
- 预留操作:生成指定页面的图片,并存储到本地。
|
||||
- 确认操作:更新操作1状态为完成。
|
||||
- 撤销操作:删除本地存储的图片。
|
||||
|
||||
其次,因为操作2是调用内部系统1的接口,禁用该域名的访问权限,那么,我是这么实现TCC三操作的。
|
||||
|
||||
- 预留操作:调用的内部系统1的禁用指定域名的预留接口。这时我们先通知系统1预留相关的资源。
|
||||
- 确认操作:调用的内部系统1的禁用指定域名的确认接口。我们执行禁用域名的操作,这时,禁用域名的操作的生效了。
|
||||
- 撤销操作:调用的内部系统1的禁用指定域名的撤销接口。我们撤销对该域名的禁用,并通知内部系统1释放相关的预留资源。
|
||||
|
||||
最后,操作3是通过MySQL XA更改多个MySQL数据库中的数据记录,并实现数据更新的事务。我是这么实现TCC三操作的:
|
||||
|
||||
- 预留操作:执行XA START和XA END准备好事务分支操作,并调用XA PREPARE,执行二阶段提交协议的提交请求阶段,预留相关资源。
|
||||
- 确认操作:调用XA COMMIT执行确认操作。
|
||||
- 撤销操作:调用XA ROLLBACK执行回滚操作,释放在Try阶段预留的资源。
|
||||
|
||||
在这里,你可以看到,确认操作是预留操作的下一个操作,而撤销操作则是用来撤销已执行的预留操作对系统产生的影响,类似在复制粘贴时,我们通过“Ctrl Z”撤销“Ctrl V”操作的执行,就像下图的样子。而这是理解TCC的关键,我希望你能注意到。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8b/1b/8b5fbc120651924ffccc862e5bafyy1b.jpg" alt="">
|
||||
|
||||
这样一来,在操作1、2、3的预留操作执行结束,如果预留操作都执行成功了,那么我将执行确认操作,继续向下执行。但如果预留操作只是部分执行成功,那么我将执行撤销操作,取消预留操作执行对系统产生的影响。通过这种方式(指令对应的操作要么全部执行,要么全部不执行),我就能实现指令执行的原子性了。
|
||||
|
||||
另外,在实现确认、撤销操作时,有一点需要我们尤为注意,因为这两个操作在执行时可能会重试,所以,它们需要支持冥等性。
|
||||
|
||||
## 内容小结
|
||||
|
||||
本节课我主要带你了解了TCC,以及如何使用TCC实现分布式事务。我希望你明确这样几个重点。
|
||||
|
||||
1.TCC是个业务层面的分布式事务协议,而XA规范是数据层面的分布式事务协议,这也是TCC和XA规范最大的区别。
|
||||
|
||||
2.TCC与业务耦合紧密,在实际场景使用时,需要我们根据场景特点和业务逻辑来设计相应的预留、确认、撤销操作,相比MySQL XA,有一定的编程开发工作量。
|
||||
|
||||
3.本质上而言,TCC是一种设计模式,也就是一种理念,它没有与任何技术(或实现)耦合,也不受限于任何技术,对所有的技术方案都是适用的。
|
||||
|
||||
最后,我想补充的是,因为TCC是在业务代码中编码实现的,所以,TCC可以跨数据库、跨业务系统实现资源管理,满足复杂业务场景下的事务需求,比如,TCC可以将对不同的数据库、不同业务系统的多个操作通过编码方式,转换为一个原子操作,实现事务。
|
||||
|
||||
另外,因为TCC 的每一个操作对于数据库来讲,都是一个本地数据库事务,那么当操作结束时,本地数据库事务的执行也就完成了,所以相关的数据库资源也就被释放了,这就能避免数据库层面的二阶段提交协议长时间锁定资源,导致系统性能低下的问题。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
我提到了自己通过TCC解决了指令执行的原子性问题。那么你不妨想想,为什么TCC能解决指令执行的原子性问题呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这节课让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
255
极客时间专栏/分布式协议与算法实战/协议和算法篇/加餐 | ZAB协议(一):主节点崩溃了,怎么办?.md
Normal file
255
极客时间专栏/分布式协议与算法实战/协议和算法篇/加餐 | ZAB协议(一):主节点崩溃了,怎么办?.md
Normal file
@@ -0,0 +1,255 @@
|
||||
<audio id="audio" title="加餐 | ZAB协议(一):主节点崩溃了,怎么办?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/15/aa/15aad23d5d73bef0c1b20c0458c529aa.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
咱们都知道,系统在运行中,不可避免会出现各种各样的问题,比如进程崩溃了、服务器死机了,这些问题会导致很严重的后果,让系统没办法运行。学完了15讲后,你应该还记得,在ZAB中,写请求是必须在主节点上处理的,而且提案的广播和提交,也是由主节点来完成的。既然主节点那么重要,如果它突然崩溃宕机了,该怎么办呢?
|
||||
|
||||
答案是选举出新的领导者(也就是新的主节点)。
|
||||
|
||||
在我看来,领导者选举,关乎着节点故障容错能力和集群可用性,是ZAB协议非常核心的设计之一。你想象一下,如果没有领导者选举,主节点故障了,整个集群都无法写入了,这将是极其严重的灾难性故障。
|
||||
|
||||
而对你来说,理解领导者选举(也就是快速领导者选举,Fast Leader Election),能更加深刻地理解ZAB协议,并在日常工作中,游刃有余地处理集群的可用性问题。比如如果写请求持续失败,可以先排查下集群的节点状态。
|
||||
|
||||
既然领导者选举这么重要,那么ZAB是如何选举领导者的呢?带着这个问题,我们进入今天的学习。
|
||||
|
||||
## ZAB如何选举领导者?
|
||||
|
||||
既然要选举领导者,那就涉及成员身份变更,那么在ZAB中,支持哪些成员身份呢?
|
||||
|
||||
### 有哪些成员身份?
|
||||
|
||||
ZAB支持3种成员身份(领导者、跟随者、观察者)。
|
||||
|
||||
<li>
|
||||
领导者(Leader): 作为主(Primary)节点,在同一时间集群只会有一个领导者。需要你注意的是,所有的写请求都必须在领导者节点上执行。
|
||||
</li>
|
||||
<li>
|
||||
跟随者(Follower):作为备份(Backup)节点, 集群可以有多个跟随者,它们会响应领导者的心跳,并参与领导者选举和提案提交的投票。需要你注意的是,跟随者可以直接处理并响应来自客户端的读请求,但对于写请求,跟随者需要将它转发给领导者处理。
|
||||
</li>
|
||||
<li>
|
||||
观察者(Observer):作为备份(Backup)节点,类似跟随者,但是没有投票权,也就是说,观察者不参与领导者选举和提案提交的投票。你可以对比着Paxos中的学习者来理解。
|
||||
</li>
|
||||
|
||||
需要你注意的是,虽然ZAB支持3种成员身份,但是它定义了4种成员状态。
|
||||
|
||||
- LOOKING:选举状态,该状态下的节点认为当前集群中没有领导者,会发起领导者选举。
|
||||
- FOLLOWING :跟随者状态,意味着当前节点是跟随者。
|
||||
- LEADING :领导者状态,意味着当前节点是领导者。
|
||||
- OBSERVING: 观察者状态,意味着当前节点是观察者。
|
||||
|
||||
为什么多了一种成员状态呢?这是因为ZAB支持领导者选举,在选举过程中,涉及了一个过渡状态(也就是选举状态)。
|
||||
|
||||
现在,你已经了解了成员身份,那么在ZAB中是如何变更成员身份,来选举领导者呢?接下来,我们就来看一下领导者的具体选举过程。
|
||||
|
||||
### 如何选举?
|
||||
|
||||
为了帮你更好地理解ZAB的领导者选举,我举个例子演示一下,为了演示方便和更容易理解(我们聚焦最核心的领导者PK),假设投票信息的格式是<proposedLeader, proposedEpoch, proposedLastZxid,node>,其中:
|
||||
|
||||
- proposedLeader,节点提议的,领导者的集群ID,也就是在集群配置(比如myid配置文件)时指定的ID。
|
||||
- proposedEpoch,节点提议的,领导者的任期编号。
|
||||
- proposedLastZxid,节点提议的,领导者的事务标识符最大值(也就是最新提案的事务标识符)。
|
||||
- node,投票的节点,比如节点B。
|
||||
|
||||
假设一个ZooKeeper集群,由节点A、B、C组成,其中节点A是领导者,节点B、C是跟随者(为了方便演示,假设epoch分别是1和1,lastZxid分别是101和102,集群ID分别为2和3)。那么如果节点A宕机了,会如何选举呢?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/46/e5/4667ae63fbdbc613e1d94667544bd0e5.jpg" alt="" title="图1">
|
||||
|
||||
首先,当跟随者检测到连接领导者节点的读操作等待超时了,跟随者会变更节点状态,将自己的节点状态变更成LOOKING,然后发起领导者选举(为了演示方便,我们假设这时节点B、C都已经检测到了读操作超时):<br>
|
||||
<img src="https://static001.geekbang.org/resource/image/2b/23/2bc48cf03938cc37d3513239da847c23.jpg" alt="" title="图2">
|
||||
|
||||
接着,每个节点会创建一张选票,这张选票是投给自己的,也就是说,节点B、C都“自告奋勇”推荐自己为领导者,并创建选票<2, 1, 101, B>和<3, 1, 102, C>,然后各自将选票发送给集群中所有节点,也就是说,B发送给B、C,C也发送给B、C。
|
||||
|
||||
一般而言,节点会先接收到自己发送给自己的选票(因为不需要跨节点通讯,传输更快),也就是说,B会先收到来自B的选票,C会先收到来自C的选票:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e3/06/e32a08ca54943c96fb415a77f2fe7a06.jpg" alt="" title="图3">
|
||||
|
||||
需要你注意的是,集群的各节点收到选票后,为了选举出数据最完整的节点,对于每一张接收到选票,节点都需要进行领导者PK,也就将选票提议的领导者和自己提议的领导者进行比较,找出更适合作为领导者的节点,约定的规则如下:
|
||||
|
||||
- 优先检查任期编号(Epoch),任期编号大的节点作为领导者;
|
||||
- 如果任期编号相同,比较事务标识符的最大值,值大的节点作为领导者;
|
||||
- 如果事务标识符的最大值相同,比较集群ID,集群ID大的节点作为领导者。
|
||||
|
||||
如果选票提议的领导者,比自己提议的领导者,更适合作为领导者,那么节点将调整选票内容,推荐选票提议的领导者作为领导者。
|
||||
|
||||
当节点B、C接收到的选票后,因为选票提议的领导者与自己提议的领导者相同,所以,领导者PK的结果,是不需要调整选票信息,那么节点B、C,正常接收和保存选票就可以了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4c/96/4c0546cfe2dbc11ed99cab414ec12e96.jpg" alt="" title="图4">
|
||||
|
||||
接着节点B、C分别接收到来自对方的选票,比如B接收到来自C的选票,C接收到来自B的选票:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5d/d5/5d3fe6b30854490faf096826c62df5d5.jpg" alt="" title="图5">
|
||||
|
||||
对于C而言,它提议的领导者是C,而选票(<2, 1, 101, B>)提议的领导者是B,因为节点C的任期编号与节点B相同,但节点C的事务标识符的最大值比节点B的大,那么,按照约定的规则,相比节点B,节点C更适合作为领导者,也就是说,节点C不需要调整选票信息,正常接收和保存选票就可以了。
|
||||
|
||||
但对于对于节点B而言,它提议的领导者是B,选票(<3, 1, 102, C>)提议的领导者是C,因为节点C的任期编号与节点B相同,但节点C的事务标识符的最大值比节点B的大,那么,按照约定的规则,相比节点B,节点C应该作为领导者,所以,节点B除了接收和保存选票信息,还会更新自己的选票为<3, 1, 102, B>,也就是推荐C作为领导者,并将选票重新发送给节点B、C:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bf/77/bf70a4f1e9c28de2bbd3c134193ae577.jpg" alt="" title="图6">
|
||||
|
||||
接着,当节点B、C接收到来自节点B,新的选票时,因为这张选票(<3, 1, 102, B>)提议的领导者,与他们提议的领导者是一样的,都是节点C,所以,他们正常接收和存储这张选票,就可以。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/04/7c/04ec7fdf75160571c8689f040292d57c.jpg" alt="" title="图7">
|
||||
|
||||
最后,因为此时节点B、C提议的领导者(节点C)赢得大多数选票了(2张选票),那么,节点B、C将根据投票结果,变更节点状态,并退出选举。比如,因为当选的领导者是节点C,那么节点B将变更状态为FOLLOWING,并退出选举,而节点C将变更状态为LEADING,并退出选举。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a9/03/a9113e0d2653c77ddc41778510784003.jpg" alt="" title="图8">
|
||||
|
||||
你看,这样我们就选举出新的领导者(节点C),这个选举的过程,很容易理解,我在这里只是假设了一种选举的情况,还会存在节点间事务标识符相同、节点在广播投票信息前接收到了其他节点的投票等情况,这里你可以思考一下,课下自己动手操作一下。
|
||||
|
||||
为了帮你在线下更好的阅读代码,自我学习,我想补充一下,逻辑时钟(logicclock)(也就是选举的轮次),会影响选票的有效性,具体来说,逻辑时钟大的节点不会接收来自值小的节点的投票信息。比如,节点A、B的逻辑时钟分别为1和2,那么,节点B将拒绝接收来自节点A的投票信息。
|
||||
|
||||
在这里,我想强调的是,领导者选举的目标,是从大多数节点中选举出数据最完整的节点,也就是大多数节点中,事务标识符值最大的节点。**另外,ZAB本质上是通过“见贤思齐,相互推荐”的方式来选举领导者的。**也就说,根据领导者PK,节点会重新推荐更合适的领导者,最终选举出了大多数节点中数据最完整的节点。
|
||||
|
||||
当然了,文字和代码是2种不同的表达,一些细节,仅仅通过文字是无法表达出来的,所以,为了帮你更通透地理解领导者选举的实现,接下来,我将以最新稳定版的[ZooKeeper](https://downloads.apache.org/zookeeper/zookeeper-3.6.0/apache-zookeeper-3.6.0.tar.gz)为例(也就是3.6.0),具体说一说代码的实现。
|
||||
|
||||
## ZooKeeper是如何实现的?
|
||||
|
||||
首先,我们来看看,在ZooKeeper中是如何实现成员身份的?
|
||||
|
||||
在ZooKeeper中,成员状态是在QuorumPeer.java中实现的,为枚举型变量,就像下面的样子。
|
||||
|
||||
```
|
||||
public enum ServerState {
|
||||
LOOKING,
|
||||
FOLLOWING,
|
||||
LEADING,
|
||||
OBSERVING
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
其实,在ZooKeeper中,没有直接定义成员身份,而是用了对应的成员状态来表示,比如,处于FOLLOWING状态的节点为跟随者。
|
||||
|
||||
在这里,我想补充一点,如果你想研究相关成员的功能和实现,那么你可以把对应的成员状态作为切入点来研究,比如,你想研究领导者的功能实现,那么,你可以在代码中搜索LEADING关键字,然后研究相应的上下文逻辑,就能得到自己想要的答案了。
|
||||
|
||||
如果跟随者将自己的状态从跟随者状态变更为选举状态,这就表示跟随者在发起领导者选举,那么,在ZooKeeper中,领导者选举是如何实现的呢?
|
||||
|
||||
## 如何实现选举?
|
||||
|
||||
领导者选举,是在FastLeaderElection.lookForLeader()中实现的。我来讲一下核心的流程,大概的流程,就像下面的样子。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/83/57/83c37bc2a25a4e892cf1a5c3a2c6c457.jpg" alt="" title="图9">
|
||||
|
||||
为帮助你更好的理解这个流程,我们来一起走读下核心代码,加深下印象。
|
||||
|
||||
1.在集群稳定运行时,处于跟随者状态的节点,在Follower.followLeader()函数中,周期性地读数据包和处理数据包:
|
||||
|
||||
```
|
||||
QuorumPacket qp = new QuorumPacket();
|
||||
while (this.isRunning()) {
|
||||
// 读取数据包
|
||||
readPacket(qp);
|
||||
// 处理数据包
|
||||
processPacket(qp);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
2.当跟随者检测到连接到领导者的读操作超时了(比如领导者节点故障了),这时会抛出异常(Exception),跳出上面的读取数据包和处理数据包的循环,并最终跟随者将节点状态变更为选举状态。
|
||||
|
||||
```
|
||||
public void run() {
|
||||
case FOLLOWING:
|
||||
......
|
||||
finally {
|
||||
// 关闭跟随者节点
|
||||
follower.shutdown();
|
||||
setFollower(null);
|
||||
// 设置状态为选举状态
|
||||
updateServerState();
|
||||
}
|
||||
break;
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
3.当节点处于选举状态时,将调用makeLEStrategy().lookForLeader()函数(实际对应的函数为FastLeaderElection.lookForLeader()),发起领导者选举。
|
||||
|
||||
```
|
||||
setCurrentVote(makeLEStrategy().lookForLeader());
|
||||
|
||||
```
|
||||
|
||||
4.在FastLeaderElection.lookForLeader()函数中,节点需要对逻辑时钟(也就是选举的轮次)的值执行加1操作,表示我们开启一轮的领导者选举,然后创建投票提案(默认推荐自己为领导者),并通知所有节点:
|
||||
|
||||
```
|
||||
synchronized (this) {
|
||||
// 对逻辑时钟的值执行加一操作
|
||||
logicalclock.incrementAndGet();
|
||||
// 创建投票提案,并默认推荐自己为领导者
|
||||
updateProposal(getInitId(), getInitLastLoggedZxid(),
|
||||
getPeerEpoch());
|
||||
}
|
||||
// 广播投票信息给所有节点
|
||||
sendNotifications();
|
||||
|
||||
```
|
||||
|
||||
5.当节点处于选举状态时,会周期性地从队列中读取接收到的投票信息,直到选举成功。
|
||||
|
||||
```
|
||||
while ((self.getPeerState() == ServerState.LOOKING) && (!stop)) {
|
||||
// 从队列中读取接收到的投票信息
|
||||
Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS);
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
6.当接收到新的投票信息时,节点会进行领导者PK,来判断谁更适合当领导者,如果投票信息中提议的节点比自己提议的节点,更适合当领导者,更新投票信息,推荐投票信息中提议的节点作为领导者,并广播给所有节点:
|
||||
|
||||
```
|
||||
else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) {
|
||||
// 投票信息中提议的节点比自己提议的节点更适合作为领导者,更新投票信息,并推荐投票信息中提议的节点
|
||||
updateProposal(n.leader, n.zxid, n.peerEpoch);
|
||||
// 将新的投票信息广播给所有节点
|
||||
sendNotifications();
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
7.如果自己提议的领导者赢得大多数选票,则执行步骤8,变更节点状态,退出选举;如果自己提议的领导者仍未赢得大多数选票,则执行步骤5,继续从接收队列中读取新的投票信息。
|
||||
|
||||
8.最后,当节点提议的领导者赢得大多数选票时,根据投票结果,判断当前节点的状态,领导者或跟随者,并变更节点状态,退出选举。
|
||||
|
||||
```
|
||||
if (voteSet.hasAllQuorums()) {
|
||||
......
|
||||
// 根据投票结果,判断并设置节点状态
|
||||
setPeerState(proposedLeader, voteSet);
|
||||
// 退出领导者选举
|
||||
Vote endVote = new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch);
|
||||
leaveInstance(endVote);
|
||||
return endVote;
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
需要你注意的是,在这里,我们只是演示一种选举情况,更多的情况,比如接收到来自逻辑时钟比当前节点小的节点的投票信息,再比如接收到来自领导者的投票信息,你可以在课下自己研究一下,遇到问题时,欢迎留言,咱们一起讨论。
|
||||
|
||||
## 内容小结
|
||||
|
||||
本节课我主要带你了解了ZAB是如何选举领导者的,以及在ZooKeeper中是如何实现的。我希望你明确这样几个重点。
|
||||
|
||||
1.领导者选举的目标,是选举出大多数节点中数据最完整的节点,也就是大多数节点中事务标识符值最大的节点。
|
||||
|
||||
2.任期编号、事务标识符最大值、集群ID的值的大小,决定了哪个节点更适合作为领导者,按照顺序,值大的节点更适合作为领导者。
|
||||
|
||||
学到这里,有同学可能会说:“老韩,我研究了一下,领导者是大多数节点中,已提交提案事务标识符最大的节点,因为在领导者选举的实现中,事务标识符采用的是dataTree.lastProcessedZxid的值,而这个变量标记的是已提交提案的事务标识符最大值。到底要怎么理解呢?”
|
||||
|
||||
我要先为你的探索和思考点个赞,我想说的是,在领导者选举的实现中,事务标识符采用的是dataTree.lastProcessedZxid的值。需要你特别注意的是,在跟随者节点正常运行时,dataTree.lastProcessedZxid表示的是已提交提案的事务标识符最大值,但当跟随者检测到异常,退出跟随者状态时(在follower.shutdown()函数中),ZooKeeper会将所有未提交提案提交,并使用lastProcessedZxid表示节点上提案(包括刚提交的提案)的事务标识符的最大值,在接下来的领导者选举中,使用的也是该值,也就是说,ZAB的领导者选举,选举出的是大多数节点中数据最完整的节点。
|
||||
|
||||
为了方便你理解,我举个具体的例子。
|
||||
|
||||
A、B、C三节点,A是领导者,B、C是跟随者,B有2个已提交提案(<1, 1>,<1, 2>),C有4个未提交提案(<1, 1>,<1, 2>,<1, 3>,<1, 4>),那么当A故障后,C就会当选领导者。因为C的dataTree.lastProcessedZxid值(也就是<1, 4>)大于B的dataTree.lastProcessedZxid值(也就是<1, 2>)。
|
||||
|
||||
最后,你可能会好奇,我为啥会写这么多内容,来分析源码实现,除了因为代码也是一种表达,能有效弥补文字的无法表达的内容之外,还因为对于一个软件来说,最准确、最新的使用手册和技术内幕就是源码。我希望你也能养成阅读源码的习惯,将源码和文档结合起来,来准确理解软件的功能和原理。
|
||||
|
||||
选举出了新领导者,它是不是就可以处理写请求了呢?当然不可以,因为ZAB集群还需要通过成员发现(Discovery)和数据同步(Synchronization)来恢复故障,然后领导者才能行使“领导”的职能,处理写请求,这也是我会在下一讲重点带你了解的。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
既然我提到在ZAB协议中,ZAB协议是通过快速领导者选举,来选举出新的领导者的。那么你不妨想想,在选举中,会出现选票被瓜分、选举失败的问题吗?为什么呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这节课让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
264
极客时间专栏/分布式协议与算法实战/协议和算法篇/加餐 | ZAB协议(三):如何处理读写请求?.md
Normal file
264
极客时间专栏/分布式协议与算法实战/协议和算法篇/加餐 | ZAB协议(三):如何处理读写请求?.md
Normal file
@@ -0,0 +1,264 @@
|
||||
<audio id="audio" title="加餐 | ZAB协议(三):如何处理读写请求?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f0/77/f0e15a20227950d5d9881f18552f5b77.mp3"></audio>
|
||||
|
||||
你好,我是韩健!
|
||||
|
||||
你应该有这样的体会,如果你想了解一个网络服务,执行的第一个功能肯定是写操作,然后才执行读操作。比如,你要了解ZooKeeper,那么肯定会在zkCli.sh命令行中执行写操作(比如“create /geekbang 123”)写入数据,然后再是读操作(比如“get /geekbang”)查询数据。这样一来,你才会直观地理解ZooKeeper是如何使用的了。
|
||||
|
||||
在我看来,任何网络服务最重要的功能就是处理读写请求,因为我们访问网络服务本质上都是在执行读写操作,ZooKeeper也不例外。**而且对ZooKeeper而言,这些功能更为重要,因为在ZooKeeper中,如何处理写请求,关乎着操作的顺序性,而操作的顺序性会影响节点的创建;如何处理读请求,关乎着一致性,它们又影响着客户端是否会读到旧数据。**
|
||||
|
||||
接下来,我会从ZooKeeper系统的角度,全面地分析整个读写请求的流程,帮助你更加全面、透彻地理解读写请求背后的原理。
|
||||
|
||||
你肯定知道,在ZooKeeper中,写请求是必须在领导者上处理,如果跟随者接收到了写请求,它需要将写请求转发给领导者,当写请求对应的提案被复制到大多数节点上时,领导者会提交提案,并通知跟随者提交提案。而读请求可以在任何节点上处理,也就是说,ZooKeeper实现的是最终一致性。
|
||||
|
||||
对你来说,理解了如何处理读写请求,不仅能理解读写这个最重要功能的核心原理,还能更好地理解ZooKeeper的性能和一致性。这样一来,当你在实际场景中安装部署ZooKeeper的时候,就能游刃有余地做资源规划了。比如,如果读请求比较多,你可以增加节点,配置5节点集群,而不是常见的3节点集群。
|
||||
|
||||
话不多说,我们进入今天的内容,一起探究ZooKeeper处理读写请求的背后原理和代码实现。
|
||||
|
||||
## ZooKeeper处理读写请求的原理
|
||||
|
||||
其实,我在[15讲](https://time.geekbang.org/column/article/229975)演示“如何实现操作顺序性”时,就已经介绍了ZooKeeper是如何处理读写请求的了。所以在这里我就不啰嗦了,只是在此基础上,再补充几点。
|
||||
|
||||
首先,在ZooKeeper中,与领导者“失联”的节点,是不能处理读写请求的。比如,如果一个跟随者与领导者的连接发生了读超时,设置了自己的状态为LOOKING,那么此时它既不能转发写请求给领导者处理,也不能处理读请求,只有当它“找到”领导者后,才能处理读写请求。
|
||||
|
||||
举个例子:当发生分区故障了,C与A(领导者)、B网络不通了,那么C将设置自己的状态为LOOKING,此时在C节点上既不能执行读操作,也不能执行写操作。<br>
|
||||
<img src="https://static001.geekbang.org/resource/image/22/ad/22dfaa624590885c4b8406deb445afad.jpg" alt="">
|
||||
|
||||
其次,当大多数节点进入到广播阶段的时候,领导者才能提交提案,因为提案提交,需要来自大多数节点的确认。
|
||||
|
||||
最后,写请求只能在领导者节点上处理,所以ZooKeeper集群写性能约等于单机。而读请求是可以在所有的节点上处理的,所以,读性能是能水平扩展的。也就是说,你可以通过分集群的方式来突破写性能的限制,并通过增加更多节点,来扩展集群的读性能。
|
||||
|
||||
熟悉了ZooKeeper处理读写请求的过程和原理后,相信你应该好奇这些功能在ZooKeeper代码中是如何实现的呢?
|
||||
|
||||
## ZooKeeper代码是如何实现读写操作的呢?
|
||||
|
||||
### 如何实现写操作?
|
||||
|
||||
我先来说一说写操作,在ZooKeeper代码中,处理写请求的核心流程就像下图一样(为了帮你更好的理解这部分内容,我来演示一下复杂的情况,也就是跟随者接收到写请求的情况)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c7/8a/c77c241713b154673e15083fd063428a.jpg" alt="">
|
||||
|
||||
接下来,咱们一起走一遍核心代码的流程,加深一下印象。
|
||||
|
||||
1.跟随者在FollowerRequestProcessor.processRequest()中接收到写请求。具体来说,写请求是系统在ZooKeeperServer.submitRequestNow()中发给跟随者的。
|
||||
|
||||
```
|
||||
firstProcessor.processRequest(si);
|
||||
|
||||
```
|
||||
|
||||
而firstProcessor,是在FollowerZooKeeperServer.setupRequestProcessors()中创建的。
|
||||
|
||||
```
|
||||
protected void setupRequestProcessors() {
|
||||
// 创建finalProcessor,提交提案或响应查询
|
||||
RequestProcessor finalProcessor = new FinalRequestProcessor(this);
|
||||
// 创建commitProcessor,处理提案提交或读请求
|
||||
commitProcessor = new CommitProcessor(finalProcessor, Long.toString(getServerId()), true, getZooKeeperServerListener());
|
||||
commitProcessor.start();
|
||||
// 创建firstProcessor,接收发给跟随者的请求
|
||||
firstProcessor = new FollowerRequestProcessor(this, commitProcessor);
|
||||
((FollowerRequestProcessor) firstProcessor).start();
|
||||
// 创建syncProcessor,将提案持久化存储,并返回确认响应给领导者
|
||||
syncProcessor = new SyncRequestProcessor(this, new SendAckRequestProcessor(getFollower()));
|
||||
syncProcessor.start();
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
需要你注意的是,跟随者节点和领导者节点的firstProcessor是不同的,这样当firstProcessor在ZooKeeperServer.submitRequestNow()中被调用时,就分别进入了跟随者和领导者的代码流程。另外,setupRequestProcessors()创建了2条处理链,就像下图的样子。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d5/e7/d5dfec8e64a2d3cb6f905421d5e918e7.jpg" alt="">
|
||||
|
||||
其中,处理链1是核心处理链,最终实现了提案提交和读请求对应的数据响应。处理链2实现了提案持久化存储,并返回确认响应给领导者。
|
||||
|
||||
2.跟随者在FollowerRequestProcessor.run()中将写请求转发给领导者。
|
||||
|
||||
```
|
||||
// 调用learner.request()将请求发送给领导者
|
||||
zks.getFollower().request(request);
|
||||
|
||||
```
|
||||
|
||||
3.领导者在LeaderRequestProcessor.processRequest()中接收写请求,并最终调用pRequest()创建事务(也就是提案),并持久化存储。
|
||||
|
||||
```
|
||||
// 创建事务
|
||||
pRequest2Txn(request.type, zks.getNextZxid(), request, create2Request, true);
|
||||
......
|
||||
// 分配事务标识符
|
||||
request.zxid = zks.getZxid();
|
||||
// 调用ProposalRequestProcessor.processRequest()处理写请求,并将事务持久化存储
|
||||
nextProcessor.processRequest(request);
|
||||
|
||||
```
|
||||
|
||||
在这里,需要你注意的是,写请求也是在ZooKeeperServer.submitRequestNow()中发给领导者的。
|
||||
|
||||
```
|
||||
firstProcessor.processRequest(si);
|
||||
|
||||
```
|
||||
|
||||
而firstProcessor,是在LeaderZooKeeperServer.setupRequestProcessors()中创建的。
|
||||
|
||||
```
|
||||
protected void setupRequestProcessors() {
|
||||
// 创建finalProcessor,最终提交提案和响应查询
|
||||
RequestProcessor finalProcessor = new FinalRequestProcessor(this);
|
||||
// 创建toBeAppliedProcessor,存储可提交的提案,并在提交提案后,从toBeApplied队列移除已提交的
|
||||
RequestProcessor toBeAppliedProcessor = new Leader.ToBeAppliedRequestProcessor(finalProcessor, getLeader());
|
||||
// 创建commitProcessor,处理提案提交或读请求
|
||||
commitProcessor = new CommitProcessor(toBeAppliedProcessor, Long.toString(getServerId()), false, getZooKeeperServerListener());
|
||||
commitProcessor.start();
|
||||
// 创建proposalProcessor,按照顺序广播提案给跟随者
|
||||
ProposalRequestProcessor proposalProcessor = new ProposalRequestProcessor(this, commitProcessor);
|
||||
proposalProcessor.initialize();
|
||||
// 创建prepRequestProcessor,根据请求创建提案
|
||||
prepRequestProcessor = new PrepRequestProcessor(this, proposalProcessor);
|
||||
prepRequestProcessor.start();
|
||||
// 创建firstProcessor,接收发给领导者的请求
|
||||
firstProcessor = new LeaderRequestProcessor(this, prepRequestProcessor);
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
需要你注意的是,与跟随者类似,setupRequestProcessors()给领导者也创建了2条处理链(其中处理链2是在创建proposalRequestProcessor时创建的)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f7/83/f780e2d141a766579ba20c3b3d0a2283.jpg" alt="">
|
||||
|
||||
其中,处理链1是核心处理链,最终实现了写请求处理(创建提案、广播提案、提交提案)和读请求对应的数据响应。处理链2实现了提案持久化存储,并返回确认响应给领导者自己。
|
||||
|
||||
4.领导者在ProposalRequestProcessor.processRequest()中,调用propose()将提案广播给集群所有节点。
|
||||
|
||||
```
|
||||
zks.getLeader().propose(request);
|
||||
|
||||
```
|
||||
|
||||
5.跟随者在Follower.processPacket()中接收到提案,持久化存储,并返回确认响应给领导者。
|
||||
|
||||
```
|
||||
// 接收、持久化、返回确认响应给领导者
|
||||
fzk.logRequest(hdr, txn, digest);
|
||||
|
||||
```
|
||||
|
||||
6.当领导者接收到大多数节点的确认响应(Leader.processAck())后,最终在CommitProcessor.tryToCommit()提交提案,并广播COMMIT消息给跟随者。
|
||||
|
||||
```
|
||||
// 通知跟随者提交
|
||||
commit(zxid);
|
||||
// 自己提交
|
||||
zk.commitProcessor.commit(p.request);
|
||||
|
||||
```
|
||||
|
||||
7.当跟随者接收到COMMIT消息后,在FollowerZooKeeperServer.commit()中提交提案,如果最初的写请求是自己接收到的,返回成功响应给客户端。
|
||||
|
||||
```
|
||||
// 必须顺序提交
|
||||
long firstElementZxid = pendingTxns.element().zxid;
|
||||
if (firstElementZxid != zxid) {
|
||||
LOG.error("Committing zxid 0x" + Long.toHexString(zxid)
|
||||
+ " but next pending txn 0x" +
|
||||
Long.toHexString(firstElementZxid));
|
||||
ServiceUtils.requestSystemExit(ExitCode.UNMATCHED_TXN_COMMIT.getValue());
|
||||
}
|
||||
// 将准备提交的提案从pendingTxns队列移除
|
||||
Request request = pendingTxns.remove();
|
||||
request.logLatency(ServerMetrics.getMetrics().COMMIT_PROPAGATION_LATENCY);
|
||||
// 最终调用FinalRequestProcessor.processRequest()提交提案,并如果最初的写请求是自己接收到的,返回成功响应给客户端
|
||||
commitProcessor.commit(request);
|
||||
|
||||
```
|
||||
|
||||
这样,ZooKeeper就完成了写请求的处理。你要特别注意一下,在分布式系统中,消息或者核心信息的持久化存储很关键,也很重要,因为这是保证集群能稳定运行的关键。
|
||||
|
||||
当然了,写入数据,最终还是为了后续的数据读取,那么在ZooKeeper中,是如何实现读操作的呢?
|
||||
|
||||
### 如何实现读操作?
|
||||
|
||||
相比写操作,读操作的处理要简单很多,因为接收到读请求的节点,只需要查询本地数据,然后响应数据给客户端就可以了。读操作的核心代码流程,如图所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f4/6d/f405d2a81f374e6e63b49c469506f26d.jpg" alt="">
|
||||
|
||||
咱们一起走一遍核心代码的流程,加深一下印象。
|
||||
|
||||
1.跟随者在FollowerRequestProcessor.processRequest()中接收到读请求。
|
||||
|
||||
2.跟随者在FinalRequestProcessor.processRequest()中查询本地数据,也就是dataTree中的数据。
|
||||
|
||||
```
|
||||
// 处理读请求
|
||||
case OpCode.getData: {
|
||||
......
|
||||
// 查询本地dataTree中的数据
|
||||
rsp = handleGetDataRequest(getDataRequest, cnxn, request.authInfo);
|
||||
......
|
||||
break;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
3.然后跟随者响应查询到数据给客户端。
|
||||
|
||||
```
|
||||
case OpCode.getData : {
|
||||
......
|
||||
// 响应查询到的数据给客户端
|
||||
cnxn.sendResponse(hdr, rsp, "response", path, stat, opCode);
|
||||
break;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
你看,这样ZooKeeper就完成读操作的处理。在这里,我想补充一点,你可以dataTree理解为Raft的状态机,提交的数据,最终存放在dataTree中。
|
||||
|
||||
## 内容小结
|
||||
|
||||
本节课我主要带你了解了ZooKeeper处理读写请求的过程,以及ZooKeeper的代码实现和核心流程。我希望你明确这样几个重点。
|
||||
|
||||
1.与领导者“失联”的跟随者(比如发生分区故障时),是既不能处理写请求,也不能处理读请求的。
|
||||
|
||||
2.在ZooKeeper中,写请求只能在领导者节点上处理,读请求可以在所有节点上处理,实现的是最终一致性。
|
||||
|
||||
因为本讲是ZAB协议的最后一讲,为了帮你后续学习本课程没有提到的内容,我想补充几点。
|
||||
|
||||
首先,ZAB的术语众多,而且有些术语表达的是同一个含义,这些术语有些在文档中出现,有些在代码中出现。而你只有准确理解术语,才能更好地理解ZAB协议的原理,所以,我补充一些内容。
|
||||
|
||||
- 提案(Proposal):进行共识协商的基本单元,你可以理解为操作(Operation)或指令(Command),常出现在文档中。
|
||||
- 事务(Transaction):也是指提案,常出现在代码中。比如,pRequest2Txn()将接收到的请求转换为事务;再比如,未提交提案会持久化存储在事务日志中。在这里需要你注意的是,这个术语很容易引起误解,因为它不是指更广泛被接受的含义,具有ACID特性的操作序列。
|
||||
|
||||
其次,在我看来,Raft算法和ZAB协议很类似,比如主备模式(也就是领导者、跟随者模型)、日志必须是连续的、以领导者的日志为准来实现日志一致等等。那为什么它们会比较类似呢?
|
||||
|
||||
**我的看法是,“英雄所见略同”。**比如ZAB协议要实现操作的顺序性,而Raft的设计目标,不仅仅是操作的顺序性,而是线性一致性,这两个目标,都决定了它们不能允许日志不连续,要按照顺序提交日志,那么,它们就要通过上面的方法实现日志的顺序性,并保证达成共识(也就是提交)后的日志不会再改变。
|
||||
|
||||
最后,我想就ZAB和Raft做个对比,来具体说说ZAB和Raft的异同。既然我们要做对比,那么首先要定义对比标准,我是这么考虑的:你应该有这样的体会,同一个功能,不同的同学实现的代码都会不一样(比如数据结构、代码逻辑),所以过于细节的比较,尤其是偏系统实现方面的,意义不大(比如跟随者是否转发写请求到领导者,不仅意义不大,而且这是ZAB和Raft都没有约定的,是集群系统需要考虑的),我们可以从核心原理上做对比。
|
||||
|
||||
<li>
|
||||
领导者选举:ZAB采用的“见贤思齐、相互推荐”的快速领导者选举(Fast Leader Election),Raft采用的是“一张选票、先到先得”的自定义算法。在我看来,Raft的领导者选举,需要通讯的消息数更少,选举也更快。
|
||||
</li>
|
||||
<li>
|
||||
日志复制:Raft和ZAB相同,都是以领导者的日志为准来实现日志一致,而且日志必须是连续的,也必须按照顺序提交。
|
||||
</li>
|
||||
<li>
|
||||
读操作和一致性:ZAB的设计目标是操作的顺序性,在ZooKeeper中默认实现的是最终一致性,读操作可以在任何节点上执行;而Raft的设计目标是强一致性(也就是线性一致性),所以Raft更灵活,Raft系统既可以提供强一致性,也可以提供最终一致性。
|
||||
</li>
|
||||
<li>
|
||||
写操作:Raft和ZAB相同,写操作都必须在领导者节点上处理。
|
||||
</li>
|
||||
<li>
|
||||
成员变更:Raft和ZAB都支持成员变更,其中ZAB以动态配置(dynamic configuration)的方式实现的。那么当你在节点变更时,不需要重启机器,集群是一直运行的,服务也不会中断。
|
||||
</li>
|
||||
<li>
|
||||
其他:相比ZAB,Raft的设计更为简洁,比如Raft没有引入类似ZAB的成员发现和数据同步阶段,而是当节点发起选举时,递增任期编号,在选举结束后,广播心跳,直接建立领导者关系,然后向各节点同步日志,来实现数据副本的一致性。**在我看来,ZAB的成员发现,可以和领导者选举合到一起,类似Raft,在领导者选举结束后,直接建立领导者关系,而不是再引入一个新的阶段;数据同步阶段,是一个冗余的设计,可以去除的,因为ZAB不是必须要先实现数据副本的一致性,才可以处理写请求,而且这个设计是没有额外的意义和价值的。**
|
||||
</li>
|
||||
|
||||
另外,ZAB和ZooKeeper强耦合,你无法在实际系统中独立使用;而Raft的实现(比如Hashicorp Raft)是可以独立使用的,编程友好。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
我提到ZooKeeper提供的是最终一致性,读操作可以在任何节点上执行。那么如果读操作访问的是备份节点,为什么无法保证每次都能读到最新的数据呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这节课让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
315
极客时间专栏/分布式协议与算法实战/协议和算法篇/加餐 | ZAB协议(二):如何从故障中恢复?.md
Normal file
315
极客时间专栏/分布式协议与算法实战/协议和算法篇/加餐 | ZAB协议(二):如何从故障中恢复?.md
Normal file
@@ -0,0 +1,315 @@
|
||||
<audio id="audio" title="加餐 | ZAB协议(二):如何从故障中恢复?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c7/68/c7c87e61b7913e09cab10f11c51aec68.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
我们上一讲提到了ZAB的领导者选举,在我看来,它只是选举了一个适合当领导者的节点,然后把这个节点的状态设置成LEADING状态。此时,这个节点还不能作为主节点处理写请求,也不能使用领导职能(比如,它没办法阻止其他“领导者”广播提案)。也就是说,集群还没有从故障中恢复过来,而成员发现和数据同步会解决这个问题。
|
||||
|
||||
总的来说,成员发现和数据同步不仅让新领导者正式成为领导者,确立了它的领导关系,还解决了各副本的数据冲突,实现了数据副本的一致性。这样一来,集群就能正常处理写请求了。在这句话里:
|
||||
|
||||
- 确立领导关系,也就是在成员发现(DISCOVERY)阶段,领导者和大多数跟随者建立连接,并再次确认各节点对自己当选领导者没有异议,确立自己的领导关系;
|
||||
- 处理冲突数据,也就是在数据同步(SYNCHRONIZATION)阶段,领导者以自己的数据为准,解决各节点数据副本的不一致。
|
||||
|
||||
对你来说,理解这两点,可以更好地理解ZooKeeper怎么恢复故障,以及当主节点崩溃了,哪些数据会丢失,哪些不会,以及背后的原因。也就是说,你能更加深刻地理解ZooKeeper的节点故障容错能力。
|
||||
|
||||
那么说了这么多,集群具体是怎么从故障中恢复过来的呢?带着这个问题,我们进入今天的学习。
|
||||
|
||||
## ZAB集群怎么从故障中恢复过来?
|
||||
|
||||
如果我们想把ZAB集群恢复到正常状态,那么新领导者就要确立自己的领导关系,成为唯一有效的领导者,然后作为主节点“领导”各备份节点一起处理读写请求。
|
||||
|
||||
### 如何确立领导关系?
|
||||
|
||||
那么通过开篇,你可以知道,选举出的领导者,是在成员发现阶段确立领导关系的。
|
||||
|
||||
在当选后,领导者会递增自己的任期编号,并基于任期编号值的大小,来和跟随者协商,最终建立领导关系。**具体说的话,就是跟随者会选择任期编号值最大的节点,作为自己的领导者,而被大多数节点认同的领导者,将成为真正的领导者。**
|
||||
|
||||
我举个例子,具体帮你理解一下。
|
||||
|
||||
假设一个ZooKeeper集群,由节点A、B、C组成。其中,领导者A已经宕机,C是新选出来的领导者,B是新的跟随者(为了方便演示,假设B、C已提交提案的事务标识符最大值分别是<1, 10>和<1, 11>,其中1是任期编号,10、11是事务标识符中的计数器值,A宕机前的任期编号也是1)。那么B、C如何协商建立领导关系呢?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9a/34/9a31fd8f5d888abfb014b07408c4be34.jpg" alt="" title="图1">
|
||||
|
||||
首先,B、C会把自己的ZAB状态设置为成员发现(DISCOVERY),这就表明,选举(ELECTION)阶段结束了,进入了下一个阶段:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5c/8c/5c933b8033238c2f58ea19c39bf12f8c.jpg" alt="" title="图2">
|
||||
|
||||
在这里,我想补充一下,ZAB定义了4种状态,来标识节点的运行状态。
|
||||
|
||||
- ELECTION(选举状态):表明节点在进行领导者选举;
|
||||
- DISCOVERY(成员发现状态):表明节点在协商沟通领导者的合法性;
|
||||
- SYNCHRONIZATION(数据同步状态):表明集群的各节点以领导者的数据为准,修复数据副本的一致性;
|
||||
- BROADCAST(广播状态):表明集群各节点在正常处理写请求。
|
||||
|
||||
关于这4种状态,你知道它们是做什么的就可以了。我就强调一点,**只有当集群大多数节点处于广播状态的时候,集群才能提交提案。**
|
||||
|
||||
接下来,B会主动联系C,发送给它包含自己接收过的领导者任期编号最大值(也就是前领导者A的任期编号,1)的FOLLOWINFO消息。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f5/56/f5209e12171cffbe1ed4a9b4835eca56.jpg" alt="" title="图3">
|
||||
|
||||
当C接收来自B的信息时,它会将包含自己事务标识符最大值的LEADINFO消息发给跟随者。
|
||||
|
||||
你要注意,领导者进入到成员发现阶段后,会对任期编号加1,创建新的任期编号,然后基于新任期编号,创建新的事务标识符(也就是<2, 0>)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f5/15/f54d28ba395fa1fd8bfa1f95e2574815.jpg" alt="" title="图4">
|
||||
|
||||
当接收到领导者的响应后,跟随者会判断领导者的任期编号是否最新,如果不是,就发起新的选举;如果是,跟随者返回ACKEPOCH消息给领导者。在这里,C的任期编号(也就是2)大于B接受过的其他领导任期编号(也就是旧领导者A的任期编号,1),所以B返回确认响应给C,并设置ZAB状态为数据同步。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ef/79/ef46c226ad137738100c9764dff96879.jpg" alt="" title="图5">
|
||||
|
||||
最后,当领导者接收到来自大多数节点的ACKEPOCH消息时,就设置ZAB状态为数据同步。在这里,C接收到了B的消息,再加上C自己,就是大多数了,所以,在接收到来自B的消息后,C设置ZAB状态为数据同步。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0f/41/0f7569a56088a5df3383b64313d55541.jpg" alt="" title="图6">
|
||||
|
||||
**现在,ZAB在成员发现阶段确立了领导者的领导关系,之后领导者就可以行使领导职能了。**而这时它首先要解决的就是数据冲突,实现各节点数据的一致性,那么它是怎么做的呢?
|
||||
|
||||
### 如何处理冲突数据?
|
||||
|
||||
当进入到数据同步状态后,领导者会根据跟随者的事务标识符最大值,判断以哪种方式处理不一致数据(有DIFF、TRUNC、SNAP这3种方式,后面我会具体说一说)。
|
||||
|
||||
因为C已提交提案的事务标识符最大值(也就是<1, 11>)大于B已提交提案的事务标识符最大值(也就是<1, 10>),所以C会用DIFF的方式修复数据副本的不一致,并返回差异数据(也就是事务标识符为<1, 11>的提案)和NEWLEADER消息给B。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/aa/7a/aa434b6ef27b7e702857d796ae53257a.jpg" alt="" title="图7">
|
||||
|
||||
**在这里,我想强调一点:B已提交提案的最大值,也是它最新提案的最大值。**因为在ZooKeeper实现中,节点退出跟随者状态时(也就是在进入选举前),所有未提交的提案都会被提交。这是ZooKeeper的设计,你知道有这么个事就可以了。
|
||||
|
||||
然后,B修复不一致数据,返回NEWLEADER消息的确认响应给领导者。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/87/06/87308ab8b9ba6eabc9217d0eeba03d06.jpg" alt="" title="图8">
|
||||
|
||||
接着,当领导者接收到来自大多数节点的NEWLEADER消息的确认响应,将设置ZAB状态为广播。在这里,C接收到B的确认响应,加上C自己,就是大多数确认了。所以,在接收到来自B的确认响应后,C设置自己的ZAB状态为广播,并发送UPTODATE消息给所有跟随者,通知它们数据同步已经完成了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a5/26/a53e5ea1bbc85f2d80e766bd85382d26.jpg" alt="" title="图9">
|
||||
|
||||
最后当B接收到UPTODATE消息时,它就知道数据同步完成了,就设置ZAB状态为广播。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/42/65/42d2162cb833a3b50fd44ba00556ed65.jpg" alt="" title="图10">
|
||||
|
||||
这个时候,集群就可以正常处理写请求了。
|
||||
|
||||
现在,我已经讲完了故障恢复的原理,那接下来,我们就来看一看ZooKeeper到底是怎么实现的吧。
|
||||
|
||||
## ZooKeeper如何恢复故障?
|
||||
|
||||
### 成员发现
|
||||
|
||||
成员发现是通过跟随者和领导者交互来完成的,**目标是确保大多数节点对领导者的领导关系没有异议,也就是确立领导者的领导地位。**
|
||||
|
||||
大概的实现流程,就像下面这样:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/90/c6/900afb50caffdee9d0fa46cc121893c6.jpg" alt="" title="图11">
|
||||
|
||||
为帮你更好地理解这个流程,我们来走一遍核心代码的流程,加深下印象。
|
||||
|
||||
第一步,领导者选举结束,节点进入跟随者状态或者领导者状态后,它们会分别设置ZAB状态为成员发现。具体来说就是:
|
||||
|
||||
- 跟随者会进入到Follower.followLeader()函数中执行,设置ZAB状态为成员发现。
|
||||
|
||||
```
|
||||
self.setZabState(QuorumPeer.ZabState.DISCOVERY);
|
||||
|
||||
```
|
||||
|
||||
- 领导者会进入到Leader.lead()函数中执行,并设置ZAB状态为成员发现。
|
||||
|
||||
```
|
||||
self.setZabState(QuorumPeer.ZabState.DISCOVERY);
|
||||
|
||||
|
||||
```
|
||||
|
||||
第二,跟随者会主动联系领导者,发送自己已接受的领导者任期编号最大值(也就是acceptedEpoch)的FOLLOWINFO消息给领导者。
|
||||
|
||||
```
|
||||
// 跟领导者建立网络连接
|
||||
connectToLeader(leaderServer.addr, leaderServer.hostname);
|
||||
connectionTime = System.currentTimeMillis();
|
||||
// 向领导者报道,并获取领导者的事务标识符最大值
|
||||
long newEpochZxid = registerWithLeader(Leader.FOLLOWERINFO);
|
||||
|
||||
```
|
||||
|
||||
第三,接收到来自跟随者的FOLLOWINFO消息后,在LearnerHandler.run()函数中,领导者将创建包含自己事务标识符最大值的LEADINFO消息,并响应给跟随者。
|
||||
|
||||
```
|
||||
// 创建LEADINFO消息
|
||||
QuorumPacket newEpochPacket = new
|
||||
QuorumPacket(Leader.LEADERINFO, newLeaderZxid, ver, null);
|
||||
// 发送LEADINFO消息给跟随者
|
||||
oa.writeRecord(newEpochPacket, "packet");
|
||||
|
||||
```
|
||||
|
||||
第四,接收到来自领导者的LEADINFO消息后,跟随者会基于领导者的任期编号,判断领导者是否合法,如果领导者不合法,跟随者发起新的选举,如果领导者合法,响应ACKEPOCH消息给领导者。
|
||||
|
||||
```
|
||||
// 创建ACKEPOCH消息,包含已提交提案的事务标识符最大值
|
||||
QuorumPacket ackNewEpoch = new QuorumPacket(Leader.ACKEPOCH, lastLoggedZxid, epochBytes, null);
|
||||
// 响应ACKEPOCH消息给领导者
|
||||
writePacket(ackNewEpoch, true);
|
||||
|
||||
```
|
||||
|
||||
第五,跟随者设置ZAB状态为数据同步。
|
||||
|
||||
```
|
||||
self.setZabState(QuorumPeer.ZabState.SYNCHRONIZATION);
|
||||
|
||||
```
|
||||
|
||||
第六,需要你注意的是,在LearnerHandler.run()函数中(以及Leader.lead()函数),领导者会调用waitForEpochAck()函数,来阻塞和等待来自大多数节点的ACKEPOCH消息。
|
||||
|
||||
```
|
||||
ss = new StateSummary(bbepoch.getInt(), ackEpochPacket.getZxid());
|
||||
learnerMaster.waitForEpochAck(this.getSid(), ss);
|
||||
|
||||
```
|
||||
|
||||
第七,当领导者接收到来自大多数节点的ACKEPOCH消息后,在Leader.lead()函数中,领导者将设置ZAB状态为数据同步。
|
||||
|
||||
```
|
||||
self.setZabState(QuorumPeer.ZabState.SYNCHRONIZATION);
|
||||
|
||||
```
|
||||
|
||||
这样,ZooKeeper就实现了成员发现,各节点就领导者的领导关系达成了共识。
|
||||
|
||||
当跟随者和领导者设置ZAB状态为数据同步,它们也就是进入了数据同步阶段,那在ZooKeeper中数据同步是如何实现的呢?
|
||||
|
||||
### 数据同步
|
||||
|
||||
数据同步也是通过跟随者和领导者交互来完成的,目标是确保跟随者节点上的数据与领导者节点上数据是一致的。大概的实现流程,如图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6e/44/6ea74f1a56a9fd0f39b82b25d2814c44.jpg" alt="" title="图12">
|
||||
|
||||
为了方便你理解,咱们一起走一遍核心代码的流程,加深下印象。
|
||||
|
||||
第一,在LearnerHandler.run()函数中,领导者调用syncFollower()函数,根据跟随者的事务标识符值最大值,判断用哪种方式处理不一致数据,把已经提交提案和未提交提案都同步给跟随者:
|
||||
|
||||
```
|
||||
peerLastZxid = ss.getLastZxid();
|
||||
boolean needSnap = syncFollower(peerLastZxid, learnerMaster);
|
||||
|
||||
```
|
||||
|
||||
在这里,需要你了解领导者向跟随者同步数据的三种方式(TRUNC、DIFF、SNAP),它们是什么含义呢?要想了解这部分内容,你首先要了解一下syncFollower()中,3个关键变量的含义。
|
||||
|
||||
- peerLastZxid:跟随者节点上,提案的事务标识符最大值。
|
||||
- maxCommittedLog、minCommittedLog:领导者节点内存队列中,已提交提案的事务标识符最大值和最小值。需要你注意的是,maxCommittedLog、minCommittedLog与ZooKeeper的设计有关。在ZooKeeper中,为了更高效地复制提案到跟随者上,领导者会将一定数量(默认值为500)的已提交提案放在内存队列里,而maxCommittedLog、minCommittedLog分别标识的是内存队列中,已提交提案的事务标识符最大值和最小值。
|
||||
|
||||
说完3个变量的含义,我来说说3种同步方式。
|
||||
|
||||
<li>
|
||||
TRUNC:当peerLastZxid大于maxCommittedLog时,领导者会通知跟随者丢弃超出的那部分提案。比如,如果跟随者的peerLastZxid为11,领导者的maxCommittedLog为10,那么领导者将通知跟随者丢弃事务标识符值为11的提案。
|
||||
</li>
|
||||
<li>
|
||||
DIFF:当peerLastZxid小于maxCommittedLog,但peerLastZxid大于minCommittedLog时,领导者会同步给跟随者缺失的已提交的提案,比如,如果跟随者的peerLastZxid为9,领导者的maxCommittedLog为10,minCommittedLog为9,那么领导者将同步事务标识符值为10的提案,给跟随者。
|
||||
</li>
|
||||
<li>
|
||||
SNAP:当peerLastZxid小于minCommittedLog时,也就是说,跟随者缺失的提案比较多,那么,领导者同步快照数据给跟随者,并直接覆盖跟随者本地的数据。
|
||||
</li>
|
||||
|
||||
在这里,我想补充一下,领导者先就已提交提案和跟随者达成一致,然后调用learnerMaster.startForwarding(),将未提交提案(如果有的话)也缓存在发送队列(queuedPackets),并最终复制给跟随者节点。也就是说,**领导者以自己的数据为准,实现各节点数据副本的一致的。**
|
||||
|
||||
需要你注意的是,在syncFolower()中,领导者只是将需要发送的差异数据缓存在发送队列(queuedPackets),这个时候还没有发送。
|
||||
|
||||
第二,在LearnerHandler.run()函数中,领导者创建NEWLEADER消息,并缓存在发送队列中。
|
||||
|
||||
```
|
||||
// 创建NEWLEADER消息
|
||||
QuorumPacket newLeaderQP = new QuorumPacket(Leader.NEWLEADER, newLeaderZxid, learnerMaster.getQuorumVerifierBytes(), null);
|
||||
// 缓存NEWLEADER消息到发送队列中
|
||||
queuedPackets.add(newLeaderQP);
|
||||
|
||||
```
|
||||
|
||||
第三,在LearnerHandler.run()函数中,领导者调用startSendingPackets()函数,启动一个新线程,并将缓存的数据发送给跟随者。
|
||||
|
||||
```
|
||||
// 发送缓存队列中的数据
|
||||
startSendingPackets();
|
||||
|
||||
```
|
||||
|
||||
第四,跟随者调用syncWithLeader()函数,处理来自领导者的数据同步。
|
||||
|
||||
```
|
||||
// 处理数据同步
|
||||
syncWithLeader(newEpochZxid);
|
||||
|
||||
```
|
||||
|
||||
第五,在syncWithLeader()函数,跟随者接收到来自领导者的NEWLEADER消息后,返回确认响应给领导者。
|
||||
|
||||
```
|
||||
writePacket(new QuorumPacket(Leader.ACK, newLeaderZxid, null, null), true);
|
||||
|
||||
```
|
||||
|
||||
第六,在LearnerHandler.run()函数中(以及Leader.lead()函数),领导者等待来自大多数节点的NEWLEADER消息的响应。
|
||||
|
||||
```
|
||||
learnerMaster.waitForNewLeaderAck(getSid(), qp.getZxid());
|
||||
|
||||
```
|
||||
|
||||
第七,当领导者接收到来自大多数节点的NEWLEADER消息的响应时,在Leader.lead()函数中,领导者设置ZAB状态为广播状态。
|
||||
|
||||
```
|
||||
self.setZabState(QuorumPeer.ZabState.BROADCAST);
|
||||
|
||||
```
|
||||
|
||||
并在LearnerHandler.run()中发送UPTODATE消息给所有跟随者,通知它们数据同步已完成了。
|
||||
|
||||
```
|
||||
queuedPackets.add(new QuorumPacket(Leader.UPTODATE, -1, null, null));
|
||||
|
||||
```
|
||||
|
||||
第八,当跟随者接收到UPTODATE消息时,就知道自己修复完数据不一致了,可以处理写请求了,就设置ZAB状态为广播。
|
||||
|
||||
```
|
||||
// 数据同步完成后,也就是可以正常处理来自领导者的广播消息了,设置ZAB状态为广播
|
||||
self.setZabState(QuorumPeer.ZabState.BROADCAST);
|
||||
|
||||
```
|
||||
|
||||
你看,这样就确保各节点数据的一致了,接下来,就可以以领导者为主,向其他节点广播消息了。
|
||||
|
||||
## 内容小结
|
||||
|
||||
本节课我主要带你了解了ZAB如何恢复故障,我希望你明确这样几个重点。
|
||||
|
||||
1.成员发现,是为了建立跟随者和领导者之间的领导者关系,并通过任期编号来确认这个领导者是否为最合适的领导者。
|
||||
|
||||
2.数据同步,是通过以领导者的数据为准的方式,来实现各节点数据副本的一致,需要你注意的是,基于“大多数”的提交原则和选举原则,能确保被复制到大多数节点并提交的提案,就不再改变。
|
||||
|
||||
在这里,我想特别强调一下,在ZooKeeper的代码实现中,处于提交(Committed)状态的提案是可能会改变的,为什么呢?
|
||||
|
||||
在ZooKeeper中,一个提案进入提交(Committed)状态,有两种方式:
|
||||
|
||||
<li>
|
||||
被复制到大多数节点上,被领导者提交或接收到来自领导者的提交消息(leader.COMMIT)而被提交。在这种状态下,提交的提案是不会改变的。
|
||||
</li>
|
||||
<li>
|
||||
另外,在ZooKeeper的设计中,在节点退出跟随者状态时(在follower.shutdown()函数中),会将所有本地未提交的提案都提交。需要你注意的是,此时提交的提案,可能并未被复制到大多数节点上,而且这种设计,就会导致ZooKeeper中出现,处于“提交”状态的提案可能会被删除(也就是接收到领导者的TRUNC消息而删除的提案)。
|
||||
</li>
|
||||
|
||||
更准确的说,**在ZooKeeper中,被复制到大多数节点上的提案,最终会被提交,并不会再改变;而只在少数节点存在的提案,可能会被提交和不再改变,也可能会被删除。**为了帮助你理解,我来举个具体的例子。
|
||||
|
||||
<li>
|
||||
如果写请求对应的提案“SET X = 1”已经复制到大多数节点上,那么它是最终会被提交,之后也不会再改变。也就是说,在没有新的X赋值操作的前提下,不管节点怎么崩溃、领导者如何变更,你查询到的X的值都为1。
|
||||
</li>
|
||||
<li>
|
||||
如果写请求对应的提案“SET X = 1”未被复制到大多数节点上,比如在领导者广播消息过程中,领导者崩溃了,那么,提案“SET X = 1”,可能被复制到大多数节点上,并提交和之后就不再改变,也可能会被删除。这个行为是未确定的,取决于新的领导者是否包含该提案。
|
||||
</li>
|
||||
|
||||
另外,我想补充下,在ZAB中,选举出了新的领导者后,该领导者不能立即处理写请求,还需要通过成员发现、数据同步2个阶段进行故障恢复。这是ZAB协议的设计决定的,不是所有的共识算法都必须这样,比如Raft选举出新的领导者后,领导者是可以立即处理写请求的。
|
||||
|
||||
最后,完成数据同步后,节点将进入广播状态,那ZAB是如何处理读写请求,又是如何广播消息的呢?下节课,我会重点带你了解这部分内容。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
我提到在ZAB中,提案提交的大多数原则和领导者选举的大多数原则,确保了被复制到大多数节点的提案就不再改变了。那么你不妨思考和推演一下,这是为什么呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这节课让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
Reference in New Issue
Block a user