This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

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

View File

@@ -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&amp;utm_medium=pc&amp;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只能在主节点上执行读操作那么在最后我给你留了一个思考题这个设计有什么局限呢欢迎在留言区分享你的看法与我一同讨论。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View 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>
候选人候选人将向其他节点发送请求投票RequestVoteRPC消息通知其他节点来投票如果赢得了大多数选票就晋升当领导者。
</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.请求投票RequestVoteRPC是由候选人在选举期间发起通知各节点进行投票
2.日志复制AppendEntriesRPC是由领导者发起用来复制日志和提供心跳消息。
我想强调的是日志复制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算法实现了“一切以我为准”的强领导者模型那么你不妨思考这个设计有什么限制和局限呢欢迎在留言区分享你的看法与我一同讨论。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View 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的日志复制理解成一个优化后的二阶段提交将二阶段优化成了一阶段减少了一半的往返消息也就是降低了一半的消息延迟。那日志复制的具体过程是什么呢
首先领导者进入第一阶段通过日志复制AppendEntriesRPC消息将日志项复制到集群其他节点上。
接着,如果领导者接收到大多数的“复制成功”响应后,它将日志项应用到它的状态机,并返回成功给客户端。如果领导者没有接收到大多数的“复制成功”响应,那么就返回错误给客户端。
学到这里,有同学可能有这样的疑问了,领导者将日志项应用到它的状态机,怎么没通知跟随者应用日志项呢?
这是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值为7PrevLogTerm值为4。
</li>
<li>
如果跟随者在它的日志中找不到与PrevLogEntry值为7、PrevLogTerm值为4的日志项也就是说它的日志和领导者的不一致了那么跟随者就会拒绝接收新的日志项并返回失败信息给领导者。
</li>
<li>
这时领导者会递减要复制的日志项的索引值并发送新的日志项到跟随者这个消息的PrevLogEntry值为6PrevLogTerm值为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会如何处理实现日志的一致呢欢迎在留言区分享你的看法与我一同讨论。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View 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 := &amp;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集群的写性能瓶颈呢欢迎在留言区分享你的看法与我一同讨论。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View 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中你可以看到哈希环的空间是按顺时针方向组织的圆环的正上方的点代表00点右侧的第一个点代表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将寻址到节点Akey-02将寻址到节点Bkey-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系统中如何设计一致哈希实现当某个集群的领导者节点出现故障并选举出新的领导者后整个系统还能稳定运行呢欢迎在留言区分享你的看法与我一同讨论。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View 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>
在实际场景中,实现数据副本的最终一致性时,一般而言,直接邮寄的方式是一定要实现的,因为不需要做一致性对比,只是通过发送更新数据或缓存重传,来修复数据的不一致,性能损耗低。在存储组件中,节点都是已知的,一般采用反熵修复数据副本的一致性。当集群节点是变化的,或者集群节点数比较多时,这时要采用谣言传播的方式,同步更新数据,实现最终一致。
## 课堂思考
既然使用反熵实现最终一致性时,需要通过一致性检测发现数据副本的差异,如果每次做一致性检测时都做数据对比的话,肯定是比较消耗性能的,那有什么办法降低一致性检测时的性能消耗呢?欢迎在留言区分享你的看法,与我一同讨论。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -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 &gt; 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) &gt; 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 &gt; N的时候对于客户端来讲整个系统能保证强一致性一定能返回更新后的那份数据。
- 当W + R &lt;= 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 &gt; 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时需要实现自定义副本的能力那么一般设置几个副本就可以了为什么呢欢迎在留言区分享你的看法与我一同讨论。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View 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呢欢迎在留言区分享你的看法与我一同讨论。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View 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 &quot;geektime&quot; | sha256sum
bb2f0f297fe9d3b8669b6b4cec3bff99b9de596c46af2e4c4a504cfe1372dc52 -
```
那我们如何通过哈希函数进行哈希运算,从而证明工作量呢?为了帮你理解这部分内容,我举个具体的例子。
我们给出的工作量要求是,基于一个基本的字符串(比如"geektime"),你可以在这个字符串后面添加一个整数值,然后对变更后(添加整数值)的字符串进行SHA256哈希运算如果运算后得到的哈希值16进制形式是以"0000"开头的,就验证通过。为了达到这个工作量证明的目标,我们需要不停地递增整数值,一个一个试,对得到的新字符串进行 SHA256 哈希运算。
按照这个规则我们需要经过35024次计算才能找到恰好前4位为0的哈希值。
```
&quot;geektime0&quot; =&gt; 01f28c5df06ef0a575fd0e529be9a6f73b1290794762de014ec84182081e118e
&quot;geektime1&quot; =&gt; a2567c06fdb5775cb1e3ce17b72754cf146fcc6da75c8f1d87d7ab6a1b8c4523
...
&quot;geektime35022&quot; =&gt;
8afc85049a9e92fe0b6c98b02b27c09fb869fbfe273d0ab84ad8c5ac17b8627e
&quot;geektime35023&quot; =&gt;
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”工作量是增加了还是减少了为什么呢欢迎在留言区分享你的看法与我一同讨论。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View 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">
接着当主节点接收到写请求后它会基于写请求中的指令也就是XY来创建一个提案Proposal并使用一个唯一的ID来标识这个提案。这里我说的唯一的ID就是指事务标识符Transaction ID也就是zxid就像下图的样子。
<img src="https://static001.geekbang.org/resource/image/d0/9b/d0063fa9275ce0a114ace27db326d19b.jpg" alt="" title="图6">
从图中你可以看到X、Y对应的事务标识符分别为&lt;1, 1&gt;&lt;1, 2&gt;,这两个标识符是什么含义呢?
你可以这么理解事务标识符是64位的long型变量有任期编号epoch和计数器counter两部分组成为了形象和方便理解我把epoch翻译成任期编号格式为&lt;epoch, counter&gt;高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协议进行更细致化的讨论敬请期待

