Raft优化
Raft的简单流程如下:
-
Leader收到client发送的request。
-
Leader将request append到自己的log。
-
Leader将对应的log entry发送给其他的follower。
-
Leader等待follower的结果,如果大多数节点提交了这个log,则apply。
-
Leader将结果返回给client。
-
Leader继续处理下一次request。
可以看到,上面的流程是一个典型的顺序操作,如果真的按照这样的方式来写,那性能是完全不行的。
Batch and Pipeline
首先可以做的就是Batch,大家知道,在很多情况下面,使用Batch能明显提升性能,譬如对于RocksDB的写入来说,我们通常不会每次写入一个值,而是会用一个WriteBatch缓存一批修改,然后在整个写入。对于Raft来说,Leader可以一次收集多个requests,然后一批发送给Follower。当然,我们也需要有一个最大发送size来限制每次最多可以发送多少数据。
如果只是用batch,Leader还是需要等待Follower返回才能继续后面的流程,我们这里还可以使用Pipeline来进行加速。大家知道,Leader会维护一个NextIndex的变量来表示下一个给Follower发送的log位置,通常情况下面,只要Leader跟Follower建立起了连接,我们都会认为网络是稳定互通的。所以当Leader给Follower发送了一批log之后,它可以直接更新NextIndex,并且立刻发送后面的log,不需要等待Follower的返回。如果网络出现了错误,或者Follower返回一些错误,Leader就需要重新调整NextIndex,然后重新发送log了。
Append Log Parallelly
对于上面提到的一次request简易Raft流程来说,我们可以将2和3并行处理,也就是Leader可以先并行的将log发送给Followers,然后再将log append。为什么可以这么做,主要是因为在Raft里面,如果一个log被大多数的节点append,我们就可以认为这个log是被committed了,所以即使Leader再给Follower发送log之后,自己append log失败panic了,只要N/2 + 1个Follower能接收到这个log并成功append,我们仍然可以认为这个log是被committed了,被committed的log后续就一定能被成功apply。
那为什么我们要这么做呢?主要是因为append log会涉及到落盘,有开销,所以我们完全可以在Leader落盘的同时让Follower也尽快的收到log并append。
这里我们还需要注意,虽然Leader能在append log之前给Follower发log,但是Follower却不能在append log之前告诉Leader已经成功append这个log。如果Follower提前告诉Leader说已经成功append,但实际后面append log的时候失败了,Leader仍然会认为这个log是被committed了,这样系统就有丢失数据的风险了。
Asynchronous Apply
上面提到,当一个log被大部分节点append之后,我们就可以认为这个log被committed了,被committed的log在什么时候被apply都不会再影响数据的一致性。所以当一个log被committed之后,我们可以用另一个线程去异步的apply这个log。所以整个Raft流程就可以变成:
-
Leader接受一个client发送的request。
-
Leader将对应的log发送给其他follower并本地append。
-
Leader继续接受其他client的requests,持续进行步骤2。
-
Leader发现log已经被committed,在另一个线程apply。
-
Leader异步apply log之后,返回结果给对应的client。
使用asychronous apply的好处在于我们现在可以完全的并行处理append log和apply log,虽然对于一个client来说,它的一次request仍然要走完完整的Raft流程,但对于多个clients来说,整体的并发和吞吐量是上去了。
SST Snapshot
在Raft里面,如果Follower落后Leader太多,Leader就可能会给Follower直接发送snapshot。在TiKV,PD也有时候会直接将一个Raft Group里面的一些副本调度到其他机器上面。上面这些都会涉及到Snapshot的处理。
在现在的实现中,一个Snapshot流程是这样的:
-
Leader scan一个region的所有数据,生成一个snapshot file
-
Leader发送snapshot file给Follower
-
Follower接受到snapshot file,读取,并且分批次的写入到RocksDB
如果一个节点上面同时有多个Raft Group的Follower在处理snapshot file,RocksDB的写入压力会非常的大,然后极易引起RocksDB因为compaction处理不过来导致的整体写入slow或者stall。幸运的是,RocksDB提供了SST机制,我们可以直接生成一个SST的snapshot file,然后Follower通过injest接口直接将SST file load进入RocksDB。