读取

Raft用于实现一个线性一致性的分布式KV系统,线性一致性通常来讲是,当我们在t时刻写入了一个值,那么在t后,我们一定能读到这个值,并且不会读到t时刻之前的值。

现在讨论读取如何实现线性一致性

Raft log read

由于Raft本身的特性,可以直接利用Raft log获取值。每次读取的时候,相当于添加一个新的Raft log,等logcommit后,再在apply的时候从状态机读取需要的值,因为Raft log被提交后是遵守线性一致的,那么查询对应的log提交后从之前apply的值获取答案自然是满足线性一致性的。

但是显然这样比较低效,每次查询都需要经过一次Raft log的流程,因此不是一个常用的方法。

优化

对于Raft,节点存在3个状态,leader/follower/candidate。任何Raft的写入操作都需要写主,然后做replicationmajority节点上,才会认为写入是成功的。

所以自然可以认为,如果t时刻的写入已经被apply了,那么读主自然能读到t时刻写入的数据。

那么问题转化为了两个

  • 如何确定当前是读主?
  • 如何保证t时刻写入被apply了?

以下有两种方法

ReadIndex Read

大致方法是,维护当前apply的最大index,以及通过心跳而非empty log去和follower交互。

  1. 将当前自己的commit index记录到一个local变量ReadIndex
  2. 向所有节点发送心跳,如果majority确认是主,那么当前节点是主
  3. leader等待applyindex超过ReadIndex
  4. 执行read请求,返回给客户端

这种方法的好处在于不需要通过Raft log进行通信,只需要通过轻量的心跳通信。

不过这里有一个隐式的要求,需要leadercommit 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,流程大概如下

  1. 进入PreVote,先不增加任期号。

  2. 发送PreVote请求,类似普通log请求

    1. term
    2. candidateId
    3. lastlogIndex
    4. lastlogTerm
  3. 等待回应

  4. 接收到PreVote请求后,节点不会增加自己的Term,而是像普通的投票一样,检查是否需要投票,然后返回投票结果。

  5. 收到PreVote结果后,有以下选择

    1. 获得了多数投票,将状态从follower变成candidate,然后开始投票流程。
    2. 未获得,所有状态不变,重置计时器。

这样的好处有

  1. 防止Term无限增长
  2. 减少不必要的选举
  3. 提高集群稳定性,使得一个leader不会因为抖动而下台