之前因为想试一试GSOC,所以看了看Casbin-Mesh的代码,这是基于Raft的一个分布式Casbin应用。这个MIT6.824里的RaftKV很类似,所以正好借此机会写下这篇博客。

实验相关

Lab03的内容是在Raft基础上构建一个分布式KV服务。我们需要实现此服务的Server和Client。

RaftKV的结构和各个模块的交互如图所示:

image-20220429211429808

相比于上个实验难度低了不少,实现上可以参考这篇大佬的实现,我就不多写了。

Raft 相关

接下来说说Raft中有关客户端交互有关的内容。

路由与线性化语义

想要在Raft之上构建允许客户端访问的服务,首先要解决路由线性化语义的问题。

路由

Raft是一个Strong Leader的共识算法,读写请求一般都需要通过Leader执行。客户端反问Raft集群时,一般会随机访问集群中一个节点,如果它不是Leader, 那么它会将保存的leader信息返回给客户端,客户端会将请求重定向到Leader节点重试。

线性化语义

此为,目前的Raft只支持At Least Once的语义,对于客户端的一次请求,Raft状态机可能会应用多次命令,而这样的语义特别不适用于基于共识的系统。

为了实现线性化语义,很显然,我们需要让发出的请求实现幂等。

一个基本的思路是客户端给每个请求分配UID, 而服务端利用这个UID维护一个Session,对成功请求的Response进行缓存。当有重复的请求到达服务端时,它可以直接利用Session缓存的Response相应,这样就实现了请求幂等。

当然这带来了Session管理的问题,但这个并非本文重点。

只读优化

解决了上面两个问题之后,我们得到了一个可用的基于Raft的服务。

但我们会发现,无论是读或是写请求,我们的应用都需要经过Leader 发起一次AppendEntries的通信,在收到了Quorum成功的ACK,以及附加的落盘操作,在Log Committed再之后才能将结果返回给客户端。

写操作会改变数据状态机,所以对于写请求这些是必要的。但读操作并不会改变状态机,我们可以对只读请求进行一些优化,让只读请求绕过Raft日志,以便减少同步写操作带来的磁盘IO开销。

问题在于,如果没有其他的措施,绕过Raft日志的只读查询结果可能是过时的。

比如,旧集群Leader和一个选出新Leader的集群发生了分区,此时客户端在旧Leader上的查询结果可能会过时。

Raft论文中提到了Read IndexLease Read两种方法来绕过Raft日志,优化只读请求。

Read Index

Read Index方案需要解决几个问题:

  • 旧任期遗留的已提交的日志

如old leader提交Log后,没来的及发送心跳就崩溃了。此时其他节点选中为新Leader,但根据Raft论文,新leader并不会主动提交旧leader时的日志。

为了解决这个问题,我们需要在新Leader当选后提交一个no-op日志,将旧Log提交。

  • CommitIndex和AppliedIndex的间隔

引入readIndex变量,领导者将当前commitIndex保存在局部变量readIndx,以此作为查询时AppliedIndex的界限,当只读请求到来时,需要先将Log应用到readIndex记录的位置,之后Leader才能查询状态机,提供读服务。

  • 在提供只读服务时候保证Leader不发生切换

为了解决这个问题,我们在收到读请求后,Leader会先进行心跳,并需要收到Quorum数量的Ack,保证在此时不存在其他任期更大的Leader,保证readIndex是集群中的最大提交索引。

具体的流程以及Batch和Follower Read的优化可以参考Raft作者的博士论文,在此不再赘述。

Lease Read

Read Index的方案其实只优化了磁盘IO的开销,它依旧需要进行一轮集群的网络通信。但实际上,这部分开销也是可以进行优化的,于是就有了Lease Read的方案。

Lease Read方案的核心思路是利用一次Leader Election至少需要经过一轮ElectionTimeout时间。在此期间,系统不会进行重新选举。这样就避免了提供只读服务时Leader切换的问题。所以我们可以利用时钟优化网络IO。

实现

在实现上,为了让时钟代替网络信息交互,我们需要额外提供一种租约机制。一旦Quorum数量的集群认可了领导者的Heartbeat,Leader可以认为在ElectionTimeout期间没有其他人能成为Leader,它可以相应的延长其租约。但Leader持有租约时,它可以直接服务只读查询而不需要额外的网络通信。

但其实服务器中间可能会存在时钟偏移,这样Follower就无法保证在Leader持有租约时不会超时。这就引出了Lease Read的关键设计:用什么策略延长租期呢?

论文中,假设$ClokcDrift$是有界的,每次心跳成功更新租约时,租约延长到$start + \frac{ElectionTimeout}{ClockDriftBound}$ 。

$ClokcDriftBound$代表了集群时钟漂移的界限,但是这个界限的发现和维护十分困难,因为导致时钟漂移的原因有很多,并且具有实时性。

如GC,虚拟机调度,云服务机器扩缩容等

实践上,一般会牺牲一定的安全性来换取LeaseRead的性能。一般使用$StatrTime +ElectionTimeout - \Delta{t}$来延长租期。$\Delta{t}$是一个正数,这就使得每次延长租约的时间小于ElectionTimeout,在网络IO开销和安全性之间Trade Off。

总结

Raft构建服务时,首先需要设计好访问服务以及路由和幂等机制。

对于只读操作,优化手段主要有两种,Read IndexLease Read。其中前者优化了读操作时的磁盘IO,后者利用时钟优化了网络IO。

参考

Implimetation doc

Raft Paper

MIT6.824 Official

Consensus: Bridging Theory and Practice - zh

Tikv lease-read