读取
Raft用于实现一个线性一致性的分布式KV系统,线性一致性通常来讲是,当我们在t时刻写入了一个值,那么在t后,我们一定能读到这个值,并且不会读到t时刻之前的值。
现在讨论读取如何实现线性一致性
Raft log read
由于Raft本身的特性,可以直接利用Raft log获取值。每次读取的时候,相当于添加一个新的Raft log,等log被commit后,再在apply的时候从状态机读取需要的值,因为Raft log被提交后是遵守线性一致的,那么查询对应的log提交后从之前apply的值获取答案自然是满足线性一致性的。
但是显然这样比较低效,每次查询都需要经过一次Raft log的流程,因此不是一个常用的方法。
优化
对于Raft,节点存在3个状态,leader/follower/candidate。任何Raft的写入操作都需要写主,然后做replication到majority节点上,才会认为写入是成功的。
所以自然可以认为,如果t时刻的写入已经被apply了,那么读主自然能读到t时刻写入的数据。
那么问题转化为了两个
- 如何确定当前是读主?
- 如何保证
t时刻写入被apply了?
以下有两种方法
ReadIndex Read
大致方法是,维护当前apply的最大index,以及通过心跳而非empty log去和follower交互。
- 将当前自己的
commit index记录到一个local变量ReadIndex中 - 向所有节点发送心跳,如果
majority确认是主,那么当前节点是主 leader等待apply的index超过ReadIndex- 执行
read请求,返回给客户端
这种方法的好处在于不需要通过Raft log进行通信,只需要通过轻量的心跳通信。
不过这里有一个隐式的要求,需要leader的commit index是最新的,因此在leader选主成功后,需要向所有其他节点发送一个空raft log同步index。
Lease Read
虽然ReadIndex的效率已经提升了很多,但是由于还是要发送心跳,有一定性能损耗。
Raft论文中采用了一种方式,即通过时钟给leader一个租约,即在发送心跳确认当前节点为leader后,在租约内不需要心跳仍然可以判断当前节点为leader。
这是因为当前leader选主成功后,剩下的节点至少在election timeout后才会进行选主,那么在election timeout这段时间内,当前leader不会变。
虽然这样提高了效率,但是依赖了节点的时钟,如果时钟出现偏差则该方法存在问题。
对于TiKV中,lease read的实现不是通过心跳,而是通过write操作。因为write肯定强制写主,因此每次write都会走一遍raft log的流程。因此记录下write的开始时间start,如果当前write成功地被apply了,那么就续期租约。
Prevote
在实现Raft中,可以发现一种现象,如果Raft出现了网络分区,那么在分区内会疯狂地增加term进行选举,经过一段时间后,分区里节点的term会异常的高。
根据Raft协议,如果一个节点的term比较高,那么原先的leader发送心跳则会被赶下leader,触发选举,但是显然处于分区中,节点的log是落后的,自然不会选举成功。这样显然会导致一小段时间不可用。虽然时间很短,但也会造成波动。
因此这里实现了PreVote这个特性,在进行真正的选举之前,先进行一次PreVote,流程大概如下
-
进入
PreVote,先不增加任期号。 -
发送
PreVote请求,类似普通log请求termcandidateIdlastlogIndexlastlogTerm
-
等待回应
-
接收到
PreVote请求后,节点不会增加自己的Term,而是像普通的投票一样,检查是否需要投票,然后返回投票结果。 -
收到
PreVote结果后,有以下选择- 获得了多数投票,将状态从
follower变成candidate,然后开始投票流程。 - 未获得,所有状态不变,重置计时器。
- 获得了多数投票,将状态从
这样的好处有
- 防止
Term无限增长 - 减少不必要的选举
- 提高集群稳定性,使得一个
leader不会因为抖动而下台