<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Consensus on NoneBack</title><link>https://noneback.github.io/zh/tags/consensus/</link><description>Recent content in Consensus on NoneBack created by</description><generator>Hugo -- gohugo.io</generator><language>zh</language><copyright>@NoneBack All rights reserved</copyright><lastBuildDate>Mon, 21 Feb 2022 01:26:46 +0800</lastBuildDate><atom:link href="https://noneback.github.io/zh/tags/consensus/index.xml" rel="self" type="application/rss+xml"/><item><title>MIT6.824-Raft</title><link>https://noneback.github.io/zh/blog/zh/mit6.824-raft/</link><pubDate>Mon, 21 Feb 2022 01:26:46 +0800</pubDate><guid>https://noneback.github.io/zh/blog/zh/mit6.824-raft/</guid><description>&lt;p&gt;这个寒假可算把搁置许久的Lab02给做完了。之前一直被卡在Test 2B的一个case里，寒假时候重新看看大佬们的实现思路，可算是完成了所有内容，于是简单记录一下。&lt;/p&gt;
&lt;h2 id="算法简介"&gt;算法简介&lt;/h2&gt;
&lt;p&gt;共识算法的基础是复制状态机，即&lt;strong&gt;按照相同顺序执行相同的确定性指令最终必然达到一致状态&lt;/strong&gt;。Raft是一种代替Paxos的分布式共识算法，相比Paxos更利于学习与理解。&lt;/p&gt;
&lt;p&gt;Raft算法核心内容可以分为三个部分： $Leader Election + Log Replication + Satety$ 。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://s2.loli.net/2022/02/19/9mGfndCtDHzMqe4.png" alt="img" loading="lazy" decoding="async"&gt;
&lt;/p&gt;
&lt;p&gt;集群机器初始为Follower，一旦一定时间内未接收到来自Leader的心跳，机器将成为Candidate并触发选举，请求剩下Follower投票。获得半数以上选票的Candidate成为Leader。&lt;/p&gt;
&lt;p&gt;Raft是一种&lt;strong&gt;强领导人&lt;/strong&gt;的强一致性的分布式共识算法，它使用Term作为逻辑时钟，一个任期中只能有领导人。领导人需要周期性发送心跳以维护其地位，同时需要处理&lt;strong&gt;复制提交&lt;/strong&gt;日志。&lt;/p&gt;
&lt;p&gt;复制日志时，Leader首先将日志复制到其他Follower上，直到半数以上的Follower成功复制，Leader才会提交此日志。&lt;/p&gt;
&lt;p&gt;安全性主要有五个部分，与实现相关的最核心的内容我认为有两个。一个是领导人只追加原则，不允许修改已提交的日志；另一个是选举安全性，避免脑裂问题，同时保证新Leader拥有比较新的日志。&lt;/p&gt;
&lt;p&gt;剩下的其他内容请参考论文原文。&lt;/p&gt;
&lt;h2 id="实现思路"&gt;实现思路&lt;/h2&gt;
&lt;p&gt;实现的思路大体上是参考了一篇大佬的博文（见参考），算法的细节很多也在原Paper的Figure2中，故而以下只讲一下实现各个功能时需要注意的地方。&lt;/p&gt;
&lt;h3 id="领导人选举"&gt;领导人选举&lt;/h3&gt;
&lt;h4 id="发起选举选举结果处理"&gt;发起选举+选举结果处理&lt;/h4&gt;
&lt;p&gt;发起选举是会开启多个goroutine后台发送RPC请求到其他结点，所以处理RPC response的时候需要确定当前结点为Candidate，以及请求未过期，即&lt;code&gt;rf.state == Candidate &amp;amp;&amp;amp; req.Term == rf.currentTerm&lt;/code&gt;。选举成功需要立即发送心跳，通知其他结点选举结果。&lt;/p&gt;
&lt;p&gt;如果发现失败的Response&lt;code&gt;resp.Term &amp;gt; rf.currentTerm&lt;/code&gt;,此时需要切换到Follower状态，更新任期，并&lt;strong&gt;重置投票信息&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;实际上一旦更新了任期，就需要重置投票信息。如果不重置votedFor信息，会有一些测试通过不了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id="请求投票rpc"&gt;请求投票RPC&lt;/h4&gt;
&lt;p&gt;前置逻辑过滤过期&lt;code&gt;req.Term &amp;lt; rf.currentTerm&lt;/code&gt;以及当前任期的重复投票请求。之后再按照算法描述的逻辑处理，注意如果成功投票，需要重置选举计时器。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 grant 投票时才重置选举超时时间，这样有助于网络不稳定条件下选主的 liveness 问题&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id="状态切换"&gt;状态切换&lt;/h4&gt;
&lt;p&gt;注意在切换角色时处理不同的计时器状态(stop or reset)，切换到Leader时需要重置matchIndex以及nextIndex的值。&lt;/p&gt;
&lt;h3 id="日志复制"&gt;日志复制&lt;/h3&gt;
&lt;p&gt;Raft算法的核心，需要注意的地方最多。&lt;/p&gt;
&lt;p&gt;我的实现是使用多个replicator和applier线程异步复制和apply的方式。&lt;/p&gt;
&lt;h4 id="日志复制rpc"&gt;日志复制RPC&lt;/h4&gt;
&lt;p&gt;前置逻辑过滤掉&lt;code&gt;req.Term &amp;lt; rf.currentTerm&lt;/code&gt;过期的请求。之后处理日志不一致以及日志被压缩以及重复日志的情况，之后复制日志再处理&lt;code&gt;commitIndex&lt;/code&gt;。&lt;/p&gt;
&lt;h4 id="发起日志复制请求结果处理"&gt;发起日志复制+请求结果处理&lt;/h4&gt;
&lt;p&gt;发起日志复制需要判断是直接复制日志或者发送快照。&lt;/p&gt;
&lt;p&gt;请求结果处理重点是如何处理&lt;code&gt;matchIndex&lt;/code&gt;和&lt;code&gt;nextIndex&lt;/code&gt;以及&lt;code&gt;commitIndex&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;matchIndex&lt;/code&gt;用来记录其他节点成功复制的最新日志，&lt;code&gt;nextIndex&lt;/code&gt;是记录发送给其他节点的下一个日志。&lt;code&gt;commitIndex&lt;/code&gt;通过排序&lt;code&gt;matchIndex&lt;/code&gt;来更新。再决定是否需要触发applier更新&lt;code&gt;appliedIndex&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;请求失败则可以回退nextIndex或者切换到Follower状态。&lt;/p&gt;
&lt;h4 id="异步apply"&gt;异步Apply&lt;/h4&gt;
&lt;p&gt;实际上就是一个后台goroutine，通过条件变量控制，使用Channel通信。每次触发会把&lt;code&gt;log[lastApplied:commitIndex]&lt;/code&gt;发送给上层，并更新&lt;code&gt;appliedIndex&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id="持久化"&gt;持久化&lt;/h3&gt;
&lt;p&gt;在需要持久化状态的属性更新时及时的刷盘。&lt;/p&gt;
&lt;h3 id="安装快照"&gt;安装快照&lt;/h3&gt;
&lt;p&gt;主要就是Leader触发的Snapshot以及RPC。应用Snapshot的时候需要先判断其新旧以及更新&lt;code&gt;log[0]&lt;/code&gt;和&lt;code&gt;appliedIndex&lt;/code&gt;以及&lt;code&gt;commitIndex&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="坑"&gt;坑&lt;/h2&gt;
&lt;h3 id="defer"&gt;Defer&lt;/h3&gt;
&lt;p&gt;首先是Golang的&lt;strong&gt;defer&lt;/strong&gt;关键字。我比较喜欢在RPC开头使用defer关键字直接打印出结点的一些数据：&lt;code&gt;defer Dprintf(&amp;quot;%+v&amp;quot;, raft.currentTerm)&lt;/code&gt;，这样在调用结束时能打印出log，但实际上，在运行到defer这一行的代码时，打印的内容已经固定。正确的使用方式应该是&lt;code&gt;defer func(ID int) { Dprintf(&amp;quot;%+v&amp;quot;, id) }()&lt;/code&gt;&lt;/p&gt;
&lt;h3 id="log-dummy-header"&gt;Log Dummy Header&lt;/h3&gt;
&lt;p&gt;Log处最好预留一个位置用于存放快照保存的Index和Term，不然Snapshot那部分的重构很痛苦。&lt;/p&gt;
&lt;h3 id="lock"&gt;Lock&lt;/h3&gt;
&lt;p&gt;参看guidance的用锁建议。使用一个大锁，而不是用多个锁。算法的正确性比性能重要。在发送RPC以及使用Channel时不要加锁，不然可能会超时。&lt;/p&gt;
&lt;h2 id="参考"&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://zh.wikipedia.org/wiki/Raft"&gt;Raft wikepedia&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://raft.github.io/"&gt;Raft Official Website&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/MIT6.824-2021/blob/master/docs/lab2.md"&gt;Potato’s Implimentation Doc&lt;/a&gt;&lt;/p&gt;</description></item></channel></rss>