mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-17 06:33:48 +08:00
del
This commit is contained in:
@@ -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
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/07 | Raft算法(一):如何选举领导者?.md
Normal file
174
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/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
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/08 | Raft算法(二):如何复制日志?.md
Normal file
117
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/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
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/09 | Raft算法(三):如何解决成员变更的问题?.md
Normal file
131
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/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
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/10 | 一致哈希算法:如何分群,突破集群的“领导者”限制?.md
Normal file
150
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/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
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/11 | Gossip协议:流言蜚语,原来也可以实现一致性.md
Normal file
110
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/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
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/13 | PBFT算法:有人作恶,如何达成共识?.md
Normal file
119
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/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
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/14 | PoW算法:有办法黑比特币吗?.md
Normal file
117
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/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
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/15 | ZAB协议:如何实现操作的顺序性?.md
Normal file
160
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/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
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/加餐 | MySQL XA是如何实现分布式事务的?.md
Normal file
109
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/加餐 | 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
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/加餐 | PBFT算法:如何替换作恶的领导者?.md
Normal file
104
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/加餐 | 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
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/加餐 | TCC如何实现指令执行的原子性?.md
Normal file
119
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/加餐 | 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
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/加餐 | ZAB协议(一):主节点崩溃了,怎么办?.md
Normal file
255
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/加餐 | 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
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/加餐 | ZAB协议(三):如何处理读写请求?.md
Normal file
264
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/加餐 | 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
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/加餐 | ZAB协议(二):如何从故障中恢复?.md
Normal file
315
极客时间专栏/geek/分布式协议与算法实战/协议和算法篇/加餐 | 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中,提案提交的大多数原则和领导者选举的大多数原则,确保了被复制到大多数节点的提案就不再改变了。那么你不妨思考和推演一下,这是为什么呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这节课让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
145
极客时间专栏/geek/分布式协议与算法实战/实战篇/16 | InfluxDB企业版一致性实现剖析:他山之石,可以攻玉.md
Normal file
145
极客时间专栏/geek/分布式协议与算法实战/实战篇/16 | InfluxDB企业版一致性实现剖析:他山之石,可以攻玉.md
Normal file
@@ -0,0 +1,145 @@
|
||||
<audio id="audio" title="16 | InfluxDB企业版一致性实现剖析:他山之石,可以攻玉" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/32/b8/32d5aed63b92f7460a84bdbd965ea1b8.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
学习了前面15讲的内容后,我们了解了很多常用的理论和算法(比如CAP定理、Raft算法等)。是不是理解了这些内容,就能够游刃有余地处理实际系统的问题了呢?
|
||||
|
||||
在我看来,还远远不够,因为理论和实践的中间是存在鸿沟的,比如,你可能有这样的感受,提到编程语言的语法或者分布式算法的论文,你说起来头头是道,但遇到实际系统时,还是无法写程序,开发分布式系统。
|
||||
|
||||
而我常说,实战是学习的最终目的。为了帮你更好地掌握前面的理论和算法,接下来,我用5讲的时间,分别以InfluxDB企业版一致性实现、Hashicorp Raft、KV系统开发实战为例,带你了解如何在实战中使用技术,掌握分布式的实战能力。
|
||||
|
||||
今天这一讲,我就以InfluxDB企业版为例,带你看一看系统是如何实现一致性的。有的同学可能会问了:为什么是InfluxDB企业版呢?因为它是排名第一的时序数据库,相比其他分布式系统(比如KV存储),时序数据库更加复杂,因为我们要分别设计2个完全不一样的一致性模型。当你理解了这样一个复杂的系统实现后,就能更加得心应手地处理简单系统的问题了。
|
||||
|
||||
那么为了帮你达到这个目的。我会先介绍一下时序数据库的背景知识,因为技术是用来解决实际场景的问题的,正如我之前常说的“要根据场景特点,权衡折中来设计系统”。所以当你了解了这些背景知识后,就能更好的理解为什么要这么设计了。
|
||||
|
||||
## 什么是时序数据库?
|
||||
|
||||
你可以这么理解,时序数据库,就是存储时序数据的数据库,就像MySQL是存储关系型数据的数据库。而时序数据,就是按照时间顺序记录系统、设备状态变化的数据,比如CPU利用率、某一时间的环境温度等,就像下面的样子:
|
||||
|
||||
```
|
||||
> insert cpu_usage,host=server01,location=cn-sz user=23.0,system=57.0
|
||||
> select * from cpu_usage
|
||||
name: cpu_usage
|
||||
time host location system user
|
||||
---- ---- -------- ------ ----
|
||||
1557834774258860710 server01 cn-sz 55 25
|
||||
>
|
||||
|
||||
```
|
||||
|
||||
在我看来,时序数据最大的特点是数据量很大,可以不夸张地说是海量。时序数据主要来自监控(监控被称为业务之眼),而且在不影响业务运行的前提下,监控埋点是越多越好,这样才能及时发现问题、复盘故障。
|
||||
|
||||
**那么作为时序数据库,InfluxDB企业版的架构是什么样子呢?**
|
||||
|
||||
你可能已经了解过,它是由META节点和DATA节点2个逻辑单元组成的,而且这两个节点是2个单独的程序。那你也许会问了,为什么不能合成到一个程序呢?答案是场景不同。
|
||||
|
||||
- META节点存放的是系统运行的关键元信息,比如数据库(Database)、表(Measurement)、保留策略(Retention policy)等。它的特点是一致性敏感,但读写访问量不高,需要一定的容错能力。
|
||||
- DATA节点存放的是具体的时序数据。它有这样几个特点:最终一致性、面向业务、性能越高越好,除了容错,还需要实现水平扩展,扩展集群的读写性能。
|
||||
|
||||
我想说的是,对于META节点来说,节点数的多少代表的是容错能力,一般3个节点就可以了,因为从实际系统运行观察看,能容忍一个节点故障就可以了。但对DATA节点而言,节点数的多少则代表了读写性能,一般而言,在一定数量以内(比如10个节点)越多越好,因为节点数越多,读写性能也越高,但节点数量太多也不行,因为查询时就会出现访问节点数过多而延迟大的问题。
|
||||
|
||||
所以,基于不同场景特点的考虑,2个单独程序更合适。如果META节点和DATA节点合并为一个程序,因读写性能需要,设计了一个10节点的DATA节点集群,这就意味着META节点集群(Raft集群)也是10个节点。在学了Raft算法之后,你应该知道,这时就会出现消息数多、日志提交慢的问题,肯定不行了。(对Raft日志复制不了解的同学,可以回顾一下[08讲](https://time.geekbang.org/column/article/205784))
|
||||
|
||||
现在你了解时序数据库,以及InfluxDB企业版的META节点和DATA节点了吧?那么怎么实现META节点和DATA节点的一致性呢?
|
||||
|
||||
## 如何实现META节点一致性?
|
||||
|
||||
你可以这样想象一下,META节点存放的是系统运行的关键元信息,那么当写操作发生后,就要立即读取到最新的数据。比如,创建了数据库“telegraf”,如果有的DATA节点不能读取到这个最新信息,那就会导致相关的时序数据写失败,肯定不行。
|
||||
|
||||
所以,META节点需要强一致性,实现CAP中的CP模型(对CAP理论不熟悉的同学,可以先回顾下[02讲](https://time.geekbang.org/column/article/195675))。
|
||||
|
||||
那么,InfluxDB企业版是如何实现的呢?
|
||||
|
||||
因为InflxuDB企业版是闭源的商业软件,通过[官方文档](https://docs.influxdata.com/enterprise_influxdb/v1.7/concepts/clustering/#architectural-overview),我们可以知道它使用Raft算法实现META节点的一致性(一般推荐3节点的集群配置)。那么说完META节点的一致性实现之后,我接着说一说DATA节点的一致性实现。
|
||||
|
||||
## 如何实现DATA节点一致性?
|
||||
|
||||
我们刚刚提到,DATA节点存放的是具体的时序数据,对一致性要求不高,实现最终一致性就可以了。但是,DATA节点也在同时作为接入层直接面向业务,考虑到时序数据的量很大,要实现水平扩展,所以必须要选用CAP中的AP模型,因为AP模型不像CP模型那样采用一个算法(比如Raft算法)就可以实现了,也就是说,AP模型更复杂,具体有这样几个实现步骤。
|
||||
|
||||
### 自定义副本数
|
||||
|
||||
首先,你需要考虑冗余备份,也就是同一份数据可能需要设置为多个副本,当部分节点出问题时,系统仍然能读写数据,正常运行。
|
||||
|
||||
那么,该如何设置副本呢?答案是实现自定义副本数。
|
||||
|
||||
关于自定义副本数的实现,我们在[12讲](https://time.geekbang.org/column/article/209130)介绍了,在这里就不啰嗦了。不过,我想补充一点,相比Raft算法节点和副本必须一一对应,也就是说,集群中有多少个节点就必须有多少个副本,你看,自定义副本数,是不是更灵活呢?
|
||||
|
||||
学到这里,有同学可能已经想到了,当集群支持多副本时,必然会出现一个节点写远程节点时,RPC通讯失败的情况,那么怎么处理这个问题呢?
|
||||
|
||||
### Hinted-handoff
|
||||
|
||||
我想说的是,一个节点接收到写请求时,需要将写请求中的数据转发一份到其他副本所在的节点,那么在这个过程中,远程RPC通讯是可能会失败的,比如网络不通了,目标节点宕机了,等等,就像下图的样子。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d1/85/d1ee3381b7ae527cbc9d607d1a1f8385.jpg" alt="" title="图1">
|
||||
|
||||
那么如何处理这种情况呢?答案是实现Hinted-handoff。在InfluxDB企业版中,Hinted-handoff是这样实现的:
|
||||
|
||||
- 写失败的请求,会缓存到本地硬盘上;
|
||||
- 周期性地尝试重传;
|
||||
- 相关参数信息,比如缓存空间大小(max-szie)、缓存周期(max-age)、尝试间隔(retry-interval)等,是可配置的。
|
||||
|
||||
在这里我想补充一点,除了网络故障、节点故障外,在实际场景中,临时的突发流量也会导致系统过载,出现RPC通讯失败的情况,这时也需要Hinted-handoff能力。
|
||||
|
||||
虽然Hinted-handoff可以通过重传的方式来处理数据不一致的问题,但当写失败请求的数据大于本地缓存空间时,比如某个节点长期故障,写请求的数据还是会丢失的,最终的节点的数据还是不一致的,那么怎么实现数据的最终一致性呢?答案是反熵。
|
||||
|
||||
### 反熵
|
||||
|
||||
需要你注意的是,时序数据虽然一致性不敏感,能容忍短暂的不一致,但如果查询的数据长期不一致的话,肯定就不行了,因为这样就会出现“Flapping Dashboard”的现象,也就是说向不同节点查询数据,生成的仪表盘视图不一样,就像图2和图3的样子。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/53/3c/535208a278935bc490e0d4f50f2ca13c.png" alt="" title="图2">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7c/21/7cc5f061f62854caa1d31aed586c8321.png" alt="" title="图3"><br>
|
||||
从上面的2个监控视图中你可以看到,同一份数据,查询不同的节点,生成的视图是不一样的。那么,如何实现最终一致性呢?
|
||||
|
||||
答案就是咱们刚刚说的反熵,而我在[11讲](https://time.geekbang.org/column/article/208182)以自研InfluxDB系统为例介绍过反熵的实现,InfluxDB企业版类似,所以在这里就不啰嗦了。
|
||||
|
||||
不过有的同学可能会存在这样的疑问,实现反熵是以什么为准来修复数据的不一致呢?我想说的是,时序数据像日志数据一样,创建后就不会再修改了,一直存放在那里,直到被删除。
|
||||
|
||||
所以,数据副本之间的数据不一致,是因为数据写失败导致数据丢失了,也就是说,存在的都是合理的,缺失的就是需要修复的。这时我们可以采用两两对比、添加缺失数据的方式,来修复各数据副本的不一致了。
|
||||
|
||||
### Quorum NWR
|
||||
|
||||
最后,有同学可能会说了,我要在公司官网上展示的监控数据的仪表板(Dashboard),是不能容忍视图不一致的情况的,也就是无法容忍任何“Flapping Dashboard”的现象。那么怎么办呢?这时我们就要实现强一致性(Werner Vogels提到的强一致性),也就是每次读操作都要能读取最新数据,不能读到旧数据。
|
||||
|
||||
那么在一个AP型的分布式系统中,如何实现强一致性呢?
|
||||
|
||||
答案是实现Quorum NWR。同样,关于Quorum NWR的实现,我们在12讲已介绍,在这里也就不啰嗦了。
|
||||
|
||||
最后我想说的是,你可以看到,实现AP型分布式系统,比实现CP型分布式要复杂的。另外,通过上面的内容学习,我希望你能注意到,技术是用来解决场景需求的,没有十全十美的技术,在实际工作中,需要我们深入研究场景特点,提炼场景需求,然后根据场景特点权衡折中,设计出适合该场景特点的分布式系统。
|
||||
|
||||
## 内容小结
|
||||
|
||||
本节课我主要带你了解时序数据库、META节点一致性的实现、DATA节点一致性的实现。以一个复杂的实际系统为例,带你将前面学习到的理论串联起来,让你知道它们如何在实际场景中使用。我希望你明确的重点如下:
|
||||
|
||||
<li>
|
||||
CAP理论是一把尺子,能辅助我们分析问题、总结归纳问题,指导我们如何做妥协折中。所以,我建议你在实践中多研究多思考,一定不能认为某某技术“真香”,十全十美了,要根据场景特点活学活用技术。
|
||||
</li>
|
||||
<li>
|
||||
通过Raft算法,我们能实现强一致性的分布式系统,能保证写操作完成后,后续所有的读操作,都能读取到最新的数据。
|
||||
</li>
|
||||
<li>
|
||||
通过自定义副本数、Hinted-handoff、反熵、Quorum NWR等技术,我们能实现AP型分布式系统,还能通过水平扩展,高效扩展集群的读写能力。
|
||||
</li>
|
||||
|
||||
最后,我想再强调下,技术是用来解决场景的需求的,只有当你吃透技术,深刻理解场景的需求,才能开发出适合这个场景的分布式系统。另外我还想让你知道的是,InfluxDB企业版一年的License费高达1.5万美刀,为什么它值这个价钱?就是因为技术带来的高性能和成本优势。比如:
|
||||
|
||||
- 相比OpenTSDB,InfluxDB的写性能是它的9.96倍,存储效率是它的8.69倍,查询效率是它的7.38倍。
|
||||
- 相比Graphite,InfluxDB的写性能是它的12倍,存储效率是6.3倍,查询效率是9倍。
|
||||
|
||||
在这里我想说的是,数倍或者数量级的性能优势其实就是钱,而且业务规模越大,省钱效果越突出。
|
||||
|
||||
另外我想说的是,尽管influxdb-comparisons的测试比较贴近实际场景,比如它的DevOps测试模型,与我们观察到常见的实际场景是一致的。但从实际效果看,InfluxDB的优势更加明显,成本优势更加突出。因为传统的时序数据库不仅仅是性能低,而且在海量数据场景下,接入和查询的痛点突出。为了缓解这些痛点,引入和堆砌了更多的开源软件。比如:
|
||||
|
||||
- 往往需要引入Kafka来缓解,因突发接入流量导致的丢数据问题;
|
||||
- 需要引入Storm、Flink来缓解,时序数据库计算性能差的问题;
|
||||
- 需要做热数据的内存缓存,来解决查询超时的问题。
|
||||
|
||||
所以在实施中,除了原有的时序数据库会被替换掉,还有大量的开源软件会被省掉,成本优势突出。在这里我想说的是,从实际实施看(自研InfluxDB系统),性能优势和成本优势也是符合这个预期的。
|
||||
|
||||
最后我想说的是,我反对堆砌开源软件,建议谨慎引入Kafka等缓存中间件。老话说,在计算机中,任何问题都可以通过引入一个中间层来解决。这句话是正确的,但背后的成本是不容忽视的,尤其是在海量系统中。**我的建议是直面问题,通过技术手段在代码和架构层面解决它,而不是引入和堆砌更多的开源软件。**其实,InfluxDB团队也是这么做,比如他们两次重构存储引擎。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
我提到没有十全十美的技术,而是需要根据场景特点,权衡折中,设计出适合场景特点的分布式系统。那么你试着思考一下,假设有这样一个场景,一个存储系统,访问它的写请求不多(比如 1K QPS),但访问它的读请求很多(比如1M QPS),而且客户端查询时,对数据的一致性敏感,也就是需要实现强一致性,那么我们该如何设计这个系统呢?为什么呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
@@ -0,0 +1,268 @@
|
||||
<audio id="audio" title="17 | Hashicorp Raft(一):如何跨过理论和代码之间的鸿沟?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0b/59/0bd6d3ca95946c5e5d26087446271059.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
很多同学在开发系统的时候,都会有这样的感觉:明明自己看了很多资料,掌握了技术背后的原理,可在开发和调试的时候还是很吃力,这是为什么呢?
|
||||
|
||||
答案很简单,因为理论和实践本来就是两回事,实践不仅需要掌握API接口的用法,还需要理解API背后的代码实现。
|
||||
|
||||
所以,如果你在使用Raft开发分布式系统的时候,仅仅阅读Raft论文或者Raft实现的API手册,是远远不够的。你还要吃透API背后的代码实现,“不仅知其然,也要知其所以然”,这样才能“一切尽在掌握中”,从而开发实现能稳定运行的分布式系统。那么怎么做才能吃透Raft的代码实现呢?
|
||||
|
||||
要知道,任何Raft实现都承载了两个目标:实现Raft算法的原理,设计易用的API接口。所以,你不仅要从算法原理的角度理解代码实现,而且要从场景使用的角度理解API接口的用法。
|
||||
|
||||
而我会用两节课的时间,**从代码实现和接口使用两个角度,**带你循序渐进地掌握当前流行的一个Raft实现:[Hashicorp Raft](https://github.com/hashicorp/raft)(以最新稳定版v1.1.1为例)。希望你在这个过程中集中注意力,勾划重点,以便提高学习效率,吃透原理对应的技术实现,彻底掌握Raft算法的实战技巧。
|
||||
|
||||
本节课,我会从算法原理的角度,聊一聊Raft算法的核心功能(领导者选举和日志复制)在Hashicorp Raft中是如何实现的。(如果Raft算法的原理你已经忘得差不多了,那你可以先回顾下7~9讲,加深印象之后,再进入今天的学习。)
|
||||
|
||||
## Hashicorp Raft如何实现领导者选举?
|
||||
|
||||
**在我看来,阅读源码的关键,在于找到代码的入口函数,**比如在Golang代码中,程序的入口函数一般为main()函数,那么领导者选举的入口函数是哪个呢?
|
||||
|
||||
我们知道,典型的领导者选举在本质上是节点状态的变更。具体到Hashicorp Raft源码中,领导者选举的入口函数run(),在raft.go中以一个单独的协程运行,来实现节点状态变迁,就像下面的样子:
|
||||
|
||||
```
|
||||
func (r *Raft) run() {
|
||||
for {
|
||||
select {
|
||||
// 关闭节点
|
||||
case <-r.shutdownCh:
|
||||
r.setLeader("")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
switch r.getState() {
|
||||
// 跟随者
|
||||
case Follower:
|
||||
r.runFollower()
|
||||
// 候选人
|
||||
case Candidate:
|
||||
r.runCandidate()
|
||||
// 领导者
|
||||
case Leader:
|
||||
r.runLeader()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
从上面这段代码中,你能看到,Follower(跟随者)、Candidate(候选人)、Leader(领导者)三个节点状态对应的功能,都被抽象成一个函数,分别是runFollower()、runCandidate()和runLeader()。
|
||||
|
||||
### 数据结构
|
||||
|
||||
在[07讲](https://time.geekbang.org/column/article/204472)中,我们先学习了节点状态,不过主要侧重理解节点状态的功能作用(比如说,跟随者相当于普通群众,领导者是霸道总裁),并没有关注它在实际代码中是如何实现的,所以我们先来看看在Hashicorp Raft中是如何实现节点状态的。
|
||||
|
||||
节点状态相关的数据结构和函数,是在state.go中实现的。跟随者、候选人和领导者的3个状态,是由RaftState定义的,一个无符号32位的只读整型数值(uint32):
|
||||
|
||||
```
|
||||
type RaftState uint32
|
||||
const (
|
||||
// 跟随者
|
||||
Follower RaftState = iota
|
||||
// 候选人
|
||||
Candidate
|
||||
// 领导者
|
||||
Leader
|
||||
// 关闭状态
|
||||
Shutdown
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
需要注意的是,**也存在一些需要使用字符串格式的节点状态的场景(比如日志输出),**这时你可以使用RaftState.String()函数。
|
||||
|
||||
你应该还记得,每个节点都有属于本节点的信息(比如任期编号),那么在代码中如何实现这些信息呢?这就要说到raftState数据结构了。
|
||||
|
||||
raftState属于结构体类型,是表示节点信息的一个大数据结构,里面包含了只属于本节点的信息,比如节点的当前任期编号、最新提交的日志项的索引值、存储中最新日志项的索引值和任期编号、当前节点的状态等,就像下面的样子:
|
||||
|
||||
```
|
||||
type raftState struct {
|
||||
// 当前任期编号
|
||||
currentTerm uint64
|
||||
|
||||
// 最大被提交的日志项的索引值
|
||||
commitIndex uint64
|
||||
|
||||
// 最新被应用到状态机的日志项的索引值
|
||||
lastApplied uint64
|
||||
|
||||
// 存储中最新的日志项的索引值和任期编号
|
||||
lastLogIndex uint64
|
||||
lastLogTerm uint64
|
||||
|
||||
// 当前节点的状态
|
||||
state RaftState
|
||||
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
节点状态与节点信息的定义就是这么简单,这里我就不多说了。而在分布式系统中要实现领导者选举,更重要的一层内容是实现RPC消息,因为领导者选举的过程,就是一个RPC通讯的过程。
|
||||
|
||||
在理论篇中我说过,Raft算法中支持多种RPC消息(比如请求投票RPC消息、日志复制RPC消息)。所以接下来我们看一看,在Hashicorp Raft中又是怎样实现RPC消息的。又因为在一个RPC消息中,最重要的部分就是消息的内容,所以我们先来看一看RPC消息对应的数据结构。
|
||||
|
||||
RPC消息相关的数据结构是在commands.go中定义的,比如,日志复制RPC的请求消息,对应的数据结构为AppendEntriesRequest。而AppendEntriesRequest是一个结构体类型,里面包含了Raft算法论文中约定的字段,比如以下这些内容。
|
||||
|
||||
- Term:当前的任期编号。
|
||||
- PrevLogEntry:表示当前要复制的日志项,前面一条日志项的索引值。
|
||||
- PrevLogTerm:表示当前要复制的日志项,前面一条日志项的任期编号。
|
||||
- Entries:新日志项。
|
||||
|
||||
具体的结构信息,就像下面的样子:
|
||||
|
||||
```
|
||||
type AppendEntriesRequest struct {
|
||||
// 当前的任期编号,和领导者信息(包括服务器ID和地址信息)
|
||||
Term uint64
|
||||
Leader []byte
|
||||
|
||||
// 当前要复制的日志项,前面一条日志项的索引值和任期编号
|
||||
PrevLogEntry uint64
|
||||
PrevLogTerm uint64
|
||||
|
||||
// 新日志项
|
||||
Entries []*Log
|
||||
|
||||
// 领导者节点上的已提交的日志项的最大索引值
|
||||
LeaderCommitIndex uint64
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我建议你可以采用上面的思路,对照着算法原理去学习其他RPC消息的实现,这样一来你就能掌握独立学习的能力了。其他RPC消息的数据结构我就不一一描述了(如果你遇到问题,可以在留言区留言)。
|
||||
|
||||
现在,你已经了解了节点状态和RPC消息的格式,掌握了这些基础知识后,我们继续下一步,看看在Hashicorp Raft中是如何进行领导者选举的。
|
||||
|
||||
### 选举领导者
|
||||
|
||||
首先,在初始状态下,集群中所有的节点都处于跟随者状态,函数runFollower()运行,大致的执行步骤,就像下图的样子:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/28/15/28753c692659fa4017c6a943c3d05b15.jpg" alt="">
|
||||
|
||||
我带你走一遍这五个步骤,便于你加深印象。
|
||||
|
||||
1. 根据配置中的心跳超时时长,调用randomTimeout()函数来获取一个随机值,用以设置心跳超时时间间隔。
|
||||
1. 进入到for循环中,通过select实现多路IO复用,周期性地获取消息和处理。如果步骤1中设置的心跳超时时间间隔发生了超时,执行步骤3。
|
||||
1. 如果等待心跳信息未超时,执行步骤4,如果等待心跳信息超时,执行步骤5。
|
||||
1. 执行continue语句,开始一次新的for循环。
|
||||
1. 设置节点状态为候选人,并退出runFollower()函数。
|
||||
|
||||
当节点推举自己为候选人之后,函数runCandidate()执行,大致的执行步骤,如图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/98/4d/989efd10cdff6825356b8a4310e5704d.jpg" alt="">
|
||||
|
||||
同样的,我们走一遍这个过程,加深一下印象。
|
||||
|
||||
1. 首先调用electSelf()发起选举,给自己投一张选票,并向其他节点发送请求投票RPC消息,请求他们选举自己为领导者。然后调用randomTimeout()函数,获取一个随机值,设置选举超时时间。
|
||||
1. 进入到for循环中,通过select实现多路IO复用,周期性地获取消息和处理。如果发生了选举超时,执行步骤3,如果得到了投票信息,执行步骤4。
|
||||
1. 发现了选举超时,退出runCandidate()函数,然后再重新执行runCandidate()函数,发起新一轮的选举。
|
||||
1. 如果候选人在指定时间内赢得了大多数选票,那么候选人将当选为领导者,调用setState()函数,将自己的状态变更为领导者,并退出runCandidate()函数。
|
||||
|
||||
当节点当选为领导者后,函数runLeader()就执行了:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/17/3e/17619089bc683d8a1a2bd20bf678503e.jpg" alt="">
|
||||
|
||||
整个过程,主要有4个步骤。
|
||||
|
||||
1. 调用startStopReplication(),执行日志复制功能。
|
||||
1. 然后启动新的协程,调用replicate()函数,执行日志复制功能。
|
||||
1. 接着在replicate()函数中,启动一个新的协程,调用heartbeat()函数,执行心跳功能。
|
||||
1. 在heartbeat()函数中,周期性地发送心跳信息,通知其他节点,我是领导者,我还活着,不需要你们发起新的选举。
|
||||
|
||||
其实,在Hashicorp Raft中实现领导者选举并不难,你只要充分理解上述步骤,并记住,领导者选举本质上是节点状态变迁,跟随者、候选人、领导者对应的功能函数分别为runFollower()、runCandidate()、runLeader(),就可以了。
|
||||
|
||||
## Hashicorp Raft如何复制日志?
|
||||
|
||||
学习[08](https://time.geekbang.org/column/article/205784)讲之后,你应该知道了日志复制的重要性,因为Raft是基于强领导者模型和日志复制,最终实现强一致性的。那么你该如何学习日志复制的代码实现呢?和学习“如何实现领导者选举”一样,你需要先了解了日志相关的数据结构,阅读日志复制相关的代码。
|
||||
|
||||
学习了理论篇后,你应该还记得日志复制是由领导者发起的,跟随者来接收的。可能有同学已经想到了,领导者复制日志和跟随者接收日志的入口函数,应该分别在runLeader()和runFollower()函数中调用的。赞!理解正确!
|
||||
|
||||
- 领导者复制日志的入口函数为startStopReplication(),在runLeader()中,以r.startStopReplication()形式被调用,作为一个单独协程运行。
|
||||
- 跟随者接收日志的入口函数为processRPC(),在runFollower()中以r.processRPC(rpc)形式被调用,来处理日志复制RPC消息。
|
||||
|
||||
不过,在分析日志复制的代码实现之前,咱们先来聊聊日志相关的数据结构,便于你更好地理解代码实现。
|
||||
|
||||
### 数据结构
|
||||
|
||||
08讲中我提到过,一条日志项主要包含了3种信息,分别是指令、索引值、任期编号,而在Hashicorp Raft实现中,日志对应的数据结构和函数接口是在log.go中实现的,其中,日志项对应的数据结构是结构体类型的,就像下面的样子:
|
||||
|
||||
```
|
||||
type Log struct {
|
||||
// 索引值
|
||||
Index uint64
|
||||
|
||||
// 任期编号
|
||||
Term uint64
|
||||
|
||||
// 日志项类别
|
||||
Type LogType
|
||||
|
||||
// 指令
|
||||
Data []byte
|
||||
|
||||
// 扩展信息
|
||||
Extensions []byte
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我强调一下,与协议中的定义不同,日志项对应的数据结构中,包含了LogType和Extensions两个额外的字段:
|
||||
|
||||
- LogType可用于标识不同用途的日志项,比如,使用LogCommand标识指令对应的日志项,使用LogConfiguration表示成员变更配置对应的日志项。
|
||||
<li>Extensions可用于在指定日志项中存储一些额外的信息。**这个字段使用的比较少,在调试等场景中可能会用到,你知道有这么个字段就可以了。**<br>
|
||||
说完日志复制对应的数据结构,我们分步骤看一下,在Hashicorp Raft中是如何实现日志复制的。</li>
|
||||
|
||||
### 领导者复制日志
|
||||
|
||||
日志复制是由领导者发起,在runLeader()函数中执行的,主要有这样几个步骤。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c2/63/c2aa9f2c31571e8cdc4dbe13ef210663.jpg" alt="">
|
||||
|
||||
1. 在 runLeader()函数中,调用startStopReplication()函数,执行日志复制功能。
|
||||
1. 启动一个新协程,调用replicate()函数,执行日志复制相关的功能。
|
||||
1. 在replicate()函数中,调用replicateTo()函数,执行步骤4,如果开启了流水线复制模式,执行步骤5。
|
||||
1. 在replicateTo()函数中,进行日志复制和日志一致性检测,如果日志复制成功,则设置s.allowPipeline = true,开启流水线复制模式。
|
||||
1. 调用pipelineReplicate()函数,采用更高效的流水线方式,进行日志复制。
|
||||
|
||||
在这里我强调一下,在什么条件下开启了流水线复制模式,很多同学可能会在这一块儿产生困惑,因为代码逻辑上有点儿绕。**你可以这么理解,是在不需要进行日志一致性检测,复制功能已正常运行的时候,开启了流水线复制模式,**目标是在环境正常的情况下,提升日志复制性能,如果在日志复制过程中出错了,就进入RPC复制模式,继续调用replicateTo()函数,进行日志复制。
|
||||
|
||||
### 跟随者接收日志
|
||||
|
||||
领导者复制完日志后,跟随者会接收日志并开始处理日志。跟随者接收和处理日志,是在runFollower()函数中执行的,主要有这样几个步骤。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/99/96/99458f26ef6387bc8b20b16380d46896.jpg" alt="">
|
||||
|
||||
1. 在runFollower()函数中,调用processRPC()函数,处理接收到的RPC消息。
|
||||
1. 在processRPC()函数中,调用appendEntries()函数,处理接收到的日志复制RPC请求。
|
||||
1. appendEntries()函数,是跟随者处理日志的核心函数。在步骤3.1中,比较日志一致性;在步骤3.2中,将新日志项存放在本地;在步骤3.3中,根据领导者最新提交的日志项索引值,来计算当前需要被应用的日志项,并应用到本地状态机。
|
||||
|
||||
讲到这儿,你应该可以了解日志复制的代码实现了吧。关于更多的Raft原理的代码实现,你可以继续阅读源码来学习,如果在学习过程中有疑问,欢迎给我留言。
|
||||
|
||||
## 内容小结
|
||||
|
||||
本节课我主要带你了解了如何从算法原理的角度理解Hashicorp Raft实现,有几个重点我想强调一下:
|
||||
|
||||
<li>
|
||||
跟随者、候选人、领导者3种节点状态都有分别对应的功能函数,当需要查看各节点状态相关的功能实现时(比如,跟随者如何接收和处理日志),都可以将对应的函数作为入口函数,来阅读代码和研究功能实现。
|
||||
</li>
|
||||
<li>
|
||||
raft.go是Hashicorp Raft的核心代码文件,大部分的核心功能都是在这个文件中实现的,平时可以多研究这个文件中的代码,直到彻底吃透,掌握。
|
||||
</li>
|
||||
<li>
|
||||
在Hashicorp Raft中,支持两种节点间通讯机制,内存型和TCP协议型,其中,内存型通讯机制,主要用于测试,2种通讯机制的代码实现,分别在文件inmem_transport.go和tcp_transport.go中。
|
||||
</li>
|
||||
<li>
|
||||
Hashicorp Raft实现,是常用的Golang版Raft算法的实现,被众多流行软件使用,如Consul、InfluxDB、IPFS等,相信你对它并不陌生。其他的实现还有[Go-Raft](https://github.com/goraft/raft)、[LogCabin](https://github.com/logcabin/logcabin)、[Willemt-Raft](https://github.com/willemt/raft)等,不过我建议你在后续开发分布式系统时,优先考虑Hashicorp Raft,因为Hashicorp Raft实现,功能完善、代码简洁高效、流行度高,可用性和稳定性被充分打磨。
|
||||
</li>
|
||||
|
||||
最后,关于如何高效地阅读源码,我还想多说一说。在我看来,高效阅读源码的关键在于抓住重点,要有“底线”,不要芝麻和西瓜一把抓,什么都想要,最终陷入到枝节琐碎的细节中出不来。什么是重点呢?我认为重点是数据结构和关键的代码执行流程,比如在Hashicorp Raft源码中,日志项对应的数据结构、RPC消息对应的数据结构、选举领导者的流程、日志复制的流程等,这些就是重点。
|
||||
|
||||
有的同学可能还有疑问:在阅读源码的时候,如果遇到不是很明白的代码,该怎么办呢?我建议你可以通过打印日志或GDB单步调试的方式,查看上下文中的变量的内容、代码执行逻辑等,帮助理解。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
在Hashicorp Raft实现中,我讲了如何实现选举领导者,以及如何复制日志等,那么在Hashicorp Raft中,网络通讯是如何实现的呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
@@ -0,0 +1,195 @@
|
||||
<audio id="audio" title="18 | Hashicorp Raft(二):如何以“集群节点”为中心使用API?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f3/03/f39013733bfe184c0d9e1e478e46e303.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
上一讲结束后,相信有的同学已经跃跃欲试,想把Hashicorp Raft使用起来了。不过,也有一些同学跟我反馈,说自己看到Hashicorp Raft的[Godoc](https://godoc.org/github.com/hashicorp/raft),阅读完接口文档后,感觉有些不知所措,无从下手,Hashicorp Raft支持了那么多的函数,自己却不知道如何将这些函数使用起来。
|
||||
|
||||
这似乎是一个共性的问题,在我看来,之所以出现这个问题,是因为文档里虽然提到了API的功能,但并没有提如何在实际场景中使用这些API,每个API都是孤立的点,缺乏一些场景化的线将它们串联起来。
|
||||
|
||||
所以,为了帮你更好地理解Hashicorp Raft的API接口,在实践中将它们用起来,我以“集群节点”为核心,通过创建、增加、移除集群节点,查看集群节点状态这4个典型的场景,具体聊一聊在Hashicorp Raft中,通过哪些API接口能创建、增加、移除集群节点,查看集群节点状态。这样一来,我们会一步一步,循序渐进地彻底吃透Hashicorp Raft的API接口用法。
|
||||
|
||||
我们知道,开发实现一个Raft集群的时候,首先要做的第一个事情就是创建Raft节点,那么在Hashicorp Raft中如何创建节点呢?
|
||||
|
||||
## 如何创建Raft节点
|
||||
|
||||
在Hashicorp Raft中,你可以通过NewRaft()函数,来创建Raft节点。我强调一下,NewRaft()是非常核心的函数,是Raft节点的抽象实现,NewRaft()函数的原型是这样的:
|
||||
|
||||
```
|
||||
func NewRaft(
|
||||
conf *Config,
|
||||
fsm FSM,
|
||||
logs LogStore,
|
||||
stable StableStore,
|
||||
snaps SnapshotStore,
|
||||
trans Transport) (*Raft, error)
|
||||
|
||||
```
|
||||
|
||||
你可以从这段代码中看到,NewRaft()函数有这么几种类型的参数,它们分别是:
|
||||
|
||||
- Config(节点的配置信息);
|
||||
- FSM(有限状态机);
|
||||
- LogStore(用来存储Raft的日志);
|
||||
- StableStore(稳定存储,用来存储Raft集群的节点信息等);
|
||||
- SnapshotStore(快照存储,用来存储节点的快照信息);
|
||||
- Transport(Raft节点间的通信通道)。
|
||||
|
||||
这6种类型的参数决定了Raft节点的配置、通讯、存储、状态机操作等核心信息,所以我带你详细了解一下,在这个过程中,你要注意是如何创建这些参数信息的。
|
||||
|
||||
Config是节点的配置信息,可通过函数DefaultConfig()来创建默认配置信息,然后按需修改对应的配置项。一般情况下,使用默认配置项就可以了。不过,有时你可能还是需要根据实际场景,来调整配置项的,比如:
|
||||
|
||||
- 如果在生产环境中部署的时候,你可以将LogLevel从DEBUG调整为WARM或ERROR;
|
||||
- 如果部署环境中网络拥堵,你可以适当地调大HeartbeatTimeout的值,比如,从1s调整为1.5s,避免频繁的领导者选举;
|
||||
|
||||
那么FSM又是什么呢?它是一个interface类型的数据结构,借助Golang Interface的泛型编程能力,应用程序可以实现自己的Apply(*Log)、Snapshot()、Restore(io.ReadCloser) 3个函数,分别实现将日志应用到本地状态机、生成快照和根据快照恢复数据的功能。FSM是日志处理的核心实现,原理比较复杂,不过不是咱们本节课的重点,现在你只需要知道这3个函数就可以了。在20讲,我会结合实际代码具体讲解的。
|
||||
|
||||
第三个参数LogStore存储的是Raft日志,你可以用[raft-boltdb](https://github.com/hashicorp/raft-boltdb)来实现底层存储,持久化存储数据。在这里我想说的是,raft-boltdb是Hashicorp团队专门为Hashicorp Raft持久化存储而开发设计的,使用广泛,打磨充分。具体用法是这样的:
|
||||
|
||||
```
|
||||
logStore, err := raftboltdb.NewBoltStore(filepath.Join(raftDir, "raft-log.db"))
|
||||
|
||||
```
|
||||
|
||||
NewBoltStore()函数只支持一个参数,也就是文件路径。
|
||||
|
||||
第四个参数StableStore存储的是节点的关键状态信息,比如,当前任期编号、最新投票时的任期编号等,同样,你也可以采用raft-boltdb来实现底层存储,持久化存储数据。
|
||||
|
||||
```
|
||||
stableStore, err := raftboltdb.NewBoltStore(filepath.Join(raftDir, "raft-stable.db"))
|
||||
|
||||
```
|
||||
|
||||
第五个参数SnapshotStore存储的是快照信息,也就是压缩后的日志数据。在Hashicorp Raft中提供了3种快照存储方式,它们分别是:
|
||||
|
||||
- DiscardSnapshotStore(不存储,忽略快照,相当于/dev/null,一般来说用于测试);
|
||||
- FileSnapshotStore(文件持久化存储);
|
||||
- InmemSnapshotStore(内存存储,不持久化,重启程序后,数据会丢失)。
|
||||
|
||||
**这3种方式,在生产环境中,建议你采用FileSnapshotStore实现快照, 使用文件持久化存储,避免因程序重启,导致快照数据丢失。**具体代码实现如下:
|
||||
|
||||
```
|
||||
snapshots, err := raft.NewFileSnapshotStore(raftDir, retainSnapshotCount, os.Stderr)
|
||||
|
||||
```
|
||||
|
||||
NewFileSnapshotStore()函数支持3个参数。也就是说,除了指定存储路径(raftDir),还要指定需要保留的快照副本的数量(retainSnapshotCount),以及日志输出的方式。**一般而言,将日志输出到标准错误IO就可以了。**
|
||||
|
||||
最后一个Transport指的是Raft集群内部节点之间的通信机制,节点之间需要通过这个通道来进行日志同步、领导者选举等等。Hashicorp Raft支持两种方式:
|
||||
|
||||
- 一种是基于TCP协议的TCPTransport,可以跨机器跨网络通信的;
|
||||
- 另一种是基于内存的InmemTransport,不走网络,在内存里面通过Channel来通信。
|
||||
|
||||
**在生产环境中,我建议你使用TCPTransport,**使用TCP进行网络通讯,突破单机限制,提升集群的健壮性和容灾能力。具体代码实现如下:
|
||||
|
||||
```
|
||||
addr, err := net.ResolveTCPAddr("tcp", raftBind)
|
||||
transport, err := raft.NewTCPTransport(raftBind, addr, maxPool, timeout, os.Stderr)
|
||||
|
||||
```
|
||||
|
||||
NewTCPTransport()函数支持5个参数,也就是,指定创建连接需要的信息。比如,要绑定的地址信息(raftBind、addr)、连接池的大小(maxPool)、超时时间(timeout),以及日志输出的方式,一般而言,将日志输出到标准错误IO就可以了。
|
||||
|
||||
以上就是这6个参数的详细内容了,既然我们已经了解了这些基础信息,那么如何使用NewRaft()函数呢?其实,你可以在代码中直接调用NewRaft()函数,创建Raft节点对象,就像下面的样子:
|
||||
|
||||
```
|
||||
raft, err := raft.NewRaft(config, (*storeFSM)(s), logStore, stableStore, snapshots, transport)
|
||||
|
||||
```
|
||||
|
||||
接口清晰,使用方便,你可以亲手试一试。
|
||||
|
||||
现在,我们已经创建了Raft节点,打好了基础,但是我们要实现的是一个多节点的集群,所以,创建一个节点是不够的,另外,创建了节点后,你还需要让节点启动,当一个节点启动后,你还需要创建新的节点,并将它加入到集群中,那么具体怎么操作呢?
|
||||
|
||||
## 如何增加集群节点
|
||||
|
||||
集群最开始的时候,只有一个节点,我们让第一个节点通过bootstrap的方式启动,它启动后成为领导者:
|
||||
|
||||
```
|
||||
raftNode.BootstrapCluster(configuration)
|
||||
|
||||
```
|
||||
|
||||
BootstrapCluster()函数只支持一个参数,也就是Raft集群的配置信息,因为此时只有一个节点,所以配置信息为这个节点的地址信息。
|
||||
|
||||
后续的节点在启动的时候,可以通过向第一个节点发送加入集群的请求,然后加入到集群中。具体来说,先启动的节点(也就是第一个节点)收到请求后,获取对方的地址(指Raft集群内部通信的TCP地址),然后调用AddVoter()把新节点加入到集群就可以了。具体代码如下:
|
||||
|
||||
```
|
||||
raftNode.AddVoter(id,
|
||||
addr, prevIndex, timeout)
|
||||
|
||||
```
|
||||
|
||||
AddVoter()函数支持4个参数,使用时,一般只需要设置服务器ID信息和地址信息 ,其他参数使用默认值0,就可以了:
|
||||
|
||||
- id(服务器ID信息);
|
||||
- addr(地址信息);
|
||||
- prevIndex(前一个集群配置的索引值,一般设置为0,使用默认值);
|
||||
- timeout(在完成集群配置的日志项添加前,最长等待多久,一般设置为0,使用默认值)。
|
||||
|
||||
当然了,也可以通过AddNonvoter(),将一个节点加入到集群中,但不赋予它投票权,让它只接收日志记录,这个函数平时用不到,你只需知道有这么函数,就可以了。
|
||||
|
||||
在这里,我想补充下,早期版本中的用于增加集群节点的函数,AddPeer()函数,已废弃,不再推荐使用。
|
||||
|
||||
你看,在创建集群或者扩容时,我们尝试着增加了集群节点,但一旦出现不可恢复性的机器故障或机器裁撤时,我们就需要移除节点,进行节点替换,那么具体怎么做呢?
|
||||
|
||||
## 如何移除集群节点
|
||||
|
||||
我们可以通过RemoveServer()函数来移除节点,具体代码如下:
|
||||
|
||||
```
|
||||
raftNode.RemoveServer(id, prevIndex, timeout)
|
||||
|
||||
```
|
||||
|
||||
RemoveServer()函数支持3个参数,使用时,一般只需要设置服务器ID信息 ,其他参数使用默认值0,就可以了:
|
||||
|
||||
- id(服务器ID信息);
|
||||
- prevIndex(前一个集群配置的索引值,一般设置为0,使用默认值);
|
||||
- timeout(在完成集群配置的日志项添加前,最长等待多久,一般设置为0,使用默认值)。
|
||||
|
||||
我要强调一下,RemoveServer()函数必须在领导者节点上运行,否则就会报错。这一点,很多同学在实现移除节点功能时会遇到,所以需要注意一下。
|
||||
|
||||
最后,我想补充下,早期版本中的用于移除集群节点的函数,RemovePeer()函数也已经废弃了,不再推荐使用。
|
||||
|
||||
关于如何移除集群节点的代码实现,也比较简单易用,通过服务器ID信息,就可以将对应的节点移除了。除了增加和移除集群节点,在实际场景中,我们在运营分布式系统时,有时需要查看节点的状态。那么该如何查看节点状态呢?
|
||||
|
||||
## 如何查看集群节点状态
|
||||
|
||||
在分布式系统中,日常调试的时候,节点的状态信息是很重要的,比如在Raft分布式系统中,如果我们想抓包分析写请求,那么必须知道哪个节点是领导者节点,它的地址信息是多少,因为在Raft集群中,只有领导者能处理写请求。
|
||||
|
||||
那么在Hashicorp Raft中,如何查看节点状态信息呢?
|
||||
|
||||
我们可以通过Raft.Leader()函数,查看当前领导者的地址信息,也可以通过Raft.State()函数,查看当前节点的状态,是跟随者、候选人,还是领导者。不过你要注意,Raft.State()函数返回的是RaftState格式的信息,也就是32位无符号整数,适合在代码中使用。**如果想在日志或命令行接口中查看节点状态信息,我建议你使用RaftState.String()函数,**通过它,你可以查看字符串格式的当前节点状态。
|
||||
|
||||
为了便于你理解,我举个例子。比如,你可以通过下面的代码,判断当前节点是否是领导者节点:
|
||||
|
||||
```
|
||||
func isLeader() bool {
|
||||
return raft.State() == raft.Leader
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
了解了节点状态,你就知道了当前集群节点之间的关系,以及功能和节点的对应关系,这样一来,你在遇到问题,需要调试跟踪时,就知道应该登录到哪台机器去调试分析了。
|
||||
|
||||
## 内容小结
|
||||
|
||||
本节课我主要以“集群节点”为核心,带你了解了Hashicorp Raft的常用API接口,我希望你明确的重点如下:
|
||||
|
||||
<li>
|
||||
除了提到的raft-boltdb做作为LogStore和StableStore,也可以调用NewInmemStore()创建内存型存储,在测试时比较方便,重新执行程序进行测试时,不需要手动清理数据存储。
|
||||
</li>
|
||||
<li>
|
||||
你还可以通过NewInmemTransport()函数,实现内存型通讯接口,在测试时比较方便,将集群通过内存进行通讯,运行在一台机器上。
|
||||
</li>
|
||||
<li>
|
||||
你可以通过Raft.Stats()函数,查看集群的内部统计信息,比如节点状态、任期编号、节点数等,这在调试或确认节点运行状况的时候很有用。
|
||||
</li>
|
||||
|
||||
我以集群节点为核心,讲解了Hashicorp Raft常用的API接口,相信现在你已经掌握这些接口的用法了,对如何开发一个分布式系统,也有了一定的感觉。既然学习是为了使用,那么我们学完这些内容,也应该用起来才是,所以,为了帮你更好地掌握Raft分布式系统的开发实战技巧,我会用接下来两节课的时间,以分布式KV系统开发实战为例,带你了解Raft的开发实战技巧。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
我提到了一些常用的API接口,比如创建Raft节点、增加集群节点、移除集群节点、查看集群节点状态等,你不妨思考一下,如何创建一个支持InmemTransport的Raft节点呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
186
极客时间专栏/geek/分布式协议与算法实战/实战篇/19 | 基于Raft的分布式KV系统开发实战(一):如何设计架构?.md
Normal file
186
极客时间专栏/geek/分布式协议与算法实战/实战篇/19 | 基于Raft的分布式KV系统开发实战(一):如何设计架构?.md
Normal file
@@ -0,0 +1,186 @@
|
||||
<audio id="audio" title="19 | 基于Raft的分布式KV系统开发实战(一):如何设计架构?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fd/08/fd4ca0d1156789e15bcb6fb830e70708.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
学完前面2讲之后,相信你已经大致了解了Raft算法的代码实现(Hashcorp Raft),也掌握了常用API接口的用法,对Raft算法的理解也更深刻了。那么,是不是掌握这些,就能得心应手的处理实际场景的问题了呢?
|
||||
|
||||
在我看来,掌握的还不够,因为Raft算法的实现只是工具。而掌握了工具的用法,和能使用工具得心应手地处理实际场景的问题,是两回事。也就是说,我们还需要掌握使用Raft算法开发分布式系统的实战能力,然后才能游刃有余的处理实际场景的问题。
|
||||
|
||||
我从这个角度出发,在接下来的2节课中,我会分别从架构和代码实现的角度,以一个基本的分布式KV系统为例,具体说一说,如何基于Raft算法构建一个分布式KV系统。**那么我希望你能课下多动手,自己写一遍,不给自己留下盲区。**如果条件允许的话,你还可以按需开发实现需要的功能,并将这套系统作为自己的“配置中心”“名字路由”维护下去,不断在实战中加深自己对技术的理解。
|
||||
|
||||
可能有同学会问:“老韩,为什么不以Etcd为例呢?它不是已经在生产环境中落地了吗?”
|
||||
|
||||
我是这么考虑的,这个基本的分布式KV系统的代码比较少,相对纯粹聚焦在技术本身,涉及的KV业务层面的逻辑少,适合入门学习(比如你可以从零开始,动手编程实现),是一个很好的学习案例。
|
||||
|
||||
另外,对一些有经验的开发者来说,这部分知识能够帮助你掌握Raft算法中,一些深层次的技术实现,比如如何实现多种读一致性模型,让你更加深刻地理解Raft算法。
|
||||
|
||||
今天这节课,我会具体说一说如何设计一个基本的分布式KV系统,也就是需要实现哪些功能,以及在架构设计的时候,你需要考虑哪些点(比如跟随者是否要转发写请求给领导者?或者如何设计接入访问的API?)
|
||||
|
||||
好了,话不多说,一起进入今天的课程吧!
|
||||
|
||||
在我看来,基于技术深度、开发工作量、学习复杂度等综合考虑,一个基本的分布式KV系统,至少需要具备这样几块功能,就像下图的样子。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1e/6d/1e8e7cac5c9159aa1dbf9a72cd90416d.jpg" alt="">
|
||||
|
||||
- 接入协议:供客户端访问系统的接入层API,以及与客户端交互的通讯协议。
|
||||
- KV操作:我们需要支持的KV操作(比如赋值操作)。
|
||||
- 分布式集群:也就是说,我们要基于Raft算法实现一个分布式存储集群,用于存放KV数据。
|
||||
|
||||
需要你注意的是,这3点就是分布式KV系统的核心功能,也就是我们需要编程实现的需求。
|
||||
|
||||
在我看来,要实现一个基本的分布式KV系统,首先要做的第一件事,就是实现访问接入的通讯协议。因为如果用户想使用这套系统,对他而言的第一件事,就是如何访问这套系统。那么,如何实现访问接入的通讯协议呢?
|
||||
|
||||
## 如何设计接入协议?
|
||||
|
||||
我想说的是,在早些时候,硬件性能低,服务也不是很多,开发系统的时候,主要矛盾是性能瓶颈,所以,更多的是基于性能的考虑,采用UDP协议和实现私有的二进制协议,比如,早期的QQ后台组件,就是这么做的。
|
||||
|
||||
现在呢,硬件性能有了很大幅度的提升,后台服务器的CPU核数都近百了,开发系统的时候,主要的矛盾已经不是性能瓶颈了,而是快速增长的海量服务和开发效率,所以这时,基于开发效率和可维护性的考虑,我们就需要优先考虑标准的协议了(比如HTTP)。
|
||||
|
||||
如果使用HTTP协议,那么就需要设计HTTP RESTful API,作为访问接口。具体怎么设计呢?
|
||||
|
||||
我想说的是,因为我们设计实现的是KV系统,肯定要涉及到KV操作,那么我们就一定需要设计个API(比如"/key")来支持KV操作。也就是说,通过访问这个API,我们能执行相关的KV操作了,就像下面的样子(查询指定key(就是foo)对应的值)。
|
||||
|
||||
```
|
||||
curl -XGET http://raft-cluster-host01:8091/key/foo
|
||||
|
||||
```
|
||||
|
||||
另外,需要你注意的是,因为这是一个Raft集群系统,除了业务层面(KV操作),我们还需要实现平台本身的一些操作的API接口,比如增加、移除集群节点等。我们现在只考虑增加节点操作的API(比如"/join"),就像下面的样子。
|
||||
|
||||
```
|
||||
http://raft-cluster-host01:8091/join
|
||||
|
||||
```
|
||||
|
||||
另外,在故障或缩容情况下,如何替换节点、移除节点,我建议你在线下对比着增加节点的操作,自主实现。
|
||||
|
||||
除此之外,在我看来,实现HTTP RESTful API,还有非常重要的一件事情要做,那就是在设计API时,考虑如何实现路由,为什么这么说呢?你这么想象一下,如果我们实现了多个API,比如"/key"和"/join",那么就需要将API对应的请求和它对应的处理函数一一映射起来。
|
||||
|
||||
我想说的是,我们可以在serveHTTP()函数(Golang)中,通过检测URL路径,来设置请求对应处理函数,实现路由。大概的原理,就像下面的样子。
|
||||
|
||||
```
|
||||
func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { // 设置HTTP请求对应的路由信息
|
||||
if strings.HasPrefix(r.URL.Path, "/key") {
|
||||
s.handleKeyRequest(w, r)
|
||||
} else if r.URL.Path == "/join" {
|
||||
s.handleJoin(w, r)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
从上面代码中,我们可以看到,当检测到URL路径为“/key”时,会调用handleKeyRequest()函数,来处理KV操作请求;当检测到URL路径为"/join"时,会调用handleJoin()函数,将指定节点加入到集群中。
|
||||
|
||||
你看,通过"/key"和"/join"2个API,我们就能满足这个基本的分布式KV系统的运行要求了,既能支持来自客户端的KV操作,也能新增节点并将集群运行起来。
|
||||
|
||||
当客户端通过通讯协议访问到系统后,它最终的目标,还是执行KV操作。那么,我们该如何设计KV操作呢?
|
||||
|
||||
## 如何设计KV操作?
|
||||
|
||||
我想说的是,常见的KV操作是赋值、查询、删除,也就是说,我们实现这三个操作就可以了,其他的操作可以先不考虑。具体可以这么实现。
|
||||
|
||||
- **赋值操作:**我们可以通过HTTP POST请求,来对指定key进行赋值,就像下面的样子。
|
||||
|
||||
```
|
||||
curl -XPOST http://raft-cluster-host01:8091/key -d '{"foo": "bar"}'
|
||||
|
||||
```
|
||||
|
||||
- **查询操作:**我们可以通过HTTP GET请求,来查询指定key的值,就像下面的样子。
|
||||
|
||||
```
|
||||
curl -XGET http://raft-cluster-host01:8091/key/foo
|
||||
|
||||
|
||||
```
|
||||
|
||||
- **删除操作:**我们可以通过HTTP DELETE请求,来删除指定key和key对应的值,就像下面的样子。
|
||||
|
||||
```
|
||||
curl -XDELETE http://raft-cluster-host01:8091/key/foo
|
||||
|
||||
```
|
||||
|
||||
在这里,尤其需要你注意的是,操作需要具有幂等性。幂等性这个词儿你估计不会陌生,你可以这样理解它:同一个操作,不管执行多少次,最终的结果都是一样的,也就是,这个操作是可以重复执行的,而是重复执行不会对系统产生预期外的影响。
|
||||
|
||||
为什么操作要具有幂等性呢?
|
||||
|
||||
因为共识算法能保证达成共识后的值(也就是指令)就不再改变了,但不能保证值只被提交一次,也就是说,共识算法是一个“at least once”的指令执行模型,是可能会出现同一个指令被重复提交的情况,为什么呢?我以Raft算法为例,具体说一说。
|
||||
|
||||
比如,如果客户端接收到Raft的超时响应后,也就是这时日志项还没有提交成功,如果此时它重试,发送一个新的请求,那么这个时候Raft会创建一个新的日志项,并最终将新旧2个日志项都提交了,出现了指令重复执行的情况。
|
||||
|
||||
在这里我想强调的是,你一定要注意到这样的情况,在使用Raft等共识算法时,要充分评估操作是否具有幂等性,避免对系统造成预期外的影响,比如,直接使用“Add”操作,就会因重复提交,导致最终的执行结果不准了,影响到业务。这就可能会出现,用户购买了100Q币,系统却给他充值了500Q币,肯定不行了。
|
||||
|
||||
说完如何设计KV操作后,因为我们的最终目标是实现分布式KV系统,那么,就让我们回到分布式系统最本源的一个问题上,如何实现分布式集群?
|
||||
|
||||
## 如何实现分布式集群?
|
||||
|
||||
我想说的是,正如在09讲中提到的,我推荐使用Raft算法实现分布式集群。而实现一个Raft集群,我们首先要考虑的是如何创建集群,为了简单起见,我们暂时不考虑节点的移除和替换等。
|
||||
|
||||
**创建集群**
|
||||
|
||||
在Raft算法中,我们可以这样创建集群。
|
||||
|
||||
- 先将第一个节点,通过Bootstrap的方式启动,并作为领导者节点。
|
||||
- 其他节点与领导者节点通讯,将自己的配置信息发送给领导者节点,然后领导者节点调用AddVoter()函数,将新节点加入到集群中。
|
||||
|
||||
创建了集群后,在集群运行中,因为Raft集群的领导者不是固定不变的,而写请求是必须要在领导者节点上处理的,那么如何实现写操作,来保证写请求都会发给领导者呢?
|
||||
|
||||
**写操作**<br>
|
||||
一般而言,有2种方法来实现写操作。我来具体说说。
|
||||
|
||||
**方法1:**跟随者接收到客户端的写请求后,拒绝处理这个请求,并将领导者的地址信息返回给客户端,然后客户端直接访问领导者节点,直到该领导者退位,就像下图的样子。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/59/c8/591314be65729ae2ca242e5e016e84c8.jpg" alt="">
|
||||
|
||||
**方法2:**跟随者接收到客户端的写请求后,将写请求转发给领导者,并将领导者处理后的结果返回给客户端,也就是说,这时跟随者在扮演“代理”的角色,就像下图的样子。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ac/96/ac7f6e9226c0dc3f323abb70b9a3b596.jpg" alt="">
|
||||
|
||||
在我看来,虽然第一种方法需要客户端的配合,但实现起来复杂度不高;另外,第二种方法,虽然能降低客户端的复杂度,客户端像访问一个黑盒一样,访问系统,对领导者变更完全无感知。
|
||||
|
||||
但是这个方法会引入一个中间节点(跟随者),增加了问题分析排查的复杂度。而且,一般情况下,在绝大部分的时间内(比如Google Chubby团队观察到的值是数天),领导者是处于稳定状态的,某个节点一直是领导者,那么引入中间节点,就会增加大量的不必要的消息和性能消耗。所以,综合考虑,我推荐方法1。
|
||||
|
||||
学习了Raft算法后,我们知道,相比写操作(只要在领导者节点执行就可以了)而言,读操作要复杂些,因为如何实现读操作,关乎着一致性的实现,也就是说,怎么实现读操作,决定了客户端是否会读取到旧数据。那么如何实现读操作呢?
|
||||
|
||||
**读操作**<br>
|
||||
其实,在实际系统中,并不是实现了强一致性就是最好的,因为实现了强一致性,必然会限制集群的整体性能。也就是说,我们需要根据实际场景特点进行权衡折中,这样,才能设计出最适合该场景特点的读操作。比如,我们可以实现类似Consul的3种读一致性模型。
|
||||
|
||||
- default:偶尔读到旧数据。
|
||||
- consistent:一定不会读到旧数据。
|
||||
- stale:会读到旧数据。
|
||||
|
||||
如果你不记得这3种模型的含义了,你可以去09讲回顾下,在这里,我就不啰嗦了。
|
||||
|
||||
也就是说,我们可以实现多种读一致性模型,将最终的一致性选择权交给用户,让用户去选择,就像下面的样子。
|
||||
|
||||
```
|
||||
curl -XGET http://raft-cluster-host02:8091/key/foo?level=consistent -L
|
||||
|
||||
```
|
||||
|
||||
## 内容小结
|
||||
|
||||
本节课我主要带你了解了一个基本的分布式KV系统的架构,和需要权衡折中的技术细节,我希望你明确的重点如下。
|
||||
|
||||
1.在设计KV操作时,更确切的说,在实现Raft指令时,一定要考虑幂等性,因为Raf指令是可能会被重复提交和执行。
|
||||
|
||||
2.推荐你采用这种方式来实现写操作:跟随者接收到客户端的写请求时,拒绝该请求并返回领导者的地址信息给客户端,然后客户端直接访问领导者。
|
||||
|
||||
3.在Raft集群中,如何实现读操作,关乎一致性的实现,推荐实现default、consistent、stale三种一致性模型,将一致性的选择权交给用户,让用户根据实际业务特点,按需选择,灵活使用。
|
||||
|
||||
最后,我想说的是,这个基本的分布式KV系统,除了适合入门学习外,也比较适合配置中心、名字服务等小数据量的系统。另外我想补充一下,对于数据层组件,不仅性能重要,成本也很重要,而决定数据层组件的成本的最关键的一个理念是冷热分离,一般而言,可以这么设计三级缓存:
|
||||
|
||||
- 热数据:经常被访问到的数据,我们可以将它们放在内存中,提升访问效率。
|
||||
- 冷数据:有时会被访问到的数据,我们可以将它们放在SSD硬盘上,访问起来也比较快。
|
||||
- 陈旧数据:偶尔会被访问到的数据,我们可以将它们放在普通磁盘上,节省存储成本。
|
||||
|
||||
在实际系统中,你可以统计热数据的命中率,并根据命中率来动态调整冷热模型。在这里,我想强调的是,冷热分离理念在设计海量数据存储系统时尤为重要,比如,自研KV存储的成本仅为Redis数十分之一,其中系统设计时非常重要的一个理念就是冷热分离。希望你能重视这个理念,在实际场景中活学活用。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
我提到了其他节点与领导者节点通讯,将自己的配置信息发送给领导者节点,然后领导者节点调用addVoter()函数,将新节点加入到集群中,那么,你不妨思考一下,当节点故障时,如何替换一个节点呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
211
极客时间专栏/geek/分布式协议与算法实战/实战篇/20 | 基于Raft的分布式KV系统开发实战(二):如何实现代码?.md
Normal file
211
极客时间专栏/geek/分布式协议与算法实战/实战篇/20 | 基于Raft的分布式KV系统开发实战(二):如何实现代码?.md
Normal file
@@ -0,0 +1,211 @@
|
||||
<audio id="audio" title="20 | 基于Raft的分布式KV系统开发实战(二):如何实现代码?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8e/64/8e4159f5178fbdd3144cd289b7ff2a64.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
学完[上一讲](https://time.geekbang.org/column/article/217049)后,相信你已经了解了分布式KV系统的架构设计,同时应该也很好奇,架构背后的细节代码是怎么实现的呢?
|
||||
|
||||
别着急,今天这节课,我会带你弄明白这个问题。我会具体讲解[分布式KV系统](https://github.com/hanj4096/raftdb)核心功能点的实现细节。比如,如何实现读操作对应的3种一致性模型。而我希望你能在课下反复运行程序,多阅读源码,掌握所有的细节实现。
|
||||
|
||||
话不多说,我们开始今天的学习。
|
||||
|
||||
在上一讲中,咱们将系统划分为三大功能块(接入协议、KV操作、分布式集群),那么今天我会按顺序具体说一说每块功能的实现,帮助你掌握架构背后的细节代码。首先,先来了解一下,如何实现接入协议。
|
||||
|
||||
## 如何实现接入协议?
|
||||
|
||||
在19讲提到,我们选择了HTTP协议作为通讯协议,并设计了"/key"和"/join"2个HTTP RESTful API,分别用于支持KV操作和增加节点的操作,那么,它们是如何实现的呢?
|
||||
|
||||
接入协议的核心实现,就是下面的样子。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b7/56/b72754232480fadd7d8eeb9bfdd15e56.jpg" alt="" title="图1">
|
||||
|
||||
我带你走一遍这三个步骤,便于你加深印象。
|
||||
|
||||
1. 在ServeHTTP()中,会根据URL路径设置相关的路由信息。比如,会在handlerKeyRequest()中处理URL路径前缀为"/key"的请求,会在handleJoin()中处理URL路径为"/join"的请求。
|
||||
1. 在handleKeyRequest()中,处理来自客户端的KV操作请求,也就是基于HTTP POST请求的赋值操作、基于HTTP GET请求的查询操作、基于HTTP DELETE请求的删除操作。
|
||||
1. 在handleJoin()中,处理增加节点的请求,最终调用raft.AddVoter()函数,将新节点加入到集群中。
|
||||
|
||||
在这里,需要你注意的是,在根据URL设置相关路由信息时,你需要考虑是路径前缀匹配(比如strings.HasPrefix(r.URL.Path, “/key”)),还是完整匹配(比如r.URL.Path == “/join”),避免在实际运行时,路径匹配出错。比如,如果对"/key"做完整匹配(比如r.URL.Path == “/key”),那么下面的查询操作会因为路径匹配出错,无法找到路由信息,而执行失败。
|
||||
|
||||
```
|
||||
curl -XGET raft-cluster-host01:8091/key/foo
|
||||
|
||||
```
|
||||
|
||||
另外,还需要你注意的是,只有领导者节点才能执行raft.AddVoter()函数,也就是说,handleJoin()函数,只能在领导者节点上执行。
|
||||
|
||||
说完接入协议后,接下来咱们来分析一下第二块功能的实现,也就是,如何实现KV操作。
|
||||
|
||||
## 如何实现KV操作?
|
||||
|
||||
上一节课,我提到这个分布式KV系统会实现赋值、查询、删除3类操作,那具体怎么实现呢?你应该知道,赋值操作是基于HTTP POST请求来实现的,就像下面的样子。
|
||||
|
||||
```
|
||||
curl -XPOST http://raft-cluster-host01:8091/key -d '{"foo": "bar"}'
|
||||
|
||||
```
|
||||
|
||||
也就是说,我们是通过HTTP POST请求,实现了赋值操作。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ad/91/ad3f4c3955f721fe60d7f041ea9aae91.jpg" alt="" title="图2">
|
||||
|
||||
同样的,我们走一遍这个过程,加深一下印象。
|
||||
|
||||
1. 当接收到KV操作的请求时,系统将调用handleKeyRequest()进行处理。
|
||||
1. 在handleKeyRequest()函数中,检测到HTTP请求类型为POST请求时,确认了这是一个赋值操作,将执行store.Set()函数。
|
||||
1. 在Set()函数中,将创建指令,并通过raft.Apply()函数将指令提交给Raft。最终指令将被应用到状态机。
|
||||
1. 当Raft将指令应用到状态机后,最终将执行applySet()函数,创建相应的key和值到内存中。
|
||||
|
||||
在这里,我想补充一下,FSM结构复用了Store结构体,并实现了fsm.Apply()、fsm.Snapshot()、fsm.Restore()3个函数。最终应用到状态机的数据,以map[string]string的形式,存放在Store.m中。
|
||||
|
||||
那查询操作是怎么实现的呢?它是基于HTTP GET请求来实现的。
|
||||
|
||||
```
|
||||
curl -XGET http://raft-cluster-host01:8091/key/foo
|
||||
|
||||
```
|
||||
|
||||
也就是说,我们是通过HTTP GET请求实现了查询操作。在这里我想强调一下,相比需要将指令应用到状态机的赋值操作,查询操作要简单多了,因为系统只需要查询内存中的数据就可以了,不涉及状态机。具体的代码流程如图所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c7/b4/c70b009d5abd3b3f63c0d1d419ede9b4.jpg" alt="" title="图3">
|
||||
|
||||
我们走一遍这个过程,加深一下印象。
|
||||
|
||||
1. 当接收到KV操作的请求时,系统将调用handleKeyRequest()进行处理。
|
||||
1. 在handleKeyRequest()函数中,检测到HTTP请求类型为GET请求时,确认了这是一个赋值操作,将执行store.Get()函数。
|
||||
1. Get()函数在内存中查询指定key对应的值。
|
||||
|
||||
而最后一个删除操作,是基于HTTP DELETE请求来实现的。
|
||||
|
||||
```
|
||||
curl -XDELETE http://raft-cluster-host01:8091/key/foo
|
||||
|
||||
```
|
||||
|
||||
也就是说,我们是通过HTTP DELETE请求,实现了删除操作。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/90/4e/90f99bc9c4aebb50c39f05412fa4594e.jpg" alt="" title="图4">
|
||||
|
||||
同样的,我们走一遍这个过程。
|
||||
|
||||
1. 当接收到KV操作的请求时,系统将调用handleKeyRequest()进行处理。
|
||||
1. 在handleKeyRequest()函数中,检测到HTTP请求类型为DELETE请求时,确认了这是一个删除操作,将执行store.Delete()函数。
|
||||
1. 在Delete()函数中,将创建指令,并通过raft.Apply()函数,将指令提交给Raft。最终指令将被应用到状态机。
|
||||
1. 当前Raft将指令应用到状态机后,最终执行applyDelete()函数,删除key和值。
|
||||
|
||||
学习这部分内容的时候,有一些同学可能会遇到,不知道如何判断指定的操作是否需要在领导者节点上执行的问题,我给的建议是这样的。
|
||||
|
||||
- 需要向Raft状态机中提交指令的操作,是必须要在领导者节点上执行的,也就是所谓的写请求,比如赋值操作和删除操作。
|
||||
- 需要读取最新数据的查询操作(比如客户端设置查询操作的读一致性级别为consistent),是必须在领导者节点上执行的。
|
||||
|
||||
说完了如何实现KV操作后,来看一下最后一块功能,如何实现分布式集群。
|
||||
|
||||
## 如何实现分布式集群?
|
||||
|
||||
### 创建集群
|
||||
|
||||
实现一个Raft集群,首先我们要做的就是创建集群,创建Raft集群,主要分为两步。首先,第一个节点通过Bootstrap的方式启动,并作为领导者节点。启动命令就像下面的样子。
|
||||
|
||||
```
|
||||
$GOPATH/bin/raftdb -id node01 -haddr raft-cluster-host01:8091 -raddr raft-cluster-host01:8089 ~/.raftdb
|
||||
|
||||
```
|
||||
|
||||
这时将在Store.Open()函数中,调用BootstrapCluster()函数将节点启动起来。
|
||||
|
||||
接着,其他节点会通过-join参数指定领导者节点的地址信息,并向领导者节点发送,包含当前节点配置信息的增加节点请求。启动命令就像下面的样子。
|
||||
|
||||
```
|
||||
$GOPATH/bin/raftdb -id node02 -haddr raft-cluster-host02:8091 -raddr raft-cluster-host02:8089 -join raft-cluster-host01:8091 ~/.raftdb
|
||||
|
||||
```
|
||||
|
||||
当领导者节点接收到来自其他节点的增加节点请求后,将调用handleJoin()函数进行处理,并最终调用raft.AddVoter()函数,将新节点加入到集群中。
|
||||
|
||||
在这里,需要你注意的是,只有在向集群中添加新节点时,才需要使用-join参数。当节点加入集群后,就可以像下面这样,正常启动进程就可以了。
|
||||
|
||||
```
|
||||
$GOPATH/bin/raftdb -id node02 -haddr raft-cluster-host02:8091 -raddr raft-cluster-host02:8089 ~/.raftdb
|
||||
|
||||
```
|
||||
|
||||
集群运行起来后,因为领导者是可能会变的,那么如何实现写操作,来保证写请求都在领导者节点上执行呢?
|
||||
|
||||
### 写操作
|
||||
|
||||
在19讲中,我们选择了方法2来实现写操作。也就是,当跟随者接收到写请求后,将拒绝处理该请求,并将领导者的地址信息转发给客户端。后续客户端就可以直接访问领导者(为了演示方便,我们以赋值操作为例)。<br>
|
||||
<img src="https://static001.geekbang.org/resource/image/0a/58/0a79be9a402addd226c0df170268a658.jpg" alt="" title="图5">
|
||||
|
||||
我们来看一下具体的内容。
|
||||
|
||||
1. 调用Set()函数执行赋值操作。
|
||||
1. 如果执行Set()函数成功,将执行步骤3;如果执行Set()函数出错,且提示出错的原因是当前节点不是领导者,那这就说明了当前节点不是领导者,不能执行写操作,将执行步骤4;如果执行Set()函数出错,且提示出错的原因不是因为当前节点不是领导者,将执行步骤5。
|
||||
1. 赋值操作执行成功,正常返回。
|
||||
1. 节点将构造包含领导者地址信息的重定向响应,并返回给客户端。然后客户端直接访问领导者节点执行赋值操作。
|
||||
1. 系统运行出错,返回错误信息给客户端。
|
||||
|
||||
需要你注意的是,赋值操作和删除操作属于写操作,必须在领导者节点上执行。而查询操作,只是查询内存中的数据,不涉及指令提交,可以在任何节点上执行。
|
||||
|
||||
而为了更好的利用curl客户端的HTTP重定向功能,我实现了HTTP 307重定向,这样,你在执行赋值操作时,就不需要关心访问节点是否是领导者节点了。比如,你可以使用下面的命令,访问节点2(也就是raft-cluster-host02,192.168.0.20)执行赋值操作。
|
||||
|
||||
```
|
||||
curl -XPOST raft-cluster-host02:8091/key -d '{"foo": "bar"}' -L
|
||||
|
||||
```
|
||||
|
||||
如果当前节点(也就是节点2)不是领导者,它将返回包含领导者地址信息的HTTP 307重定向响应给curl。这时,curl根据响应信息,重新发起赋值操作请求,并直接访问领导者节点(也就是节点1,192.168.0.10)。具体的过程,就像下面的Wireshark截图。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/27/fe/27b9005d47f65ca9d231da6e5bddbafe.jpg" alt="" title="图6">
|
||||
|
||||
相比写请求必须在领导者节点上执行,虽然查询操作属于读操作,可以在任何节点上执行,但是如何实现却更加复杂,因为读操作的实现关乎着一致性的实现。那么,具体怎么实现呢?
|
||||
|
||||
### 读操作
|
||||
|
||||
我想说的是,我们可以实现3种一致性模型(也就是stale、default、consistent),这样,用户就可以根据场景特点,按需选择相应的一致性级别,是不是很灵活呢?
|
||||
|
||||
具体的读操作的代码实现,就像下面的样子。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/42/97/42cdc5944e200f20f0cdcfef6891cc97.jpg" alt="" title="图7">
|
||||
|
||||
我们走一遍这个过程。
|
||||
|
||||
1. 当接收到HTTP GET的查询请求时,系统会先调用level()函数,来获取当前请求的读一致性级别。
|
||||
1. 调用Get()函数,查询指定key和读一致性级别对应的数据。
|
||||
1. 如果执行Get()函数成功,将执行步骤4;如果执行Get()函数出错,且提示出错的原因是当前节点不是领导者节点,那么这就说明了,在当前节点上执行查询操作不满足读一致性级别,必须要到领导者节点上执行查询操作,将执行步骤5;如果执行Get()函数出错,且提示出错的原因不是因为当前节点不是领导者,将执行步骤6。
|
||||
1. 查询操作执行成功,返回查询到的值给客户端。
|
||||
1. 节点将构造,包含领导者地址信息的重定向响应,并返回给客户端。然后客户端直接访问领导者节点查询数据。
|
||||
1. 系统运行出错,返回错误信息给客户端。
|
||||
|
||||
在这里,为了更好地利用curl客户端的HTTP重定向功能,我同样实现了HTTP 307重定向(具体原理,前面已经介绍了,这里就不啰嗦了)。比如,你可以使用下面的命令,来实现一致性级别为consistent的查询操作,不需要关心访问节点(raft-cluster-host02)是否是领导者节点。
|
||||
|
||||
```
|
||||
curl -XGET raft-cluster-host02:8091/key/foo?level=consistent -L
|
||||
|
||||
```
|
||||
|
||||
## 内容小结
|
||||
|
||||
本节课我主要带你了解了接入协议、KV操作、分布式集群的实现,我希望你记住下面三个重点内容:
|
||||
|
||||
<li>
|
||||
我们可以借助HTTP请求类型,来实现相关的操作,比如,我们可以通过HTTP GET请求实现查询操作,通过HTTP DELETE请求实现删除操作。
|
||||
</li>
|
||||
<li>
|
||||
你可以通过HTTP 307 重定向响应,来返回领导者的地址信息给客户端,需要你注意的是,curl已支持HTTP 307重定向,使用起来很方便,所以推荐你优先考虑curl,在日常中执行KV操作。
|
||||
</li>
|
||||
<li>
|
||||
在Raft中,我们可以通过raft.VerifyLeader()来确认当前领导者,是否仍是领导者。
|
||||
</li>
|
||||
|
||||
在这里,我还想强调的一点,任何大系统都是由小系统和具体的技术组成的,比如能无限扩展和支撑海量服务的QQ后台,是由多个组件(协议接入组件、名字服务、存储组件等)组成的。而做技术最为重要的就是脚踏实地彻底吃透和掌握技术本质,小系统的关键是细节技术,大系统的关键是架构。所以,在课程结束后,我会根据你的反馈意见,再延伸性地讲解大系统(大型互联网后台)的架构设计技巧,和我之前支撑海量服务的经验。
|
||||
|
||||
这样一来,我希望能帮你从技术到代码、从代码到架构、从小系统到大系统,彻底掌握实战能力,跨过技术和实战的鸿沟。
|
||||
|
||||
虽然这个分布式KV系统比较简单,但它相对纯粹聚焦在技术,能帮助你很好的理解Raft算法、Hashicorp Raft实现、分布式系统开发实战等。所以,我希望你不懂就问,有问题多留言,咱们一起讨论解决,不要留下盲区。
|
||||
|
||||
另外,我会持续维护和优化这个项目,并会针对大家共性的疑问,开发实现相关代码,从代码和理论2个角度,帮助你更透彻的理解技术。我希望你能在课下采用自己熟悉的编程语言,将这个系统重新实现一遍,在实战中,加深自己对技术的理解。如果条件允许,你可以将自己的分布式KV系统,以“配置中心”、“名字服务”等形式,在实际场景中落地和维护起来,不断加深自己对技术的理解。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
我提到了通过-join参数,将新节点加入到集群中,那么,你不妨思考一下,如何实现代码移除一个节点呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
144
极客时间专栏/geek/分布式协议与算法实战/开篇词/学习路径 | 分布式协议与算法你应该这么学.md
Normal file
144
极客时间专栏/geek/分布式协议与算法实战/开篇词/学习路径 | 分布式协议与算法你应该这么学.md
Normal file
@@ -0,0 +1,144 @@
|
||||
<audio id="audio" title="学习路径 | 分布式协议与算法你应该这么学" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fa/b3/fac8db3d7dc8775fd171c8a69b3ec6b3.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
在正式开始学习这门课之前,我想先和你聊一聊怎么学,因为掌握了学习路径、建立了全局观之后,你才能达到事半功倍的效果。
|
||||
|
||||
我们都知道,分布式协议和算法(为了不啰嗦,咱们下文都简称分布式算法)很实用、也很火,很多后端工程师在面试的时候,都会被问及分布式、高可用、一致性这些专业名词背后的算法原理和实现方式。
|
||||
|
||||
但是分布式算法也是比较新的,快速发展的。比如,1989年莱斯利·兰伯特(Leslie Lamport)提出了Paxos,2006年,谷歌研发团队让Paxos在生产环境中落地,但是Paxos缺乏编程实现的必须细节,最终的算法实现仍是建立在一个未证明的算法之上。再后来,也就是到了2013,斯坦福大学的迭戈·安加罗(Diego Ongaro)和约翰·奥斯特霍德(John Ousterhout)提出了Raft,但是2016年,Raft仍在解决成员变更的Bug。
|
||||
|
||||
正因为技术比较新,所以尚未能沉淀为书,很多同学都找不到分布式算法方面的经典书籍,再加上互联网上中文资料错误多,他们在学习相关的分布式算法的时候,会觉得吃力和困惑。
|
||||
|
||||
那么,如何才能掌握一个相对新、而且又在蓬勃快速发展的技术知识呢?这就是我这节课想要跟你分享的内容:**如何高效地学习和掌握分布式算法?**
|
||||
|
||||
在我看来,开发分布式系统最关键的就是根据场景特点,选择合适的算法,在一致性和可用性之间妥协折中,而妥协折中的关键就在于能否理解各算法的特点。
|
||||
|
||||
也就是说,我们先要弄清楚每个算法的特点是什么,适合怎样的场景,这样当你在开发分布式系统时,才能做到心中有数,游刃有余地选择适合的算法,来解决实际场景的问题。
|
||||
|
||||
那么问题来了:这些算法究竟有什么特点?适合怎样的场景呢?
|
||||
|
||||
## 分布式算法的四度空间
|
||||
|
||||
为了帮你更好地理解最常用的分布式算法的特点,我从拜占庭容错、一致性、性能和可用性四个纬度帮你整理了一张表,你可以对照着看一下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1c/33/1cc7514e341fab7bd7044b37285f4433.jpg" alt="">
|
||||
|
||||
## 拜占庭容错
|
||||
|
||||
拜占庭错误是莱斯利·兰伯特在《拜占庭将军问题》中提出的一个错误模型,描述了一个完全不可信的场景,除了存在故障行为,还存在恶意行为。顾名思义,拜占庭容错(Byzantine Fault Tolerance,BFT),就是指能容忍拜占庭错误了。
|
||||
|
||||
而非拜占庭容错,又叫故障容错(Crash Fault Tolerance,CFT),解决的是分布式系统中存在故障,但不存在恶意节点的共识问题,比如进程奔溃,服务器硬件故障等等。
|
||||
|
||||
一般而言,在可信环境(比如企业内网)中,系统具有故障容错能力就可以了,常见的算法有二阶段提交协议(2PC)、TCC(Try-Confirm-Cancel)、Paxos算法、ZAB协议、Raft算法、Gossip协议、Quorum NWR算法。
|
||||
|
||||
而在不可信的环境(比如有人做恶)中,这时系统需要具备拜占庭容错能力,常见的拜占庭容错算法有POW算法、PBFT算法。
|
||||
|
||||
## 一致性
|
||||
|
||||
一般来讲,我们将一致性分为三类。
|
||||
|
||||
- 强一致性:保证写操作完成后,任何后续访问都能读到更新后的值。
|
||||
- 弱一致性:写操作完成后,系统不能保证后续的访问都能读到更新后的值。
|
||||
- 最终一致性:保证如果对某个对象没有新的写操作了,最终所有后续访问都能读到相同的最近更新的值。
|
||||
|
||||
但是我要提醒你注意,强一致性是具有多种含义的。
|
||||
|
||||
首先,在埃里克·布鲁尔的猜想中,CAP中的强一致性(也就是C)是指ACID的C,系统状态的一致性,而这种一致性,可以通过二阶段提交协议来实现。
|
||||
|
||||
其次,在CAP定理中,CAP中的强一致性(也就是C)是指原子一致性(也就是线性一致性)。其中,Paxos、Raft能实现线性一致性,而ZooKeeper基于读性能的考虑,它通过ZAB协议提供的是最终一致性。
|
||||
|
||||
一般而言,在需要系统状态的一致性时,你可以考虑采用二阶段提交协议、TCC。在需要数据访问是的强一致性时,你可考虑Raft算法。在可用性优先的系统,你可以采用Gossip协议来实现最终一致性,并实现Quorum NWR来提供强一致性。
|
||||
|
||||
## 可用性
|
||||
|
||||
可用性说的是任何来自客户端的请求,不管访问哪个非故障节点,都能得到响应数据,但不保证是同一份最新数据,可用性强调的是服务可用。
|
||||
|
||||
一般来讲,采用Gossip协议实现最终一致性系统,它的可用性是最高的,因为哪怕只有一个节点,集群还能在运行并提供服务。其次是Paxos算法、ZAB协议、Raft算法、Quorum NWR算法、PBFT算法、POW算法,它们能容忍一定数节点故障。
|
||||
|
||||
最后是二阶段提交协议、TCC,只有当所有节点都在运行时,才能工作,可用性最低。
|
||||
|
||||
## 性能
|
||||
|
||||
一般来讲,采用Gossip协议的AP型分布式系统,具备水平扩展能力,读写性能是最高的。其次是Paxos算法、ZAB协议、Raft算法,因为它们都是领导者模型,写性能受限于领导者,读性能取决于一致性实现。最后是二阶段提交协议和TCC,因为在实现事务时,需要预留和锁定资源,性能相对低。
|
||||
|
||||
以上就是这些算法的特点了,了解完这部分内容之后,我想你一定有这样的疑问:“老韩,这些算法看起来很深奥,我怎样才能搞懂它们呢?按部就班的学吗?”
|
||||
|
||||
根据我多年的经验,你之所以觉得这些算法和相关的分布式技术,学起来比较难,是因为它们比较新,缺乏体系化。如果这时有个全景图,帮你建立全局观,那么你就可以体系化的理解相关算法了,在提高学习效率同时,也能在实际场景中“按图索骥”的选用相关的算法,而这些就是我接下来想和你具体聊一聊的。
|
||||
|
||||
## 专栏内容该如何学?
|
||||
|
||||
**拜占庭将军问题:最复杂的分布式容错模型**<br>
|
||||
**难度:**一颗星<br>
|
||||
**学习材料:** 01讲、加餐 | 拜占庭将军问题:如何基于签名消息实现作战计划的一致性?
|
||||
|
||||
拜占庭容错是分布式领域最复杂的容错模型,是你必须要了解的。另外,口信消息型拜占庭问题之解、签名消息型拜占庭问题之解,你可以通过预设不同的忠将数、叛将数,来推演下,在推演中学习和掌握。
|
||||
|
||||
**CAP理论:酸碱平衡之道**<br>
|
||||
**难度:** 二颗星<br>
|
||||
**学习材料:** 02讲、03讲、04讲
|
||||
|
||||
学习CAP理论的关键,不是仅仅知道CAP不可能三角,而是要能在C和A之间,根据实际场景特点,妥协权衡折中。这也是CAP猜想提出的初衷,希望业界能重视可用性,而不是只考虑ACID。
|
||||
|
||||
**分布式事务:进退与共**<br>
|
||||
**难度:** 二颗星<br>
|
||||
**学习材料:** 03讲,加餐 | MySQL XA是如何实现分布式事务的,加餐 | TCC如何实现指令的原子性
|
||||
|
||||
事务是指具有ACID特性的一组操作,要么全部执行,要么全部不执行,实现的是系统状态的一致性。一般在支付,或其他需要原子操作的场景下比较常用。
|
||||
|
||||
实现分布式事务,最常用的方法是二阶段提交协议和TCC,这两个算法的适用场景是不同的,二阶段提交协议实现的是数据层面的事务,比如XA规范采用的就是二阶段提交协议;TCC实现的是业务层面的事务,比如当操作不仅仅是数据库操作,还涉及其他业务系统的访问操作时,这时就应该考虑TCC了。
|
||||
|
||||
**分布式强一致性:你必须给我最新的数据**<br>
|
||||
**难度:** 五颗星<br>
|
||||
**学习内容:** 05讲、06讲、07讲、08讲、09讲、10讲。
|
||||
|
||||
很多同学经常误解的一个点,就是将Consensus(共识)当成了一致性,也就是称为Paxos、Raft为一致性算法,其实Paxos和Raft是共识算法。而之所以出现这个问题,是因为在很多中文文章中,将Consensus和Consistency都翻译成了一致性,其实这样是不合适的,因为共识(Consensus)和一致性(Consistency)是两个完全不同的概念。
|
||||
|
||||
- 共识:各节点就指定值(Value)达成共识,而且达成共识后的值,就不再改变了。
|
||||
- 一致性:是指写操作完成后,能否从各节点上读到最新写入的数据,如果立即能读到,就是强一致性,如果最终能读到,就是最终一致性。
|
||||
|
||||
提到共识算法,Paxos是一个必须要提及的话题,而且ZAB协议、Raft算法都可以看作是Paxos变种,所以,你需要了解Paxos算法。
|
||||
|
||||
但因为Paxos算法的可理解性和可编程性痛点突出,所以在实际场景中,最常的共识算法是Raft,我们可以基于Raft实现强一致性系统,Raft是需要彻底掌握的,在学习时,你可以结合17讲、18讲、19讲、20讲来一起学习,从前传(Paxos)到理论,再到实战,彻底吃透和掌握。
|
||||
|
||||
而一致哈希是常用的寻址算法,能突破集群性能的领导者限制,也是需要我们掌握的。
|
||||
|
||||
**分布式最终一致性:数据旧点没关系**<br>
|
||||
**难度:**三颗星<br>
|
||||
**学习材料:** 11讲、12讲。
|
||||
|
||||
无论实现分布式事务还是强一致性,性能和可用性都是挑战,在一些对性能或可用性要求比较高的场景,比如时序数据、统计数据、状态数据(QQ登录状态),最终一致性是首选,因为最终一致性系统不仅能提供出色的性能,还能实现水平扩展。而Gossip协议是实现最终一致性的常用方法。
|
||||
|
||||
如果实现了最终一致性,但有时可能需要临时提供强一致性能力,这个时候,你可以用Quorum NWR来实现。
|
||||
|
||||
**ZAB协议:ZooKeeper背后的一致性秘密**<br>
|
||||
**难度:** 二颗星<br>
|
||||
**学习材料:** 15讲,加餐 | ZAB协议(一):主节点崩溃了,怎么办?加餐 | ZAB协议(二):如何从故障中恢复?加餐 | ZAB协议(三):如何处理读写请求?
|
||||
|
||||
ZooKeeper是一个常用的分布式协调服务,而且ZAB协议在共识算法的发展过程中起到了一个承前启后的作用,它受Paxos算法、原子广播协议的启发,又影响到后来的Raft算法。但从实战的角度,ZAB协议的实现,无法剥离ZooKeeper代码独立使用,**所以这部分内容,我建议日常使用ZooKeeper的同学仔细学习一下,其他同学的话,可以选学。**
|
||||
|
||||
**拜占庭容错算法:有人作恶,如何达成共识**<br>
|
||||
**难度:** 二颗星<br>
|
||||
**学习材料:** 13讲、14讲,加餐 | PBFT算法:如何替换作恶的领导者?
|
||||
|
||||
在一个完全不可信的环境中(比如有人作恶),如果需要达成共识,那么我们就必须考虑拜占庭容错算法,常用的拜占庭容错算法有POW算法、PBFT算法,它们在区块链中应用广泛。
|
||||
|
||||
**实战:实践是最好的学习方式**<br>
|
||||
**难度:**四颗星<br>
|
||||
**学习材料:** 16讲、17讲、18讲、19讲、20讲。
|
||||
|
||||
你可能有这样的体会,技术的学习往往是在模仿中开始的,在实战中顿悟升华。分布式算法的学习也不例外,技术是需要在实战中学习,也只有在实战中,你才能真正的理解技术。
|
||||
|
||||
他山之石,可以攻玉,为了帮助你更好地理解实际场景中一致性的实现,我会剖析InfluxDB企业版的一致性实现(强一致性和最终一致性两个方案)。也会分析一个流行的Raft实现(Hashicorp Raft),除了在代码中理解Raft算法,也会带你熟悉一下Hashicorp Raft的API接口,最终在19、20讲,带你使用API接口开发实现自己的分布式KV系统。
|
||||
|
||||
我啰嗦了那么多,其实就是为了让你更高效地掌握常用的分布式算法。另外,为了帮你更好的理解算法的特点和整体学习的思路,我做了个知识地图,方便你梳理整个知识体系。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/12/13/122bdf34957c6277352ea51c43552213.png" alt="">
|
||||
|
||||
## 总结
|
||||
|
||||
生有涯知无涯,只有抓住技术本质,才能举一反三,以不变应万变。而本课程我带你了解的这些算法和理论,都是最经典和经得起时间检验的。
|
||||
|
||||
但学习的过程绝不会一帆风顺,如果你在学习过程中有困惑、茫然,甚至是沮丧,希望你能多留言,咱们聊一聊,一起想想办法,**让我们把分布式算法学习这件意义非凡的事情坚持下去,一起攻克分布式系统设计的关键难题。**
|
||||
|
||||
现在,就让我们正式开始分布式算法之旅吧!一起享受技术的乐趣。
|
||||
77
极客时间专栏/geek/分布式协议与算法实战/开篇词/开篇词 | 想成为分布式高手?那就先把协议和算法烂熟于心吧.md
Normal file
77
极客时间专栏/geek/分布式协议与算法实战/开篇词/开篇词 | 想成为分布式高手?那就先把协议和算法烂熟于心吧.md
Normal file
@@ -0,0 +1,77 @@
|
||||
<audio id="audio" title="开篇词 | 想成为分布式高手?那就先把协议和算法烂熟于心吧" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/68/00/6809b2e59f9d1e9cbf5df26a635af800.mp3"></audio>
|
||||
|
||||
你好,我是韩健,你叫我“老韩”就可以了。
|
||||
|
||||
在专栏开始之前,我想先和你聊聊自己的经历,加深彼此的了解。在重庆大学的软件工程专业毕业之后,我就开始和分布式系统打交道,至今有十多年了。早期,我接触了电信级分布式系统,比如内核态HA Cluster,现在是互联网分布式系统,比如名字服务、NoSQL存储、监控大数平台。
|
||||
|
||||
我曾经做过创业公司的CTO,后来加入腾讯之后,负责过QQ 后台海量服务分布式中间件,以及时序数据库 InfluxDB 自研集群系统的架构设计和研发工作。
|
||||
|
||||
你可能会问我,为什么要单独讲分布式协议和算法呢?(为了不啰嗦,咱们下文都简称分布式算法)在我看来,它其实就是决定分布式系统如何运行的核心规则和关键步骤。 **如果一个人想真正搞懂分布式技术,开发出一个分布式系统,最先需要掌握的就是这部分知识。**
|
||||
|
||||
举个例子,学数学的时候,我们总是会学到很多公式或者定理,我上学的时候,还觉得这些定理枯燥至极。但后来我明白了,这些定理和公式其实就是前人花了很长时间思考、验证、总结出来的规律,如果我们能在这之上做事情,更容易快速地找到正确答案。同样,你学习咱们这个专栏也是这个道理。
|
||||
|
||||
## 分布式算法是分布式技术中的核心
|
||||
|
||||
可能有些同学会说:“老韩,你别忽悠我,我可是系统看过分布式领域的经典书的,比如《分布式系统:概念与设计》《分布式系统原理与范型》,这些书里分布式算法的篇幅可不多啊。”
|
||||
|
||||
是的,这也是我奇怪的地方。不过,你可以看看网上关于分布式的提问,这里面点击量大的肯定与分布式算法有关,这是不是侧面说明了它的重要性呢?
|
||||
|
||||
而且从我多年的经验来看,很多同学读了那几本厚重的经典书之后,在实际工作中还是云里雾里。我想,如果他们来问我,我会建议他们先把各种分布式算法搞清楚。**因为分布式系统里,最重要的事情,就是如何选择或设计适合的算法,解决一致性和可用性相关的问题了。**
|
||||
|
||||
可尽管它是分布式技术中的核心与关键,但实际掌握的人或者公司却很少。我来说个真实的事儿。
|
||||
|
||||
我刚刚提到的InfluxDB其实是一个开源的时序数据库系统,当然,开源的只是单机版本,如果你要使用集群功能,要么就是基于开源版本自研,要么就是购买人家的企业版本。
|
||||
|
||||
而这里面,企业版本一个节点一年License授权费就是1.5万美刀,是不是很贵?那贵在哪里呢?相比于单机版本,企业版本的技术壁垒又是什么?
|
||||
|
||||
在我自己折腾了一番InfluxDB系统后,我捂着胸口和你说,它的护城河就是**以分布式算法为核心的分布式集群能力。**
|
||||
|
||||
我知道有很多技术团队曾经试图自己实现InfluxDB的企业版本功能,但最后还是放弃了,因为这里面坑太多了。比如,实现集群能力的时候,怎么支持基于时序进行分片?怎么支持水平扩展?甚至还有些人在接入性能敏感的场景,该使用反熵(Anti-Entropy)算法的时候,却用了Raft算法,使得集群性能约等同于单机。
|
||||
|
||||
可以看到,分布式系统的价值和意义的确很大,但如果不能准确理解分布式算法,可能不仅开发实现的分布式系统无法稳定运行,而且你还会因为种种现网故障,逐渐影响到职业发展,丧失职场竞争力。
|
||||
|
||||
再说点儿更实际的,**现阶段,掌握分布式算法也是你面试架构师、技术专家等高端岗位时的敲门砖。** 你可以搜索看看,知名的公司在招聘架构师或者高级工程师时,岗位要求中是不是写着熟悉分布式算法相关理论等内容?不过从我作为面试官的经验来看,懂这部分的候选人实在是少之又少。
|
||||
|
||||
别看啰嗦了这么多,我只是想强调,不管你是基于技术追求的考虑,还是基于长期职业发展和提升职场竞争力的考量,“分布式算法”都是你在这个时代应该掌握的基本功。
|
||||
|
||||
当然了,我也知道,分布式算法虽然很重要,但是也比较难学,原因有这样几点。
|
||||
|
||||
- 除了算法本身抽象,不容易理解之外,即使是非常经典的论文,也存在在一些关键细节上没有讲清楚的情况。比如,你比较熟悉的拜占庭将军问题,在阅读口信消息型拜占庭问题之解时,你是不是感到很吃力呢?那是因为论文没有说透彻,而我会在[01讲](https://time.geekbang.org/column/article/195662)带你了解这些内容。
|
||||
- 信息时代资料丰富,但质量参差不齐,甚至有错误。网上信息大多是“复制粘贴”的结果,而且因为分布式领域的研究多以英文论文的形式出现,中文翻译内容的错误非常多,这也给自主学习带来很多不必要的障碍和误导。如果你没有足够的好奇心和探究精神,很难完全吃透关键细节。
|
||||
- 很多资料是为了讲解理论而讲解理论,无法站在“用”的角度,将理论和实战结合。最终,你只能在“嘴”上理解,而无法动手。
|
||||
|
||||
## 方法得当,知识并不难学
|
||||
|
||||
在我看来,要想掌握这部分内容,不仅要理解常用算法的原理、特点和局限,还要能根据场景特点选择适合的分布式算法。
|
||||
|
||||
所以,为了更好地帮你轻松、透彻地搞懂分布式技术,理解其中最核心和最为精妙的内容,我希望将自己支撑海量互联网服务中的分布式算法实战心得分享给你。
|
||||
|
||||
我将课程划分了三个模块,分别是理论篇、协议和算法篇以及实战篇。
|
||||
|
||||
其中,理论篇,我会带你搞懂分布式架构设计核心且具有“实践指导性”的基础理论,这里面会涉及典型的分布式问题,以及如何认识分布式系统中相互矛盾的特性,帮助你在实战中根据场景特点选择适合的分布式算法。
|
||||
|
||||
协议和算法篇,会让你掌握它们的原理、特点、适用场景和常见误区等。比如,你以为开发分布式系统使用Raft算法就可以了,其实它比较适合性能要求不高的强一致性场景;又比如在面试时,如果被问到“Paxos和Raft的区别在哪里”,你都会在第二部分中找到答案。
|
||||
|
||||
实战篇,教你如何将所学知识落地,我会带你掌握分布式基础理论和分布式算法在工程实践中的应用。比如,剖析InfluxDB企业版的CP架构和AP架构的设计和背后的思考,以及Raft、Quorum NWR、Anti-Entropy等分布式算法的具体实现。
|
||||
|
||||
从实战篇中,你可以掌握如何根据场景特点选择适合的分布式算法,以及如何使用和实现分布式算法的实战技巧。这样,当你需要据场景特点选择适合的分布式算法时,就能举一反三,独立思考,设计开发了。
|
||||
|
||||
除此之外,我还会带你剖析Hashicorp Raft的实现,并以一个分布式KV系统的开发实战为例,来聊聊如何使用Raft算法实际开发一个分布式系统,以此让你全面拥有分布式算法的实战能力。
|
||||
|
||||
总体来说,学完这次课程,你会有以下几个收获:
|
||||
|
||||
1. 破除你对分布式协议和算法的困惑,帮助你建立信心;
|
||||
1. 可落地的 4 大分布式基础理论;
|
||||
1. 8 个最常用的分布式协议和算法;
|
||||
1. 3 大实战案例手把手教学;
|
||||
1. 以实战为中心的分布式内容体系。
|
||||
|
||||
## 写在最后
|
||||
|
||||
我承诺课程的每一讲都是干货,也会第一时间和你交流答疑,也请你监督。只要你紧跟脚步,不懂就问,课后多加思考和练习,相信你一定会学有所成。
|
||||
|
||||
与此同时,我希望所有对技术有追求的工程师,都能在学完课程之后,顺利攻下这一关。再具体一点说,就是能够在工作中根据场景特点,灵活地设计架构和使用分布式算法开发出适合该场景的分布式系统,并且对架构设计的理解更上一层。姑且把这段话当成我们的教学目标吧。
|
||||
|
||||
最后,欢迎你在留言区说一说自己在技术上的困惑,或者想通过这个专栏收获些什么,这样可以方便我在后续的备课中,针对性地讲解内容。重要的是,也能帮你在学完之后回顾这些疑难问题,感受到自己切实的进步和能力的提升。
|
||||
|
||||
期待与你在这个课程中碰撞出更多的思维火花,未来的两个月里,让我们成为朋友,携手同行,共同进步!
|
||||
145
极客时间专栏/geek/分布式协议与算法实战/理论篇/01 | 拜占庭将军问题:有叛徒的情况下,如何才能达成共识?.md
Normal file
145
极客时间专栏/geek/分布式协议与算法实战/理论篇/01 | 拜占庭将军问题:有叛徒的情况下,如何才能达成共识?.md
Normal file
@@ -0,0 +1,145 @@
|
||||
<audio id="audio" title="01 | 拜占庭将军问题:有叛徒的情况下,如何才能达成共识?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/12/90/129c68b3904facbd8f64ac38164b7690.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
在日常工作中,我常听到有人吐槽“没看懂拜占庭将军问题”“中文的文章看不懂,英文论文更看不下去”。想必你也跟他们一样,有类似的感受。
|
||||
|
||||
在我看来,拜占庭将军问题(The Byzantine Generals Problem),它其实是借拜占庭将军的故事展现了分布式共识问题,还探讨和论证了解决的办法。而大多数人觉得它难理解,除了因为分布式共识问题比较复杂之外,还与莱斯利·兰伯特(Leslie Lamport)的讲述方式有关,他在一些细节上(比如,口信消息型拜占庭问题之解的算法过程上)没有说清楚。
|
||||
|
||||
实际上,它是分布式领域最复杂的一个容错模型,一旦搞懂它,你就能掌握分布式共识问题的解决思路,还能更深刻地理解常用的共识算法,在设计分布式系统的时候,也能根据场景特点选择适合的算法,或者设计适合的算法了。而我把拜占庭将军的问题放到第一讲,主要是因为它很好地抽象了分布式系统面临的共识问题,理解了这个问题,会为你接下来的学习打下基础。
|
||||
|
||||
那么接下来,我就以战国时期六国抗秦的故事为主线串联起整篇文章,让你读懂、学透。
|
||||
|
||||
## 苏秦的困境
|
||||
|
||||
战国时期,齐、楚、燕、韩、赵、魏、秦七雄并立,后来秦国的势力不断强大起来,成了东方六国的共同威胁。于是,这六个国家决定联合,全力抗秦,免得被秦国各个击破。一天,苏秦作为合纵长,挂六国相印,带着六国的军队叩关函谷,驻军在了秦国边境,为围攻秦国作准备。但是,因为各国军队分别驻扎在秦国边境的不同地方,所以军队之间只能通过信使互相联系,这时,苏秦面临了一个很严峻的问题:如何统一大家的作战计划?
|
||||
|
||||
万一一些诸侯国在暗通秦国,发送误导性的作战信息,怎么办?如果信使被敌人截杀,甚至被敌人间谍替换,又该怎么办?这些都会导致自己的作战计划被扰乱,然后出现有的诸侯国在进攻,有的诸侯国在撤退的情况,而这时,秦国一定会趁机出兵,把他们逐一击破的。
|
||||
|
||||
**所以,如何达成共识,制定统一的作战计划呢?苏秦他很愁。**
|
||||
|
||||
这个故事,是拜占庭将军问题的一个简化表述,苏秦面临的就是典型的共识难题,也就是如何在可能有误导信息的情况下,采用合适的通讯机制,让多个将军达成共识,制定一致性的作战计划?
|
||||
|
||||
你可以先停下来想想,这个问题难在哪儿?我们又是否有办法,帮助诸侯国们达成共识呢?
|
||||
|
||||
## 二忠一叛的难题
|
||||
|
||||
为了便于你理解和层层深入,我先假设只有3个国家要攻打秦国,这三个国家的三位将军,咱们简单点儿,分别叫齐、楚、燕。同时,又因为秦国很强大,所以只有半数以上的将军参与进攻,才能击败敌人(注意,这里是假设哈,你别较真),在这个期间,将军们彼此之间需要通过信使传递消息,然后协商一致之后,才能在同一时间点发动进攻。
|
||||
|
||||
举个例子,有一天,这三位将军各自一脸严肃地讨论明天是进攻还是撤退,并让信使传递信息,按照“少数服从多数”的原则投票表决,两个人意见一致就可以了,比如:
|
||||
|
||||
1. 齐根据侦查情况决定撤退;
|
||||
1. 楚和燕根据侦查信息,决定进攻。
|
||||
|
||||
那么按照原则,齐也会进攻。最终,3支军队同时进攻,大败秦军。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0b/d2/0bf66342fa03d73cf4b62b7497939bd2.jpg" alt="">
|
||||
|
||||
**可是,问题来了:** 一旦有人在暗通秦国,就会出现作战计划不一致的情况。比如齐向楚、燕分别发送了“撤退”的消息,燕向齐和楚发送了“进攻”的消息。**撤退:进攻=1:1,无论楚投进攻还是撤退,都会成为2:1,这个时候还是会形成一个一致性的作战方案。**
|
||||
|
||||
但是,楚这个叛徒在暗中配合秦国,让信使向齐发送了“撤退”,向燕发送了“进攻”,那么:
|
||||
|
||||
- 燕看到的是,撤退:进攻=1:2;
|
||||
- 齐看到的是,撤退:进攻=2:1。
|
||||
|
||||
按照“少数服从多数”的原则,就会出现燕单独进攻秦军,当然,最后肯定是因为寡不敌众,被秦军给灭了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b3/0b/b3f01caa021dae1cdf4051cceb6d3d0b.jpg" alt="">
|
||||
|
||||
在这里,你可以看到,叛将楚通过发送误导信息,非常轻松地干扰了齐和燕的作战计划,导致这两位忠诚将军被秦军逐一击败。**这就是所说的二忠一叛难题。** 那么苏秦应该怎么解决这个问题呢?我们来帮苏秦出出主意。
|
||||
|
||||
如果你觉得上面的逻辑有点绕的话,可以找张白纸,自己比划比划。
|
||||
|
||||
## 苏秦该怎么办?
|
||||
|
||||
#### 解决办法一:口信消息型拜占庭问题之解
|
||||
|
||||
先来说说第一个解决办法。首先,三位将军都分拨一部分军队,由苏秦率领,苏秦参与作战计划讨论并执行作战指令。这样,3位将军的作战讨论,就变为了4位将军的作战讨论,这能够增加讨论中忠诚将军的数量。
|
||||
|
||||
然后呢,4位将军还约定了,如果没有收到命令,就执行预设的默认命令,比如“撤退”。除此之外,还约定一些流程来发送作战信息、执行作战指令,比如,进行两轮作战信息协商。为什么要执行两轮呢?先卖个关子,你一会儿就知道了。
|
||||
|
||||
**第一轮:**
|
||||
|
||||
- 先发送作战信息的将军作为指挥官,其他的将军作为副官;
|
||||
- 指挥官将他的作战信息发送给每位副官;
|
||||
- 每位副官,将从指挥官处收到的作战信息,作为他的作战指令;如果没有收到作战信息,将把默认的“撤退”作为作战指令。
|
||||
|
||||
**第二轮:**
|
||||
|
||||
- 除了第一轮的指挥官外,剩余的3位将军将分别作为指挥官,向另外2位将军发送作战信息;
|
||||
- 然后,这3位将军按照“少数服从多数”,执行收到的作战指令。
|
||||
|
||||
为了帮助你直观地理解苏秦的整个解决方案,我来演示一下作战信息协商过程。**而且,我会分别以忠诚将军和叛将先发送作战信息为例来演示,** 这样可以完整地演示叛将对作战计划干扰破坏的可能性。
|
||||
|
||||
首先是3位忠诚的将军先发送作战信息的情况。
|
||||
|
||||
为了演示方便,假设苏秦先发起作战信息,作战指令是“进攻”。那么在第一轮作战信息协商中,苏秦向齐、楚、燕发送作战指令“进攻”。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b6/01/b6143760b8095ba31fcfc97daf619d01.jpg" alt="">
|
||||
|
||||
在第二轮作战信息协商中,齐、楚、燕分别作为指挥官,向另外2位发送作战信息“进攻”,因为楚已经叛变了,所以,为了干扰作战计划,他就对着干,发送“撤退”作战指令。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9c/50/9c47038ca1c39422040fa3f4b65a2950.jpg" alt="">
|
||||
|
||||
最终,齐和燕收到的作战信息都是“进攻、进攻、撤退”,按照原则,齐和燕与苏秦一起执行作战指令“进攻”,实现了作战计划的一致性,保证了作战的胜利。
|
||||
|
||||
那么,如果是叛徒楚先发送作战信息,干扰作战计划,结果会有所不同么?我们来具体看一看。在第一轮作战信息协商中,楚向苏秦发送作战指令“进攻”,向齐、燕发送作战指令“撤退”。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/93/5f/93cda6d0a5646593826e6338d733825f.jpg" alt="">
|
||||
|
||||
然后,在第二轮作战信息协商中,苏秦、齐、燕分别作为指挥官,向另外两位发送作战信息。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8f/14/8fdac1df9027711347f178bb9d3ccf14.jpg" alt="">
|
||||
|
||||
最终,苏秦、齐和燕收到的作战信息都是“撤退、撤退、进攻”,按照原则,苏秦、齐和燕一起执行作战指令“撤退”,实现了作战计划的一致性。也就是说,无论叛将楚如何捣乱,苏秦、齐和燕,都执行一致的作战计划,保证作战的胜利。
|
||||
|
||||
这个解决办法,其实是兰伯特在论文《[The Byzantine Generals Problem](https://www.microsoft.com/en-us/research/publication/byzantine-generals-problem/)》中提到的口信消息型拜占庭问题之解:**如果叛将人数为m,将军人数不能少于3m + 1 ,那么拜占庭将军问题就能解决了。** 不过,作者在论文中没有讲清楚一些细节,为了帮助你阅读和理解论文,在这里我补充一点:
|
||||
|
||||
**这个算法有个前提**,也就是叛将人数m,或者说能容忍的叛将数m,是已知的。在这个算法中,叛将数m决定递归循环的次数(也就是说,叛将数m决定将军们要进行多少轮作战信息协商),即m+1轮(所以,你看,只有楚是叛变的,那么就进行了两轮)。你也可以从另外一个角度理解:n位将军,最多能容忍(n - 1) / 3位叛将。**关于这个公式,你只需要记住就好了,推导过程你可以参考论文。**
|
||||
|
||||
不过,这个算法虽然能解决拜占庭将军问题,但它有一个限制:如果叛将人数为m,那么将军总人数必须不小于3m + 1。
|
||||
|
||||
在二忠一叛的问题中,在存在1位叛将的情况下,必须增加1位将军,将3位将军协商共识,转换为4位将军协商共识,这样才能实现忠诚将军的一致性作战计划。那么有没有办法,在不增加将军人数的时候,直接解决二忠一叛的难题呢?
|
||||
|
||||
#### 解决办法二:签名消息型拜占庭问题之解
|
||||
|
||||
其实,苏秦还可以通过签名的方式,在不增加将军人数的情况下,解决二忠一叛的难题。首先,苏秦要通过印章、虎符等信物,实现这样几个特性:
|
||||
|
||||
- 忠诚将军的签名无法伪造,而且对他签名消息的内容进行任何更改都会被发现;
|
||||
- 任何人都能验证将军签名的真伪。
|
||||
|
||||
这时,如果忠诚的将军,比如齐先发起作战信息协商,一旦叛将小楚修改或伪造收到的作战信息,那么燕在接收到楚的作战信息的时候,会发现齐的作战信息被修改,楚已叛变,这时他将忽略来自楚的作战信息,最终执行齐发送的作战信息。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e9/05/e9c5ff8d9591f9a00adcfcfd4981cb05.jpg" alt="">
|
||||
|
||||
如果叛变将军楚先发送误导的作战信息,那么,齐和燕将按照一定规则(比如取中间的指令)在排序后的所有已接收到的指令中(比如撤退、进攻)中选取一个指令,进行执行,最终执行一致的作战计划。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3a/2e/3a697c2ed35f4c446b6414770a5d392e.jpg" alt="">
|
||||
|
||||
这个解决办法,是兰伯特在论文中提到的签名消息型拜占庭问题之解。而通过签名机制约束叛将的叛变行为,任何叛变行为都会被发现,也就会实现无论有多少忠诚的将军和多少叛将,忠诚的将军们总能达成一致的作战计划。
|
||||
|
||||
我想,如果当时苏秦能够具备分布式系统设计的思维,掌握这几种算法,应该就不用担心作战计划被干扰了吧。
|
||||
|
||||
## 内容小结
|
||||
|
||||
本节课,为了帮助你理解拜占庭将军问题,我讲了苏秦协商作战的故事,现在让我们跳回现实世界,回到计算机世界的分布式场景中:
|
||||
|
||||
- 故事里的各位将军,你可以理解为计算机节点;
|
||||
- 忠诚的将军,你可以理解为正常运行的计算机节点;
|
||||
- 叛变的将军,你可以理解为出现故障并会发送误导信息的计算机节点;
|
||||
- 信使被杀,可以理解为通讯故障、信息丢失;
|
||||
- 信使被间谍替换,可以理解为通讯被中间人攻击,攻击者在恶意伪造信息和劫持通讯。
|
||||
|
||||
这样一来,你是不是就理解了计算机分布式场景中面临的问题,并且知道了解决的办法呢?
|
||||
|
||||
那么我想强调的是,拜占庭将军问题描述的是最困难的,也是最复杂的一种分布式故障场景,除了存在故障行为,还存在恶意行为的一个场景。你要注意,在存在恶意节点行为的场景中(比如在数字货币的区块链技术中),必须使用拜占庭容错算法(Byzantine Fault Tolerance,BFT)。除了故事中提到两种算法,常用的拜占庭容错算法还有:PBFT算法,PoW算法(为了重点突出,这些内容我会在后面讲解)。
|
||||
|
||||
而在计算机分布式系统中,最常用的是非拜占庭容错算法,即故障容错算法(Crash Fault Tolerance,CFT)。**CFT解决的是分布式的系统中存在故障,但不存在恶意节点的场景下的共识问题。** 也就是说,这个场景可能会丢失消息,或者有消息重复,但不存在错误消息,或者伪造消息的情况。常见的算法有Paxos算法、Raft算法、ZAB协议(这些内容我同样会在后面讲解)。
|
||||
|
||||
那么,如何在实际场景选择合适的算法类型呢?答案是:如果能确定该环境中各节点是可信赖的,不存在篡改消息或者伪造消息等恶意行为(例如DevOps环境中的分布式路由寻址系统),推荐使用非拜占庭容错算法;反之,推荐使用拜占庭容错算法,例如在区块链中使用PoW算法。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
文中我提了两类容错算法,分别是拜占庭容错算法和非拜占庭容错算法,那么在常见的分布式软件系统中,哪些场景必须要使用拜占庭容错算法呢?哪些场景使用非拜占庭容错算法就可以了呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
148
极客时间专栏/geek/分布式协议与算法实战/理论篇/02 | CAP理论:分布式系统的PH试纸,用它来测酸碱度.md
Normal file
148
极客时间专栏/geek/分布式协议与算法实战/理论篇/02 | CAP理论:分布式系统的PH试纸,用它来测酸碱度.md
Normal file
@@ -0,0 +1,148 @@
|
||||
<audio id="audio" title="02 | CAP理论:分布式系统的PH试纸,用它来测酸碱度" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/1d/34/1d05c9912ee0ba691048ab868e40e734.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
很多同学可能都有这样的感觉,每次要开发分布式系统的时候,就会遇到一个非常棘手的问题,那就是如何根据业务特点,为系统设计合适的分区容错一致性模型,以实现集群能力。这个问题棘手在当发生分区错误时,应该如何保障系统稳定运行,不影响业务。
|
||||
|
||||
这和我之前经历的一件事比较像,当时,我负责自研InfluxDB系统的项目,接手这个项目后,**我遇到的第一个问题就是,如何为单机开源版的InfluxDB设计分区容错一致性模型。**因为InfluxDB有META和DATA两个节点,它们的功能和数据特点不同,所以我还需要考虑这两个逻辑单元的特点,然后分别设计分区容错一致性模型。
|
||||
|
||||
那个时候,我想到了CAP理论,并且在CAP理论的帮助下,成功地解决了问题。讲到这儿,你可能会问了:为什么CAP理论可以解决这个问题呢?
|
||||
|
||||
因为在我看来,CAP理论是一个很好的思考框架,它对分布式系统的特性做了高度抽象,比如抽象成了一致性、可用性和分区容错性,并对特性间的冲突(也就是CAP不可能三角)做了总结。一旦掌握它,你就像拥有了引路人,自然而然就能根据业务场景的特点进行权衡,设计出适合的分区容错一致性模型。
|
||||
|
||||
那么问题来了:我说的一致性、可用性和分区容错性是什么呢?它们之间有什么关系?你又该如何使用CAP理论来思考和设计分区容错一致性模型呢?这些问题就是我们本节课所要讲的重点了。我建议你集中注意力,认真学习内容,并学以致用,把CAP理论应用到日常工作中。
|
||||
|
||||
## CAP三指标
|
||||
|
||||
我刚刚提到,CAP理论对分布式系统的特性做了高度抽象,形成了三个指标:
|
||||
|
||||
- 一致性(Consistency)
|
||||
- 可用性(Availability)
|
||||
- 分区容错性(Partition Tolerance)
|
||||
|
||||
一致性说的是客户端的每次读操作,不管访问哪个节点,要么读到的都是同一份最新写入的数据,要么读取失败。
|
||||
|
||||
你可以把一致性看作是分布式系统,对访问自己的客户端的一种承诺:不管你访问哪个节点,要么我给你返回的都是绝对一致的最新写入的数据,要么你读取失败。你可以看到,一致性强调的是数据正确。
|
||||
|
||||
为了帮你理解一致性这个指标,我给你举一个具体的例子。比如,2个节点的KV存储,原始的KV记录为“X = 1”。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5d/6d/5d666afe96731c95556ee7d5a1194d6d.jpg" alt="">
|
||||
|
||||
紧接着,客户端向节点1发送写请求“SET X = 2”。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/58/e7/58519857f8f6b3edf8ebe38d3c18ede7.jpg" alt="">
|
||||
|
||||
如果节点1收到写请求后,只将节点1的X值更新为2,然后返回成功给客户端。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/56/c4/56922def2471e194d3e5420ad006e1c4.jpg" alt="">
|
||||
|
||||
那么,此时如果客户端访问节点2执行读操作,就无法读到最新写入的X值,这就不满足一致性了。<br>
|
||||
<img src="https://static001.geekbang.org/resource/image/31/1f/31b628027351e4b5a6ffb31cdcba431f.jpg" alt="">
|
||||
|
||||
如果节点1收到写请求后,通过节点间的通讯,同时将节点1和节点2的X值都更新为2,然后返回成功给客户端。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/89/51/890b9890584a4f17306da88fedea3451.jpg" alt="">
|
||||
|
||||
那么在完成写请求后,不管客户端访问哪个节点,读取到的都是同一份最新写入的数据,这就叫一致性。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9b/c2/9bf9c133ad815219791f87799bee5cc2.jpg" alt="">
|
||||
|
||||
一致性这个指标,描述的是分布式系统非常重要的一个特性,强调的是数据正确。也就是说,对客户端而言,每次读都能读取到最新写入的数据。
|
||||
|
||||
不过集群毕竟不是单机,当发生分区故障的时候,有时不能仅仅因为节点间出现了通讯问题,无法响应最新写入的数据,之后在客户端查询数据时,就一直返回给客户端出错信息。这句话怎么理解呢?我来举个例子。
|
||||
|
||||
业务集群中的一些关键系统,比如名字路由系统(基于Raft算法的强一致性系统),如果仅仅因为发生了分区故障,无法响应最新数据(比如不满足“大多数”,没有了领导者),为了不破坏一致性,那么客户端查询相关路由信息时,系统就一直返回给客户端出错信息,此时相关的业务都将因为获取不到指定路由信息而不可用、瘫痪,这可以说是灾难性的故障了。
|
||||
|
||||
这个时候,我们就需要牺牲数据正确,每个节点使用本地数据来响应客户端请求,来保证服务可用,**这就是我要说的另外一个指标,可用性。**
|
||||
|
||||
可用性说的是任何来自客户端的请求,不管访问哪个非故障节点,都能得到响应数据,但不保证是同一份最新数据。你也可以把可用性看作是分布式系统对访问本系统的客户端的另外一种承诺:我尽力给你返回数据,不会不响应你,但是我不保证每个节点给你的数据都是最新的。**这个指标强调的是服务可用,但不保证数据正确。**
|
||||
|
||||
我还是用一个例子,帮助你理解一下。比如,用户可以选择向节点1或节点2 发起读操作,如果不管节点间的数据是否一致,只要节点服务器收到请求,就响应X的值,那么,2个节点的服务是满足可用性的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b2/a4/b27f4ff771fa8e9b671f5864c96d9aa4.jpg" alt="">
|
||||
|
||||
最后的分区容错性说的是,当节点间出现任意数量的消息丢失或高延迟的时候,系统仍然在继续工作。也就是说,分布式系统在告诉访问本系统的客户端:不管我的内部出现什么样的数据同步问题,我会一直运行。**这个指标,强调的是集群对分区故障的容错能力。**
|
||||
|
||||
来看下面的图,当节点1和节点2通信出问题的时候,如果系统仍能继续工作,那么,2个节点是满足分区容错性的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a6/65/a61a426bccbe27e48ca75db909ae8265.jpg" alt="">
|
||||
|
||||
因为分布式系统与单机系统不同,它涉及到多节点间的通讯和交互,节点间的分区故障是必然发生的,**所以我要提醒你的是,在分布式系统中分区容错性是必须要考虑的。**
|
||||
|
||||
现在你了解了一致性、可用性和分区容错性,那么你在设计分布式系统时,是选择一致性?还是可用性?还是分区容错性?还是都可以选择呢?这三个特性有什么冲突么?这些问题就与我接下来要讲的“CAP不可能三角”有关了。
|
||||
|
||||
## CAP不可能三角
|
||||
|
||||
CAP不可能三角说的是对于一个分布式系统而言,一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance)3个指标不可兼得,只能在3个指标中选择2个。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/67/48/67aa4a0e56dcc8ad34bf1b0232f12748.jpg" alt="">
|
||||
|
||||
CAP不能三角最初是埃里克·布鲁尔(Eric Brewer)基于自己的工程实践,提出的一个猜想,后被赛斯·吉尔伯特(Seth Gilbert)和南希·林奇(Nancy Lynch)证明,证明过程可以参考论文[《Brewer’s conjecture and the feasibility of consistent, available, partition-tolerant web services》](https://dl.acm.org/citation.cfm?id=564601),你记住结论就好了。不过,为了帮你阅读论文,我补充一点:
|
||||
|
||||
**基于证明严谨性的考虑,赛斯·吉尔伯特(Seth Gilbert)和南希·林奇(Nancy Lynch)对指标的含义做了预设和限制,比如,将一致性限制为原子一致性。**
|
||||
|
||||
说了这么多,那么CAP理论是怎么解决我在开篇提到的问题呢?或者说,你要如何使用CAP理论来思考和设计分区容错一致性模型呢?
|
||||
|
||||
## 如何使用CAP理论
|
||||
|
||||
我们都知道,只要有网络交互就一定会有延迟和数据丢失,而这种状况我们必须接受,还必须保证系统不能挂掉。所以就像我上面提到的,节点间的分区故障是必然发生的。也就是说,分区容错性(P)是前提,是必须要保证的。
|
||||
|
||||
现在就只剩下一致性(C)和可用性(A)可以选择了:要么选择一致性,保证数据正确;要么选择可用性,保证服务可用。那么CP和AP的含义是什么呢?
|
||||
|
||||
- 当选择了一致性(C)的时候,一定会读到最新的数据,不会读到旧数据,但如果因为消息丢失、延迟过高发生了网络分区,那么这个时候,当集群节点接收到来自客户端的读请求时,为了不破坏一致性,可能会因为无法响应最新数据,而返回出错信息。
|
||||
- 当选择了可用性(A)的时候,系统将始终处理客户端的查询,返回特定信息,如果发生了网络分区,一些节点将无法返回最新的特定信息,它们将返回自己当前的相对新的信息。
|
||||
|
||||
**这里我想强调一点,大部分人对CAP理论有个误解,认为无论在什么情况下,分布式系统都只能在C和A中选择1个。**其实,在不存在网络分区的情况下,也就是分布式系统正常运行时(这也是系统在绝大部分时候所处的状态),就是说在不需要P时,C和A能够同时保证。只有当发生分区故障的时候,也就是说需要P时,才会在C和A之间做出选择。而且如果读操作会读到旧数据,影响到了系统运行或业务运行(也就是说会有负面的影响),推荐选择C,否则选A。
|
||||
|
||||
那么我当时是怎么根据场景特点,进行CAP权衡,设计适合的分布式系统呢?为了便于你理解,我先来说说背景。
|
||||
|
||||
开源版的InfluxDB,缺乏集群能力和可用性,而且,InfluxDB是由META节点和DATA节点2个逻辑单元组成,这2个节点的功能和数据特点不同,需要我们分别为它们设计分区容错一致性模型。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/93/5d/9397c99a462d1dcb9ca69328ba34515d.jpg" alt="" title="InfluxDB程序的逻辑架构示意图">
|
||||
|
||||
我具体是这么设计的:
|
||||
|
||||
<li>
|
||||
**作为分布式系统,分区容错性是必须要实现的,**不能因为节点间出现了分区故障,而出现整个系统不工作的情况。
|
||||
</li>
|
||||
<li>
|
||||
考虑到META节点保存的是系统运行的关键元信息,比如数据库名、表名、保留策略信息等,所以必须实现一致性。也就是说,每次读,都要能读取到最新数据,这样才能避免因为查询不到指定的元信息,时序数据记录写入失败或者系统没办法正常运行。比如,创建了数据库telegraf之后,如果系统不能立刻读取到这条新的元信息,那么相关的时序数据记录,就会因为找不到指定数据库信息而写入失败,**所以,我选择CAP理论中的C和P,采用CP架构。**
|
||||
</li>
|
||||
<li>
|
||||
DATA节点保存的是具体的时序数据记录,比如一条记录CPU负载的时序数据,“cpu_usage,host=server01,location=cn-sz user=23.0,system=57.0”。虽然这些数据不是系统运行相关的元信息,但服务会被访问频繁,水平扩展、性能、可用性等是关键,**所以,我选择了CAP理论中的A和P,采用AP架构。**
|
||||
</li>
|
||||
|
||||
你看,我用CAP理论进行思考,并分别设计了InfluxDB的META节点和DATA节点的分区容错一致性模型,而你也可以采用类似的思考方法,设计出符合自己业务场景的分区容错一致性模型。
|
||||
|
||||
那么假设我当时没有受到CAP理论的影响,或者对CAP理论理解不深入,DATA节点不采用AP架构,而是直接使用了现在比较流行的共识算法,比如使用Raft算法,会有什么痛点呢?
|
||||
|
||||
- 受限于Raft的强领导者模型。所有写请求都在领导者节点上处理,整个集群的写性能等于单机性能。这样会造成集群接入性能低下,无法支撑海量或大数据量的时序数据。
|
||||
- 受限于强领导者模型,以及Raft的节点和副本一一对应的限制,无法实现水平扩展,分布式集群扩展了读性能,但写性能并没有提升。这样会出现写性能低下,和因为架构上的限制,无法提升写性能的问题。
|
||||
|
||||
关于Raft算法的一些细节(比如强领导模型),我会在07讲详细带你了解,这里你知道有这么回事儿就可以了。
|
||||
|
||||
那么在这里,我也想考考你:如果META节点采用AP架构,会有什么痛点呢?你可以思考一下。
|
||||
|
||||
## 内容小结
|
||||
|
||||
本节课我主要带你了解了CAP理论,以及CAP理论的应用,我希望你明确的重点如下:
|
||||
|
||||
<li>
|
||||
CA模型,在分布式系统中不存在。因为舍弃P,意味着舍弃分布式系统,就比如单机版关系型数据库MySQL,如果MySQL要考虑主备或集群部署时,它必须考虑P。
|
||||
</li>
|
||||
<li>
|
||||
CP模型,采用CP模型的分布式系统,舍弃了可用性,一定会读到最新数据,不会读到旧数据。一旦因为消息丢失、延迟过高发生了网络分区,就影响用户的体验和业务的可用性(比如基于Raft的强一致性系统,此时可能无法执行读操作和写操作)。典型的应用是Etcd,Consul和Hbase。
|
||||
</li>
|
||||
<li>
|
||||
AP模型,采用AP模型的分布式系统,舍弃了一致性,实现了服务的高可用。用户访问系统的时候,都能得到响应数据,不会出现响应错误,但会读到旧数据。典型应用就比如Cassandra和DynamoDB。
|
||||
</li>
|
||||
|
||||
在多年的开发实践中,我一直喜欢埃里克·布鲁尔的猜想,不是因为它是CAP理论的本源,意义重大,而是因为它源自高可用、高扩展大型互联网系统的实践,强调在数据一致性(ACID)和服务可用性(BASE)之间权衡妥协。在我看来,CAP理论像PH试纸一样,可以用来度量分布式系统的酸碱值,帮助我们思考如何设计合适的酸碱度,在一致性和可用性之间进行妥协折中,设计出满足场景特点的分布式系统。关于酸(Acid)和碱(Base),我会在03和04讲带你了解。
|
||||
|
||||
最后我想说的是,在当前分布式系统开发中,延迟是非常重要的一个指标,比如,在QQ后台的名字路由系统中,我们通过延迟评估服务可用性,进行负载均衡和容灾;再比如,在Hashicorp/Raft实现中,通过延迟评估领导者节点的服务可用性,以及决定是否发起领导者选举。所以,我希望你在分布式系统的开发中,也能意识到延迟的重要性,能通过延迟来衡量服务的可用性。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
既然我提了CAP理论是一个很好的思考框架,能帮助我们思考,如何进行权衡,设计适合业务场景特性的分布式系统,那么你不妨思考一下,CP模型的KV存储和AP模型的KV存储,分别适合怎样的业务场景呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这节课让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
128
极客时间专栏/geek/分布式协议与算法实战/理论篇/03 | ACID理论:CAP的酸,追求一致性.md
Normal file
128
极客时间专栏/geek/分布式协议与算法实战/理论篇/03 | ACID理论:CAP的酸,追求一致性.md
Normal file
@@ -0,0 +1,128 @@
|
||||
<audio id="audio" title="03 | ACID理论:CAP的酸,追求一致性" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/15/aa/155bbe97c390b62fcf9a008de2db76aa.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
提到ACID,我想你并不陌生,很多同学也会觉得它容易理解,在单机上实现ACID也不难,比如可以通过锁、时间序列等机制保障操作的顺序执行,让系统实现ACID特性。但是,一说要实现分布式系统的ACID特性,很多同学就犯难了。那么问题来了,为什么分布式系统的ACID特性在实现上,比较难掌握呢?
|
||||
|
||||
在我看来,ACID理论是对事务特性的抽象和总结,方便我们实现事务。你可以理解成:如果实现了操作的ACID特性,那么就实现了事务。而大多数人觉得比较难,是因为分布式系统涉及多个节点间的操作。加锁、时间序列等机制,只能保证单个节点上操作的ACID特性,无法保证节点间操作的ACID特性。
|
||||
|
||||
那么怎么做才会让实现不那么难呢?答案是你要掌握分布式事务协议,比如二阶段提交协议和TCC(Try-Confirm-Cancel)。这也是我接下来重点和你分享的内容。
|
||||
|
||||
不过在带你了解二阶段提交协议和TCC之前,咱们先继续看看苏秦的故事,看这回苏秦又遇到了什么事儿。
|
||||
|
||||
最近呢,秦国按捺不住自己躁动的心,开始骚扰魏国边境,魏王头疼,向苏秦求助,苏秦认为“三晋一家亲”,建议魏王联合赵、韩一起对抗秦国。但是这三个国家实力都很弱,需要大家都同意联合,一致行动,如果有任何一方不方便行动,就取消整个计划。
|
||||
|
||||
根据侦查情况,明天发动反攻胜算比较大。苏秦想协调赵、魏、韩,明天一起行动。**那么对苏秦来说,他面临的问题是,如何高效协同赵、魏、韩一起行动,并且保证当有一方不方便行动时,取消整个计划。**
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/09/55/0951213850cbf7a9f6db1d99511bd455.jpg" alt="">
|
||||
|
||||
苏秦面对的这个新问题,就是典型的如何实现分布式事务的问题,**赵、魏、韩明天攻打秦国,这三个操作组成一个分布式事务,要么全部执行,要么全部不执行。**
|
||||
|
||||
了解了这个问题之后,我们看看如何通过二阶段提交协议和TCC,来帮助苏秦解决这个难题。
|
||||
|
||||
## 二阶段提交协议
|
||||
|
||||
二阶段提交协议,顾名思义,就是通过二阶段的协商来完成一个提交操作,那么具体是怎么操作的呢?
|
||||
|
||||
首先,苏秦发消息给赵,赵接收到消息后就扮演协调者(Coordinator)的身份,由赵联系魏和韩,发起二阶段提交:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c4/2b/c43a3bffad5dee2d4f465df3fc22c52b.jpg" alt="">
|
||||
|
||||
赵发起二阶段提交后,先进入**提交请求阶段(又称投票阶段)。** 为了方便演示,我们先假设赵、魏、韩明天都能去攻打秦国:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/72/af/72f1414c2d4f2e7a66b8dd246165a8af.jpg" alt="">
|
||||
|
||||
也就是说,第一步,赵分别向魏、韩发送消息:“明天攻打秦国,方便吗?”
|
||||
|
||||
第二步,赵、魏、韩,分别评估明天能否去攻打秦国,如果能,就预留时间并锁定,不再安排其他军事活动。
|
||||
|
||||
第三步,赵得到全部的回复结果(包括他自己的评估结果),都是YES。
|
||||
|
||||
赵收到所有回复后,进入**提交执行阶段(又称完成阶段),** 也就是具体执行操作了,大致步骤如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f5/dc/f5deb827fd03e10106ed6b6839f1d7dc.jpg" alt="">
|
||||
|
||||
首先,赵按照“要么全部执行,要么放弃”的原则,统计投票结果,因为所有的回复结果都是YES,所以赵决定执行分布式事务,明天攻打秦国。
|
||||
|
||||
然后,赵通知魏、韩:“明天攻打秦国。”
|
||||
|
||||
接到通知之后,魏、韩执行事务,明天攻打秦国。
|
||||
|
||||
最后,魏、韩将执行事务的结果返回给赵。
|
||||
|
||||
这样一来,赵就将事务执行的结果(也就是赵、魏、韩明天一起攻打秦国),返回给苏秦,那么,这时苏秦就解决了问题,协调好了明天的作战计划。
|
||||
|
||||
在这里,赵采用的方法就是二阶段提交协议。在这个协议中:
|
||||
|
||||
- 你可以将“赵明天攻打秦国、魏明天攻打秦国、韩明天攻打秦国”,理解成一个分布式事务操作;
|
||||
- 将赵、魏、韩理解为分布式系统的三个节点,其中,赵是协调者(Coordinator),将苏秦理解为业务,也就是客户端;
|
||||
- 将消息理解为网络消息;
|
||||
- 将“明天能否攻打秦国,预留时间”,理解为评估事务中需要操作的对象和对象状态,是否准备好,能否提交新操作。
|
||||
|
||||
需要注意的是,在第一个阶段,每个参与者投票表决事务是放弃还是提交。一旦参与者投票要求提交事务,那么就不允许放弃事务。也就是说,**在一个参与者投票要求提交事务之前,它必须保证能够执行提交协议中它自己那一部分,即使参与者出现故障或者中途被替换掉。** 这个特性,是我们需要在代码实现时保障的。
|
||||
|
||||
还需要你注意的是,在第二个阶段,事务的每个参与者执行最终统一的决定,提交事务或者放弃事务。这个约定,是为了实现ACID中的原子性。
|
||||
|
||||
[二阶段提交协议](https://courses.cs.washington.edu/courses/cse551/09au/papers/CSE550BHG-Ch7.pdf)最早是用来实现数据库的分布式事务的,不过现在最常用的协议是XA协议。这个协议是X/Open国际联盟基于二阶段提交协议提出的,也叫作X/Open Distributed Transaction Processing(DTP)模型,比如MySQL就是通过MySQL XA实现了分布式事务。
|
||||
|
||||
但是不管是原始的二阶段提交协议,还是XA协议,都存在一些问题:
|
||||
|
||||
- 在提交请求阶段,需要预留资源,在资源预留期间,其他人不能操作(比如,XA在第一阶段会将相关资源锁定);
|
||||
- 数据库是独立的系统。
|
||||
|
||||
因为上面这两点,我们无法根据业务特点弹性地调整锁的粒度,而这些都会影响数据库的并发性能。那用什么办法可以解决这些问题呢?答案就是TCC。
|
||||
|
||||
## TCC(Try-Confirm-Cancel)
|
||||
|
||||
TCC是Try(预留)、Confirm(确认)、Cancel(撤销) 3个操作的简称,它包含了预留、确认或撤销这2个阶段。那么你如何使用TCC协议,解决苏秦面临的问题呢?
|
||||
|
||||
首先,我们先**进入到预留阶段**,大致的步骤如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/84/e2/84a56f9ef977e4bc10424f34e59140e2.jpg" alt="">
|
||||
|
||||
第一步,苏秦分别发送消息通知赵、魏、韩,让他们预留明天的时间和相关资源。然后苏秦实现确认操作(明天攻打秦国),和撤销操作(取消明天攻打秦国)。
|
||||
|
||||
第二步,苏秦收到赵、魏、韩的预留答复,都是OK。
|
||||
|
||||
如果预留阶段的执行都没有问题,就进入**确认阶段**,大致步骤如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/28/db/28d109d73b31eb750912c55e795af1db.jpg" alt="">
|
||||
|
||||
第一步,苏秦执行确认操作,通知赵、魏、韩明天攻打秦国。
|
||||
|
||||
第二步,收到确认操作的响应,完成分布式事务。
|
||||
|
||||
如果预留阶段执行出错,比如赵的一部分军队还在赶来的路上,无法出兵,那么就进入撤销**阶段**,大致步骤如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b1/8a/b10a640509628053bacb0897c741608a.jpg" alt="">
|
||||
|
||||
第一步,苏秦执行撤销操作,通知赵、魏、韩取消明天攻打秦国的计划。
|
||||
|
||||
第二步,收到撤销操作的响应。
|
||||
|
||||
你看,在经过了预留和确认(或撤销)2阶段的协商,苏秦实现这个分布式事务:赵、魏、韩三国,要么明天一起进攻,要么明天都按兵不动。
|
||||
|
||||
其实在我看来,TCC本质上是补偿事务,**它的核心思想是针对每个操作都要注册一个与其对应的确认操作和补偿操作(也就是撤销操作)。** 它是一个业务层面的协议,你也可以将TCC理解为编程模型,TCC的3个操作是需要在业务代码中编码实现的,为了实现一致性,确认操作和补偿操作必须是等幂的,因为这2个操作可能会失败重试。
|
||||
|
||||
另外,TCC不依赖于数据库的事务,而是在业务中实现了分布式事务,这样能减轻数据库的压力,但对业务代码的入侵性也更强,实现的复杂度也更高。所以,我推荐在需要分布式事务能力时,优先考虑现成的事务型数据库(比如MySQL XA),当现有的事务型数据库不能满足业务的需求时,再考虑基于TCC实现分布式事务。
|
||||
|
||||
## 内容小结
|
||||
|
||||
本节课我主要带你了解了实现分布式系统ACID特性的方法,二阶段提交协议和TCC,我希望你明确这样几个重点。
|
||||
|
||||
<li>
|
||||
二阶段提交协议,不仅仅是协议,也是一种非常经典的思想。二阶段提交在达成提交操作共识的算法中应用广泛,比如XA协议、TCC、Paxos、Raft等。我希望你不仅能理解二阶段提交协议,更能理解协议背后的二阶段提交的思想,当后续需要时,能灵活地根据二阶段提交思想,设计新的事务或一致性协议。
|
||||
</li>
|
||||
<li>
|
||||
幂等性,是指同一操作对同一系统的任意多次执行,所产生的影响均与一次执行的影响相同,不会因为多次执行而产生副作用。常见的实现方法有Token、索引等。它的本质是通过唯一标识,标记同一操作的方式,来消除多次执行的副作用。
|
||||
</li>
|
||||
|
||||
另外,我想补充一下,三阶段提交协议,虽然针对二阶段提交协议的“协调者故障,参与者长期锁定资源”的痛点,通过引入了询问阶段和超时机制,来减少资源被长时间锁定的情况,不过这会导致集群各节点在正常运行的情况下,使用更多的消息进行协商,增加系统负载和响应延迟。也正是因为这些问题,三阶段提交协议很少被使用,所以,你只要知道有这么个协议就可以了,但如果你想继续研究,可以参考《[Concurrency Control and Recovery in Database Systems](https://courses.cs.washington.edu/courses/cse551/09au/papers/CSE550BHG-Ch7.pdf)》来学习。
|
||||
|
||||
最后我想强调的是,你可以将ACID特性理解为CAP中一致性的边界,最强的一致性,也就是CAP的酸(Acid)。根据CAP理论,如果在分布式系统中实现了一致性,可用性必然受到影响。比如,如果出现一个节点故障,则整个分布式事务的执行都是失败的。实际上,绝大部分场景对一致性要求没那么高,短暂的不一致是能接受的,另外,也基于可用性和并发性能的考虑,**建议在开发实现分布式系统,如果不是必须,尽量不要实现事务,可以考虑采用最终一致性**。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
既然我提了一些实现分布式事务的方法,比如二阶段提交协议、TCC等,那么你不妨思考一下,事务型分布式系统有哪些优点,哪些缺点呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
100
极客时间专栏/geek/分布式协议与算法实战/理论篇/04 | BASE理论:CAP的碱,追求可用性.md
Normal file
100
极客时间专栏/geek/分布式协议与算法实战/理论篇/04 | BASE理论:CAP的碱,追求可用性.md
Normal file
@@ -0,0 +1,100 @@
|
||||
<audio id="audio" title="04 | BASE理论:CAP的碱,追求可用性" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e3/17/e3135226b8d39af5eb78735ec2234017.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
很多同学可能喜欢使用事务型的分布式系统,或者是强一致性的分布式系统,因为使用起来很方便,不需要考虑太多,就像使用单机系统一样。但是学了CAP理论后,你肯定知道在分布式系统中要实现强一致性必然会影响可用性。比如,在采用两阶段提交协议的集群系统中,因为执行提交操作,需要所有节点确认和投票。
|
||||
|
||||
所以,集群的可用性是每个节点可用性的乘积,比如,假设3个节点的集群,每个节点的可用性为99.9%,那么整个集群的可用性为99.7%,也就是说,每个月约宕机129.6分钟,**这是非常严重的问题。** 而解决可用性低的关键在于,根据实际场景,尽量采用可用性优先的AP模型。
|
||||
|
||||
讲到这儿,可能会有一些同学“举手提问”:这也太难了,难道没有现成的库或者方案,来实现合适的AP模型?是的,的确没有。因为它是一个动态模型,是基于业务场景特点妥协折中后设计实现的。不过,你可以借助BASE理论帮助你达成目的。
|
||||
|
||||
在我看来,BASE理论是CAP理论中的AP的延伸,是对互联网大规模分布式系统的实践总结,强调可用性。几乎所有的互联网后台分布式系统都有BASE的支持,这个理论很重要,地位也很高。一旦掌握它,你就能掌握绝大部分场景的分布式系统的架构技巧,设计出适合业务场景特点的、高可用性的分布式系统。
|
||||
|
||||
而它的核心就是基本可用(Basically Available)和最终一致性(Eventually consistent)。也有人会提到软状态(Soft state),在我看来,软状态描述的是实现服务可用性的时候系统数据的一种过渡状态,也就是说不同节点间,数据副本存在短暂的不一致。你只需要知道软状态是一种过渡状态就可以了,我们不多说。
|
||||
|
||||
那么基本可用以及最终一致性到底是什么呢?你又如何在实践中使用BASE理论提升系统的可用性呢?这些就是本节课的重点了,而我建议你集中注意力,认真学习本节课的内容,学以致用,将BASE理论应用到日常工作中。
|
||||
|
||||
## 实现基本可用的4板斧
|
||||
|
||||
在我看来,基本可用是说,当分布式系统在出现不可预知的故障时,允许损失部分功能的可用性,保障核心功能的可用性。就像弹簧一样,遇到外界的压迫,它不是折断,而是变形伸缩,不断适应外力,实现基本的可用。
|
||||
|
||||
具体说的话,你可以把基本可用理解成,当系统节点出现大规模故障的时候,比如专线的光纤被挖断、突发流量导致系统过载(出现了突发事件,服务被大量访问),这个时候可以通过服务降级,牺牲部分功能的可用性,保障系统的核心功能可用。
|
||||
|
||||
就拿12306订票系统基本可用的设计为例,这个订票系统在春运期间,因为开始售票后先到先得的缘故,会出现极其海量的请求峰值,如何处理这个问题呢?
|
||||
|
||||
咱们可以在不同的时间,出售不同区域的票,将访问请求错开,削弱请求峰值。比如,在春运期间,深圳出发的火车票在8点开售,北京出发的火车票在9点开售。**这就是我们常说的流量削峰。**
|
||||
|
||||
另外,你可能已经发现了,在春运期间,自己提交的购票请求,往往会在队列中排队等待处理,可能几分钟或十几分钟后,系统才开始处理,然后响应处理结果,**这就是你熟悉的延迟响应。** 你看,12306订票系统在出现超出系统处理能力的突发流量的情况下,会通过牺牲响应时间的可用性,保障核心功能的运行。
|
||||
|
||||
而12306通过流量削峰和延迟响应,是不是就实现了基本的可用呢?现在它不会再像最初的时候那样,常常404了吧?
|
||||
|
||||
再比如,你正负责一个互联网系统,突然出现了网络热点事件,好多用户涌进来,产生了海量的突发流量,系统过载了,大量图片因为网络超时无法显示。那么这个时候你可以通过哪些方法,保障系统的基本可用呢?
|
||||
|
||||
**相信你马上就能想到体验降级,** 比如用小图片来替代原始图片,通过降低图片的清晰度和大小,提升系统的处理能力。
|
||||
|
||||
**然后你还能想到过载保护,** 比如把接收到的请求放在指定的队列中排队处理,如果请求等待时间超时了(假设是100ms),这个时候直接拒绝超时请求;再比如队列满了之后,就清除队列中一定数量的排队请求,保护系统不过载,实现系统的基本可用。
|
||||
|
||||
**你看,和12306的设计类似,只不过你负责的互联网系统是通过牺牲部分功能的可用性,保障核心功能的运行。**
|
||||
|
||||
我说了这么多,主要是想强调:基本可用在本质上是一种妥协,也就是在出现节点故障或系统过载的时候,通过牺牲非核心功能的可用性,保障核心功能的稳定运行。
|
||||
|
||||
我希望你能在后续的分布式系统的开发中,**不仅掌握流量削峰、延迟响应、体验降级、过载保护这4板斧**,更能理解这4板斧背后的妥协折中,从而灵活地处理不可预知的突发问题。
|
||||
|
||||
带你了解了基本可用之后,我再来说说BASE理论中,另一个非常核心的内容:最终一致性。
|
||||
|
||||
## 最终的一致
|
||||
|
||||
在我看来,最终一致性是说,系统中所有的数据副本在经过一段时间的同步后,最终能够达到一个一致的状态。也就是说,在数据一致性上,存在一个短暂的延迟。
|
||||
|
||||
几乎所有的互联网系统采用的都是最终一致性,只有在实在无法使用最终一致性,才使用强一致性或事务,比如,对于决定系统运行的敏感元数据,需要考虑采用强一致性,对于与钱有关的支付系统或金融系统的数据,需要考虑采用事务。
|
||||
|
||||
你可以将强一致性理解为最终一致性的特例,也就是说,你可以把强一致性看作是不存在延迟的一致性。**在实践中,你也可以这样思考:** 如果业务的某功能无法容忍一致性的延迟(比如分布式锁对应的数据),需要实现的是强一致性;如果能容忍短暂的一致性的延迟(比如QQ状态数据),就可以考虑最终一致性。
|
||||
|
||||
那么如何实现最终一致性呢?你首先要知道它以什么为准,因为这是实现最终一致性的关键。一般来说,在实际工程实践中有这样几种方式:
|
||||
|
||||
- 以最新写入的数据为准,比如AP模型的KV存储采用的就是这种方式;
|
||||
- 以第一次写入的数据为准,如果你不希望存储的数据被更改,可以以它为准。
|
||||
|
||||
那实现最终一致性的具体方式是什么呢?常用的有这样几种。
|
||||
|
||||
- 读时修复:在读取数据时,检测数据的不一致,进行修复。比如Cassandra 的Read Repair实现,具体来说,在向Cassandra系统查询数据的时候,如果检测到不同节点的副本数据不一致,系统就自动修复数据。
|
||||
- 写时修复:在写入数据,检测数据的不一致时,进行修复。比如Cassandra 的Hinted Handoff实现。具体来说,Cassandra集群的节点之间远程写数据的时候,如果写失败就将数据缓存下来,然后定时重传,修复数据的不一致性。
|
||||
- 异步修复:这个是最常用的方式,通过定时对账检测副本数据的一致性,并修复(更多信息可以参考[11讲](https://time.geekbang.org/column/article/208182)的反熵)。
|
||||
|
||||
在这里,我想强调的是因为写时修复不需要做数据一致性对比,性能消耗比较低,对系统运行影响也不大,所以我推荐你在实现最终一致性时优先实现这种方式。而读时修复和异步修复因为需要做数据的一致性对比,性能消耗比较多,在开发实际系统时,你要尽量优化一致性对比的算法,降低性能消耗,避免对系统运行造成影响。
|
||||
|
||||
另外,我还想补充一点,在实现最终一致性的时候,**我推荐同时实现自定义写一致性级别(比如All、Quorum、One、Any,更多信息你可以看一下[12讲](https://time.geekbang.org/column/article/209130)),** 让用户可以自主选择相应的一致性级别,比如可以通过设置一致性级别为All,来实现强一致性。
|
||||
|
||||
现在,想必你了解了BASE理论的核心内容了吧?不过这是理论层面上的,那么在实践中,该如何使用BASE理论的呢?
|
||||
|
||||
## 如何使用BASE理论
|
||||
|
||||
我以自研InfluxDB系统中DATA节点的集群实现为例,带你来使用BASE理论。咱们先来看看如何保障基本可用。
|
||||
|
||||
DATA节点的核心功能是读和写,所以基本可用是指读和写的基本可用。那么我们可以通过分片和多副本,实现读和写的基本可用。也就是说,将同一业务的数据先分片,然后再以多份副本的形式分布在不同的节点上。比如下面这张图,这个3节点2副本的集群,除非超过一半的节点都故障了,否则是能保障所有数据的读写的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ae/d6/ae5fd43f4c878d0acdc188e9889d29d6.jpg" alt="">
|
||||
|
||||
那么如果实现最终一致性呢?就像我上文提到的样子,我们可以通过写时修复和异步修复实现最终一致性。另外,还实现自定义写一致性级别,支持All、Quorum、One、Any 4种写一致性级别,用户在写数据的时候,可以根据业务数据的特点,设置不同的写一致性级别。
|
||||
|
||||
## 内容小结
|
||||
|
||||
本节课我主要带你了解了BASE理论,以及BASE理论的应用,我希望你明确几个重点:
|
||||
|
||||
<li>
|
||||
BASE理论是对CAP中一致性和可用性权衡的结果,它来源于对大规模互联网分布式系统实践的总结,是基于CAP定理逐步演化而来的。它的核心思想是,如果不是必须的话,不推荐实现事务或强一致性,鼓励可用性和性能优先,根据业务的场景特点,来实现非常弹性的基本可用,以及实现数据的最终一致性。
|
||||
</li>
|
||||
<li>
|
||||
BASE理论主张通过牺牲部分功能的可用性,实现整体的基本可用,也就是说,通过服务降级的方式,努力保障极端情况下的系统可用性。
|
||||
</li>
|
||||
<li>
|
||||
ACID理论是传统数据库常用的设计理念,追求强一致性模型。BASE理论支持的是大型分布式系统,通过牺牲强一致性获得高可用性。BASE理论在很大程度上,解决了事务型系统在性能、容错、可用性等方面痛点。另外我再多说一句,BASE理论在NoSQL中应用广泛,是NoSQL系统设计的事实上的理论支撑。
|
||||
</li>
|
||||
|
||||
最后我强调一下,对于任何集群而言,不可预知的故障的最终后果,都是系统过载。如何设计过载保护,实现系统在过载时的基本可用,是开发和运营互联网后台的分布式系统的重中之重。那么我建议你,在开发实现分布式系统,要充分考虑如何实现基本可用。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
我在文章中提了一些实现基本可用的方法,比如流量削峰、延迟响应、体验降级、过载保护等,那么你不妨思考一下,还有哪些方法可以用来实现基本可用呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
123
极客时间专栏/geek/分布式协议与算法实战/理论篇/加餐 | 拜占庭将军问题:如何基于签名消息实现作战计划的一致性?.md
Normal file
123
极客时间专栏/geek/分布式协议与算法实战/理论篇/加餐 | 拜占庭将军问题:如何基于签名消息实现作战计划的一致性?.md
Normal file
@@ -0,0 +1,123 @@
|
||||
<audio id="audio" title="加餐 | 拜占庭将军问题:如何基于签名消息实现作战计划的一致性?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/33/00/33d67684c3c705efb685afbb1f600c00.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
在[01讲](https://time.geekbang.org/column/article/195662)中,为了不啰嗦,让你举一反三地学习,我对签名消息型拜占庭问题之解,没有详细展开,而是聚焦在最核心的点“签名约束了叛徒的作恶行为”,但从留言来看,很多同学在理解签名和如何实现作战一致性上,还是遇到了问题。比如不理解如何实现作战计划的一致性。
|
||||
|
||||
另外,考虑到签名消息是一些常用的拜占庭容错算法(比如PBFT)的实现基础,很重要,所以这节课我会对签名消息型拜占庭问题之解进行补充。在今天的内容中,除了具体讲解如何基于签名消息实现作战计划的一致性之外,我还会说一说什么是签名消息。希望在帮你掌握签名消息型拜占庭问题之解的同时,还帮你吃透相关的基础知识。
|
||||
|
||||
当然,在学完01讲之后,相信你已经明白了,签名消息拜占庭问题之解,之所以能够容忍任意数量的叛徒,关键就在于通过消息的签名,约束了叛徒的作恶行为,也就是说,任何篡改和伪造忠将的消息的行为,都会被发现。
|
||||
|
||||
既然签名消息这么重要,那么什么是签名消息呢?
|
||||
|
||||
## 什么是签名消息?
|
||||
|
||||
签名消息指的就是带有数字签名的消息,你可以这么理解“数字签名”:类似在纸质合同上进行签名来确认合同内容和证明身份。
|
||||
|
||||
在这里我想说的是,数字签名既可以证实内容的完整性,又可以确认内容的来源,实现不可抵赖性(Non-Repudiation)。既然签名消息优点那么多,**那么如何实现签名消息呢?**
|
||||
|
||||
你应该还记得密码学的学术CP(Bob和Alice)吧(不记得的话也没关系,你把他们当作2个人就可以了),今天Bob要给Alice发送一个消息,告诉她,“我已经到北京了”,但是Bob希望这个消息能被Alice完整地接收到,内容不能被篡改或者伪造,我们一起帮Bob和Alice想想办法,看看如何实现这个消息。
|
||||
|
||||
首先,为了避免密钥泄露,我们推荐Bob和Alice使用非对称加密算法(比如RSA)。也就是说,加密和解密使用不同的秘钥,在这里,Bob持有需要安全保管的私钥,Alice持有公开的公钥。
|
||||
|
||||
然后,Bob用哈希算法(比如MD5)对消息进行摘要,然后用私钥对摘要进行加密,生成数字签名(Signature),就像下图的样子:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8d/75/8d69065c69944b74d77592a2aa5ea075.jpg" alt="" title="图1">
|
||||
|
||||
接着,Bob将加密摘要和消息一起发送给Alice:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/92/e0/924cd677d20a1fa7dca41936fc1e5ee0.jpg" alt="" title="图2">
|
||||
|
||||
接下来,当Alice接收到消息和加密摘要(Signature)后,她会用自己的公钥对加密摘要(Signature)进行解密,并对消息内容进行摘要(Degist-2),然后将新获取的摘要(Degist-2)和解密后的摘要(Degist-1)进行对比,如果2个摘要(Digest-1和Digest-2)一致,就说明消息是来自Bob的,并且是完整的,就像下图的样子:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fe/9d/fe974b9037e9ef4e85c227b5a867d39d.jpg" alt="" title="图3">
|
||||
|
||||
你看,通过这种方法,Bob的消息就能被Alice完整接收到了,任何篡改和伪造Bob消息的行为,都会因为摘要不一致,而被发现。**而这个消息就是签名消息。**
|
||||
|
||||
现在,你应该理解了什么是签名消息了吧?另外,关于在留言区提到的“为什么签名消息能约束叛将们的作恶行为?”,在这里,我再补充下,通过上面的Bob和Alice的故事,我们可以看到,在数字签名的约束下,叛将们是无法篡改和伪造忠将的消息的,因为任何篡改和伪造消息的行为都会被发现,也就是作恶的行为被约束了。也就是说,叛将这时能做“小”恶(比如,不响应消息,或者叛将们相互串通发送指定的消息)但他们无法篡改或伪造忠将的消息了。
|
||||
|
||||
既然数字签名约束了叛将们的作恶行为,那么苏秦怎么做才能实现作战的一致性的呢?也就是忠将们执行一致的作战计划。
|
||||
|
||||
## 如何实现作战计划的一致性?
|
||||
|
||||
之前我已经提到了,苏秦可以通过签名消息的方式,不仅能在不增加将军人数的情况下,解决二忠一叛的难题,还能实现无论叛将数多少,忠诚的将军们始终能达成一致的作战计划。
|
||||
|
||||
为了方便你理解,我以二忠二叛(更复杂的叛徒作恶模型,因为叛徒们可以相互勾结串通)为例具体演示一下,是怎样实现作战计划的一致性的:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/25/f9/25bdee6e350e7d4f70401c649f40bef9.jpg" alt="" title="图4">
|
||||
|
||||
需要你注意的是,4位将军约定了一些流程来发送作战信息、执行作战指令。
|
||||
|
||||
**第一轮:**
|
||||
|
||||
- 先发送作战指令的将军,作为指挥官,其他的将军作为副官。
|
||||
- 指挥官将他的签名的作战指令发送给每位副官。
|
||||
- 每位副官,将从指挥官处收到的新的作战指令(也就与之前收的作战指令不同),按照顺序(比如按照首字母字典排序)放到一个盒子里。
|
||||
|
||||
**第二轮:**
|
||||
|
||||
- 除了第一轮的指挥官外,剩余的3位将军将分别作为指挥官,在上一轮收到的作战指令上,加上自己的签名,并转发给其他将军。
|
||||
|
||||
**第三轮:**
|
||||
|
||||
- 除了第一、二轮的指挥官外,剩余的2位将军将分别作为指挥官,在上一轮收到的作战指令上,加上自己的签名,并转发给其他将军。
|
||||
|
||||
最后,各位将军按照约定,比如使用盒子里最中间的那个指令来执行作战指令。(假设盒子中的指令为A、B、C,那中间的指令也就是第n /2个命令。其中,n为盒子里的指令数,指令从0开始编号,也就是B)。
|
||||
|
||||
为了帮你直观地理解,如何基于签名消息实现忠将们作战计划的一致性,我来演示一下作战信息协商过程。**而且我会分别以忠将和叛将先发送作战信息为例来演示,**这样可以完整地演示叛将对作战计划干扰破坏的可能性。
|
||||
|
||||
那么忠诚的将军先发送作战信息的情况是什么呢?
|
||||
|
||||
为了演示方便,假设苏秦先发起带有签名的作战信息,作战指令是“进攻”。那么在第一轮作战信息协商中,苏秦向齐、楚、燕发送作战指令“进攻”。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/66/be/66add595b082b18e3a6feaeb4b5e6ebe.jpg" alt="" title="图5">
|
||||
|
||||
在第二轮作战信息协商中,齐、楚、燕分别作为指挥官,向另外2位发送作战信息“进攻”。可是楚、燕已经叛变了,**但在签名的约束下,他们无法篡改和伪造忠将的消息,**为了达到干扰作战计划的目的,他们俩一个选择发送消息,一个默不作声,不配合。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6b/d0/6b5878c8dce6279dabc8db92d666cfd0.jpg" alt="" title="图6">
|
||||
|
||||
在第三轮作战信息协商中,齐、楚分别作为指挥官,将接收到的作战信息,附加上自己的签名,并转发给另外一位(这时的叛徒燕,还是默不作声,不配合)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/35/23/355f1a1453547731c9a1d05090eaa123.jpg" alt="" title="图7">
|
||||
|
||||
最终,齐收到的作战信息都是“进攻”(它收到了苏秦和楚的),按照“执行盒子最中间的指令”的约定,齐会和苏秦一起执行作战指令“进攻”,实现忠将们作战计划的一致性。
|
||||
|
||||
那么如果是叛徒楚先发送作战信息,干扰作战计划,结果会有所不同吗?我们来具体看一看。在第一轮作战信息协商中,楚向苏秦发送作战指令“进攻”,向齐、燕发送作战指令“撤退”。(当然还有其他的情况,这里只是选择了其中一种,其他的情况,你可以都推导着试试,看看结果是不是一样?)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d6/0b/d6e1997399d8e37177a4a1e98e31cb0b.jpg" alt="" title="图8">
|
||||
|
||||
然后,在第二轮作战信息协商中,苏秦、齐、燕分别作为指挥官,将接收到的作战信息,附加上自己的签名,并转发给另外两位。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7e/2c/7e9066308ae3338a53c2e5561d43b22c.jpeg" alt="" title="图9">
|
||||
|
||||
**为了达到干扰作战计划的目的,叛徒楚和燕相互勾结了。**比如,燕拿到了楚的私钥,也就是燕可以伪造楚的签名,这个时候,燕为了干扰作战计划,给苏秦发送作战指令“进攻”,给齐发送作战指令却是“撤退”。
|
||||
|
||||
接着,在第三轮作战信息协商中,苏秦、齐、燕分别作为指挥官,将接收到的作战信息,附加上自己的签名,并转发给另外一位。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bd/e3/bd82590e3db9184067d455cf0d3a74e3.jpg" alt="" title="图10">
|
||||
|
||||
最终,苏秦和齐收到的作战信息都是“撤退、进攻”,按照“执行盒子最中间的指令”的约定,苏秦、齐和燕一起执行作战指令“撤退”,实现了作战计划的一致性。也就是说,无论叛将楚和燕如何捣乱,苏秦和齐都能执行一致的作战计划,保证作战的胜利。
|
||||
|
||||
另外在这里,我想补充一点,签名消息的拜占庭问题之解,也是需要进行m+1轮(其中m为叛将数,所以你看,只有楚、燕是叛变的,那么就进行了三轮协商)。你也可以从另外一个角度理解:n位将军,能容忍(n - 2) 位叛将(只有一位忠将没有意义,因为此时不需要达成共识了)。**关于这个公式,你只需要记住就好了,推导过程你可以参考论文。**
|
||||
|
||||
最后,我想说的是,签名消息型拜占庭问题之解,解决的是忠将们如何就作战计划达成共识的问题,也就只要忠将们执行了一致的作战计划就可以了。但它不关心这个共识是什么,比如,在适合进攻的时候,忠将们可能执行的作战计划是撤退。也就是,这个算法比较理论化。
|
||||
|
||||
关于理论化这一点,有的同学会想知道它如何去用,在我看来呢,这个算法解决的是共识的问题,没有与实际场景结合,是很难在实际场景中落地的。在实际场景中,你可以考虑后来的改进过后的拜占庭容错算法,比如PBFT算法。
|
||||
|
||||
## 内容小结
|
||||
|
||||
本节课我主要带你了解了什么签名消息,以及忠将们如何通过签名消息实现作战的一致性,我希望你明确这样几个重点:
|
||||
|
||||
1.数字签名是基于非对称加密算法(比如RSA、DSA、DH)实现的,它能防止消息的内容被篡改和消息被伪造。
|
||||
|
||||
2.签名消息约束了叛徒的作恶行为,比如,叛徒可以不响应,可以相互勾结串通,但叛徒无法篡改和伪造忠将的消息。
|
||||
|
||||
3.需要你注意的是,签名消息拜占庭问题之解,虽然实现了忠将们作战计划的一致性,但它不关心达成共识的结果是什么。
|
||||
|
||||
最后,我想说的是,签名消息、拜占庭将军问题的签名消息之解是非常经典的基础知识,影响和启发了后来的众多拜占庭容错算法(比如PBFT),理解了本讲的内容后,你能更好地理解其他的拜占庭容错算法,以及它们如何改进的?为什么要这么改进?比如,在PBFT中,基于性能的考虑,大部分场景的消息采用消息认证码(MAC),只有在视图变更(View Change)等少数场景中采用了数字签名。
|
||||
|
||||
## 课堂思考
|
||||
|
||||
我演示了在“二忠二叛”情况下,忠将们如何实现作战计划的一致性,那么你不妨推演下,在“二忠一叛”情况下,忠将们如何实现作战计划的一致性呢?欢迎在留言区分享你的看法,与我一同讨论。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
49
极客时间专栏/geek/分布式协议与算法实战/结束语/结束语 | 静下心来,享受技术的乐趣.md
Normal file
49
极客时间专栏/geek/分布式协议与算法实战/结束语/结束语 | 静下心来,享受技术的乐趣.md
Normal file
@@ -0,0 +1,49 @@
|
||||
<audio id="audio" title="结束语 | 静下心来,享受技术的乐趣" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d7/5a/d73b5543529b44c1cb285e885369a05a.mp3"></audio>
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
一晃几个月的时间就过去了,这段日子里,我们一起在课程里沟通交流,与我而言,这是一段很特别的经历。我看到很多同学凌晨还在学习、留言,留言区里经常会看到熟悉的身影,比如约书亚、唔多志、每天晒白牙、小晏子,很感谢你们一直保持着学习的热情。
|
||||
|
||||
就要说再见了,借今天这个机会,我想跟你唠点儿心里话。我问自己,如果只说一句话会是啥?想来想去,我觉得就是它了:**静下心来,享受技术的乐趣。**其实这与我之前的经历有关,我想你也能从我的经历中,看到你自己的影子。
|
||||
|
||||
我们都有这样的感觉,无论任何事情,如果想把它做好,其实都不容易。我记得自己在开发InfluxDB系统期间,为了确保进度不失控,常常睡在公司,加班加点;在写稿期间,为了交付更高质量的课程,我总是会有很多想法,偶尔会通宵写稿,核对每句话、每个细节;再比如,为了解答kernel_distribution同学的一个关于外部PPT的问题,我通过Google找到相关代码的出处,然后反复推敲,在凌晨4点准备了一个答案。
|
||||
|
||||
当然,技术的学习就更加不容易了,不是读几遍材料、调调代码就可以了,而是需要我们设计检测模型,来验证自己是否准确地理解了技术。我曾见过一些团队,做技术决策的依据是不成立的,设计和开发的系统,尽管迭代多版,也始终稳定不下来。在我看来,这些团队最大的问题,就是对技术的理解不准、不够。
|
||||
|
||||
在我看来,我们需要调整下心态,也就是静下心来,全身心地投入,去体会技术的乐趣,“Hack it and enjoy it!”。然后学习和工作中的小成就,又会不断地给我们正反馈,激励我们,最终可以行云流水般地把事情越做越好。
|
||||
|
||||
具体到我们课程的主题,也就是分布式技术,该怎么继续精进呢?我们都知道,分布式技术属于新技术,仍在快速发展(比如Raft在2013年才提出),没有体系化的学习材料,而且知识碎片,学习起来尤为不易。今天我想再补充几点个人看法。
|
||||
|
||||
**首先是“杨不悔”。**也就是我们要“衣带渐宽终不悔,为伊消得人憔悴”。想想你在大学的时候,是不是很执着呢?学习分布式技术,也需要这么个劲头儿。
|
||||
|
||||
**其次是“张无忌”。**也就是我们要“不唯书不唯上只唯实”。理论是为了解决问题的,而不是为了“正确”,理论也是在实战中不断发展的,所以在日常学习和使用技术时,我们要注意妥协,没有十全十美的技术,我们需要根据场景特点,权衡折中使用技术,并且实战也会进一步加深我们对技术的理解。
|
||||
|
||||
**最后是“师夷长技以制夷”。**也就是我们要科学上网,多阅读英文资料。
|
||||
|
||||
另外,有些同学可能刚刚接触分布式系统和分布式技术,我对你的建议是“单点突破,再全面开花”。比如,你可以反复研究20讲的分布式KV系统,然后研究Raft算法,最后再去研究其他分布式算法,循序渐进地学习。
|
||||
|
||||
为了帮助你更好地学习,掌握“渔”的技巧。在这里,我推荐一些适合入门和深究的学习材料(当然材料不能太多,太多了,相当于没推荐)。
|
||||
|
||||
- [迭戈·安加罗(Diego Ongaro)的博士论文](https://github.com/ongardie/dissertation#readme):安加罗的博士论文,对Raft算法做了很详细的描述,我建议你反复读,结合源码(比如Hashicorp Raft)读,直到读懂每一句话。
|
||||
- [《Paxos Made Live》](http://www.read.seas.harvard.edu/~kohler/class/08w-dsi/chandra07paxos.pdf):这是Google团队的Paxos实践总结,我建议你从工程实践的角度去阅读,多想想如果是你,你会怎么做。
|
||||
- [《Eventually Consistent》](https://www.allthingsdistributed.com/2008/12/eventually_consistent.html):了解下沃纳·威格尔(亚马逊CTO)对一致性的理解和定义。
|
||||
|
||||
说到这里,我还想强调一点,希望能在后续的工作和学习中帮到你。那就是,“技术要具有成本优势”。什么意思呢?
|
||||
|
||||
基于开源软件,我们很容易“堆砌”一套业务需要的功能。基于大型互联网后台(比如QQ)的架构理念,我们能支撑极其海量的服务和流量。也就是说,实现功能或支撑海量流量,相关的软件和理念,都已经很成熟,不是挑战了,但功能背后的成本问题突出。
|
||||
|
||||
而成本就是钱,功能背后的成本问题是需要重视和解决的,比如,自研KV存储相比Redis降低了数量级倍数的成本。另外,分布式技术本身就是适用于规模业务的,而且随着业务规模的增加,成本的痛点会更加突出。我希望你能注意到这点,在根据实际场景设计系统架构时,如果需要的话,也将成本作为一个权衡点考虑进去。
|
||||
|
||||
为什么要考虑这些?**因为我真心希望你是分布式系统的架构师、开发者,而不仅仅是开源软件的使用者。**
|
||||
|
||||
好了,专栏到此就告一段落了。但专栏的结束,也是另一种开始。我会花时间处理还没来得及回复的留言,也会针对一些同学的共性问题策划答疑或者加餐(这是一个承诺,也请你监督)。总的来说,我会继续帮你吃透算法原理,让你掌握分布式系统的开发实战能力。当然,你可以随时在遇到问题时,在留言区留言,我们一起交流讨论。
|
||||
|
||||
在文章结尾,我为你准备了一份调查问卷,题目不多,希望你能抽出两三分钟填写一下。我非常希望听听你对这个专栏的意见和建议,期待你的反馈!
|
||||
|
||||
[<img src="https://static001.geekbang.org/resource/image/b8/72/b8538443cdc6fff2962a5bf1f692bd72.jpg" alt="">](https://jinshuju.net/f/e470QX)
|
||||
|
||||
最后,我想用一段话结束今天的分享,学习技术的路上你可能会遇到对无法准确理解某技术原理的问题,但你不要觉得孤单,因为这是一个正常的情况,大家都会遇到。如果你觉得某技术的原理,理解起来很吃力,你不妨先把这个技术使用起来,然后多想想,如果是你,你会怎么设计,接着你可以带着自己的猜测去研究技术背后的原理。
|
||||
|
||||
希望你能在繁忙的工作中,保持一颗极客的初心,享受技术的乐趣!
|
||||
|
||||
编辑角:目前课程已经结束了,为了交付更好的内容,《分布式协议与算法实战》于2020年4.26日启动迭代计划,15讲为迭代版本,后面也会对ZAB协议进行更细致化的讨论,敬请期待!
|
||||
10
极客时间专栏/geek/分布式协议与算法实战/结束语/结课测试 | 这些分布式协议与算法的知识,你都掌握了吗?.md
Normal file
10
极客时间专栏/geek/分布式协议与算法实战/结束语/结课测试 | 这些分布式协议与算法的知识,你都掌握了吗?.md
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
你好,我是韩健。
|
||||
|
||||
《分布式协议与算法实战》这门课程已经完结一段时间了,在完结的这段时间里,我依然会收到很多留言,很感谢你一直以来的认真学习和支持!
|
||||
|
||||
为了帮助你检验自己的学习效果,我特别给你准备了一套结课测试题。这套测试题共有20道题目,包括 15道单选题和5道多选题,满分 100 分。
|
||||
|
||||
还等什么,点击下面按钮开始测试吧!
|
||||
|
||||
[<img src="https://static001.geekbang.org/resource/image/28/a4/28d1be62669b4f3cc01c36466bf811a4.png" alt="">](http://time.geekbang.org/quiz/intro?act_id=195&exam_id=488)
|
||||
Reference in New Issue
Block a user