使用磁盘锁的形式,保证集群中只能有一个Leader获取磁盘锁,对外提供服务,避免数据错乱发生。但是,也会存在一个问题,若该Leader节点宕机,则不能主动释放锁,那么其他的Follower就永远获取不了共享资源。于是有人在HA中设计了"智能"锁。正在服务的一方只有在发现心跳线全部断开(察觉不到对端)时才启用磁盘锁。平时就不上锁了。
仲裁机制。比如提供一个参考的IP地址,心跳机制断开时,节点各自ping一下参考IP,如果ping不通,那么表示该节点网络已经出现问题,则该节点需要自行退出争抢资源,释放占有的共享资源,将服务的提供功能让给功能更全面的节点。
过半原则:根据鸽巢原理,raft中任意一个操作都需要过半的服务器的认同,这样能保证始终只有一个leader。此外,服务器通常选择奇数台机器部署,这样可以用较少的机器实现相同的集群容忍度。
快速领导者选举算法:在选举的过程中进行过半验证,这样不需要等待所有server都认同,速度比较快。
Lab3 KV-raft在此,从一个比lab2更高层次的角度看待分布式系统。lab2中的raft是用于机器之间互相沟通形成一致的log和state,但机器之间并不关心log中存储的command是什么,因此全部使用interface{}作为command的接口。lab3中,我们要实现的是client调用Get()、Append()、Put(),server通过raft达成集群内的一致,然后将raft apply的command正式执行。raft系统在这一过程中,只起到了一致性的作用,是命令的被调用和真正执行之间的一层。
这里需要注意的是线性一致性,为了实现这一点,给command递增的index(由raft调用start后返回),使用一个map记录每个client最近最后一个被执行command的index以及执行结果,由此可以推测出command序列执行到哪一条了,防止重复执行。
另外,由于raft系统在start和apply之间需要一定的时间,因此,客户端调用读写函数,读写函数调用start通知raft集群之后,注册一个index对应的待相应result channel,存储在以index为key的map中。当raft系统达成一致,apply这条命令的时候,从apply函数调用真正的读写过程,执行结果push到index对应的channel中。于是,客户端调用的读写函数只需要直接去result channel中取出这条命令的执行结果。这样做非常的简洁流畅,用channel阻塞的时间来等待raft系统一致、apply执行读写。
关于start和apply之间leader被更换的讨论:
一条command,在其start和apply中间,可能raft系统已经更换了leader,对于新的leader来说,它没有为这条command创建channel(start不是通过新leader进行的),试图将result放入channel的时候会失败,导致直接返回。然而,旧leader虽然降级为follower,但仍然会对这条apply,因此即使更换leader也没关系,但需要注意的是,从channel取出result的时候,就不必判断这个机器是不是leader了,只要在start的时候判断了就可以了。
关于command的index是否会发生变化的讨论:
command的index是由start调用的时候,leader的log中当前log最后一条entry的index+1决定的顺序index,如果这条command的entry被覆盖,那就会超时,client将更换server重新执行,如果没有被覆盖,将会保持这个index。
如果command的entry被覆盖了,且这个index对应的map中仍然有channel在等待答案(发生于leader降级,被新的leader清除了index对应位置,并且没有覆盖,leader又当选为leader,并建立了新的index位置),那么将会发生不匹配,因此,应该在从channel中取出result的时候检查op是否是在等待的那个。
如果op正好与在等待的那个一致,但是seq又不是那个呢?没有关系,只要执行内容一致就可以了。client中等待之后那条op结果的timer会超时,重新执行之后那条op。
B部分是压缩,kv中有一个变量maxraftState限制了log的长度,若即将超过这个长度,就对log进行压缩。同时,kv的data和peocessed也应该被持久化存储。
此外,LAB3可以使用init函数完成logger注册,并记录。当然,这不是必需的。
关于golang中的init函数