其实整个项目中一个最主要的看点就是选举算法,而这部分也是逻辑最复杂最难理解的部分。不同的实现在不同的场景下的策略也不尽相同,而且场景非常之多。接下来我们一起来看一下Cocklebur的实现思路。
一个问题摆在我们面前:集群启动后,如何选举出一个主节点,其余都是从节点?实际上用状态描述去理解这个问题就是,集群启动后每个节点都是LOOKING(选举进行时)状态,如何通过节点间有限次的通信最终使得集群中有且仅有一个节点为LEADERING(主节点)状态,其余都是FOLLOWING(从节点)状态。
图1 通过一种方法是集群的每个节点达成一致
我们暂且抛开Paxos算法,我们只考虑如何让进群所有节点达成一致,确切的说是活着的节点。你可能会回忆小时候小伙伴们一起愉快的玩耍时,大家都会一致的推荐某个小伙伴为头儿当领队,原因就是他最高。其实这个选举过程中默认了一些条件:选举过程不会失败(选举一次性成功)、每个节点彼此都快速通信(不会有人上厕所)、没有节点会突然挂掉(不要往太坏了想~可能是他妈叫他回家吃饭)。也就是说,一次选举过程非常简单,大家彼此看看,谁个子高自然就会当领队了。
但是分布式环境中远没有这么简单,资源有限(节点不是随便相互看几遍就OK的,看一眼就消耗不少资源)、网络延迟(相当于在选举的时候有人上厕所了)、节点崩溃(他妈叫他回家吃饭了),任何情况都有可能发生。我们的目标是尽量快速的让集群中每个可对外服务的节点(通信正常的节点)都有最新的数据,从而整体对外提供服务。
再来看看Paxos的角色。目前在集群中每个节点都可能会成为Leader,所以每个节点都既是提案者(proposeor)也是接受者(acceptor)。当然他们也是Learner,由于acceptor本身是知道选举结果的,那么就不需要再通知Learner了。
名词解释
问题已经引出,那么我们来看看Cocklebur的类Paxos是如何实现的。首先我们需要来了解一下算法中涉及的一些概念:
名称 |
变量名 |
描述 |
数据版本号 |
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的选举流程就结束了。有些行为需要特别指出:一、选举过程中只要满足多数派,候选者就开始提议做Leader,这个过程不会等待。所以这就会导致选举出来的那个Leader可能不是拥有最新数据且最大字典序的那一个。为了保证数据一致性,数据同步工作在Leader集群稳定之后进行(这个在后续章节讲述)。二、如果一个LOOKING状态的主机得到的表决器状态为LEADERING或者FOLLOWING,那么他将尝试加入到这个稳定集群之中。这有利于为Cocklebur集群动态加入节点,也保证了多数派之后可以纳入那些延迟的节点。三、满足多数派之后,候选者第一个给自己加ACK_LOCK,但是在候选者为多数派其他成员加锁这么短暂的过程中,可能某些多数派成员的cur_rec_host被改变,那么候选者很可能功亏一篑。实际上可能性最大的就是真的存在一个字典序更大的主机在候选者加锁之前与多数派成员交换了表决器。所以这里将做一些小小的改进,那就是:如果一个主机成为了多数派成员,那么它更新host_stat时将不受字典序影响,除非有更大的Xid。四、再次追问:如果多数派成员此时接到了更大的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在实际场景中的面貌。如果实践与理解相结合,我们可以打磨出更加好用的服务组件。