01
背景说明
rocksdb是一个被广泛采用的KV系统,其功能已经逐渐演变成很多上层应用的一个基础组件,像Ceph的bluestore,nebula的点边存储,还有tikv系统,底层都是依赖rocksdb来做数据存储或元数据管理的,因此rocksdb的吞吐能力表现对上层应用系统的能力建设可以起到非常重要的一环。
随着分布式系统开始陆续上云,计算与存储相分离的体系架构也开始广泛得到部署应用,在该体系模式之下存储节点与计算节点往往对物理资源有着不同的需求纬度。存储节点比较侧重磁盘以及带宽,而计算节点则更侧重CPU资源。如果我们能够通过较少的CPU来跑满整个磁盘IO上限,那么便可以预留出更多的CPU资源去服务于计算节点。
基于NVMe设备,一种有效改善吞吐能力的办法是借助内核旁路机制,通过用户态的驱动程序来与底层的NVMe设备进行直接的交互,从而避免系统调用所带来的overhead开销。为此SPDK应运而生,其不但提供了一个用户态的文件系统(blobfs),还将磁盘驱动程序放到了用户态空间,使得数据的访问可以在用户态直接进行,这样便有效避免了操作系统的上下文切换以及数据在用户态和内核态之间的拷贝传递。
得益于rocksdb对Env的灵活封装,使得底层的数据存储几乎可以是任何的文件系统,包括blobfs。然而通过benchmark我们发现,在某些特定的workload场景下,blobfs的性能表现并没有达到预期的那样理想,比如在对readrandom测试过程中,发现无论是请求时延还是吞吐能力,其表现都是不如内核态的。
为此,我们对blobfs内部的工作流程做了一些调研和梳理,通过分析其内部的执行逻辑来寻找可做改善的性能空间。
02
BLOBFS改造 - 多IO Channel支持
blobfs原生的线程工作模型如上图左侧所示:首先其内部会创建两个不同的io_device(blobfs_md和blobfs_sync)来应对不同类型的IO请求(严格意义上讲是三个,只不过blobfs_io尚未投入使用),如果请求涉及元数据的访问操作(eg. open, close, create, rename),需要采用blobfs_md所创建的io_channel,其他情况使用blobfs_sync所创建的io_channel即可。io_channel是blobfs暴露给上层应用的两个通信管道,相当于是交互的入口,然而其本身并不是线程安全的,不同的线程没有办法同时共用同一个io_channel来与blobfs进行交互,只能供单线程内部调用使用。所以SPDK在功能实现上将其与reactor_0进行了绑定,只有reactor_0所对应的线程可以与blobfs做交互,而其他线程或者reactor则需要通过中间队列来做异步中转(即向reactor_0发送事件消息,然后由reactor_0来做统一的处理)。
该线程模型虽然有效规避了meta访问raceCondition的问题,但是在功能实现上也带来了相应的问题短板。
-
慢IO拖慢长尾时延更加明显。
由于所有的IO请求都需要路由给reactor_0,并且IO事件的处理是采用单个qpair来进行的,处理过程中不能出现乱序(io mess up),这样与多IO链路相比,单链路的慢IO将会影响后续更多的IO请求,从而导致长尾时延得到拖慢。
-
跨NUMA通信问题。
如果reactor_0与reactor_1隶属于不同的NUMA节点,则reactor_1在使用reactor_0所产生的数据时将需要跨NUMA进行通信,从而牺牲一部分性能。
-
锁同步产生性能开销。
锁同步开销主要体现在以下几个方面:
(1) event队列的出队入队开销,并发线程越多开销会越明显;
(2) reactor_0处理完IO请求之后需要通过信号量来对调用线程进行通知;
(3) spdk_fs_request对象池的出队入队开销(IO调用线程从对象池中取元素,reactor_0向对象池中添加元素)。
-
横向扩展困难
在单线程不能跑满IO负荷的情况下,该处理模型没有办法做横向扩展,导致磁盘的带宽资源出现浪费。
为此我们考虑对blobfs原生的IO处理模型进行相应的扩展,来修复以上问题,扩展后的模型如上图右侧所示。首先reactor_0依然保留其原生的处理逻辑,采用blobfs对外暴露的两个io_channel来与其进行交互,所不同的是其他reactor不在将所有的IO请求全部路由给reactor_0进行处理,而是先对IO请求类型进行判断,如果请求涉及元数据的访问操作则将其路由给reactor_0,否则将采用自己独立的io_channel来与blobfs进行直接的交互。体现到rocksdb层面的调用关系便是每当有文件open或close操作时将其路由给reactor_0,而针对文件的读取操作则采用自己独立的io_channel进行。由于rocksdb自身有TableCache机制来缓存每个文件的Reader,因此文件open操作并不是一个频繁的动作。同时rocksdb的文件组织格式sst是没有追加写入的,并且只有当Reader引用计数为0的时候文件才能删除,所以不用担心文件在read过程中发生数据不匹配的情况。
经过以上处理之后,IO的执行链路由一个扩展成了多个,单个慢IO的影响范围将会得到缩减。并且reactor之间不在需要做数据交换(使用其他reactor读取到内存的数据),有效避免了跨NUMA通信问题。同时锁同步开销也不在是瓶颈,reactor之间不在需要信号量的同步机制,spdk_fs_request对象池的使用也只在线程内部进行,不在需要加锁同步。
03
Run-To-Complete模型