谈谈raft fig8 —— 迷惑的提交条件和选举条件
谈谈raft fig8 —— 迷惑的提交条件和选举条件
前言
这篇文章的思路其实在两个月前就已经成型了,但由于实习太累了,一直没来得及写出来。大概一个月前在群里和群友争论fig8的一些问题时,发现很多群友对fig 8是充满了迷惑的。我个人在做lab的时候也对fig 8的问题感到非常头疼,真正大概理解这个问题的来龙去脉(大概?)的时间大概是两个月前复习raft的时候,而此时距离我做完lab已经足足一年有余了......这个问题难于理解,其实raft的作者要至少背一部分锅的——2014年的论文里,对于这个问题的讨论不是很充足,很多该回扣的点并没有回扣到。我这里尝试按照我自己的理解,梳理一下fig 8问题的原因。如果你想要看懂这篇文章,请至少把raft的论文完整的读几遍,如果懂basic paxos再好不过了,因为理解了basic paxos对于理解fig 8的问题非常有帮助。
迷惑的提交条件
如果一个操作得到了提交,那么相当于系统给出了一个保证:只要是这个系统是能推进的,那么这个操作日志迟早会被apply。如果日志能够提交,这条日志必须要满足某些条件,在满足了所有的条件的瞬间,这条日志的状态就是已提交的,即使它的index高于commitIndex。
那么细化来说,这些条件是什么呢?对于数据库中的事务,提交条件是该事务所有的WAL Log均已落盘;对于Percolator式的分布式事务,提交条件是Primary成功提交;但是对于Raft中的日志呢?如果回顾论文你可能会比较惊讶,直到讨论fig 8的问题之前,作者似乎都回避了这个问题,只是一味的说,日志只要成功备份到了Majority上,就提交它。初次阅读论文时,我们潜意识中便隐隐约约的埋藏了这样一个先入为主的错误认知——只要日志备份到了Majority上,就可以提交它,连带着提交这条日志之前的所有日志。然后论文来到了5.4,作者开始打我们的脸,告诉了我们日志的真正提交条件:要么被连带提交,要么只能提交一条term == leader.term的日志。
读到这一部分,你只需要知道,在raft算法中,日志真正的提交条件为,要么被连带提交,要么只能提交一条term == leader.term的日志,就可以了。如果日志不满足这个提交条件,那么它被截断是合理的。
迷惑的选举条件
现在让我们先忘掉raft日志的提交条件,先梳理一下raft算法的部分基本规则:
- 采用逻辑时钟term,一个term内至多有一个leader,candidate之间通过选举算法竞选leader;
- 日志流是单向的,只允许日志从leader流向follower;
- 对于日志冲突的场景,直接截断follower不匹配的日志,替代以leader的日志;
综合2和3,我们不难发现,leader必须要拥有所有commitable的日志;根据2,leader是无法从其他机器上学习到它缺失的commitable的日志的,根据3,leader可能截断那些commitable的日志。而根据1我们可以知道,leader当选的充分必要条件为它在那个term内得到了Majority的选票。
现在让我们来审视选举算法。论文中定义了candidate与follower间比较日志新旧的方法:
Raft determines which of two logs is more up-to-date by comparing the index and term of the last entries in the logs. If the logs have last entries with different terms, then the log with the later term is more up-to-date. If the logs end with the same term, then whichever log is longer is more up-to-date.
比较最后一条日志,到底谁最up-to-date。如果candidate更加up-to-date,且其他更优先的条件均已满足(term不超过leader,尚未投出票等等),那么follower会投票给candidate。
算法看似很自然直观,但你会发现,这和我们之前所强调的,当选的leader必须拥有所有commitable的日志这一条件,似乎并不存在一个包含关系,假设存在一个Majority认定candidate的日志更加up-to-date,那个这个candidate真的拥有所有的commitable的日志吗?
既然我们提到了commitable的日志,那么肯定要讨论commitable的条件是什么。现在让我们成为那个当开始读论文的初学者,根据论文前文,理所应当的认定那个先入为主的条件吧(也就是说,令日志的commitable的条件为日志“已经备份到Majority上”),看一看fig 8的场景(终于到fig 8了,泪目):
在fig 8的c阶段,s1是term = 4的leader,它先将(index = 2, term = 2)的日志备份到了{s1, s2, s3}上。根据我们所认定的那个commitable条件,既然(index = 2, term = 2)的日志成功备份到了Majority上,那么这条日志就是commitable的,当前日志的commitable集合为{1, 2}。到了d阶段,s5的日志相比于{s2, s3, s4}来说都是more up-to-date的,因此它能得到多数派的投票,成为term 5的leader,但当它成为了term = 5的leader的那一刻它开始了背刺行为:它截断了index = 2的日志,而这条日志是commitable的。究其原因,是因为在d阶段中,commitable的日志集合为{1, 2},而s5并不持有2,因此s5并不持有全部的commitable的日志。
总的来说,在commitable条件设定为“备份到了Majority上”的场景下,我们的选举算法无法满足最核心的那个约束:当选的leader必须持有全部commitable的日志,这正是5.4 safty所讨论的安全性问题。
两个月前当终于想到了这些东西时,我的第一反应就是去谴责那个该死的选举算法,选leader这么重要的算法居然无法满足这么重要的约束条件,那么这个算法就未免太寒颤了。但我很快发现这还真的不是个我行我上的问题:在一轮拉票的过程中仅涉及到了candidate和follower,这两个server间的交互,一次交互你还能指望candidate得到多少的信息呢?而在完全异步的网络环境下,即使是能和一个Majority进行交流,又能做到多少的事情呢?
补日志or加强提交约束?
补日志
我们用精确的语言描述一下fig 8的错误场景:
在commitable的条件为“备份到了Majority上”的场景下,使用现存的选举算法,无法保证当选的leader持有所有commitable的日志,导致commitable的日志存在被截断的风险。
我们不妨先把思维发散一下:既然当选的leader不保证持有全部commitable的日志,那缺啥补啥呗!先把所有commitable的日志补全,再去ReplicateLogs,不就可以了吗?既然现在commitable的日志已经备份到了Majority上,那我只要和任意一个Majority取得联系就肯定能获得commitable的日志!诶等等这样的话选举算法的more-up-to-date貌似意义不大了,反正日志会补全,那保证单主就可以了吧!诶等等如果我们把这个联络Majority的过程普及化,任意一个index处的日志,都要先联络到Majority协商好这个index处写什么日志,再去写下这个日志,这样的话单主的约束也可以去掉,因为每个主都要先和Majority取得联系,那么它们必定会有交集,获悉到其他主想写的日志!
恭喜你,你已经进入了basic paxos的大门。basic paxos解决的是多个机器对单个Value的共识问题。basic paxos中,也存在一个全局单增的逻辑时钟proposeId。proposer们不能随便的提议Value,在提议Value之前,它们必须要和Majority取得联系,要求它们拒绝一切proposeId < myProposeId的提案,同时,如果Majority中有部分机器已经accept了一个Value(这个Value可能就是已提交的结果),这个proposer就要考虑自己是否需要将自己的提案改成那个Value。这个过程被称为basic paxos的Prepare阶段(basic paxos的第二个阶段为Accept阶段,这里不讨论它)。现在将Value改成单条日志,对单条日志进行Prepare阶段,如果这条日志已经被提交了,那么proposer就可以学到这条日志!对所有的日志均走一遍basic paxos流程,就可以保证所有的日志是一致的,这就是最朴素的multi paxos思想,实际的multi paxos方法会复杂很多,存在着大量的优化。
虽然提到了paxos,但你完全不必去担忧上述那段文字你无法理解。我这里仅仅是想表达一个观点,解决fig 8里的问题,方法不止一种,而raft采用了非常精彩的办法。在raft的一些变种中,也存在着融合paxos思想的方案,例如说PolarFS的Parallel Raft,不需要leader持有全部commitable的日志,只需要leader有最新的checkpoint就可以了,leader上任后,通过merge阶段补全所有日志,这个merge阶段其实就非常类似于paxos的perpare阶段。
不管怎样,先补全commitable的日志,再去ReplicateLogs是一个解决fig 8中所描述问题的方法。值得注意的是,这种方案下,日志commitable的条件仍然是“备份到Majority”上就可以了。如果有人问你,为什么paxos将日志备份到Majority上就可以提交,而raft这样做就要在fig 8上吃瘪,你就可以告诉他,因为paxos的proposer可以通过prepare阶段补全日志。关于paxos算法此处不再讨论,如果想要更详细的了解paxos算法务必阅读《paxos made simple》这篇论文。
加强提交约束
再次回顾一下fig 8中的错误场景:
在commitable的条件为“备份到了Majority上”的场景下,使用现存的选举算法,无法保证当选的leader持有所有commitable的日志。
raft的办法是,将commitbale的条件进行加强:日志要么被连带提交,要么只能提交term = leader.term的日志。如果leader能够提交一条自己term内的日志,毫无疑问,这条日志之前的所有日志均是一致的。
现在再看一下fig 8:
将commitable的条件强化为“日志要么被连带提交,要么只能提交term = leader.term的日志”之后,无论是d还是e,均是允许的,因为index = 2的日志并不满足commitable的条件,所以这条日志不会被c中的s1提交,客户也收不到ok的结果,会一直等待,所以在图d中截断它是允许的。但这个个例并不是重点,真正的重点是,在这样的commitable条件下,“leader必须持有全部的commitable的日志”这一约束条件是满足的!我个人尝试着做了一下证明,放在了文章的末尾。
nop entry的引入
原论文中提到了nop entry。raft作者建议任何leader在上台后,先不要急着响应客户请求生成操作日志,而是先提交一条自己term内的nop-entry,成功提交后,nop entry之前的所有日志就保持了主从一致了,此时再响应客户服务。
nop entry的引入其实还解决了一个更加棘手的问题:在提交条件被强化后,如果新的leader上台后,迟迟不生成新的日志,那么leader就无法提交那些它已经成功备份到Majority,但term < leader.term的日志。
尽管如此,我个人强烈不建议在6.824里引入nop。因为nop也是要占据logIndex的,后台的测试代码会对每个index的日志进行校验。nop并不是测试代码添加的,因此测试代码在匹配index时会报错。在6.824里,无需考虑也不能考虑leader无法显示提交日志导致进度得不到推进的场景。
总结
关于fig8的讨论就到这里了。我们再次梳理一下原论文中的一些比较坑的点。首先是fig 8中场景d的正确性。在commitable的条件为"备份到Majority上即可"时,a → b → c → d是错误的,因为c中index = 2的日志成功备份到了Majority上,commitable的集合为index = {1, 2},index = 2的日志不允许被截断。当commitable的条件为“日志要么被连带提交,要么只能提交term = leader.term的日志”时,a → b → c → d是正确的,因为在c中,index = 2 的日志term不等于s1的term,因此s1并不会提交它,所以到了d时,commitable的集合仍然是index = {1},所以d可以截断index = 2的日志。
为什么我们要强化提交条件?因为我们在raft算法的设定下,我们必须要保证leader必须持有全部的commitable的日志"这一条件,而选举算法无法保证这样的leader当选,为此,要么让leader在ReplicateLogs前补全所有commitable的日志,要么另辟蹊径。raft的解决办法是强化了提交条件,在这个提交条件下,选举算法所选出的leader必定持有全部的commitable的日志。
前文中我提到“raft作者要为fig 8难以理解背一部分锅”,也很大程度上是因为作者没有对fig 8出现的根本原因进行讨论,在提出加强提交条件后也没有用文墨去回扣到选举条件上。
关于提高约束条件后算法正确性的证明
证明算法正确,等价于证明将commitable的约束条件提升至"日志要么被连带提交,要么只能提交term = leader.term的日志",后,当选的leader必定持有全部的commitable的日志。可以用反证法,下文是我自己的证明,个人不保证证明是正确的,毕竟人菜瘾大(
反证法
(1) 令TL是最后一次保持约束条件且成功commit了日志的leader,TL的最后一条commitable的日志index为c_index,term为c_term,假设在TL之后的第一个错误leader为FL,即第一个不持有全部的commitable的日志,但仍然成功当选的leader。(TL.term, FL.term)间可能存在0个或者多个leader,这些leader进行过ReplicateLog,但没能成功commit任何新的日志,也正是因此,在这期间c_term和c_index是不变的。
(2) 根据假设可知FL ∉ Majority_1。根据TL的定义可知,集群中必定有一个Majority_1,它们都持有这些commitable的日志。令TL的最后一次提交时,TL的term为commit_term。根据当前的commitable条件,有commit_term == c_term。
(3) 根据投票算法我们可知,存在一个Majority_2认为FL的日志是more up-to-date的。这只有两种可能:要么FL.last_log.term == c_term,但index更高,要么虽然FL.last_log.index < c.index,但FL.last_log.term > c_term。第一种情景是不可能的,因为这样的机器只能出现在Majority_1中,这与我们的假设矛盾,
(4) 考虑第二种场景,如果FL.last_log.term == FL.term,那么这条日志是FL自己添加的,因此它不可能靠这个日志去赢得选举,这条日志只能是来自于之前term的leader。因此到了这里我们得到了一个恒等式:TL.term == commit_term == c_term < FL.last_log.term < FL.term。
(5) 根据这个不等式可知,存在一个before_leader,它是term为 FL.last_log.term时刻的leader,它给自己添加了一条/几条日志,还没来得及commit就step down了。根据(4) 中FL.last_log.term > c_term == commit_term == TL.term可知,before_leader 的当选时刻是在TL成功的commit之后;又根据我们对TL的定义可知,before_leader肯定是一个持有全部commitable日志的leader(但它并没有成功commit更多的日志,否则要将它归于TL),因此before_leader在生成日志(FL.last_log.term, FL.last_log.idx)时,必定是持有所有的commitable的日志的,且FL.last_log.index > c.index (新日志是append进来的)。对应的,既然FL.last_log之前的日志来自于before_leader,那么它们的日志在这之前应该一致,因此FL必须有这些commitable的日志,这与我们的假设相矛盾。综上所述,我们的假设均被否定,结论得证。
谈谈raft fig8 —— 迷惑的提交条件和选举条件的更多相关文章
- Raft协议实战之Redis Sentinel的选举Leader源码解析
这可能是我看过的写的最详细的关于redis 选举的文章了, 原文链接 Raft协议是用来解决分布式系统一致性问题的协议,在很长一段时间,Paxos被认为是解决分布式系统一致性的代名词.但是Paxos难 ...
- shell脚本进阶之条件测试与条件语句
接着上篇博客,今天整理一下关于条件测试和条件语句方面的知识. shell脚本进阶之条件测试 在编写shell脚本时,经常需要对一些条件进行判断,可以使用测试命令test辅助完成测试过程.t ...
- 在Bootstrap开发框架中使用bootstrapTable表格插件和jstree树形列表插件时候,对树列表条件和查询条件的处理
在我Boostrap框架中,很多地方需要使用bootstrapTable表格插件和jstree树形列表插件来共同构建一个比较常见的查询界面,bootstrapTable表格插件主要用来实现数据的分页和 ...
- PHPlaravel中从数据库中选择数据是增加时间条件及各种条件
注:附加条件后要加get函数. 1.public function getForDataTable($startTime,$endTime){ return $this->query() -&g ...
- yii2:多条件多where条件下碰到between时,between语句如何处理呢?
yii2:多条件多where条件下碰到between时,between语句如何处理呢? 我有一张表:id,name,telphone,ticket_no,status,create_time等字段, ...
- Excel_PoweQuery——条件计数、条件求和
岁月不居,时节如流. 时光荏苒,岁月如梭. 前面两段充分体现了博主深厚的文学素养,别和博主争,博主说啥就是啥. 其实,对于大量数据的处理,这几年微软Office做的不单单是2007的时候把Excel的 ...
- 支持向量机(SVM)必备概念(凸集和凸函数,凸优化问题,软间隔,核函数,拉格朗日乘子法,对偶问题,slater条件、KKT条件)
SVM目前被认为是最好的现成的分类器,SVM整个原理的推导过程也很是复杂啊,其中涉及到很多概念,如:凸集和凸函数,凸优化问题,软间隔,核函数,拉格朗日乘子法,对偶问题,slater条件.KKT条件还有 ...
- mybatis——逆向工程中 where (条件1)and (条件2 or 条件3 or 条件4)
where (条件1)and (条件2 or 条件3 or 条件4) = where (条件1 and 条件2)or (条件1 and 条件3) or (条件1 and 条件4) 结果 是这样的 WH ...
- 谈谈Raft
本文主要参考 极客时间-etcd 实战课 GitChat-分布式锁的最佳实践之:基于 Etcd 的分布式锁 谈到分布式协调组件,我们第一个想到的应该是大名鼎鼎的Zookeeper,像我们常用的Kafk ...
随机推荐
- 面试官:你的App卡顿过吗?你是如何监控的?
一.故事开始 面试官:平时开发中有遇到卡顿问题吗?你一般是如何监控的? 来面试的小伙:额...没有遇到过卡顿问题,我平时写的代码质量比较高,不会出现卡顿. 面试官:... 这回答似乎没啥问题,但是如果 ...
- Unsatisfied dependency expressed through field 'rabbitTemplate'错误总结
1. 在springboot中整合RabbitMq的过程中,遇到如下错误:(截取部分) org.springframework.beans.factory.UnsatisfiedDependencyE ...
- Linux命令(六)之防火墙iptables的相关操作以及端口的开放
.personSunflowerP { background: rgba(51, 153, 0, 0.66); border-bottom: 1px solid rgba(0, 102, 0, 1); ...
- SpringBoot开发十六-帖子详情
需求介绍 实现帖子详情,在帖子标题上增加访问详情页面的链接. 代码实现 开发流程: 首先在数据访问层新增一个方法 实现查看帖子的方法 业务层同理增加查询方法 最后在表现层处理查询请求 数据访问层增加根 ...
- 微信SDK的使用
一.导入依赖 <!--微信支付--> <dependency> <groupId>com.github.wxpay</groupId> <arti ...
- 一篇文章搞懂密码学基础及SSL/TLS协议
SSL协议是现代网络通信中重要的一环,它提供了传输层上的数据安全.为了方便大家的理解,本文将先从加密学的基础知识入手,然后展开对SSL协议原理.流程以及一些重要的特性的详解,最后会扩展介绍一下国密SS ...
- 安全工具推荐之w13scan篇
先上链接:https://github.com/w-digital-scanner/w13scan 这是一款漏洞发现工具,支持三大主流平台:windows.Linux.Mac 又一款完全免费开源的工具 ...
- 11-SpringCloud Hystrix
Hystrix简介 分布式系统面临的问题 复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败. 服务雪崩 多个微服务之间调用的时候,假设微服务A调用微服务B和微服务 ...
- noip34
因为改不动T3而来水博客的屑 昨晚没睡好,大致看了一遍题面后,选择了死亡231,然后就死的很惨. T1 一开始大致看题面的时候,就略了一眼,加上没读全题,啥思路也没有,最后四十分钟滚回来看了看,发现就 ...
- 关于 go-fastdfs-web 的SpringBoot 后台管理
1.问题的产生: 1.公司需要存储图片数据,采用Go语言的fastdfs,实现存储,我的职责就是部署,SpringBoot版本的管理平台. 2.当我看见代码之后我的内心是拒绝的,没有注释....... ...