braft icon indicating copy to clipboard operation
braft copied to clipboard

如何基于braft实现超低延迟存储系统?

Open rpbear88 opened this issue 3 years ago • 3 comments

各位好,

我们正在使用braft构建我们自建的存储系统,我们做了一个原型系统来验证braft的性能,原型系统的一些要点如下:

  • 网络通信层使用brpc的RDMA支持,开启polling+inplace并且绑核,但是没有使用isolcpu来隔离独立的核给RDMA使用
  • braft在WAL采用了MemoryLogStorage,想忽略这块在磁盘IO上的消耗,因为目前基于文件系统实现的LogStorage的延迟较高,针对一个8K的日志提交延迟会超过300us,远差于基于SPDK实现的IO性能
  • 服务端程序自行起了一个pthread用于运行我们的存储引擎,存储引擎主要负责在该线程内使用SPDK进行IO的提交

因此,整个服务端处理写IO的流程是这样的:

  1. 服务端收到写请求后从RDMA,其polling线程原地将数据得到后创建一个background的bthread(我们称之为task bthread)来运行任务。(由于运行创建该bthread的worker线程用于RDMA的pooling,没有机会来处理本地的_rq任务,所以最终该任务会被其他worker偷过去运行?)
  2. task bthread进入到我们业务状态机后提交给braft
  3. braft执行完成后调用on_apply,此时会将raft同步完成的请求提交给存储引擎(通过SPDK的ring队列),此处会产生worker thread和引擎thread间的数据交换
  4. 存储引擎从任务队列中获取任务后进行SPDK的提交和polling
  5. SPDK提交完成后,在存储引擎所在的pthread直接原地(没有起新的background bthread)调用done->Run进行数据回复

目前在单队列的4K写场景下,braft从得到任务到调用on_apply耗时约90us(这个结果显然不能满足实现低于100us的IO延迟的存储系统的要求),我们还在调查这个延迟具体消耗在什么位置。但是考虑到raft本身的一些weakness,我们想从原理上看看是否有更好的方案,下面是我们提出的一个方案(感谢@PFZheng提供的思路)。

元数据和数据使用不同的复制方式

普通的主从同步方式,从并发的角度来说性能是优于raft的,因为它没有顺序提交的问题,但是主从方式做replication的缺点在于:

  1. 数据不是强一致,如果leader切换的话,可能会出现数据倒转等问题,这在某些应用场景是不可接受的
  2. 对于冲突数据的修改,并发的请求不能确定各节点执行的order

从数据同步的角度来说,raft和主从协议性能损耗可以认为是一样的,在raft中leader的日志只要确定了log id,是可以并行发给follower的。但是在日志持久化阶段,raft日志必须是串行提交进日志系统,这是其一个缺陷。在apply阶段,从实现层面来说,不冲突的请求可以并行apply,但是目前实现一般都是顺序apply,因为会依此更新apply idx,所以apply的顺序不能乱。所以这里的一个解决思路可能并不是去并行apply,这样在既有的raft框架内改动也较大,而是尝试让apply尽可能快,那么这个顺序apply的开销就会很小,比如像redis那样纯内存的操作,那么性能也是可以做到非常强悍的。

由于我们的存储引擎对数据的修改采用的是非原地写(out-of-place write),我们考虑整个流程处理成如下步骤:

  • 1 leader收到写请求后给请求赋值一个session id,然后将session id + 请求一起直接发给各个follower,这一步称为data replication,不考虑任何一致性协议,仅仅是一次普通的replication请求
  • 2.1 leader和follower收到data replication后都将请求直接提交给存储引擎(绕过raft的WAL),存储引擎由于采用了非原地写,并不会覆盖既有数据,此时对被修改数据的读取依然返回之前的数据版本
  • 2.2 leader将session id,新写入的数据所在的数据寻址信息(如磁盘偏移)等作为关键数据记入raft log并进行metadata replication
  • 3 follower收到metadata replication后,查看该session 是否已经完成了data replication,如果不是则等待其完成,如果是则记入WAL。需要注意的是,由于data replication的执行是乱序的,WAL中确认执行完的log entry并不一定连续,这会导致两个要求:
    • 3.1. 依然需要等日志连续之后才能ack给leader,这样依然能遵守raft协议对commit条件的约束要求
    • 3.2. 节点启动时需要确认有效的WAL尾部,识别出未完成的log entry(容易做到,由于我们在WAL中写入的metadata都是固定长度的,相比之下,普通的WAL将数据落入其中会导致log entry是变长的)
  • 4 leader确认log被majority成员replicate后进行apply,各节点进行apply的话只是将之前out-of-place的写产生的数据commit到存储引擎的内存索引中,比如记录哪些块要被释放等,这个操作是纯内存的,因为启动时可以从WAL中replay

我们的改造主要有两个方面,第3步的改造,相当于让原本顺序执行的提交日志项变成了并行提交,理论上对性能提升有帮助。而apply阶段则变成纯内存操作,不涉及IO。

从原理上来说,我觉得raft的方案是有一个悲观锁的解决方式,用串行化来解决可能存在的争端但是在冲突不大的情况下性能不行。而结合主从复制的模式,相当于是一个乐观锁的解决方案。

很想听听大家对这个问题的解决方案的看法。

rpbear88 avatar Jun 24 '21 03:06 rpbear88

这个方案的前提依然是,我们认为braft的实现原理上没有特别多的导致性能差的点(虽然实验数据看起来并不是这样,我们还在进行调整),依然认为最大的性能问题来自于raft算法本身。

而对braft来说,leader上本地日志提交和两个peer的replicator对log entry队列的争抢虽然有一些锁的保护,但是锁竞争并不会很大(和peer数相等)。

rpbear88 avatar Jun 24 '21 03:06 rpbear88

Raft的串行化阶段对这种超低延时的存储系统不够友好。这个改造方案里最复杂的其实是配置变更

PFZheng avatar Jun 25 '21 05:06 PFZheng

@rpbear88 你们遇到的问题和阿里 polardb 团队很类似, shard-stoage 的数据库复制的时候需要 raft 乱序提交 + 乱序执行来降低延迟, 他们提出了parallel raft 算法, 但是我看了论文对比了一下 multi-paxos, 这个对 raft 改造很大... 所以你们可以考虑不基于共识的方案, 简单来说就是放弃 raft...

pidb avatar Jul 07 '23 06:07 pidb