View 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 ManagerTM是分布式事务的协调者。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不能满足并发需求时如何重新设计底层数据系统来避免采用分布式事务呢为什么呢欢迎在留言区分享你的看法与我一同讨论。
最后,感谢你的阅读,如果这节课让你有所收获,也欢迎你将它分享给更多的朋友。

View 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是通过视图变更来选举出新的主节点的。那么你不妨想想集群是在视图变更时能否继续处理来自客户端的写请求呢为什么呢欢迎在留言区分享你的看法与我一同讨论。
最后,感谢你的阅读,如果这节课让你有所收获,也欢迎你将它分享给更多的朋友。

View 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能解决指令执行的原子性问题呢欢迎在留言区分享你的看法与我一同讨论。
最后,感谢你的阅读,如果这节课让你有所收获,也欢迎你将它分享给更多的朋友。

View 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假设投票信息的格式是&lt;proposedLeader, proposedEpoch, proposedLastZxidnode&gt;,其中:
- proposedLeader节点提议的领导者的集群ID也就是在集群配置比如myid配置文件时指定的ID。
- proposedEpoch节点提议的领导者的任期编号。
- proposedLastZxid节点提议的领导者的事务标识符最大值也就是最新提案的事务标识符
- node投票的节点比如节点B。
假设一个ZooKeeper集群由节点A、B、C组成其中节点A是领导者节点B、C是跟随者为了方便演示假设epoch分别是1和1lastZxid分别是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都“自告奋勇”推荐自己为领导者并创建选票&lt;2, 1, 101, B&gt;&lt;3, 1, 102, C&gt;然后各自将选票发送给集群中所有节点也就是说B发送给B、CC也发送给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而选票&lt;2, 1, 101, B&gt;提议的领导者是B因为节点C的任期编号与节点B相同但节点C的事务标识符的最大值比节点B的大那么按照约定的规则相比节点B节点C更适合作为领导者也就是说节点C不需要调整选票信息正常接收和保存选票就可以了。
但对于对于节点B而言它提议的领导者是B选票&lt;3, 1, 102, C&gt;提议的领导者是C因为节点C的任期编号与节点B相同但节点C的事务标识符的最大值比节点B的大那么按照约定的规则相比节点B节点C应该作为领导者所以节点B除了接收和保存选票信息还会更新自己的选票为&lt;3, 1, 102, B&gt;也就是推荐C作为领导者并将选票重新发送给节点B、C
<img src="https://static001.geekbang.org/resource/image/bf/77/bf70a4f1e9c28de2bbd3c134193ae577.jpg" alt="" title="图6">
接着当节点B、C接收到来自节点B新的选票时因为这张选票&lt;3, 1, 102, B&gt;提议的领导者与他们提议的领导者是一样的都是节点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) &amp;&amp; (!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个已提交提案(&lt;1, 1&gt;&lt;1, 2&gt;)C有4个未提交提案(&lt;1, 1&gt;&lt;1, 2&gt;&lt;1, 3&gt;&lt;1, 4&gt;)那么当A故障后C就会当选领导者。因为C的dataTree.lastProcessedZxid值也就是&lt;1, 4&gt;大于B的dataTree.lastProcessedZxid值也就是&lt;1, 2&gt;)。
最后,你可能会好奇,我为啥会写这么多内容,来分析源码实现,除了因为代码也是一种表达,能有效弥补文字的无法表达的内容之外,还因为对于一个软件来说,最准确、最新的使用手册和技术内幕就是源码。我希望你也能养成阅读源码的习惯,将源码和文档结合起来,来准确理解软件的功能和原理。
选举出了新领导者它是不是就可以处理写请求了呢当然不可以因为ZAB集群还需要通过成员发现Discovery和数据同步Synchronization来恢复故障然后领导者才能行使“领导”的职能处理写请求这也是我会在下一讲重点带你了解的。
## 课堂思考
既然我提到在ZAB协议中ZAB协议是通过快速领导者选举来选举出新的领导者的。那么你不妨想想在选举中会出现选票被瓜分、选举失败的问题吗为什么呢欢迎在留言区分享你的看法与我一同讨论。
最后,感谢你的阅读,如果这节课让你有所收获,也欢迎你将它分享给更多的朋友。

