news 2026/10/13 5:35:54

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux块层架构演变:从单队列到blk-mq多队列深度解析

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-mq(multi-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 是什么,为什么需要它

BIO(Block 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 调度器怎么选:一个实操判断流程

选调度器不用纠结,按这个流程走基本不会错:

  1. 先看设备类型。NVMe 或高性能 SSD,直接上none或kyber
  2. 机械盘或混合场景,用mq-deadline
  3. 桌面环境、多任务交互明显卡顿,试bfq
  4. 拿不准就用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 的完整路径串一遍,方便建立整体认知:

  1. 应用调用write(),进入文件系统或直接 IO 路径
  2. 构造 BIO,提交到块层
  3. BIO 尝试合并进当前 CPU 的 plug 链表里的 request
  4. plug 触发 unplug,request 进入软件队列
  5. 调度器从软件队列取出 request,申请硬件队列 tag
  6. request 下发到硬件队列,驱动消费
  7. 驱动完成 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 的iodepth=1模式。

坑三:把块层问题和驱动问题混为一谈。块层负责调度和合并,驱动负责实际硬件操作。如果 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 和更轻量的调度。理解这条主线,比记住具体参数更重要,因为参数会变,但"块层在翻译上下层需求"这个本质不会变。我自己每次遇到新的存储设备或新的性能问题,都是回到这条主线上重新推一遍,基本都能找到方向。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 5:35:51

数字图像处理中的对比度增强:从直方图到CLAHE实战

1. 对比度到底在调什么&#xff1a;从一张“发灰”的照片说起很多人第一次接触数字图像处理&#xff0c;都是从一张“发灰”的照片开始的。明明拍摄现场光线不错&#xff0c;相机屏幕上看也还行&#xff0c;可导出到电脑上一看&#xff0c;整个画面像蒙了一层雾&#xff0c;黑的…

作者头像 李华
网站建设 2026/10/13 5:34:27

Carla 0.9.15 zlib-1.2.13 编译避坑指南:ABI兼容与符号导出实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 5:34:21

手写Tool Use面试题:从函数到生产级工具调度系统

1. 面试现场的真实压力&#xff1a;当“手写 Tool Use”成为临场能力试金石“请手写一个 Tool Use 实现。”这句话从面试官嘴里说出来的时候&#xff0c;我正端着一次性纸杯喝第三口温水——不是因为紧张&#xff0c;而是前两轮算法题刚结束&#xff0c;嗓子发干。但这句话一落…

作者头像 李华
网站建设 2026/10/13 5:34:12

显示驱动板卡启动引导流程详解:从电源时序到点亮屏幕

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 5:34:05

内网离线安装GCC指南:rpm包依赖解析与本地yum源搭建

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 5:34:04

STM32F407架构深度解析:总线矩阵、存储映射与DMA实战避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华