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

126 lines
12 KiB
Markdown

This file contains ambiguous Unicode characters!

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

# 09 | Raft算法如何解决成员变更的问题
你好,我是韩健。
在日常工作中,你可能会遇到服务器故障的情况,这时你就需要替换集群中的服务器。如果遇到需要改变数据副本数的情况,则需要增加或移除集群中的服务器。总的来说,在日常工作中,集群中的服务器数量是会发生变化的。
讲到这儿也许你会问“老韩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个节点组成的新集群
![](https://static001.geekbang.org/resource/image/85/04/853b678cb8a088ce1bc9f91fc62bde04.jpg?wh=1142*400)
那么Raft算法是如何保障在集群配置变更时集群能稳定运行不出现2个领导者呢带着这个问题我们正式进入今天的学习。
老话说得好,“认识问题,才能解决问题”。为了帮你更好地理解单节点变更的方法,我们先来看一看,成员变更时,到底会出现什么样的问题?
## 成员变更的问题
在我看来在集群中进行成员变更的最大风险是可能会同时出现2个领导者。比如在进行成员变更时节点A、B和C之间发生了分区错误节点A、B组成旧配置中的“大多数”也就是变更前的3节点集群中的“大多数”那么这时的领导者节点A依旧是领导者。
另一方面节点C和新节点D、E组成了新配置的“大多数”也就是变更后的5节点集群中的“大多数”它们可能会选举出新的领导者比如节点C。那么这时就出现了同时存在2个领导者的情况。
![](https://static001.geekbang.org/resource/image/82/9e/827a4616e65633015c1f77f3425b1a9e.jpg?wh=1142*293)
如果出现了2个领导者那么就违背了“领导者的唯一性”的原则进而影响到集群的稳定运行。你要如何解决这个问题呢也许有的同学想到了一个解决方法。
因为我们在启动集群时配置是固定的不存在成员变更在这种情况下Raft的领导者选举能保证只有一个领导者。也就是说这时不会出现多个领导者的问题那我可以先将集群关闭再启动新集群啊。也就是先把节点A、B、C组成的集群关闭然后再启动节点A、B、C、D、E组成的新集群。
**在我看来,这个方法不可行。** 为什么呢因为你每次变更都要重启集群意味着在集群变更期间服务不可用肯定不行啊太影响用户体验了。想象一下你正在玩王者荣耀时不时弹出一个对话框通知你系统升级游戏暂停3分钟。这体验糟糕不糟糕
既然这种方法影响用户体验,根本行不通,那到底怎样解决成员变更的问题呢?**最常用的方法就是单节点变更。**
## 如何通过单节点变更解决成员变更的问题?
单节点变更就是通过一次变更一个节点实现成员变更。如果需要变更多个节点那你需要执行多次单节点变更。比如将3节点集群扩容为5节点集群这时你需要执行2次单节点变更先将3节点集群变更为4节点集群然后再将4节点集群变更为5节点集群就像下图的样子。
![](https://static001.geekbang.org/resource/image/7e/55/7e2b1caf3c68c7900d6a7f71e7a3a855.jpg?wh=1142*790)
现在让我们回到开篇的思考题看看如何用单节点变更的方法解决这个问题。为了演示方便我们假设节点A是领导者
![](https://static001.geekbang.org/resource/image/25/40/25cabfbad4627ec4c39b8d32a567d440.jpg?wh=1142*670)
目前的集群配置为\[A, B, C\]我们先向集群中加入节点D这意味着新配置为\[A, B, C, D\]。成员变更,是通过这么两步实现的:
* 第一步领导者节点A向新节点节点D同步数据
* 第二步领导者节点A将新配置\[A, B, C, D\]作为一个日志项复制到新配置中所有节点节点A、B、C、D然后将新配置的日志项应用Apply到本地状态机完成单节点变更。
![](https://static001.geekbang.org/resource/image/7f/07/7f687461706f3b226d79a55b618e4c07.jpg?wh=1142*486)
在变更完成后,现在的集群配置就是\[A, B, C, D\]我们再向集群中加入节点E也就是说新配置为\[A, B, C, D, E\]。成员变更的步骤和上面类似:
* 第一步领导者节点A向新节点节点E同步数据
* 第二步领导者节点A将新配置\[A, B, C, D, E\]作为一个日志项复制到新配置中的所有节点A、B、C、D、E然后再将新配置的日志项应用到本地状态机完成单节点变更。
![](https://static001.geekbang.org/resource/image/7d/43/7d3b5da84db682359ab82579fdd2e243.jpg?wh=1142*427)
这样一来,我们就通过一次变更一个节点的方式,完成了成员变更,保证了集群中始终只有一个领导者,而且集群也在稳定运行,持续提供服务。
我想说的是,在正常情况下,**不管旧的集群配置是怎么组成的,旧配置的“大多数”和新配置的“大多数”都会有一个节点是重叠的。** 也就是说不会同时存在旧配置和新配置2个“大多数”
![](https://static001.geekbang.org/resource/image/5f/b8/5fe7c8d90857737d7314263eae2166b8.jpg?wh=1142*906)![](https://static001.geekbang.org/resource/image/4a/27/4a00b7e1b89922cd9f785c6f153aca27.jpg?wh=1142*899)
从上图中你可以看到,不管集群是偶数节点,还是奇数节点,不管是增加节点,还是移除节点,新旧配置的“大多数”都会存在重叠(图中的橙色节点)。
需要你注意的是在分区错误、节点故障等情况下如果我们并发执行单节点变更那么就可能出现一次单节点变更尚未完成新的单节点变更又在执行导致集群出现2个领导者的情况。
如果你遇到这种情况可以在领导者启动时创建一个NO\_OP日志项也就是空日志项只有当领导者将NO\_OP日志项应用后再执行成员变更请求。这个解决办法你记住就可以了可以自己在课后试着研究下。具体的实现可参考Hashicorp Raft的源码也就是runLeader()函数中:
```
noop := &logFuture{
log: Log{
Type: LogNoop,
},
}
r.dispatchLogs([]*logFuture{noop})
```
当然有的同学会好奇“联合共识”在我看来因为它难以实现很少被Raft实现采用。比如除了Logcabin外未见到其他常用Raft实现采用了它所以这里我就不多说了。如果你有兴趣可以自己去阅读论文加深了解。
## 内容小结
以上就是本节课的全部内容了,本节课我主要带你了解了成员变更的问题和单节点变更的方法,我希望你明确这样几个重点。
1. 成员变更的问题主要在于进行成员变更时可能存在新旧配置的2个“大多数”导致集群中同时出现两个领导者破坏了Raft的领导者的唯一性原则影响了集群的稳定运行。
2. 单节点变更是利用“一次变更一个节点不会同时存在旧配置和新配置2个大多数”的特性实现成员变更。
3. 因为联合共识实现起来复杂不好实现所以绝大多数Raft算法的实现采用的都是单节点变更的方法比如Etcd、Hashicorp Raft。其中Hashicorp Raft单节点变更的实现是由Raft算法的作者迭戈·安加罗Diego Ongaro设计的很有参考价值。
除此之外考虑到本节课是Raft算法的最后一讲所以在这里我想多说几句帮助你更好地理解Raft算法。
有很多同学把Raft当成一致性算法其实Raft不是一致性算法而是共识算法是一个Multi-Paxos算法实现的是如何就一系列值达成共识。并且Raft能容忍少数节点的故障。虽然Raft算法能实现强一致性也就是线性一致性Linearizability但需要客户端协议的配合。在实际场景中我们一般需要根据场景特点在一致性强度和实现复杂度之间进行权衡。比如Consul实现了三种一致性模型。
* default客户端访问领导者节点执行读操作领导者确认自己处于稳定状态时在leader leasing时间内返回本地数据给客户端否则返回错误给客户端。在这种情况下客户端是可能读到旧数据的比如此时发生了网络分区错误新领导者已经更新过数据但因为网络故障旧领导者未更新数据也未退位仍处于稳定状态。
* consistent客户端访问领导者节点执行读操作领导者在和大多数节点确认自己仍是领导者之后返回本地数据给客户端否则返回错误给客户端。在这种情况下客户端读到的都是最新数据。
* stale从任意节点读数据不局限于领导者节点客户端可能会读到旧数据。
一般而言在实际工程中Consul的consistent就够用了可以不用线性一致性只要能保证写操作完成后每次读都能读到最新值就可以了。比如为了实现幂等操作我们使用一个编号(ID)来唯一标记一个操作并使用一个状态字段nil/done来标记操作是否已经执行那么只要我们能保证设置了ID对应状态值为done后能立即和一直读到最新状态值就可以了也就通过防止操作的重复执行实现了幂等性。
总的来说Raft算法能很好地处理绝大部分场景的一致性问题我推荐你在设计分布式系统时优先考虑Raft算法当Raft算法不能满足现有场景需求时再去调研其他共识算法。
比如我负责过多个QQ后台的海量服务分布式系统其中配置中心、名字服务以及时序数据库的META节点采用了Raft算法。在设计时序数据库的DATA节点一致性时基于水平扩展、性能和数据完整性等考虑就没采用Raft算法而是采用了Quorum NWR、失败重传、反熵等机制。这样安排不仅满足了业务的需求还通过尽可能采用最终一致性方案的方式实现系统的高性能降低了成本。
## 课堂思考
在最后我给你留了一个思考题强领导者模型会限制集群的写性能那你想想看有什么办法能突破Raft集群的写性能瓶颈呢欢迎在留言区分享你的看法与我一同讨论。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。