目录
- 一、Blobstore设计框架
- 二、Cache机制
- 三、Blob FS I/O操作
- 四、SPDK FUSE (Filesystem in Userspcae)
前言
Blob FS是spdk面向于用户态的轻量级的文件系统
SPDK通过绕过内核(kernel bypass)的方案,构筑了用户态驱动,并利用异步轮询、无锁机制等,极大地提升了I/O性能。然而,正因为采用了kernel bypass的设计,使得原本内核中的文件系统不能使用。因此,SPDK提供了Blobstore用来支持上层存储服务,并基于此封装了Blob FS(Blob Filesystem)文件系统。
一、Blobstore设计框架
Blob FS被设计为面向Blobstore的轻量级文件系统,它提供给用户基本的文件操作: read、write、open、delete等。在介绍Blob FS的框架之前,首先介绍两个概念spdk_io_device和spdk_io_channel,在SPDK的早期设计中,spdk_io_device被设计为存储设备的抽象,并将特定线程的队列抽象为spdk_io_channel。这样的好处是为了避免竞争以及其带来的全局锁定,在SPDK线程模型中,每个spdk_thread通过各自的spdk_io_channel对spdk_io_device进行访问,这样不同的spdk_thread被隔离。这种类似的设计被推广,spdk_io_device被抽象成任何地址,不仅仅是底层存储设备的映射,也包含了对线程资源的逻辑管理结构。spdk_io_channel也随之被抽象成与spdk_io_device相关联的线程上下文,通过spdk_get_io_channel()函数,可以很方便的获取某个spdk_io_device上对应的channel。在SPDK中,所有的spdk_io_device会通过RB_TREE来维护,这有利于查找。
Blob FS在工作状态下会注册两个spdk_io_device,分别是blobfs_md以及blobfs_sync,前者带有md后缀,在Blob FS框架下,这被设计为与元数据(metadata)的操作有关,例如create,后者则是与I/O(read & write)操作有关。两个spdk_io_device绑定在一个reactor 0上,相当于对外提供了两个交互通道。根据前文所述,不同的spdk_thread没有办法共用一个spdk_channel,这就保证了只有reactor 0才能Blob FS进行交互,有效避免了多线程之间的竞争以及同步问题,例如对元数据的操作只能经由reactor 0实现,其他用户线程或者绑定在其他线程中的reactor对元数据的操作均需要通过SPDK中的消息机制来实现,交由reactor 0来进行处理,这种设计与SPDK的run-to-completion理念相符,所以工作流程可总结如下图所示。
二、Cache机制
为了加速文件的I/O性能,Blob FS提供了一套cache机制。DPDK提供的大页管理是SPDK实现零拷贝机制的基础,实际上这也是Blob FS中cache机制的基础。借助DPDK内存池对大页的管理,一次性申请到了一块较大的缓冲区mempool,该区域除了头部以外主要由固定大小的内存单元组成,并构成了ring队列,可方便的用于存取数据。
针对内存池对大页的管理,DPDK用到的接口主要有三个,SPDK对其进行了封装:
在Blob FS中,固定大小的内存单元其大小被设置为CACHE_BUFFER_SIZE,该值体现了cache机制中基本的存储单元,内存单元的数量可以采用默认值也可以通过Blob FS预留的接口进行重新设置。为了方便管理整个cache机制中的内存,Blob FS创建了一个新的spdk_thread线程g_cache_pool_thread。它主要维护了g_caches和g_cache_pool两个数据结构,前者维护了所有拥有cache的文件列表,后者则指向了前文提到的借助DPDK大页管理所申请到的内存池。与此同时,在该线程上还注册了一个poller函数用于监测内存池的使用情况,当内存池中空闲内存单元数量下降到20%以下时,会对已缓存的内存池对象进行清理释放,将内存单元返还给内存池,并且会优先释放低优先级的文件缓存,用户程序可根据需要调整缓存文件的优先级。
Blob FS对文件的管理依赖了spdk_file数据结构,在spdk_file中利用了cache_tree数据结构来对该文件的cache进行管理。cache tree由多层树状节点构成,维护了两种数据节点: tree node和buffer node,其中tree node充当了索引的作用,他可以根据根节点以及对应的offset,自上而下查找对应的buffer node;buffer node是实际缓存文件数据的节点,他位于整个cache tree的最底层,buffer node中用于缓存文件数据的空间实际上就是前文中提到的借助内存池申请到的一个个内存单元,因此单个buffer node中所能缓存文件数据的大小也就是CACHE_BUFFER_SIZE,在默认情况下,该值为256KB。
三、Blob FS I/O操作
尽管Blob FS中提供了同步和异步两种I/O操作,但当前集成并使用的是同步读。
1. spdk_file_read
首先,Blob FS会依据本次读取数据的大小进行判断,当读取数据大于CACHE_READAHEAD_THRESHOLD(默认情况下,该值为128KB)时,触发异步预读(readahead)操作,提前将一部分数据读取到内存中,随后对读取的数据进行切分,使得每次读取数据不超过buffer node的大小,即CACHE_BUFFER_SIZE的值。切分以后的读取,会先根据当前offset的值在cache tree中查询对应的buffer node,如果可以找到对应的buffer node节点,即说明缓存命中,直接将其缓存的文件数据执行memcpy拷贝到payload中,否则则需要通过Blobstore提供的异步读取直接从存储设备读取,并利用sem信号量进行同步,直到所有的读取完成,一次性返回整个payload,所以整个读取过程需要等待所有的payload准备完成才会返回。
2. spdk_file_write
目前SPDK主分支中的Blob FS提供的写入接口当前仅支持append类型,即不支持覆盖写入操作(注1)。对于append类型写入,Blob FS会首先检查其是否满足cache机制条件,如果不满足则借助Blobstore提供的写入接口,直接写入存储设备中。而一旦满足cache机制,则利用spdk_mempool_get函数向内存池中申请一个内存单元作为buffer node用于存储文件数据,随后将写入的payload进行切分,保证每次写入数据不超该buffer node的大小,并利用memcpy拷贝至buffer node中,当当前buffer node已被写满但存储文件还未被完全写入时,会将当前buffer node添加到cache tree中,再重新申请内存单元,重复上述操作,直到所有的payload都被写入buffer node中。随后触发异步flush操作,将cache tree中存储的文件数据利用Blobstore写入到存储设备中,并更新内存中的元数据。
从上述I/O过程可以发现,cache机制起到的加速作用主要体现在两点:
Read阶段中利用预读机制,先将当前offset附近的部分数据从存储设备中读入到内存中,方便下次直接从内存中读取,而不借助Blobstore,从而提升效率。
Write阶段中将payload优先写入到cache tree中,再利用异步flush操作写入到存储设备中,write本身不需要等待在落盘结束。
值得注意的是,分析Blob FS中cache的默认参数配置以及机制设计,可以发现Blob FS提供的cache机制更适用于大文件的读取。
注1:Blob FS对随机写的支持可参考 https://review.spdk.io/gerrit/c/spdk/spdk/+/5420,但是对目前的Blob FS cache机制有影响。更多Blob FS的patch,也可以关注https://review.spdk.io/。
四、SPDK FUSE (Filesystem in Userspcae)
SPDK提供了FUSE插件,可以将Blob FS像内核文件系统一样挂载,方便测试。可利用本地NVMe设备,基于SPDK FUSE挂载Blob FS,方法可参考以下步骤:
1. ./configure --with-fuse && make
2. scripts/gen_nvme.sh --json-with-subsystems > config.json
3. ./test/blobfs/mkfs/mkfs config.json Nvme0n1
4. mkdir /mnt/fuse
5. ./test/blobfs/fuse/fuse config.json Nvme0n1 /mnt/fuse
当然,也可以借助rpc框架,调用bdev子系统中Malloc Bdev模块,启用Blob FS文件系统,启用方法如下:
1. ./build/bin/spdk_tgt // Terminal A
2. ./scripts/rpc.py bdev_malloc_create 512 4096 // Terminal B
3. ./scripts/rpc.py blobfs_create Malloc0
4. ./scripts/rpc.py blobfs_mount Malloc0 /mnt/fuse
注意:Blobstore本身提供了持久化存储服务,但是这也要求面向的Bdev对象提供持久化存储,例如NVMe Bdev,而前文rpc调用的Malloc Bdev并不能提供持久化存储,因此基于Malloc Bdev模块的Blobstore不具有持久化存储能力。
总结
本文对SPDK提供的用户态存储服务Blobstore以及Blob FS进行了更为深入的探讨,对其设计细节进行了补充,并结合cache机制,分析了Blob FS所提供的I/O操作的流程,最后提供了借助FUSE插件对Blob FS进行调试分析的两种方法。
粉丝福利, 免费领取C/C++ 开发学习资料包、技术视频/项目代码,1000道大厂面试题,内容包括(C++基础,网络编程,数据库,中间件,后端开发/音视频开发/Qt开发/游戏开发/Linuxn内核等进阶学习资料和最佳学习路线)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