简介
在ublk简介一文中我们介绍了Linux社区新提出的ublk:基于io_uring的全新高性能用户态块设备。ublk已经合入Linux 6.0主线,并且操作系统团队已经在ublk上开展了一系列实践,并在2022年中国LInux内核开发者大会上分享了阿里云在ublk上的实践。为了适配阿里云的分布式云存储产品,我们对ublk添加了NEED_GET_DATA特性和故障恢复特性。前者对数据拷贝进行优化,属于小特性,不多介绍;后者则是ublk产品化必备的特性,非常重要。本文对ublk故障恢复方案进行介绍,我们前后一共设计了3版方案,在不断的优化与迭代过程中收获颇丰,最终顺利合入6.1主线,并被LWN发文,在此分享给大家。
ublk背景知识
ublk提供了用户态块设备的能力。简单地说,就是提供一个/dev/ublkb*这样的块设备,所有这个设备上的IO请求的读写逻辑都是您在用户态编写的代码,比如pread/pwrite到一个具体地文件(这样就是loop设备了)。使用用户态块设备,你可以方便地向上层业务提供您的存储系统(如ceph),业务只需要对/dev/ublkb*执行标准的读写操作即可。如果您有一个用户态的存储系统(可以是跨多个机器的网络分布式存储),想要向业务暴露/dev/ublkb* 来提供IO读写功能的,就可以考虑ublk。比如您的业务用了容器或者虚拟机,就可以暴露/dev/ublkb*作为他们的磁盘,读写/dev/ublkb*会导致实际IO passthrough(透传)给您的用户态存储系统(如ceph)。如果您没有存储系统,也可以使用ublk,因为ublksrv可以自己在本地运行daemon处理实际IO,如作为qcow target运行向容器或虚拟机提供qcow磁盘。ublk的系统架构如图所示:
target(backend)------------> null/loop/qcow/socket/ceph/rpc...
^
|
|
libublksrv
fio ^
| | user
--------|-----------------------------|---------------
| | kernel
v |
/dev/ubdbX |
| |
| |
v io_uring
blk-mq ^
| |
-----------> ublk_drv---------
如同FUSE要运行fuse daemon来把IO转发给用户态文件系统,ublk需要在用户态运行daemon,称为ublksrvd,进行数据转发。在现有的ublk实现中,daemon一旦退出,会导致内核驱动自动删掉所有资源,包括/dev/ublkb*块设备,避免资源泄露。这种方法使得内核驱动实现简单,但不可用于生产环境,因为daemon进程很容易退出:如内存oom被内核干掉;daemon代码写错了导致段错误;各种用户态的攻击手段或管理员退出等等。为了让ublk产品化,我们需要设计故障恢复机制。
故障恢复设计
先确定一下设计目标:为了让前端业务顺利运行,在daemon退出(称为故障)后,我们不能让/dev/ublkb*消失;且要允许用户通过某些手段,启动新的daemon并关联到该/dev/ublkb*设备,接管所有IO,整个系统继续运行。
让前端业务顺利运行
为了满足第一个目标,我们需要一个检测daemon是否退出的机制,现有的ublk会周期性地运行一个monitor_work,检查daemon是否为PF_EXITING,该标记会在进程挂掉时被内核设置。现有的ublk的逻辑是直接abort掉所有的blk-mq请求(blk_mq_end_request),然后del_gendisk()删掉/dev/ublkb*块设备。现在我们需要在开启故障恢复特性时,保留该块设备。因此,我们设计了UBLK_F_USER_RECOVERY标记,用户在创建ublk块设备时指定feature标记,内核在monitor_work检查标记判断是否开启故障恢复特性,否则走原来的删除逻辑,是则保留该块设备(代码上就是什么都不做)
那么,该如何处理故障后发来的用户请求呢?因为ublk位于通用块设备层,它需要实现->queue_rq()接口,每来一个blk-mq请求,->queue_rq()都会被调用一次。现有的ublk实现的ublk_queue_rq()函数会通过io_uring passthrough机制的io_uring command通知用户态daemon有新的请求需要处理(详情看ublk简介)。在发生故障后,我们不能这么做,因为没有用户态daemon了,这里我们想了好几种方案:
-
V1:在ublk_queue_rq()中,请求被放到一个链表 pending_list 中,然后等到新的daemon起来后,把pending list中的请求都下发下去。这种思想是很直接的,但在代码实现时,遇到了很大的困难:由于ublk使用io_uring passthrough机制,每个blk-mq请求都对应一个io_uring command,而这个cmd必须是用户态daemon事先发给内核(io_uring sqe),然后在ublk_queue_rq(),发送io_uring cqe。这使得我们“刷”pending list的时机很难控制:我们要在新的daemon把所有的(队列深度个)io_uring command都发给内核ready后,才能“刷”pending list。然而,倘若只有一部分cmd ready新daemon又crash了该怎么办呢?我们还要考虑旧的cmd的释放问题(因为旧的daemon已经退出了,但是它发给内核的cmd还没被内核返回cqe,导致io_uring ctx在内核没被释放)避免内存泄漏……总之,这种pendling list的方案看上去直接,但很难写出代码,我们卡了一个月有余……在某次浏览其他块设备驱动代码时,笔者突然发现了新曙光。
-
V2:blk-mq提供了blk_mq_freeze_queue()和 blk_mq_quiesce_queue() 函数,顾名思义两者都能让请求下发“暂停”,其功能的差异本文不予赘述。总之,在旧daemon退出后,monitor_work可以调用blk_mq_quiesce_queue(),阻止->queue_rq()的调用,这样新请求就进不来了。这里可能有人要问: 那前端业务会不会不能提交IO,导致业务卡住啊? 经过代码分析,笔者认为不会卡住IO下发,因为blk-mq在接受到上层提交IO(如submit_bio)后,会先让它们排队,队列深度是用户自己设置的(比如128)。当队列quiesced后,请求都留在blk-mq的队列里,在调用 blk_mq_quiesce_unqueue() 后,再对每个请求都->queue_rq()一下。其实 blk_mq_quiesce_queue() 已经帮我们完美地做了一个pending list了,所以以后还是要先积累知识,多看代码,不可贸然下手啊……你能想到的问题,别人早就踩过坑了……
解决了上述“故障后到来的请求该如何处理”问题后,我们又突然想到一类请求没有处理:“故障到来前已经发给旧daemon,但daemon挂了所以我也不知道他做完了没”,为此,我们设计了UBLK_F_USER_RECOVERY_REISSUE标记,用户可选地设置该feature。新daemon关联到/dev/ublkb*后,这类请求会被重新下发一次,因此要求后端的分布式存储有处理重复写入的能力。在代码上,若开启该标记,monitor_work会blk_mq_requeue_request请求(此时是quiesced状态,不会发生->queue_rq),否则直接blk_mq_end_request。可以说,UBLK_F_USER_RECOVERY_REISSUE是从实际业务出发而设计的。
启动新的daemon
这部分的设计简单了许多,我们考虑把恢复分为2阶段:START_RECOVERY和END_RECOVERY。在START_RECOVERY我们必须重置一些状态:比如ublk会记忆旧daemon的mm struct,task struct,都要清理掉;ublk自己的IO结构体有一些状态位,也置位初始。在END_RECOVERY阶段我们要等待新的daemon把所有的io_uring command都下发完成后,unquiesce队列,然后就可以愉快地ublk_queue_rq()并把IO透传给后端分布式存储了!之所以设计成两阶段,一是为了用户方便用(用户自己发START_RECOVERY,启动新的daemon,再发END_RECOVERY,几个阶段很清晰);二是简化了开发(因为ublk记录很多用户态daemon的信息,START_RECOVERY释放并重置信息,END_RECOVERY确认新信息)。
总结
ublk故障恢复是笔者在ublk上开发的第一个大型特性,也是第一次接触block层的代码。由于不熟悉block的实现,我们走了很多弯路,幸好在社区的指导和自我努力下,我们完成了设计与编码,历时3月有余。我们积累了很多Linux知识(如linux work的原理,io_uring ctx的管理,gendisk的管理,block层的API),并为ublk在分布式存储的应用扫清了道路。
参考文献
【1】 https://lwn.net/Articles/906097/
【2】https://developer.aliyun.com/article/989552
【3】https://lore.kernel.org/all/20220523131039.17697-1-ankit.kumar@samsung.com/
【4】https://lore.kernel.org/all/8a52ed85-3ffa-44a4-3e28-e13cdc793732@linux.alibaba.com/
【5】https://github.com/ming1/ubdsrv
【6】https://lore.kernel.org/all/20220713140711.97356-1-ming.lei@redhat.com/
【7】https://lore.kernel.org/all/20220923153919.44078-1-ZiyangZhang@linux.alibaba.com/