4.集群状态机(集群元数据)
为了保证搜索引擎正常的分布搜索和分布式存储能力,集群中的元数据便必须得是一致且安全的。而集群状态机就是指该集群状态。
该集群状态保存有以下信息:
当前主节点Id
各节点信息,包括节点id、地址、状态、是否是MasterNode等等
分片路由,这个将会在分片模块重点阐述
任期term
其他集群信息,如集群名称等等
通常情况下,集群状态机会维护在内存中一个数据结构中。
每隔一段时间会讲该状态机持久化到硬盘中,便于宕机后快速恢复。
每个节点都会有一份集群元数据,便于对web层发来的请求做出决策。
5.tranlog机制
在raft算法中,存储集群状态的并不是刚才讲的集群状态机,而是日志。这是因为在分布式场景中,如果要做到分布式一致性以及较少的传输损耗,追加写入的日志将会是分布式共识的不二之选。
参考raft算法,我们也是根据其设计了相应的机制
5.1 tranlog数据结构
tranlog真实含义是操作日志,也就是状态机变更的操作,也就是说我们需要归纳出设计集群状态变更的几种操作
关键字段包括:
id 日志id,自增
term 日志任期
operation 操作指令
object 操作对象
committed 日志是否提交
5.2 确保tranlog的分布式一致
简单来说,该方案设计采用了二阶段提交的思路,不过根据自己的实现对其做了优化
第一阶段(prepared):
客户端向Leader发起节点变更请求/自身节点变动
自身增加一条对应的tranlog
向所有节点发送AppendEntry RPC调用
各个节点将会判断term是否一致以及该日志的前一条日志id是否和自身节点的当前日志id一致,如果不一致则拒绝,Leader会重新发送一个sendHeart RPC来同步日志,如果成功则返回true,否则返回false;如果一致,就在当前节点中增加一条日志,返回是否成功
Leader节点判断日志同步成功的MasterNode节点数是否达到了“大多数”,如果是则提交该日志到状态机,给客户端返回成功响应;否则返回失败响应
第二阶段(commit):
PS:这个和原来raft算法的不太一样
确认阶段由心跳RPC去进行日志同步的操作,具体步骤如下:
心跳RPC调用,发送最新提交的日志id和前一个节点的id
Follower判断自身日志是否一致,如果存在该日志但未提交,节点会提交该日志到状态机中
如果不一致,则返回自身最新提交日志id和最新增加的日志id
Leader开始日志同步,将差异数据传输给节点
如果认真阅读上述设计方案,可能会有以下几个疑问:
为什么只计算MaterNode节点作为大多数判断?
因为我们要保证的大多数节点更新后,最新的日志总占大多数。
而这点就是为了确保新一轮选举,选出的Leader一定会是日志完整性最高的节点(不给日志完整性低的节点投票)。而参与竞选的正是MasterNode,所以我们只需对MasterNode计数即可。
如何判断日志一致?
判断一致是根据最新提交日志id和最新增加的日志id的二元组来决定的,两者都一致则意味着日志是一致的
为什么日志复制时要验证term?
为了保证自身节点日志完整度小于投票的节点
提交状态如何保证大多数?
上述机制能保证——如果主节点提交,那么大多数节点则有该日志。但是该机制无法保证选出来的领导者能保证提交日志的大多数。
试想这么一种情况:Leader在提交日志时,突然宕机,那么新选出来的领导者是有该日志的,但是没有提交,可目前已经有节点进行了提交,这肯定是不行。
领导者选举能保证新领导者一定包含这条committed的日志项,但这条日志项在新领导者上处于未提交状态,这时新领导者,需要尝试从最新日志项开始,向其他节点复制日志,如果,某条uncommitted的日志项,被发现已经成功复制到大多数节点上,或者发现该日志已经被提交,这时这条日志项将处于提交状态,并被应用到状态机,通知其他节点提交这条日志。
但还是有一种极其特殊的情况:假设有A B C D E五个节点,A B C成功复制了最新的日志项,leader节点A提交了并返回成功,B C未提交,这时leader节点A挂了,选举出新leader节点B,这时候只能发现2个节点具备某条uncommitted的日志项,不满足大多数。这样尽管Leader提交了,但是其他节点并没有提交同时也满足不了大多数条件,所以在新的Leader看来该日志没有被提交。
而当Leader节点恢复时发现最新的日志是提交状态,所以在日志恢复时,需要做一步额外的验证——检查自己最新的日志是否提交,如果提交则需检查该日志是否在集群也提交了(如果该日志在集群中已经不存在了,则直接复制Leader的state并持久化)
5.3 tranlog加载到集群状态机
在日志提交时,我们需要将tranlog加载到集群状态机中,来影响真正的集群元数据。
要注意的是删除节点的操作有些需要注意的地方:
非Master节点直接移除出cluster状态,Master节点则采取假删的策略,修改节点状态为Dead
这样的好处就是依然会对Master节点发送心跳,而不至于一时的网络延时导致其中一个MasterNode开始无限竞选(其中一个节点因为网络延时开始竞选,但此时其他节点又接受到Leader的日志更新,根据不给日志完整性低的节点投票的原则,网络时延的那个节点会不停选举,但始终不能竞选成功,而集群状态又失去了该节点联系,所以会导致该节点丢失)
不同的指令意味着不同操作,在此就不赘述了。
5.4 tranlog持久化机制——保证crash-safe能力
考虑到极端情况,当日志复制成功后,如果节点宕机,则会丢失tranlog,这样就不能保证大多数节点拥有完整性最高的日志。
所以在日志复制时会直接将增加的日志进行持久化,追加写入到日志文件当中。
一些优化
为了节省资源,这里只有MasterNode才会持久化日志,因为非MasterNode不会参与竞选,他们的日志恢复强依赖于Leader节点
当发现日志差异过大时,这意味着一项项复制日志性能损耗太大,此时会选择直接复制集群元数据并持久化的方式来进行同步
当集群状态机进行持久化之后,那么这之前的日志都可以不必保存,之前写入的文件便可以删除。之后的日志可以追加写入一个新的文件
6.自动发现机制(初始选举问题)
以上采用raft算法解决的集群间的分布式共识问题,但是并没有解决初始的选举问题。换句话讲,上述集群选举机制都是在各个节点彼此知道各自的存在的情况下进行的,但是在真实情况下,各个节点一开始是不知道彼此的存在的,那么如何解决这个问题呢?
这里借鉴了ES的设计思路,引入种子节点的概念。
什么是种子节点?
种子节点可以在配置文件中配置,它是一系列ip地址的集合。
要注意,种子节点必须要保证其可用,不然种子节点挂了就无法进行自动节点发现了
自动发现机制
在节点上线时,会先从种子节点获取当前的集群状态。
如果当前有Leader节点,则会向Leader节点发送加入的RPC。
如果当前没有Leader节点,MasterNode则会开始竞选流程,向其他节点发起拉票RPC,而拉票的对象就包括种子节点;非Master节点则会轮询种子节点,直到当前集群成功出现了一个Leader。
三、具体实现
这里暂时不进行讲解了,详情请看GoDance代码仓库中的cluster模块。
该模块在陆续完善中,未来将实现一个真正可以运行的分布式搜索引擎,敬请期待。
总结
说实话实现raft算法真的有点复杂,尤其是细节部分。在网上搜集的各种资料质量参差不齐,看论文又太煎熬。很多细节都靠猜,靠自己尝试去补全细节,但很多时候自己去尝试推演发现有问题,于是又是各种搜索…最终也算实现了raft算法,自己的推演也终于跑通,之后应该会再写一篇,总结一下这个过程中遇到问题和一些自己的思考。
愿我们以梦为马,不负人生韶华!
与君共勉!