<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Raft on NoneBack</title><link>https://noneback.github.io/zh/tags/raft/</link><description>Recent content in Raft on NoneBack created by</description><generator>Hugo -- gohugo.io</generator><language>zh</language><copyright>@NoneBack All rights reserved</copyright><lastBuildDate>Fri, 15 Apr 2022 10:49:57 +0800</lastBuildDate><atom:link href="https://noneback.github.io/zh/tags/raft/index.xml" rel="self" type="application/rss+xml"/><item><title>MIT6.824-RaftKV</title><link>https://noneback.github.io/zh/blog/zh/mit6.824-raftkv/</link><pubDate>Fri, 15 Apr 2022 10:49:57 +0800</pubDate><guid>https://noneback.github.io/zh/blog/zh/mit6.824-raftkv/</guid><description>&lt;p&gt;之前因为想试一试GSOC，所以看了看Casbin-Mesh的代码，这是基于Raft的一个分布式Casbin应用。这个MIT6.824里的RaftKV很类似，所以正好借此机会写下这篇博客。&lt;/p&gt;
&lt;h2 id="实验相关"&gt;实验相关&lt;/h2&gt;
&lt;p&gt;Lab03的内容是在Raft基础上构建一个分布式KV服务。我们需要实现此服务的Server和Client。&lt;/p&gt;
&lt;p&gt;RaftKV的结构和各个模块的交互如图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://s2.loli.net/2022/04/29/xuQMp28PRH7rheb.png" alt="image-20220429211429808" loading="lazy" decoding="async"&gt;
&lt;/p&gt;
&lt;p&gt;相比于上个实验难度低了不少，实现上可以参考这篇大佬的&lt;a href="https://github.com/OneSizeFitsQuorum/MIT6.824-2021/blob/master/docs/lab3.md"&gt;实现&lt;/a&gt;，我就不多写了。&lt;/p&gt;
&lt;h2 id="raft-相关"&gt;Raft 相关&lt;/h2&gt;
&lt;p&gt;接下来说说Raft中有关客户端交互有关的内容。&lt;/p&gt;
&lt;h3 id="路由与线性化语义"&gt;路由与线性化语义&lt;/h3&gt;
&lt;p&gt;想要在Raft之上构建允许客户端访问的服务，首先要解决&lt;strong&gt;路由&lt;/strong&gt;和&lt;strong&gt;线性化语义&lt;/strong&gt;的问题。&lt;/p&gt;
&lt;h4 id="路由"&gt;路由&lt;/h4&gt;
&lt;p&gt;Raft是一个&lt;strong&gt;Strong Leader&lt;/strong&gt;的共识算法，读写请求一般都需要通过Leader执行。客户端反问Raft集群时，一般会随机访问集群中一个节点，如果它不是Leader, 那么它会将保存的leader信息返回给客户端，客户端会将请求重定向到Leader节点重试。&lt;/p&gt;
&lt;h4 id="线性化语义"&gt;线性化语义&lt;/h4&gt;
&lt;p&gt;此为，目前的Raft只支持&lt;strong&gt;At Least Once&lt;/strong&gt;的语义，对于客户端的一次请求，Raft状态机可能会应用多次命令，而这样的语义特别不适用于基于共识的系统。&lt;/p&gt;
&lt;p&gt;为了实现线性化语义，很显然，我们需要让发出的请求实现幂等。&lt;/p&gt;
&lt;p&gt;一个基本的思路是客户端给每个请求分配UID, 而服务端利用这个&lt;code&gt;UID&lt;/code&gt;维护一个Session,对成功请求的Response进行缓存。当有重复的请求到达服务端时，它可以直接利用Session缓存的Response相应，这样就实现了请求幂等。&lt;/p&gt;
&lt;p&gt;当然这带来了Session管理的问题，但这个并非本文重点。&lt;/p&gt;
&lt;h3 id="只读优化"&gt;只读优化&lt;/h3&gt;
&lt;p&gt;解决了上面两个问题之后，我们得到了一个可用的基于Raft的服务。&lt;/p&gt;
&lt;p&gt;但我们会发现，无论是读或是写请求，我们的应用都需要经过Leader 发起一次&lt;code&gt;AppendEntries&lt;/code&gt;的通信，在收到了Quorum成功的ACK，以及附加的落盘操作，在Log Committed再之后才能将结果返回给客户端。&lt;/p&gt;
&lt;p&gt;写操作会改变数据状态机，所以对于写请求这些是必要的。但读操作并不会改变状态机，我们可以对只读请求进行一些优化，让只读请求绕过Raft日志，以便减少同步写操作带来的磁盘IO开销。&lt;/p&gt;
&lt;p&gt;问题在于，如果没有其他的措施，绕过Raft日志的只读查询结果可能是过时的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;比如，旧集群Leader和一个选出新Leader的集群发生了分区，此时客户端在旧Leader上的查询结果可能会过时。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Raft论文中提到了&lt;strong&gt;Read Index&lt;/strong&gt;和&lt;strong&gt;Lease Read&lt;/strong&gt;两种方法来绕过Raft日志，优化只读请求。&lt;/p&gt;
&lt;h4 id="read-index"&gt;Read Index&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Read Index&lt;/strong&gt;方案需要解决几个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;旧任期遗留的已提交的日志&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;如old leader提交Log后，没来的及发送心跳就崩溃了。此时其他节点选中为新Leader，但根据Raft论文，新leader并不会主动提交旧leader时的日志。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，我们需要在新Leader当选后提交一个no-op日志，将旧Log提交。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;CommitIndex和AppliedIndex的间隔&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;引入&lt;code&gt;readIndex&lt;/code&gt;变量，领导者将当前&lt;code&gt;commitIndex&lt;/code&gt;保存在局部变量&lt;code&gt;readIndx&lt;/code&gt;，以此作为查询时AppliedIndex的界限，当只读请求到来时，需要先将Log应用到&lt;code&gt;readIndex&lt;/code&gt;记录的位置，之后Leader才能查询状态机，提供读服务。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;在提供只读服务时候保证Leader不发生切换&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;为了解决这个问题，我们在收到读请求后，Leader会先进行心跳，并需要收到Quorum数量的Ack，保证在此时不存在其他任期更大的Leader，保证&lt;code&gt;readIndex&lt;/code&gt;是集群中的最大提交索引。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;具体的流程以及Batch和Follower Read的优化可以参考Raft作者的博士论文，在此不再赘述。&lt;/p&gt;
&lt;h4 id="lease-read"&gt;Lease Read&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Read Index&lt;/strong&gt;的方案其实只优化了磁盘IO的开销，它依旧需要进行一轮集群的网络通信。但实际上，这部分开销也是可以进行优化的，于是就有了&lt;strong&gt;Lease Read&lt;/strong&gt;的方案。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lease Read&lt;/strong&gt;方案的&lt;strong&gt;核心思路&lt;/strong&gt;是利用一次Leader Election至少需要经过一轮ElectionTimeout时间。在此期间，系统不会进行重新选举。这样就避免了提供只读服务时Leader切换的问题。所以我们可以利用时钟优化网络IO。&lt;/p&gt;
&lt;h5 id="实现"&gt;实现&lt;/h5&gt;
&lt;p&gt;在实现上，为了让时钟代替网络信息交互，我们需要额外提供一种租约机制。一旦Quorum数量的集群认可了领导者的&lt;code&gt;Heartbeat&lt;/code&gt;，Leader可以认为在&lt;code&gt;ElectionTimeout&lt;/code&gt;期间没有其他人能成为Leader，它可以相应的延长其租约。但Leader持有租约时，它可以直接服务只读查询而不需要额外的网络通信。&lt;/p&gt;
&lt;p&gt;但其实服务器中间可能会存在&lt;strong&gt;时钟偏移&lt;/strong&gt;，这样Follower就无法保证在Leader持有租约时不会超时。这就引出了&lt;code&gt;Lease Read&lt;/code&gt;的关键设计：&lt;strong&gt;用什么策略延长租期呢？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;论文中，假设$ClokcDrift$是有界的，每次心跳成功更新租约时，租约延长到$start + \frac{ElectionTimeout}{ClockDriftBound}$ 。&lt;/p&gt;
&lt;p&gt;$ClokcDriftBound$代表了集群时钟漂移的界限，但是这个界限的发现和维护十分困难，因为导致时钟漂移的原因有很多，并且具有实时性。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如GC，虚拟机调度，云服务机器扩缩容等&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;实践上，一般会牺牲一定的安全性来换取&lt;code&gt;LeaseRead&lt;/code&gt;的性能。一般使用$StatrTime +ElectionTimeout - \Delta{t}$来延长租期。$\Delta{t}$是一个正数，这就使得每次延长租约的时间小于&lt;code&gt;ElectionTimeout&lt;/code&gt;，在网络IO开销和安全性之间Trade Off。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;Raft构建服务时，首先需要设计好访问服务以及路由和幂等机制。&lt;/p&gt;
&lt;p&gt;对于只读操作，优化手段主要有两种，&lt;strong&gt;Read Index&lt;/strong&gt; 和 &lt;strong&gt;Lease Read&lt;/strong&gt;。其中前者优化了读操作时的磁盘IO，后者利用时钟优化了网络IO。&lt;/p&gt;
&lt;h2 id="参考"&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/OneSizeFitsQuorum/MIT6.824-2021/blob/master/docs/lab3.md"&gt;Implimetation doc&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://pdos.csail.mit.edu/6.824/papers/raft-extended.pdf"&gt;Raft Paper&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://pdos.csail.mit.edu/6.824/index.html"&gt;MIT6.824 Official&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/OneSizeFitsQuorum/raft-thesis-zh_cn"&gt;Consensus: Bridging Theory and Practice - zh&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://pingcap.com/zh/blog/lease-read"&gt;Tikv lease-read&lt;/a&gt;&lt;/p&gt;</description></item></channel></rss>