适合阅读本文的人群
- 对Raft协议感兴趣的人
- 看过一些Raft相关介绍的博文,但是看不懂的(比如我)
很多Raft相关的分析文章,或者是著名的《In Search of an Understandable Consensus Algorithm》 论文,都会把下面的一张图放出来:
再介绍选主
、日志复制
、状态机
等一系列概念,恕我直言,真是看的人昏昏欲睡。也许有的表述确实看懂了,可正如古人所说,纸上得来终觉浅,要知此事需躬行。Raft协议跟我们日常写代码有什么关系,我们到底能不能用上它呢?
我个人觉得,看博文(看介绍)–> 理解Raft --> 理解ETCD是怎么使用Raft的 --> 把Raft应用到日常开发,这条学习路径未免太长太枯燥,不如换个思路,把这条路径反过来学习,或许更能坚持下去。
让我们先沉浸在日常的一个最普通的场景:
func main(){
var d HashSet
d[1] = 2
for (每个 k,v) in d{
print(k,v)
}
}
几行伪代码,作用也很简单,声明一个HashSet
,然后写入一个key、value,然后遍历这个Set
打印出来,一切都是那么的简单易懂。
func main(){
var d HashSet
{
// 线程1
d[1] = 2
}
{
// 线程2
d[1] = 3
}
for (每个 k,v) in d{
print(k,v)
}
}
但是人嘛,总是贪心不足的,总想尽可能压榨机器性能,于是有了线程的概念。上面的代码有基础的同学应该能看出来,线程1 和线程2 在并发修改HashSet
,必须加以控制,否则d[1]
有可能是2也有可能是3,加控制无非就是锁、并发安全的HashSet
等手段,虽然让代码变长且影响执行效率,但也还能忍受。
// 进程1
func main(){
var d HashSet
d[1] = 2
for (每个 k,v) in d{
print(k,v)
}
}
// 进程2
func main(){
var d HashSet
d[1] = 3
for (每个 k,v) in d{
print(k,v)
}
}
时间再到了现在,线程已经不能满足贪婪的人类了, 于是我们加了更多的机器,在不同的机器上跑同样的程序,来提高性能。这时问题又升级了,虽然大家的HashSet
都叫d,但显然,进程1上塞到HashSet
的数据,进程2是不知道的,反之亦然。
那怎么解决呢,显然,我们要引入一个第三方服务,把HashSet
放到它那里,而我们只要做个“老板”,对它发号施令就行了。它会保证:
- 只要数据被它接收且确认,就不会丢失
- 随时都可以读取以前的数据
老话说:鸡蛋不能放在同一个篮子里,所以我们还得要求这个第三方服务要把数据复制成多份,分开存储。所以,整个流程就变成了这样一个故事:
- 一天,作为老板的你,向自己的秘书长下达一个指示。
- 秘书长马上召集所有秘书开会,要求把领导指示记在自己的笔记本上,并深刻的讨论,最后大家达成共识,老板说的对!
- 秘书长把会议结果写成会议纪要。
- 秘书长把会议纪要发给你看,你看到自己的指示一字不差的出现在纪要中,欣慰的笑了。
好,现在在回头看这张图,对比之下很好理解了,Client就是老板,Consensus Module 就是秘书处(一个秘书头子和几个小秘书),Log就是秘书们的笔记本,State Machine就是会议纪要。那Raft协议在哪里呢? 不难理解,开会的整个流程就是Raft协议规定好的了。😁
Raft协议假定了所有秘书是安全可信的(非拜占庭容错条件),在此框架下,处理了秘书长的选举;秘书们离职和入职流程(节点变更);规定秘书们怎么在自己的笔记本上记录(RaftLog);规定了达成共识的条件(少数服从多数制);甚至规定了秘书们发生内斗(网络分区),老板应该相信那拨人是值得信任的(多数节点>少数节点)。
那么反过来,Raft不管什么呢?首先,秘书们的通信是不管的,相当于是秘书们其实是异地办公,作为“老板”,得给它们一人发一部手机(通信协议,ETCD中是用gRPC);其次,会议纪要的存储是不管的(ETCD中,用 boltdb 来存“会议纪要”)。
所以,Raft也许离我们的生活并不遥远,就像文章开头说的,从应用的角度出发,也许才能更好的理解它,也能更好的理解所有一致性算法(Raft、ZAB、Paxos)它们到底做了什么,解决的是什么问题。
文章中的比喻也许略有不妥,请原谅作者的才疏学浅,有错误欢迎指正👏🏻。