资讯动态

Linux块层架构演变:从单队列到blk-mq多队列深度解析

发布时间:2026/10/13 5:35:56 来源:尧图企业网站定制
1. 块层到底在管什么从一次磁盘告警说起前阵子帮朋友排查一台文件服务器的性能问题现象很典型iostat里%util长期贴着 100%但吞吐量只有理论值的零头await高得离谱。第一反应是盘不行了换盘、换线、换背板折腾一圈没变化。最后用blktrace抓了一段 trace 才看明白问题根本不在硬件而在块层——上层应用发下来的是大量 4K 随机写而底层设备是机械盘块层的合并和调度策略没调对请求在队列里排成了长龙。这件事让我重新把块层Block Layer的架构从头捋了一遍。很多人做存储、做数据库、做虚拟化天天跟块设备打交道但对中间这一层的演进其实只有一个模糊印象知道有 BIO、有 request、有mq-deadline、有blk-mq但说不清它们之间怎么串起来更说不清为什么 Linux 要花十几年把这一层反复重构。这篇就围绕Linux 块设备之 Block Layer 层架构演变这个主题把我自己踩过的坑、读过的代码、做过的压测整理出来。内容会覆盖从单队列到多队列的演进逻辑、BIO 与 request 的分层设计、plug 机制、调度器的取舍以及blk-mq之后软件队列和硬件队列怎么协作。适合做存储栈、内核、数据库调优、虚拟化这块的同行参考也适合刚接触块设备、想搞明白一个 write 到底怎么落到盘上的朋友。先给个全局印象块层是文件系统和具体块设备驱动之间的一层抽象。往上它要接住文件系统、直接 IO、页缓存回写发下来的 IO往下它要把这些 IO 整理、合并、排序再交给驱动。它的核心矛盾从来没变过——上层希望 IO 越简单越好下层希望 IO 越整齐越好块层就是在这两者之间做翻译和调度。理解了这条主线后面所有的架构变化都能对上号。2. 从单队列到多队列块层演进的主线逻辑2.1 早期单队列模型为什么够用又为什么不够用最早的块层设计非常直观整个块设备只有一个请求队列request queue所有 CPU 发下来的 IO 都往这个队列里塞然后由一个调度器决定顺序最后逐个下发给驱动。这个模型在机械盘时代是合理的因为机械盘本身就是串行设备——磁头只有一个你并发再多物理上也只能一个一个寻道。单队列反而帮了大忙它把随机 IO 排成近似顺序 IO减少了寻道次数。但问题随着两件事爆发了。第一是SSD 普及固态盘内部是多通道并行的能同时处理几十甚至上百个请求单队列成了瓶颈队列锁竞争严重CPU 大量时间花在抢queue_lock上。第二是多核服务器几十上百个核同时发 IO全挤到一个队列上锁争用直接把扩展性打死。我实测过一台 64 核机器跑 fio 随机读单队列模式下核数加到 16 以后 IOPS 基本不涨了perf一看全耗在__blk_run_queue的自旋锁上。所以单队列的根本矛盾是队列是全局共享资源而现代硬件和 CPU 都是并行的。这个矛盾不解决块层就永远是扩展性天花板。2.2 blk-mq 的核心思想每个 CPU 一个软件队列blk-mqmulti-queue block layer的解法很干脆既然一个队列抢得厉害那就拆成多个。它为每个 CPU或每组 CPU分配一个软件队列software queue每个软件队列有自己的锁CPU 往自己的队列里塞 IO几乎无竞争。然后这些软件队列再映射到设备真实的硬件队列hardware queue上由驱动去消费。这里有个关键设计点值得说清楚软件队列的数量和硬件队列的数量是解耦的。软件队列通常按 CPU 数量来nr_hw_queues和nr_sw_queues是两回事硬件队列数量由设备决定比如 NVMe 盘可能支持几十个队列每个队列对应一个提交/完成通道。映射关系由块层维护一个硬件队列可能对应多个软件队列。这种设计带来的直接好处是提交路径上的锁竞争基本消失。每个 CPU 操作自己的软件队列只有在往硬件队列 flush 的时候才需要协调。我在同一台 64 核机器上再测blk-mq模式下 IOPS 能随核数线性涨到 32 核左右才趋缓提升非常明显。2.3 演进时间线几个关键节点把演进过程按时间捋一下方便建立坐标系阶段关键特征解决的问题遗留问题单队列时代全局 request queue IO 调度器机械盘寻道优化多核扩展性差过渡期per-CPU plug 批量下发减少队列锁持有时间队列本身仍是瓶颈blk-mq 引入软件队列 硬件队列分离锁竞争、多核扩展调度器需重写blk-mq 调度器mq-deadline/bfq/kyber多队列下的 IO 排序复杂度上升持续优化tag 机制、IO polling降低延迟、提升吞吐调优门槛变高这张表不是学术分期是我自己理解时用的一个粗框架。真正写代码的时候你会发现很多机制是叠加存在的比如 plug 机制在blk-mq时代依然在用只是实现变了。3. BIO 与 request块层的两级抽象怎么配合3.1 BIO 是什么为什么需要它BIOBlock IO是块层最上层的抽象可以理解成一个 IO 请求的描述符。文件系统要读一个页或者应用做直接 IO最终都会构造一个 BIO里面记录了目标设备、起始扇区、长度、方向读/写、以及数据页的地址。为什么要有 BIO 这一层因为上层发下来的 IO 形态是碎片化的。文件系统可能一次提交几十个不连续的页直接 IO 可能一次几 MB 但跨了不连续的扇区。BIO 把这些描述清楚但不做合并、不做排序它只是一个忠实的搬运工。BIO 提交后进入块层块层再决定怎么处理。这里有个容易混淆的点BIO 不等于一个物理 IO。一个 BIO 可能被合并进已有的 request也可能被拆成多个 request。BIO 是逻辑请求request 才是物理请求。3.2 request 的诞生合并与拆分BIO 进入块层后会尝试合并到已有的 request 里。合并分两种前向合并新 BIO 的结束扇区正好接上某个 request 的起始扇区后向合并新 BIO 的起始扇区正好接上某个 request 的结束扇区合并成功的话多个 BIO 就变成一个更大的 request下发给驱动时就是一次更大的 IO效率更高。合并失败就新建一个 request。如果 BIO 太大超过设备能处理的最大请求大小max_sectors_kb还要拆成多个 request。我踩过一个坑有次调一个自研块设备驱动发现大块顺序写性能特别差抓 trace 发现每个 BIO 都被拆成了 4K 的小 request。查了半天是驱动里max_sectors设得太小块层只能按这个上限拆。把max_sectors_kb调大之后顺序写吞吐直接翻倍。这个参数在/sys/block/dev/queue/下面能看能改调优时值得先确认一下。3.3 plug 机制批量下发的关键plug机制是块层里一个很巧妙的设计理解它对理解整个提交路径很关键。简单说plug 就是先攒一攒再发。当上层开始发 IO 时块层先把请求攒在 per-CPU 的 plug 链表里不急着往硬件队列送等攒到一定量或者显式 unplug 时再一次性批量下发。为什么要这么做因为每次往硬件队列送请求都有开销拿锁、更新 tag、通知驱动。如果来一个发一个开销占比很高。批量下发能把这部分开销摊薄。而且批量下发时块层还能做一次全局的合并和排序效果比单个处理好得多。plug 的触发 unplug 的时机有几个plug 链表满了、显式调用blk_finish_plug、或者调度器判断该 flush 了。文件系统在提交一批 IO 时通常会显式 plug/unplug 包起来这也是为什么文件系统的批量写往往比裸直接 IO 更高效的原因之一。提示如果你在做性能分析看到 IO 下发有明显的一阵一阵的批量特征大概率就是 plug 机制在起作用这是正常现象不是抖动。4. IO 调度器多队列时代的选择与取舍4.1 调度器到底在调度什么IO 调度器的职责是在请求下发给驱动之前决定先发哪个、后发哪个、要不要合并。它的目标通常有三个而且互相冲突吞吐尽量合并成大 IO减少寻道或通道切换延迟让每个请求尽快被处理别饿死公平多个进程竞争时按权重分配带宽机械盘时代调度器主要优化寻道所以有了经典的电梯算法elevator。SSD 时代寻道不再是问题调度器的重点转向了延迟控制和公平性。4.2 主流调度器对比现在内核里常见的几个调度器我按实际使用体验整理一下调度器适用场景核心机制我的使用建议mq-deadline通用、单盘读优先 截止时间默认首选机械盘和 SSD 都稳kyber低延迟设备按延迟目标限流NVMe 上表现好配置简单bfq桌面、多任务按进程权重公平调度桌面交互场景体验好服务器慎用none高性能 SSD不排序直接下发NVMe 高并发场景延迟最低none这个调度器名字容易让人误解以为它什么都不做。其实它只是不做排序但合并和 tag 管理还是有的。在 NVMe 这种本身并行度极高的设备上none往往是最优解因为任何排序都是多余开销。我做过对比测试同一块 NVMe 盘none比mq-deadline在 4K 随机读上延迟低 15% 左右。4.3 调度器怎么选一个实操判断流程选调度器不用纠结按这个流程走基本不会错先看设备类型。NVMe 或高性能 SSD直接上none或kyber机械盘或混合场景用mq-deadline桌面环境、多任务交互明显卡顿试bfq拿不准就用mq-deadline它是兼容性最好的切换方法很简单# 查看当前调度器 cat /sys/block/nvme0n1/queue/scheduler # 切换为 none echo none /sys/block/nvme0n1/queue/scheduler注意切换调度器会短暂影响在途 IO生产环境建议在低峰期操作并且先在小流量设备上验证。5. 软件队列与硬件队列的协作细节5.1 tag 机制硬件队列的号码牌硬件队列的深度是有限的比如 NVMe 队列深度可能是 1024。块层怎么知道某个硬件队列还有没有空位靠的就是tag。每个硬件队列维护一组 tag提交请求前先申请一个 tag拿到才能发请求完成后释放 tag。tag 机制解决了两个问题一是流控队列满了就等待不会把驱动压垮二是完成匹配驱动完成 IO 后通过 tag 就能找到对应的 request不用额外维护映射表。blk-mq的 tag 分配是 per 硬件队列的而且做了无锁优化高并发下开销很小。这也是blk-mq相比单队列的一个重要改进点。5.2 提交路径的完整走位把一次写 IO 的完整路径串一遍方便建立整体认知应用调用write()进入文件系统或直接 IO 路径构造 BIO提交到块层BIO 尝试合并进当前 CPU 的 plug 链表里的 requestplug 触发 unplugrequest 进入软件队列调度器从软件队列取出 request申请硬件队列 tagrequest 下发到硬件队列驱动消费驱动完成 IO通过 tag 回调块层释放资源这条路径里软件队列到硬件队列的映射是blk-mq的核心。一个软件队列可能对应多个硬件队列块层会根据负载做分发。这个分发策略对性能影响不小尤其是在多 NUMA 节点机器上跨节点的队列访问会带来额外延迟。5.3 NUMA 感知容易被忽略的优化点在多路服务器上块层的队列映射是 NUMA 感知的。软件队列会尽量映射到本地 NUMA 节点的硬件队列减少跨节点内存访问。这个优化在普通机器上感知不到但在多路大内存机器上配置不当会导致明显的延迟抖动。我遇到过一次一台双路机器NVMe 盘插在 CPU0 的 PCIe 槽上但业务主要跑在 CPU1 上结果 IO 延迟比预期高不少。后来通过绑核和调整中断亲和性把 IO 相关的中断和处理都收敛到 CPU0 附近延迟才降下来。这类问题在块层本身看不出来得结合 NUMA 拓扑一起分析。6. 常见问题与排查技巧实录6.1 块层问题排查速查表现象可能原因排查手段解决方向IOPS 上不去队列锁竞争perf 看自旋锁确认是否启用 blk-mq延迟高且抖动调度器不合适对比不同调度器换 none 或 kyber大块写性能差max_sectors 太小看 queue 参数调大 max_sectors_kb队列长期满硬件队列深度不足看 tag 使用率换更高队列深度设备多核扩展差中断亲和性差看 /proc/interrupts调整中断绑核6.2 几个我踩过的坑坑一盲目相信默认调度器。内核默认调度器是跟着发行版走的不一定适合你的负载。有次在一台全 NVMe 的机器上默认是mq-deadline换成none之后随机读 IOPS 涨了 20%。默认值只是不会错不是最优。坑二忽略 plug 的影响。做延迟测试时如果测试工具本身用了 plug 批量提交测出来的延迟是批量延迟不是单 IO 延迟。要测真实单 IO 延迟得用O_DIRECT加单线程同步写或者用 fio 的iodepth1模式。坑三把块层问题和驱动问题混为一谈。块层负责调度和合并驱动负责实际硬件操作。如果 trace 显示 request 已经下发到驱动但迟迟不完成问题在驱动或硬件如果 request 在软件队列里排队很久问题在调度器或队列深度。分清楚这两段排查效率会高很多。6.3 blktrace 使用要点blktrace是块层排查的利器但用起来有几个要点# 抓取指定设备的 trace输出到当前目录 blktrace -d /dev/nvme0n1 -o trace # 解析成可读格式 blkparse -i trace -o trace.txt # 只看关键事件 blkparse -i trace -f %T %a %d %s %n看 trace 时重点关注几个事件Q请求入队、G获取 request、I插入调度器、D下发驱动、C完成。从Q到D的时间是块层内部耗时从D到C是驱动和硬件耗时。这两段一分开问题定位就清晰了。提示blktrace 本身有开销高 IOPS 场景下可能影响被测系统建议短时间抓取或者用硬件 trace 方案。7. 架构演变带来的实际影响与调优建议7.1 对上层应用的影响块层从单队列演进到blk-mq对上层应用最直接的影响是并发模型变了。单队列时代应用开太多并发线程反而会因为锁竞争降低性能blk-mq时代并发度可以放心提上去直到打满硬件队列深度。所以老一代的IO 线程数不要超过核数的经验在blk-mq下要重新评估。数据库这块感受特别明显。以前调 MySQL 的innodb_io_capacity要小心翼翼怕并发太高把块层锁打爆。现在 NVMe blk-mq环境下这个值可以设得激进很多因为块层能扛住。7.2 对虚拟化的影响虚拟化场景下块层要处理的是虚拟块设备到物理块设备的映射。virtio-blk和virtio-scsi这些前端驱动最终都要经过宿主机块层。blk-mq之后virtio 也做了多队列支持每个 vCPU 对应一个 virtqueue减少了虚拟化层的锁竞争。如果你在跑虚拟机集群确认一下 virtio 多队列有没有开对 IO 性能影响很大。7.3 调优的几个实用建议最后给几条我自己验证过的调优建议先确认队列模型ls /sys/block/dev/mq/有内容说明是blk-mq没有就是老单队列调度器按设备选NVMe 用none机械盘用mq-deadline别一刀切队列深度要匹配nr_requests和硬件队列深度要协调太小浪费硬件能力太大增加延迟中断亲和性别忽略多核机器上把块设备中断绑到固定核减少 cache 抖动压测要分层先测裸设备再测文件系统最后测应用一层层定位瓶颈块层这十几年的演变本质上是在追着硬件跑。硬件从串行变并行块层就从单队列变多队列硬件延迟从毫秒降到微秒块层就引入 polling 和更轻量的调度。理解这条主线比记住具体参数更重要因为参数会变但块层在翻译上下层需求这个本质不会变。我自己每次遇到新的存储设备或新的性能问题都是回到这条主线上重新推一遍基本都能找到方向。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