etcd的raft实现可以说是一个非常标准的raft实现,对于状态机返回的各种状态,我们只要依照etcd建议的处理流程来处理就可以了。
etcd为了加快log的应用,可能还有点为了逻辑解耦,将log的apply和log的持久化放在两个线程中处理,一个是在server.go里的applyAll,主要用于应用快照、应用日志并且检测是否要对当前的存储进行快照。
但是这里并行的处理有一个问题,我也在etcd的github上提了issue,当然我还不清楚是这种情况本身是允许的,还是可能这里是一个潜在的bug。
讨论这个问题,我们必须分两种情况来看,一种是集群模式,一个是单点后期扩展为集群模式,两种逻辑是不一样的。
我们首先来看一下在单机模式下的log的处理,我们把用户的一次crud称之为一个log,其余的log我们在这里就直接忽略了。在一个log被propose后,在进入raft状态机的时候,raft首先会将其加入自身的日志,并且根据各个follower的progress来检查该index是否是quorum的,假设成立,那么我们就可以认为此log是可以commit的。
上面是笼统的流程,因为在单机模式下,一个log被propose后,在自身的progress中其实该log的index已经属于match的状态了,所以该log已经是属于committed状态了,然后由于该log还在内存中,处于unstable状态,于是在raft状态机向逻辑层输出ready的时候,我们首先要将该log持久化,然后将hardState持久化,在这两部完成之后,该log才算是被committed的,即使重启节点也是一样的。
在raft线程处理ready的时候,etcd的处理是并行的,将日志的落盘与日志的应用并行化,也就是在持久化log和hardState之前,该log就被应用了。
这还需要区分v2和v3的api,v2的store属于纯内存的,它是从wal中的快照和后续的log在内存中恢复的,所以由于存在内存中并没有落盘,然后日志也没有落盘,所以这是没有问题的,下一次启动该log并不会出现在v2store中。
然后我们来看看v3的存储,v3的存储使用了boltDB,是一个文件型数据库应用了log必定会导致log被持久化在存储中了,而对应导致这个存储变更的log却没有落盘,那么会导致这个变更永久的存在于了这个节点中,而在它的日志中却不存在了。
假设我们在单节点集群中增加了一个节点,该节点会复制老节点的log,然后我们访问新节点,我们会发现老节点的key无法在新节点中读出,导致了集群的不一致。
多节点集群中是不存在这个问题的,因为它的entries和committedEntries永远不会同时存在,也就是committedEntries始终已经在之前的entries中被持久化了。
为什么会这样呢?我们来梳理一下共识的逻辑:
所以我们可以看见问题的核心在于committedEntries和entries有重合,这只有在单节点模式和落后的follower追数据的时候才会出现,那么我们来看一下follower追数据的场景。
当follower追数据的时候,假设发生了单节点提到的情况,被应用至了存储却没有落盘,那么在重启后该日志由于是被提交的日志,所以还是会复制到follower中,这个log是不可能会丢失的,集群数据还是一致的。
并行的落盘与存储始终有风险,这部分还是等官方后续的反馈吧。