以前两篇文章介绍了作Elasticell的原因和Multi-Raft的实现细节:git
Elasticell-缘起github
这篇主要介绍Elasticell在Raft上作的一些优化工做。异步
一次简单的Raft就一个值达成一致的过程性能
Leader收到一个请求优化
Leader存储这个Raft Log,而且给Follower发送AppendEntries消息spa
Follower收到而且存储这个Raft Log,给Leader回复AppendEntries消息线程
Leader收到大多数的Follower的回复,给Follower发送AppendEntries消息,确认这个Log已经被Commit了进程
Leader本身Apply这个Log事件
Leader Apply完成后,回复客户端
Follower收到Commit的AppendEntries消息后,也开始Apply这个Log
若是都顺序处理这些,性能就可想而知了,下面咱们来看一下在Elasticell中针对Raft所作的一些优化工做。
Append Log并行化
2这个步骤,Leader能够AppendEntries消息和Raft Log的存储并行。为何?
若是Leader没有Crash,那么和顺序的处理结果一致。
若是Leader Crash了,那么若是大于n/2+1的follower收到了这个消息并Append成功,那么这个Raft Log就必定会被Commit,新选举出来的Leader会响应客户端;不然这个Raft Log就不会被Commit,客户端就会超时/错误/或重试后的结果(看实现方式)。
异步Apply
一旦一个Log被Committed,那么何时被Apply都不会影响正确性,因此能够异步的Apply这个Log。
物理链接多路复用
当系统中的Raft-Group愈来愈多的时候,每一个Raft-Group中的全部副本都会两两建链,从物理上看,最后会看到2台物理机器(能够是物理机,虚机,容器等等)之间会存在大量的TCP连接,形成连接爆炸。Elasticell的作法:
使用链路复用技术,让单个Store进程上全部的Raft-Group都复用一个物理连接
包装Raft消息,增长Header(Header中存在Raft-Group的元信息),这样在Store收到Raft消息的时候,就可以知道这些消息是属于哪个Raft-Group的,从而驱动Raft。
Batching & Pipelining
不少同窗会认为实现强一致存储会影响性能,其实并不是如此,在合理的优化实现下,强一致存储对于系统的吞吐量并不会有多大的影响,这主要来自于一致性协议的两个重要的细节Batching和Pipelining,理念能够参见论文[1],事实上,在阿里近期反复提到的X-DB跨机房优化中也实现了相似的功能X-Paxos,所以下面看看Raft的Batching和Pipelining如何在Elasticell中达到相似的效果。
在Elasticell中Batching在各个阶段都有涉及,Batching能够提升系统的吞吐量(和Latency矛盾)。Elasticell中的单个Raft-Group使用一个Goroutine来处理Raft事件,好比Step,Request,Raft Ready,Tick,Apply Result等等。
Proposal阶段,收集在上一次和本次处理Raft事件之间的全部请求,并把相同类型的请求作合并,并作一个Proposal,减小Raft的网络请求
Raft Ready阶段, 收集在上一次和本次处理Raft事件之间的全部的Ready信息,Leader节点Batch写入Raft Log
Apply阶段,因为Apply是异步处理,能够把相同类型的操做合并Apply(例如把多个Redis的Set操做合并为一个MSet操做),减小CGO调用
Raft的Leader给Follower发送AppendEntries的时候,若是等待上一次的AppendEntries返回,再发下一个AppendEntries,那么必然性能不好。因此须要作Pipelining来加速,不等上一次的AppendEntries返回,持续的发送AppendEntries。
若是要保证性能和正确性,须要作到如下两点:
Leader到某一个Follower之间的发送管道必须是有序的,保证Follower有序的处理AppendEntries。
可以处理丢失AppendEntries的情况,好比连续发送了Index是2,3,4的三个Append消息,其中3这个消息丢包了,Follower收到了2和4,那么Leader必须从新发送3,4两个Append消息(由于4这个消息会被Follower丢弃)。
对于第二点,Etcd的库已经作了处理,在Follower收到Append消息的时候,会检查是否是匹配已经接收到的最后一个Raft Log,若是不匹配,就返回Reject消息,那么按照Raft协议,Leader收到这个Reject消息,就会从3(4-1)重试。
Elasticell的实现方式:
保证用于发送Raft消息的连接在每两个节点直接只有一个
把当前节点待发送的Raft消息按照对端节点的ID作简单的hash,放到不一样的线程中去,由这些线程负责发送(线程的数量就至关于Pipelining的管道数)
这样就能保证每一个Follower收到的Raft消息是有序的,而且每一个Raft都只有一个Goroutine来处理Raft事件,这些消息可以保证被顺序的处理。
Batching和Pipelining的trade off
Batching可以提升系统的吞吐量(会带来系统Latency增大),Pipelining可以下降系统的Latency(也能在必定程度上提升吞吐量),这个2个优化在决策的时候是有冲突的(在Pipelining中发送下一个请求的时候,须要等多少的Batch Size,也许多等一会就回收集更多的请求),目前Elasticell采用的方式是在不影响Pipelining的前提下,尽量多的收集2次Pipelining之间的请求Batching处理策略,显然这并非一个最优的解决方案。
尚未作的优化
以上是Elasticell目前已经作的一些优化,还有一些是将来须要作的:
不使用RocksDB存储Raft Log,因为Raft Log和RocksDB的WAL存在功能重复的地方,这样就多了一次文件IO
Raft的heartbeat合并,当一个节点上的Raft-Group的不少的时候,heartbeat消息过多
Batching Apply的时候,当前节点上全部正在Apply的Raft-Group一块儿作Batching而不是在一个Raft-Group上作Batching
更高效的Batching和Pipelining模式,参考论文[1]
了解更多
https://github.com/deepfabric/elasticell
参考
[1] Tuning Paxos for high-throughput with batching and pipelining