Elasticell-聊聊Raft的优化

以前两篇文章介绍了作Elasticell的原因和Multi-Raft的实现细节:git

这篇主要介绍Elasticell在Raft上作的一些优化工做。异步

 

一次简单的Raft就一个值达成一致的过程性能

  1. Leader收到一个请求优化

  2. Leader存储这个Raft Log,而且给Follower发送AppendEntries消息spa

  3. Follower收到而且存储这个Raft Log,给Leader回复AppendEntries消息线程

  4. Leader收到大多数的Follower的回复,给Follower发送AppendEntries消息,确认这个Log已经被Commit了进程

  5. Leader本身Apply这个Log事件

  6. Leader Apply完成后,回复客户端

  7. Follower收到Commit的AppendEntries消息后,也开始Apply这个Log

若是都顺序处理这些,性能就可想而知了,下面咱们来看一下在Elasticell中针对Raft所作的一些优化工做。

 

Append Log并行化

2这个步骤,Leader能够AppendEntries消息和Raft Log的存储并行。为何?

  1. 若是Leader没有Crash,那么和顺序的处理结果一致。

  2. 若是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的作法:

  1. 使用链路复用技术,让单个Store进程上全部的Raft-Group都复用一个物理连接

  2. 包装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。

 

若是要保证性能和正确性,须要作到如下两点:

  1. Leader到某一个Follower之间的发送管道必须是有序的,保证Follower有序的处理AppendEntries。

  2. 可以处理丢失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的实现方式:

  1. 保证用于发送Raft消息的连接在每两个节点直接只有一个

  2. 把当前节点待发送的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目前已经作的一些优化,还有一些是将来须要作的:

  1. 不使用RocksDB存储Raft Log,因为Raft Log和RocksDB的WAL存在功能重复的地方,这样就多了一次文件IO

  2. Raft的heartbeat合并,当一个节点上的Raft-Group的不少的时候,heartbeat消息过多

  3. Batching Apply的时候,当前节点上全部正在Apply的Raft-Group一块儿作Batching而不是在一个Raft-Group上作Batching

  4. 更高效的Batching和Pipelining模式,参考论文[1]

了解更多

https://github.com/deepfabric/elasticell

参考

[1] Tuning Paxos for high-throughput with batching and pipelining

相关文章
相关标签/搜索