如果您想快速了解ublk的意义、作用及性能,请直接看第二节Q&A部分。
一、简介
用户态块设备,就是提供/dev/ublkbX这样的标准块设备给业务,业务读写这个块的实际IO处理由您编写的用户态的代码决定。这就好比您使用FUSE,所有对挂载于FUSE的目录的读写都是您编写的IO handler来处理一样。使用用户态块设备,您可以方便地向上层业务以块设备/dev/ublkbX的形式提供您的自定义存储系统(如ceph)的服务,上层业务只需要对块设备执行标准的读写操作即可。
ublk是社区提出的基于最新的、流行的io_uring passthrough机制实现的用户态块设备,目前已经在block社区引起广泛讨论,并即将进入5.20内核主线。社区提供的代码包括内核态的ublk_drv.ko内核模块和用户态的ublksrv。
fio target(backend): null/loop/qcow/socket/ceph/rpc...
libublksrv user
----------------------------------------------------
ublk_drv.ko io_uring kernel
ublk_drv.ko是一个非常轻量级的内核模块:通过实现quque_rq()接口,获取来自blk-mq的IO请求;然后通过io_uring_passthrough机制把新的IO请求到来的通知回传给用户。真实的IO描述信息(如长度、opcode和flag等)由内核写入一块被用户态只读mmap()后的共享内存区域中,供用户态的ublksrv随后获取IO信息。
ublksrv是ublk的用户态部分,负责处理由内核发来的IO请求。ublksrv把IO请求交给:①注册的target(null、loop和qcow等)完成,每个target都按照各自的需求实现处理IO的接口,而ublksrv将自己作为daemon运行;②使用ublksrv的library API的自定义存储,ublksrv的逻辑嵌入到存储的IO处理框架(如ceph等大规模分布式系统)中,IO收发与处理运行在IO处理框架的上下文中。
目前,主流的用户态的块设备框架有nbd(nbdkit)和tcmu(tcmu-runner),nbd的性能与tcmu相近。我们特别调研了tcmu,它表现出了一些问题:
(1)tcmu使用SCSI协议,使得软件栈开销较大,我们使用fio iodepth=1, numjobs=1测试验证了这一事实。事实上,SCSI协议对于仅仅想要提供通用块设备的业务场景来说并不是必须的。
(2)tcmu不支持多队列,没有利用blk-mq的特点;由于tcmu设计基于早年的uio框架,tcmu只能提供一个IO命令队列与唯一的fd供线程轮询(poll),使得多个io worker在分发IO请求时必须加全局锁;在io worker数量很大时,锁竞争的现象会很严重。
(3)tcmu内部分配了data buffer,增加了额外的拷贝开销。以写请求为例,数据先复制到tcmu内部buffer,再复制到业务的后端IO框架的buffer(如RPC库的buffer);针对tcmu拷贝开销问题,Anolis社区给出了bypass和零拷贝等解决方案并开源。然而,我们的努力只是缓解了tcmu的拷贝问题,并没有解决tcmu的根本缺陷。
相比现有的tcmu、nbd等用户态块设备方案,ublk不受到任何协议的限制(如unix domain socket或SCSI协议),代码简洁软件栈开销很小;此外,ublk直接一一映射了blk-mq的多队列,每个ublk队列都包括一个io_uring实例互不干扰,不需要加锁同步任何信息,使得ublk的可扩展性优秀;最新的ublksrv还提供了library API,使得ublksrv嵌入到现有的IO处理框架中,使得这些框架提供/dev/ublkbX块设备直接使用;ublksrv支持data bypass机制,即允许直接把bio vectors的数据拷贝到业务后端的data buffer中,也将在未来支持异步dma、零拷贝或splice机制(目前上游社区暂无完美的零拷贝方案)。总之,ublk的潜力不容小觑。
我们在ublk开发初期便参与了社区讨论与代码设计,并及时测试新版本的性能。目前我们在block社区邮件列表对ublk进行特性贡献。ublk的性能也被我们完整地测试过。对比tcmu-runner,ublk在单IO的全链路时延、多线程场景IOPS和CPU开销上均有优势。测试数据详见后文Q&A部分。
二、Q&A
为了方便您快速了解ublk,我们自问自答了一些问题,也欢迎您评论提问。
Q1: 什么是用户态块设备,意义是什么?什么业务能用ublk?
A1: 用户态块设备就是提供一个/dev/ubdbX这样的块设备,所有这个设备上的IO请求的读写逻辑都是您在用户态编写的代码,比如pread/pwrite到一个具体地文件(这样就是loop设备了)。使用用户态块设备,你可以方便地向上层业务提供您的存储系统(如ceph),业务只需要对块设备执行标准的读写操作即可。如果您有一个用户态的存储系统(可以是跨多个机器的网络分布式存储),想要向业务暴露/dev/ublkbX块设备 ,提供IO读写功能的,就可以考虑ublk。比如您的业务用了容器或者虚拟机,就可以暴露/dev/ublkbX作为他们的磁盘,读写这些磁盘会导致实际IO passthrough(透传)给您的用户态存储系统(如ceph)。如果您没有存储系统,也可以使用ublk,因为ublksrv可以自己在本地运行daemon处理实际IO,如作为qcow target运行向容器或虚拟机提供qcow磁盘。
Q1: 现在ublk在linux社区是一个什么状态?你们现在ublk的支持怎样了,能否在现在的4.19或者5.10上跑起来?
A1: ublk利用了io_uring_passthrough新机制,是io_uring新特性的最佳实践之一,受到block与io_uring社区广泛关注。自ublk的idea提出以来,社区大佬们也赞誉有加。目前ublk已经合并在linux-block开发分支中,合并入5.20主线只是时间问题。 io_uring_passthrough机制加入需要修改内核本体,所以升级内核小版本是必须的。现在我们已经在Anolis社区给出了一个POC的用户态library与内核(基于5.17)并测试(附在文后),您可以关注。适配Anolis 4.19或5.10内核的开发难度本身不大,如果您有适配ublk来POC的需求也欢迎找到我们讨论。
Q3:感觉ublk和FUSE、SPDK或者NVMe over Fabrics很像?有没有什么对比?
A3:关于FUSE,它其实给你创建一个用户态文件系统的方法,你实现了后端的IO处理逻辑,就可以mount到一个目录让业务用了,其实和ublk是关注在不同存储层次(文件系统和块设备)的方案。
SPDK实际上bypass了整个内核,所以提供出支持POSIX API的块设备是困难的,您需要让业务使用SPDK的API才行,这样也许不太方便。此外SPDK也有CPU占用高、迁移成本大的问题(比如您必须把您的存储逻辑嵌入到SPDK中,而ublk提供的library允许您把ublk嵌入到您的存储逻辑中,后者改造代价更低)。
对于NVMf,我们认为那是另一种方案,通过网络提供远端NVMe存储,我觉得其实跟nbd更像一点。
Q5: ublk的一个IO的全链路是怎样的?
A5: 业务(如fio)向/dev/ublkbX发起IO请求,内核blk-mq模块通过queue_rq()向用户态ublksrv passthrough该IO请求,并填写映射的IO描述符信息;随后用户态ublksrv获取该IO信息并转交给特定的target(或者在library中)执行具体的IO处理(如loop设备的pread/pwrite);在IO完成后,ublksrv应通知给内核的blk-mq模块该IO已经完成。整个系统的框架如下:
target(backend)------------> null/loop/qcow/socket/ceph/rpc...
^
|
|
libublksrv
fio ^
| | user
--------|-----------------------------|---------------
| | kernel
v |
/dev/ubdbX |
| |
| |
v io_uring
blk-mq ^
| |
-----------> ublk_drv---------
Q6:ublk的性能如何?
A6: 对比了tcmu-runner和ublk的全链路时延和多线程IOPS
测试环境:
NVMe SSD: Intel DC P3600 Series 800GB,
Intel(R) Xeon(R) CPU E5-2682 v4 @ 2.50GHz,
Linux 5.17(加入io_uring uring_cmd支持)
测试选项:
测试使用fio,direct=1,bs=4k
ublksrv使用此处的代码,内核必须使用支持io_uring passthrough和ublk的版本。
tcmu-runner的后端存储为user: fbo,一个对文件进行pread/pwrite的handler。我们修改了fbo的代码使其支持写入(本来只读)。
ublksrv后端是一个对文件进行pread/pwrite的backend,在此处您找到的ubd_test.c即为测试代码。
测试参数:
tcmu-runner的nr_rthreads是一个handler上的io worker的个数,您可以理解为有nr_rthreads个io worker不停地在一个SCSI队列上竞争拿取IO请求,每个串行地处理各自的IO请求(pread/pwrite)。
ublk的nr_queues是blk-mq的hardware queue的个数,hardware queue一一对应ublk的queue与其io_uring实例。ublk的queue互不干扰地拿取自己队列上的IO并串行地处理(pread/pwrite)。
测试数据:
-
单任务时延(usec) fio参数:iodepth=1, numjobs=1 tcmu-runner设置nr_rthreads=1,ublksrv设置nr_queues=1
Type |
TCMU |
UBLK |
seq-read |
38.47 |
24.50 |
rand-read |
119.46 |
105.86 |
seq-write |
38.45 |
26.48 |
rand-write |
38.36 |
26.76 |
在4种类型的请求下,ublksrv的单任务时延均小于tcmu-runner,展示出ublk软件栈开销小的优势。
-
多任务IOPS(k) fio参数:iodepth=64, numjobs=4 tcmu-runner设置nr_rthreads=4,ublksrv设置nr_queues=4
Type |
TCMU |
UBLK |
seq-read |
32.7 |
41.3 |
rand-read |
38.0 |
40.7 |
seq-write |
161 |
202 |
rand-write |
165 |
204 |
在4种类型的请求下,ublksrv的多任务IOPS均大于tcmu-runner,其中seq-write和rand-write优势明显(由于pwrite并未显著增加单个IO的时延),seq-read和rand-read的优势不大(由于pread是性能瓶颈,且ublksrv只是串行地执行队列上的所有IO)。
Q7:怎么感觉ublk没什么提升啊?rand-read提升不明显?
A7: 为了公平对比,在测试中只是简单地串行对每个IO执行pread/pwrite,没有进行任何优化(没有batch)。我们的测试只是简单的模拟,因此您会注意到ublk比tcmu的优势并不大,其实pread/pwrite阻塞执行拖后腿了。实际使用ublksrv时,后端IO处理的具体实现可以大幅度地改变IOPS。您可以把后端实现为aio/io_uring的批量提交与收割,或者批量把IO放入您的分布式存储的RPC接口。事实上我们也在此处实现了ubd_test_io_uring.c,在每个队列上一次性批量提交IO并等待全部完成,而非上面测试时简单地一个个pread/pwrite。我们注意到仅仅这种简单替换就使得ublk在iodepth=64, numjobs=4, nr_queues=4时的rand-read的IOPS提升到了250K。说明您后端的IO处理方式才影响性能,而非ublk本身。
Q8: 后面ublk还有什么支持?
A8: 异步dma、零拷贝、更完善的library和故障恢复,这些机制将确保最终ublk能胜任生产环境。整个更新至少持续一年。在保持跟随社区进展的同时,我们也会及时在ATA同步ublk的开发进展,希望大家多多关注ublk。