可能是你能找到的最完整最详细的中文版的Raft算法说明博客
内容来源都是原生的论文,可以保证内容的可靠性,并且对论文里面的很多细节做了扩展说明
客户端、服务端交互的完整流程:
- Raft的客户端,发送的所有的请求都会转给 leader 。(读请求和写请求)
- 当客户端第一次启动的时候,它会随机挑选一个服务器进行通信。如果客户端第一次挑选的服务器不是 leader ,那么该服务器会拒绝客户端的请求并且提供关于它最近接收到的领导人的信息(AppendEntries 请求包含了 leader 的网络地址)。
- 如果 leader 已经崩溃了,客户端请求就会超时;客户端之后会再次随机挑选服务器进行重试。
个人理解:
所以客户端的交互可以优化:客户端在第一次和leader交互的时候,要记住leader的地址,这样第二次交互的时候,直接绕过follower找leader,提升效率
客户端的交互模式体现了Raft算法的CP属性,强调一致性和牺牲可用性,意味着可能在leader选举期间,该集群不可用,但是可用状态下集群的操作,都是半数节点认可的操作。
客户端超时重试机制
我们 Raft 的目标是要实现线性化语义(每一次操作立即执行,只执行一次,在它的调用和回复之间)。但是,如上述,Raft 可能执行同一条命令多次:例如,如果 leader 在提交了该日志条目之后,响应客户端之前崩溃了,那么客户端会和新的 leader 重试这条指令,导致这条命令被再次执行。解决方案就是客户端对于每一条指令都赋予一个唯一的序列号。然后,状态机跟踪每个客户端已经处理的最新的序列号以及相关联的回复。如果接收到一条指令,该指令的序列号已经被执行过了,就立即返回结果,而不重新执行该请求。
个人理解:
为了实现这个机制,需要同时在客户端和服务端做一些额外的操作,来避免重复执行。
客户端每次发起一个request,会生成一个RequestRaftClient,而且创建一个RequestHashMap来维护所有的新创建而未完全执行的命令:
并且会定时遍历,check requestTime,如果超时就会重置requestTime并且重新发送一次命令,但是requestID不能变,也就是重试。
一旦接受到服务端的Response,会从RequestHashMap中删除这个request
RequestRaftClient
1. requestID
2. requestData
3. requestTime
4. repeatTime
服务器中的每个Node也会存储一样的数据结构,并且会缓存一段时间,而且会持久化,RequestHashMap。
收到request的时候,持久化,requestID和result状态为 0,当处于操作中的状态时,服务器收到同一个requestID,不会创建新的request,而是继续等待结果
如果result为1或者2,就可以直接反馈,这样可以实现幂等性的重试,也避免了未响应客户端而宕机,
- 一种可能是网络丢失,leader并没有挂,只是前一个response丢失了,那么可以直接再反馈一次执行结果给client即可,如果还在执行,leader也不会发起重试,会等待操作结果。
- 一种可能是leader挂了,新leader也会通过选举完的第一次heartBeat更新最新状态,从而避免重复操纵。因为每个节点都保存了这个request消息
RequestRaftServer
requestID:
Result:0(操作中),1(操作成功),2(操作失败)
Raft的强一致性
只读的操作可以直接处理而不需要记录日志。但是,如果不采取任何其他措施,这么做可能会有返回过时数据(stale data)的风险,因为 leader 响应客户端请求时可能已经被新的 leader 替代了,但是它还不知道自己已经不是最新的 leader 了。线性化的读操作肯定不会返回过时数据,Raft 需要使用两个额外的预防措施来在不使用日志的情况下保证这一点。
首先,leader 必须有关于哪些日志条目被提交了的最新信息。
Leader 完整性特性保证了 leader 一定拥有所有已经被提交的日志条目,但是在它任期开始的时候,它可能不知道哪些是已经被提交的。为了知道这些信息,它需要在它的任期里提交一个日志条目。Raft 通过让 leader 在任期开始的时候提交一个空的没有任何操作的日志条目到日志中来处理该问题。
第二,leader 在处理只读请求之前必须检查自己是否已经被替代了(如果一个更新的 leader 被选举出来了,它的信息就是过时的了)。Raft 通过让 leader 在响应只读请求之前,先和集群中的过半节点交换一次心跳信息来处理该问题。
Raft的消息丢失分析
Raft集群中,client发起过的消息不会丢失,这依赖于客户端(幂等性重试)和服务端的共同设计
只要是服务端过半节点复制成功了该命令,那么即使leader挂了,该命令仍然会被新leader执行
复制节点数未过半数的命令,仍然有可能会被新leader继续执行,但是有可能会丢失
客户端通过幂等性的重试机制,既可以保证命令不会被重复执行,也可以实现未半数复制成功而被新leader丢弃的命令仍然会执行
复制过半的日志命令为什么一定会被新leader执行?
- 因为如果老leader把日志复制过半了,那么新leader一定也有这条日志,否则就无法赢得选举,因为他的日志条数一定要是最新最多的。
- 新leader上任时,会发起一个特别的AppendEntries RPC,这不同于心跳消息,也不同于一般的日志消息,他会发送一个命令为空的log,半数节点复制成功后,就会commit这个空的log,以及commit这个log之前的所有log,包括那个未commit的老命令。所以无论老leader遗留的log是否commit,最终新leader都会完成使命,提交掉。
- 复制节点数未过半数的命令,仍然有可能会被新leader继续执行,但是有可能会丢失?
因为未复制过半,所以仍然有可能不包含这条消息的节点,也就是日志最多的节点未必会赢得选举。
比如5个节点的集群中 S1 leader挂了, S2 里面有2条日志(S1只复制到了S2,未复制到S3 S4 S5), S3 S4 S5 只有1条日志,但是S5第一个发起选举,并且 S3 S4都投票给他,那么S5选举成功了,会命令S2丢弃第二条日志
这个时候要保证这个客户端发起的命令不会丢失,就依赖于客户端的重试机制了,一定时间内服务端因为丢弃了这个request,客户端发起重试,那么S5继续添加 第二条日志,最终S2 S3 S4都会复制并且完成响应
- 幂等性的request消息,参考客户端交互