目标
persistence state to disk each time it changed, but there won't persisted to disk, instead, it will save and restore persistent state from a Persister object.
Complete the functions persist() and readPersist() in raft.go by adding code to save and restore persistent state. You will need to encode (or "serialize") the state as an array of bytes in order to pass it to the Persister. Use the labgob encoder; see the comments in persist() and readPersist(). labgob is like Go's gob encoder but prints error messages if you try to encode structures with lower-case field names.
简单点说就是在进行一些操作后,要将数据进行持久化。通常的做法是持久化在磁盘中,课程中简化这一步,使用Persister对象的persist()和readPersist()进行持久化和读取数据。
编码实现
-
persist( ) 方法,将必要的数据进行备份,给了demo代码,思路很简单。
-
readPersist( ) 方法:在
make()方法中有调用了这个方法,假如之前进行过persist()操作,就读取之前备份的数据。
这两个方法的实现都很简单,难点在于2C的测试用例增加了很多场景,会暴露出之前很多没注意到的细节。 下面主要记录一下在2C中踩到的坑和优化的点:
注意点
选举限制
Raft 通过⽐较两份⽇志中最后⼀条⽇志条⽬的索引值和任期号定义谁的⽇志⽐较新。如果两份 ⽇志最后的条⽬的任期号不同,那么任期号⼤的⽇志更加新。如果两份⽇志最后的条⽬任期号 相同,那么⽇志⽐较⻓的那个就更加新。
就是说vote步骤时,选举出来的leader一定要是log最新的,最新意味的
-
最后一个log.term最新
-
当存在多个log.term相等时,选log.idx最大的。
通过这个机制一定可以保证commit过得日志不会丢失。
全量同步优化
leader进行全量同步时,需要进行一个小优化,不然无法通过TestReliableChurn2C测试用例。
原本的全量方案是这样的,当一个follower恢复后,可能索引很小,与leader相差上百,这意味需要上百次的心跳才能找到同步点,但是测试用例中只给了10s中的时间,所以会导致同步失败。
优化的方案是这样,不再向前-1的遍历,而是找到日志中,上一个term的位置作为下次同步是位置,这样可以大大减少所需的心跳次数。
同时还有一个地方可以注意下,就是leader发送心跳的间隔,课程要求中说一秒钟不能超过10次,意味着最小间隔为100ms,其实设为50ms也可以,如果发现同步的时间过长,导致测试失败,不妨调小这个间隔。
过半提交算法
使用一个数组matchIndex表示peers已经匹配的索引的位置,假如现在情况如下:

其他4个节点已经匹配的索引分别为10, 8, 6, 12,那么我们要做的是找到 最大的,过半的位置,可以采用下面这个方法:
-
排序(正序,倒序都行)
-
中间位置的值就是我们要找的索引位置,对应图中的8。
活锁优化
在选举阶段,很容易发生一段时间的活锁情况,然后在10s中没有选举出leader,就会导致测试用例失败。
活锁发生的原因不止一种,其中比较常见的原因有 :vote time 应该要大于 heartbeat time。
逻辑是这样votetime实在candidate进行vote时,收到的reply小于半数时,休眠vote time时间,说明存在vote竞争的情况,存在竞争的时候,是加快竞争频率还是减缓竞争频率呢?当然是保证提高一定的vote time,让竞争减少。我的配置参数是这样:
-
heartbeat = 150 ~ 300ms
-
vote = 200 ~400 ms
日志提交限制
报错如下:apply error: commit index=5 server=2 6046 != server=1 511。这就是安全性存在漏洞,在 figur8中特别指出了,commit时必须包含本term的日志

测试用例通过
跑个100次测试
