ublk故障恢复方案设计

简介: 简介在ublk简介一文中我们介绍了Linux社区新提出的ublk:基于io_uring的全新高性能用户态块设备。ublk已经合入Linux 6.0主线,并且操作系统团队已经在ublk上开展了一系列实践,并在2022年中国LInux内核开发者大会上分享了阿里云在ublk上的实践。为了适配阿里云的分布式云存储产品,我们对ublk添加了NEED_GET_DATA特性和故障恢复特性。前者对数据拷贝进行优化

简介

在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/

相关实践学习
快速体验阿里云云消息队列RocketMQ版
本实验将带您快速体验使用云消息队列RocketMQ版Serverless系列实例进行获取接入点、创建Topic、创建订阅组、收发消息、查看消息轨迹和仪表盘。
消息队列 MNS 入门课程
1、消息队列MNS简介 本节课介绍消息队列的MNS的基础概念 2、消息队列MNS特性 本节课介绍消息队列的MNS的主要特性 3、MNS的最佳实践及场景应用 本节课介绍消息队列的MNS的最佳实践及场景应用案例 4、手把手系列:消息队列MNS实操讲 本节课介绍消息队列的MNS的实际操作演示 5、动手实验:基于MNS,0基础轻松构建 Web Client 本节课带您一起基于MNS,0基础轻松构建 Web Client
相关文章
|
存储 自然语言处理 固态存储
ublk:来自Linux社区的新热点,基于io_uring的全新高性能用户态块设备
如果您想快速了解ublk的意义、作用及性能,请直接看第二节Q&A部分。一、简介用户态块设备,就是提供/dev/ublkbX这样的标准块设备给业务,业务读写这个块的实际IO处理由您编写的用户态的代码决定。这就好比您使用FUSE,所有对挂载于FUSE的目录的读写都是您编写的IO handler来处理一样。使用用户态块设备,您可以方便地向上层业务以块设备/dev/ublkbX的形式提供您的自定义
|
存储 监控 关系型数据库
B-tree不是万能药:PostgreSQL索引失效的7种高频场景与破解方案
在PostgreSQL优化实践中,B-tree索引虽承担了80%以上的查询加速任务,但因多种原因可能导致索引失效,引发性能骤降。本文深入剖析7种高频失效场景,包括隐式类型转换、函数包裹列、前导通配符等,并通过实战案例揭示问题本质,提供生产验证的解决方案。同时,总结索引使用决策矩阵与关键原则,助你让索引真正发挥作用。
934 0
|
存储 弹性计算 固态存储
三分钟磁盘存储性能IOPS、I/O及吞吐量指标详解
什么是磁盘I/O?存储IOPS是什么?磁盘性能指标吞吐量是指什么?
18825 39
|
消息中间件 存储 缓存
Kafka为什么是高性能高并发高可用架构
Kafka为什么是高性能高并发高可用架构
1035 0
|
存储 人工智能 PyTorch
【AI系统】张量并行
在大模型训练中,单个设备难以满足需求,模型并行技术应运而生。其中,张量并行(Tensor Parallelism, TP)将模型内部的参数和计算任务拆分到不同设备上,特别适用于大规模模型。本文介绍了张量并行的基本概念、实现方法及其在矩阵乘法、Transformer、Embedding和Cross Entropy Loss等场景中的应用,以及通过PyTorch DeviceMesh实现TP的具体步骤。
1875 11
【AI系统】张量并行
|
Linux Windows 内存技术
PCIe初始化枚举和资源分配流程分析
本文主要是对PCIe的初始化枚举、资源分配流程进行分析,代码对应的是alikernel-4.19,平台是arm64 ## 1. PCIe architecture ### 1.1 pcie的拓扑结构 在分析PCIe初始化枚举流程之前,先描述下pcie的拓扑结构。 如下图所示: ![11.png](https://ata2-img.oss-cn-zhangjiakou.aliyuncs
9923 1
PCIe初始化枚举和资源分配流程分析
|
Java 开发者
javafx jlink 遇到的非模块化的依赖打包报错“模块异常”的问题和处理
【9月更文挑战第18天】在使用JavaFX的jlink进行应用打包时,非模块化依赖可能导致“模块异常”报错。此文档详细分析了该问题的原因,并提供了四种解决方案:模块化依赖、自动模块转换、手动创建模块描述符及检查模块依赖关系。通过这些方法,可以有效解决此类问题,提高项目的可维护性和扩展性。建议开发者优先选用模块化设计。
1569 1
|
缓存 监控 算法
Linux内核的SLAB内存管理机制
Linux内核的SLAB内存管理机制
1460 4