其实整个项目中一个最主要的看点就是选举算法,而这部分也是逻辑最复杂最难理解的部分。不一样的实如今不一样的场景下的策略也不尽相同,并且场景很是之多。接下来咱们一块儿来看一下Cocklebur的实现思路。node
一个问题摆在咱们面前:集群启动后,如何选举出一个主节点,其他都是从节点?实际上用状态描述去理解这个问题就是,集群启动后每一个节点都是LOOKING(选举进行时)状态,如何经过节点间有限次的通讯最终使得集群中有且仅有一个节点为LEADERING(主节点)状态,其他都是FOLLOWING(从节点)状态。算法
图1 经过一种方法是集群的每一个节点达成一致网络
咱们暂且抛开Paxos算法,咱们只考虑如何让进群全部节点达成一致,确切的说是活着的节点。你可能会回忆小时候小伙伴们一块儿愉快的玩耍时,你们都会一致的推荐某个小伙伴为头儿当领队,缘由就是他最高。其实这个选举过程当中默认了一些条件:选举过程不会失败(选举一次性成功)、每一个节点彼此都快速通讯(不会有人上厕所)、没有节点会忽然挂掉(不要往太坏了想~多是他妈叫他回家吃饭)。也就是说,一次选举过程很是简单,你们彼此看看,谁个子高天然就会当领队了。分布式
可是分布式环境中远没有这么简单,资源有限(节点不是随便相互看几遍就OK的,看一眼就消耗很多资源)、网络延迟(至关于在选举的时候有人上厕所了)、节点崩溃(他妈叫他回家吃饭了),任何状况都有可能发生。咱们的目标是尽可能快速的让集群中每一个可对外服务的节点(通讯正常的节点)都有最新的数据,从而总体对外提供服务。spa
再来看看Paxos的角色。目前在集群中每一个节点均可能会成为Leader,因此每一个节点都既是提案者(proposeor)也是接受者(acceptor)。固然他们也是Learner,因为acceptor自己是知道选举结果的,那么就不须要再通知Learner了。设计
名词解释code
问题已经引出,那么咱们来看看Cocklebur的类Paxos是如何实现的。首先咱们须要来了解一下算法中涉及的一些概念:blog
名称递归 |
变量名seo |
描述 |
数据版本号 |
Xid |
这是该节点所持有数据的版本号,版本号越高,那么数据越新。对于选举过程来讲,它是该节点的固有属性。就像上面例子中的身高,是评判标准之一。 |
主机名称 |
my_host_name |
节点的主机名或者ip,网络中的惟一标识。 |
当前状态 |
cur_mode |
LOOKING(正在选举,尚未找到Leader)、FOLLOWING(已经找到Leader的从节点)、LEADERING(已经找到Leader的主节点,就是它本身)。 |
当前推荐节点 |
cur_rec_host |
对于LOOKING阶段,是该节点当前所知道xid最大的那个主机名,对于其余状态下的节点就是Leader主机名。 |
已交换表决器节点列表 |
heard_from |
通讯过的主机名称集合。 |
已知节点列表 |
known_hosts |
与heard_from不一样,known_hosts每每要更大一点,它存放了heard_from的递归列表。好比,1跟2交换表决器,此时2跟4交换过,1跟3交换过,那么1的known_hosts即为2、4,而2的known_hosts为1、3。这种机制也是节点选举加速的关键点之一。 |
表决器 |
Voter |
实际上就是节点间通讯所传递的内容:rec_host、my_host_name、known_hosts、cur_mode、xid、logical_clock。 |
逻辑时钟 |
logical_clock |
每一次选举都会有一个逻辑时钟,若是某个节点得知本次选举失败,则逻辑时钟加1。逻辑时钟的本质是标识了该节点状态信息的版本,若是一个节点因为延迟发现当前的选举早已经不是以前那次选举了,那么它当前的状态信息也就做废了,因此要清空状态以后提升逻辑时钟,直到与当前选举的时钟匹配。 |
状态信息 |
host_stat |
上面除了表决器以外全部信息都属于状态信息。 |
多数派 |
majority |
超过集群节点总数一半的最小整数个节点,好比3的多数派数目是2,4的多数派数目是3。 |
ACK锁 |
Ack_lock |
在算法第二阶段,知足Leader条件的节点会争取其余节点的承诺,保证其余节点再也不接受表决器交换,以便顺利成为Leader,那么这些“准Follower”若是以为没问题就会把本身锁住,保证再也不交换表决器。不熟悉的同窗参看该系列博客的第一篇:理解Paxos |
注:有人为集群若是某些节点死掉了多数派怎么保证?答:永远用集群锁配置的节点的个数来决定。也就是说配置一旦生效,多数派数目就已经肯定,若是一个集群中存活的数目不超过半数,那么该集群永远不会完成选举。其实这样作的好处就是最大限度的保证集群数据的可靠性,虽然这是算法所要求的,可是咱们只要保证一个多数派可服务就能最大限度的保证这其中存在最新数据,由于Cocklebur在写数据时也是超过一半才算成功的。
算法实现
对于实现过程可能会有一些疑问,对于交换表决器这种行为必定是要作rpc调用的。那么Client和Server应该分别怎么去实现呢?其实对于选举算法咱们更关注Client端的实现逻辑,Server只是去接收一下表决器而已,在更新本身的状态信息(host_stat)时注意同步问题便可。那么咱们主要来看看Client端的选举逻辑是怎么实现的。
源码中,主要算法逻辑在CockLeaderElection::lookForLeader() 中。
过程1 选举初始化 获取多数派个数; 获取集群主机列表; 释放ACK锁; 重置host_stat; 初始化交换表决器的通讯客户端; 逻辑时钟++;
过程2 选举过程 While (自身状态为LOOKING && 主机列表不为空) Do 遍历主机列表(不包含该节点) 当前的目标节点为cur_task_node // 第二阶段--被ACK锁定的逻辑 If 自身被ACK_LOCK锁定 Then
等待cur_rec_host 发送成为Leader的消息; if 在超时时间内发现cur_rec_host成为Leader Then break;//此时cur_mode已经为Following,因此while直接结束 Else if 等待超时 Then 重置host_stat//由于以前的候选人不知道干吗去了 解ACK_LOCK; break; //继续跟其余节点交换,由于此时依然为Looking,因此while将继续 End if End if 生成表决器;//实际上就是读取自当前的host_stat // 交换表决器的逻辑 If 该节点没有对目标节点(cur_task_node)加锁成功 then 与cur_task_node交换表决器;// 主要没加锁成功,那就说明没有赢得该节点,就须要换票。 If 拿到目标主机的表决器发现rev_host为空 Then continue;//交换失败,多是宕机或目标主机被别的节点加锁了。 End if If 目标主机的逻辑时钟比本身的要大 Then 本次选举失败;break; End if 更新host_stat; 生成表决器; If 在交换表决器以后发现目标主机是Leadering或者Following Then 该节点则会设置自身状态为Following,而且把rec_host设置为它所读取到的Leader。 Break; //完成选举 End if End if // 得知一个多数派信息以后的逻辑 // 即该节点已经获知了其中一个多数派全部主机信息,也就是说该节点有条件决定谁是Leader了。(注:此时有人会问,了解一个多数派并不能全面了解整个集群,万一该多数派以外有节点还有更新的信息怎么办?缘由见“问题一”。) If known_hosts.size() >= num_major // 搜集信息达到了多数派,若是没有达到怎么办?见“问题二” Then If cur_rec_host == my_host_name //说明该节点是这个多数派的Leader候选者 Then 开始逐个遍历known_hosts去询问本身是否能够当Leader。同时计数机去记录确定答复的数量; If 收到的ack许诺达到了多数派的要求 then 修改状态为Leadering,并向全部多数派成员发送本身当Leader的消息; Break;//选举结束; Else then Continue;//继续去和其余的成员交换表决器(为什么不停下来呢?问题三) End if End if End if
End while
另外须要补充更新host_stat时,咱们的rec_host选择算法是先比较Xid较大的,若是Xid同样大则选择字典序较大的。
问题一:缘由就是,咱们也不能肯定该多数派以外的其余主机必定就有更(四声)新数据,并且也不能肯定其余主机通讯是否正常。若是你放不下其余主机,意味着要去重试屡次其余主机,这样的策略并不适合在线应用,何况这种状况不容易发生,由于上面咱们阐述为什么任意多数派都能保证拥有一份最新数据。其实总结一点就是见好就收,达到了多数派立刻就开始张罗选出Leader。另外,即便这个多数派没有最新数据,那么在集群稳定后,后续加入的Follower若是有最新咱们也会加以数据同步处理。
问题二:若是收集了一个多数派信息以后,该主机发现本身不是候选者,它的行为以后是如何的呢?答案是他依然会与其余人交换表决器。由于发现本身不是候选人不表明以后的某个时刻就没机会了,极可能那个候选者宕机了,那么其他的主机中说不定那台就有机会了。因此观机待变。
问题三:从算法上看,一个候选者是十分激进的,当他发现本身没有获得预期的ACK锁许诺,那么他会直接去与其余节点去交换表决器。之因此这样,是为了把以前锁住的主机代价尽可能减小到最小。由于若是该候选者失败了,那么以前被ACK_LOCK锁住的主机在一段时间内不能接受任何表决器。只能等待超时才能解锁。
总结
整个Cocklebur的选举流程就结束了。有些行为须要特别指出:1、选举过程当中只要知足多数派,候选者就开始提议作Leader,这个过程不会等待。因此这就会致使选举出来的那个Leader可能不是拥有最新数据且最大字典序的那一个。为了保证数据一致性,数据同步工做在Leader集群稳定以后进行(这个在后续章节讲述)。2、若是一个LOOKING状态的主机获得的表决器状态为LEADERING或者FOLLOWING,那么他将尝试加入到这个稳定集群之中。这有利于为Cocklebur集群动态加入节点,也保证了多数派以后能够归入那些延迟的节点。3、知足多数派以后,候选者第一个给本身加ACK_LOCK,可是在候选者为多数派其余成员加锁这么短暂的过程当中,可能某些多数派成员的cur_rec_host被改变,那么候选者极可能功亏一篑。实际上可能性最大的就是真的存在一个字典序更大的主机在候选者加锁以前与多数派成员交换了表决器。因此这里将作一些小小的改进,那就是:若是一个主机成为了多数派成员,那么它更新host_stat时将不受字典序影响,除非有更大的Xid。4、再次追问:若是多数派成员此时接到了更大的Xid怎么办?那么候选者在向其加ACK_LOCK时必定会失败,它极可能会加入了一个新的多数派,而其他已经被加锁的成员则会等待锁超时。其实这里存在一个更加严重的问题,那就是若是集群中刚好已经挂掉几台机器,而拥有最大Xid的那台没法造成本身的多数派,(咳咳,这里说不清楚,我立刻举个例子说明此问题!):1已经被加ACK_LOCK,2已经被4改变了主意,3 是候选者,4是拥有最大xid那个 5已经挂掉。OK,咱们来看看这样一种窘态,3准备Lead1,2时被4阻挠,由于4有最大的Xid,而4只能与2交换表决器,1,3都被加锁。那么3若是发现本身候选失败时就会去找4,那么4就得知2,3能够加锁。因此4,2,3将会组成集群,因此1就会很迟的加入集群。若是节点更多,那么意味着更多像1这样的节点不能快速加入,因此ACK锁的超时时间的设定就是个关键,过短了对于候选者延迟是个挑战,太长了对于上面状况是个挑战。
其实,面对分布式问题时,咱们的策略应该更多的去考虑你所在的具体的应用场景。在该场景下,去综合考虑全部问题发生的几率。而程序的实现也可以更加的灵活,经过参数的配置去适应这些事件发生的几率。Cocklebur的某些地方设计的并很差,可是经过剖析整个流程咱们能够很清楚的摸清楚Paxos在实际场景中的面貌。若是实践与理解相结合,咱们能够打磨出更加好用的服务组件。