View 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(&quot;Committing zxid 0x&quot; + Long.toHexString(zxid)
+ &quot; but next pending txn 0x&quot; +
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, &quot;response&quot;, 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 ElectionRaft采用的是“一张选票、先到先得”的自定义算法。在我看来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>
其他相比ZABRaft的设计更为简洁比如Raft没有引入类似ZAB的成员发现和数据同步阶段而是当节点发起选举时递增任期编号在选举结束后广播心跳直接建立领导者关系然后向各节点同步日志来实现数据副本的一致性。**在我看来ZAB的成员发现可以和领导者选举合到一起类似Raft在领导者选举结束后直接建立领导者关系而不是再引入一个新的阶段数据同步阶段是一个冗余的设计可以去除的因为ZAB不是必须要先实现数据副本的一致性才可以处理写请求而且这个设计是没有额外的意义和价值的。**
</li>
另外ZAB和ZooKeeper强耦合你无法在实际系统中独立使用而Raft的实现比如Hashicorp Raft是可以独立使用的编程友好。
## 课堂思考
我提到ZooKeeper提供的是最终一致性读操作可以在任何节点上执行。那么如果读操作访问的是备份节点为什么无法保证每次都能读到最新的数据呢欢迎在留言区分享你的看法与我一同讨论。
最后,感谢你的阅读,如果这节课让你有所收获,也欢迎你将它分享给更多的朋友。

View 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已提交提案的事务标识符最大值分别是&lt;1, 10&gt;&lt;1, 11&gt;其中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创建新的任期编号然后基于新任期编号创建新的事务标识符也就是&lt;2, 0&gt;)。
<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已提交提案的事务标识符最大值也就是&lt;1, 11&gt;大于B已提交提案的事务标识符最大值也就是&lt;1, 10&gt;所以C会用DIFF的方式修复数据副本的不一致并返回差异数据也就是事务标识符为&lt;1, 11&gt;的提案和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, &quot;packet&quot;);
```
第四接收到来自领导者的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为10minCommittedLog为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中提案提交的大多数原则和领导者选举的大多数原则确保了被复制到大多数节点的提案就不再改变了。那么你不妨思考和推演一下这是为什么呢欢迎在留言区分享你的看法与我一同讨论。
最后,感谢你的阅读,如果这节课让你有所收获,也欢迎你将它分享给更多的朋友。