有问题可以留言,咋们一起成长(*๓´╰╯`๓)
当第一次理解raft协议的基本原理之后, 给我的第一印象就是raft能够保证多个副本的日志(状态机)强一致.这种强一致性就相当于你开启副本了同步复制机制, 只要系统已经提交客户端的请求,那么接下来客户端向任何一个副本发起读请求都能够读到之前提交的数据.但是不同于简单粗暴的同步复制方案,分布式系统的可用性将降低至(P为一个副本的失效概率,N为分布式系统中的副本个数).
即使我认识到了raft这么强的一致性,但是还是忽视了一个细节陈述:raft保证提交的状态机最终会被应用.对于TiKV或者RheaKV,最终状态机会被应用到rocksdb中.最终这词似乎在弱化raft的一致性,似乎在向最终一致性靠拢.不过可以放心,raft不是最终一致性协议,它就是强一致性,因为它保证了“当日志被提交后,那么在该瞬间可以在N+1个副本上找到该日志条目”, 只不过这个日志条目当前可能还没有被应用到rocksdb上而已,而该应用操作会发生在未来的某个时刻. 因此如果你希望看到最新提交点rocksdb的状态, 那么只需要综合计算已经提交但还为被应用的日志条目和rocksdb数据就好了. 而raft的线性一致性读的原理就是这么一回事情.
为了更好的理解raft的线性一致性读,先来认识一下什么是线性一致性.博客[1]对线性一致性已经有了一个比较完整描述.线性一致性是一种很强的一致性语义了,它强调操作时间的因果关系, 即使两个操作在逻辑上是完全独立的.即使在博客[1]中, clientC读取的不是x,而是y, 但是也要保证如果clientC读取x,也是发现x=2.
那么raft是如何实现线性一致性的呢? 博客[1,2,3]的解释比较可以理解.总结一下:
1、就是我们之前提到的“综合计算已经提交但还为被应用的日志条目和rocksdb数据”.而raft的综合计算方式是等待提交点CommitIndex之前的日志条目都被应用到rocksdb中, 然后向rocksdb发起读请求;
2、CommitIndex作为逻辑时间,将请求在时间线上进行排序;
以上就是raft的线性一致性读的核心思想.不过这里还需要考虑网络分区的问题.raft是能够保证在网络分区发生时仍然能够正常工作, 这里的正常工作是其中一个分区能够正常接受并复制日志条目,而另一个分区将不能复制日志条目.当然这并不意味者后者不能再接受请求,因为该分区的leader仍然会认为自己是leader,并接收读请求, 当处理的读请求时,两个分区的提交点就会不同,导致线性一致性读被破坏. 因此在ReadIndex方案中,leader会通过心跳rpc来证明在当前提交点下,自己仍然是leader. 不过心跳增加了网络开销,另一中LeaseRead方案就通过租期来保证在该租期内自己的leader身份不会被替换,因此在该租期内的一致性读就不需要像ReadIndex方案一样,通过心跳rpc来证明自己的leader身份.当然这需要不同副本对租期的长短有一个相对一致的认知, 因为租期通过选举超时计算得到.如果不同副本服务器的时钟严重频率不一致,即存在严重的漂移,那么它们对选举超时很难保持一致.
参考文献
[1] https://pingcap.com/blog-cn/linearizability-and-raft/
[2]https://www.sofastack.tech/blog/sofa-jraft-linear-consistent-read-implementation/
[3]https://qtozeng.top/2019/01/15/etcd-raft-ReadIndex-%E7%BA%BF%E6%80%A7%E4%B8%80%E8%87%B4%E6%80%A7%E8%AF%BB%E6%BA%90%E7%A0%81%E7%AE%80%E6%9E%90/