news 2026/10/27 14:51:33

Linux IO调度器选型与调优:原理、参数与生产实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux IO调度器选型与调优:原理、参数与生产实践

前阵子帮人排查一个写入抖动的问题:一台跑日志采集的机器,平时写入很平稳,但每到整点批量落盘那几分钟,单次写入延迟就从几百微秒窜到几十毫秒,上层业务开始零星报超时。我先看了磁盘健康度、文件系统挂载参数、RAID卡缓存策略,都没查出毛病,最后翻到/sys/block/*/queue/scheduler才反应过来——这台机器的 IO 调度器还停在默认值上,而它的负载模型跟这个默认值假设的场景压根不是一回事。换掉调度器、顺手调了两个参数之后,整点的延迟尖峰从 40ms 压到了 6ms 左右。

这件事让我意识到,Linux IO 优化里最容易被忽略、又最容易被"玄学调参"带偏的一环,就是调度器。它不像文件系统参数那样有官方文档反复强调,也不像缓存命中率那样在监控面板上一眼可见,很多人对它只有一个模糊印象:机械盘用 bfq、固态盘用 none,完事。但实际生产环境里,这个结论至少有一半场景是错的,而且错得很隐蔽——你不会看到报错,只会看到延迟曲线莫名其妙地抖。

这篇内容我想把 Linux IO 调度器这层拆开讲清楚:它到底在块层的哪个位置干活、几种主流调度器各自在赌什么、怎么查现状怎么切、参数怎么算、什么业务该配什么。适合已经能看懂iostat输出、但面对queue/scheduler里那一串选项不知道该选谁的运维和开发同学,也适合刚接触性能调优、想搞清楚"为什么我的固态盘换了调度器反而更慢"的朋友。全文基于常见生产实践整理,涉及具体数值的地方我会说明推导过程,你按自己的硬件和负载再校一遍。

1. 调度器到底在块层的哪一环,为什么它值得单独调

1.1 一次写请求从应用到磁盘的完整路径

要理解调度器,得先知道它在链条上的位置。应用调用write(),数据先进内核页缓存,标记为脏页;内核的回写线程或者fsync触发真正的落盘,文件系统把这段写入翻译成一个个 bio(块层的基本 IO 单元),每个 bio 描述"从某块设备的某个偏移开始,写多长数据";bio 提交到块层之后,会进入设备对应的请求队列。调度器就是站在这个队列门口的"分拣员":它决定这些请求以什么顺序、以多大的批次、在队列里待多久之后,被放进设备驱动,最终由硬件执行。

这里有个容易混淆的点:调度器不等于缓存,也不等于限流。它看不到文件,只看到块号;它不知道哪个请求来自数据库、哪个来自日志,只能根据读/写、同步/异步、块号连续性这些有限信息做判断。所以指望调度器解决所有 IO 问题是不现实的,它能做的是在请求已经产生、硬件还没执行的这个窗口里,尽量把顺序排得对硬件友好一点。

另外要明确,调度器只对走块层的 IO 生效。裸设备读写、文件系统的普通读写、O_DIRECT直读直写,都会经过块层,受调度器影响;而某些绕过块层的路径(比如部分设备自带的直通接口)就不在它的管辖范围内。你在做调优之前,最好先确认自己的业务 IO 到底走不走这条链路,不然调了半天可能调了个寂寞。

1.2 调度器的三个动作:合并、排序、限流

不管哪种调度器,核心动作基本就三个,理解了这三个动作,后面看各种调度器的差异就顺了。

合并是把相邻或者重叠的请求拼成一个更大的请求。两个 4KB 的连续写,合并成一个 8KB 下发,硬件只需要处理一次寻址,效率自然更高。合并分两种:前向合并(新请求接在已有请求后面)和后向合并(接在前面)。合并的收益在机械盘上极其明显,因为省下的是物理寻道时间;在闪存盘上收益小很多,但仍然能减少命令数量。

排序是调整请求的下发顺序。机械盘的磁头是物理移动的,从外圈跳到内圈要花几毫秒,所以按块号升序排列、让磁头一路扫过去(也就是电梯算法)能大幅减少总寻道距离。但排序会带来一个问题:排在后面的请求可能一直等不到执行,也就是"饥饿"。所以几乎所有调度器都要在排序和公平之间做权衡。

限流是控制单位时间内放多少请求进硬件。这一条在多队列架构和高速设备上变得特别重要,因为设备本身太快了,瓶颈往往不在磁盘而在下游(比如后端存储、网络盘、写回压力)。限流做得好,能把延迟曲线压平;做得不好,就是把吞吐换成了延迟,两头不讨好。

注意:这三个动作在不同调度器里的权重完全不同。none 基本只做合并(甚至不合并),mq-deadline 重点做排序加超时兜底,bfq 重点做公平分配,kyber 重点做限流。选调度器本质上就是选你更看重哪个动作。

1.3 机械盘与闪存盘的物理差异决定了策略分歧

为什么调度器的选择会跟硬件强绑定?因为机械盘和闪存盘的"痛点"完全是两回事。机械盘的随机 IO 代价极高,一次随机寻道大概几毫秒到十几毫秒,而顺序读能达到上百 MB/s,所以哪怕把随机请求重排成近似顺序,收益都是数量级的。这时候排序算法价值巨大,bfq 这种愿意花时间做复杂排队的调度器就有发挥空间。

闪存盘没有寻道概念,随机读写的延迟基本一致(当然实际还有垃圾回收、写放大、通道争用这些因素,但对上层来说随机和顺序的差距远小于机械盘)。它的队列深度可以做到很深,几百上千个请求同时在飞是常态。这时候你在软件层花 CPU 时间去排序、去算公平,反而成了额外开销,甚至可能因为串行化破坏了硬件本身的并行能力。所以闪存盘更倾向于"少管闲事",让请求尽快下发给硬件,由硬件自己的调度器去处理。

这个差异直接导致了一个经验法则:/sys/block/<dev>/queue/rotational这个值如果是 1,说明是旋转设备,适合带排序的调度器;如果是 0,说明是固态设备,优先考虑轻量调度器。但要注意,这个值只是设备上报的"自我声明",有些虚拟磁盘、RAID 逻辑卷会报错,不能完全信,得结合实际情况判断。

2. 主流调度器的原理拆解与选型对照

2.1 none:不是"不调度",是把决策权交出去

none(在老内核里叫 noop)经常被误解成"关掉调度"。它其实还在队列里,只是不做排序、不做公平分配、不做超时兜底,只保留最基本的合并动作,然后尽快把请求交给下层。你可以把它理解成一个"传送带":东西放上去就走,不排队不分拣。

这种极简策略在两类设备上特别合适。第一类是本身具备强大内部调度的设备,比如企业级 NVMe 固态盘,硬件队列深度动辄几百上千,内部有自己的芯片做请求调度和垃圾回收,软件层再插一手排序纯属添乱。第二类是根本不担心寻道的设备,比如内存盘、部分高速缓存设备。

我见过最常见的误用,是在机械盘上配 none。有人看到"固态盘用 none"这条经验,就顺手全站统一了,结果机械盘上的随机写延迟直接翻倍,因为原本能被合并重排的请求现在完全按提交顺序下发,磁头来回横跳。所以这条经验必须跟设备类型绑死,不能跨硬件套用。

另一个细节是,none 在不同内核版本里的行为略有差异。早期 noop 还会做一定程度的合并,后来改名为 none 之后,合并逻辑更加保守。如果你的内核比较新,看到的是 none 而不是 noop,这是正常的命名演进,功能定位一致,不用纠结。

2.2 mq-deadline:用截止时间兜住最坏延迟

mq-deadline 是我在生产环境里推荐频率最高的一个,尤其适合"既要吞吐、又不能有请求饿死"的通用场景。它的思路很直白:给每个请求挂一个截止时间,读请求默认 500ms,写请求默认 5000ms,然后维护读、写两个队列,按块号排序下发,同时盯着截止时间,快超时的请求插队优先处理。

为什么读写要用不同的截止时间?因为读通常是同步的,应用在等结果,卡住就是用户可感知的卡顿;写大多是异步的,进页缓存就算返回了,真正的落盘可以晚一点。所以读的截止时间给得紧,写的给得松,默认值 500ms 对 5000ms 就是这个逻辑的体现。

它的排序逻辑比 bfq 简单得多,开销小,同时又比 none 多了超时保护。这个平衡点让它在数据库、中间件、通用服务器上都很稳。我在跑关系型数据库的机器上做过对比,同样的随机写压力下,mq-deadline 的 P99 延迟比 none 低一截,因为 none 在队列堆积时会任由请求顺序下发,而后端存储在压力下响应变慢,先到的慢请求会拖住后面所有请求,形成队头阻塞。mq-deadline 的超时插入机制能缓解这个问题。

不过它也不是万能的。如果你的设备队列深度本来就很浅,或者 IO 压力极低,mq-deadline 的排序开销就是纯浪费,这时候 none 更划算。判断标准很简单:看iostat里的aqu-sz(平均队列长度),长期在 1 以下说明队列基本是空的,调度器的排序逻辑没多少活儿可干。

2.3 bfq:把"公平"做成可感知的响应速度

bfq(Budget Fair Queueing)是从单队列时代的 cfq 演化来的,核心目标不是最小化总耗时,而是让多个进程之间的 IO 带宽分配得公平,尤其是保证交互式任务的响应速度。它给每个进程(准确地说是每个 cgroup 或者每个 IO 上下文)分配一个预算,按预算轮转,谁的预算用完谁就让位。

这种设计在桌面环境里效果非常直观:你在后台跑一个大文件拷贝,同时打开浏览器、拖动窗口,系统不会卡成幻灯片。因为 bfq 会优先照顾那些请求量小但需要及时响应的进程。这也是为什么很多面向桌面的发行版会把它设成默认值。

但放到服务器上,bfq 就要谨慎了。它的调度逻辑比 mq-deadline 复杂,CPU 开销更高,在万级 IOPS 的场景下,调度器本身可能吃掉可观的 CPU。而且它的公平性假设是"多个进程竞争同一块盘",如果你的业务是单进程高并发(比如一个多线程的存储服务),公平调度的收益就很有限,反而多了一层开销。

我的经验是:bfq 适合机械盘 + 多任务交互的场景,或者你确实需要按进程/cgroup 做 IO 带宽隔离。如果是纯 SSD 服务器、单一大流量业务,用 bfq 大概率是负优化。另外要注意,bfq 在高队列深度下的表现不如 mq-deadline 稳定,我在压测里见过 bfq 在 iodepth 超过 64 之后延迟曲线明显翘尾的情况。

2.4 kyber:为高速设备准备的轻量限流器

kyber 相对小众,但设计思路很有意思。它不排序、不做复杂的公平分配,只维护读和写两个队列,然后用令牌桶的方式控制放行速率,目标是让请求的延迟贴近设定的目标值。读的默认延迟目标是 2ms,写是 10ms,通过动态调整队列深度来逼近这个目标。

它的适用场景是"设备很快、但下游可能成为瓶颈"的情况。比如一块高性能 NVMe,本身能扛几十万 IOPS,但如果后端是网络存储或者有写回限流,盲目把请求全推下去只会让队列越堆越长,延迟越来越离谱。kyber 通过限制在途请求数量,把延迟控制在可预期范围内,牺牲一点峰值吞吐换稳定的响应。

我在几台全闪存节点上试过 kyber,效果介于 none 和 mq-deadline 之间:随机读的 P99 比 none 稳,但吞吐略低;顺序大块写的表现反而不如 none,因为限流对顺序写这种本来就高效的负载来说是多余的约束。所以它的定位很明确——不是通用选择,而是特定瓶颈下的调节工具。

用它之前建议先确认内核支持情况,不同版本对 kyber 的实现和默认参数有过调整,参数名也可能有差异,ls /sys/block/<dev>/queue/iosched/看一眼实际有哪些文件最靠谱。

2.5 单队列时代的 cfq 与 deadline,为什么现在不用了

现在还能在一些老系统上看到 cfq 和 deadline,这两个是单队列块层的产物。单队列的意思是整个设备只有一个请求队列,所有 CPU 核心的 IO 都要往这一个队列里塞,靠一把锁保护。核数少的时候没问题,核数一多,这把锁就成了瓶颈,队列本身反而成了性能天花板。

cfq 是当年的"完全公平队列",思路和 bfq 一脉相承,但实现更重。它在机械盘时代是不错的选择,到了多核 + 闪存的时代就明显吃力了。deadline 是单队列版的截止时间调度器,逻辑和 mq-deadline 类似,但同样受限于单队列架构。

后来块层重构为多队列架构(blk-mq),每个 CPU 有独立的软件队列,再映射到硬件的提交队列,锁竞争基本消除。老的 cfq 和 deadline 在这套架构下没法直接复用,于是有了 bfq 和 mq-deadline 这两个"多队列版本"。所以在比较新的内核上,你在scheduler文件里根本看不到 cfq,这是正常的,不是被裁掉了,是架构换代了。

判断自己的系统是哪套架构很简单:scheduler文件里出现mq-deadline、kyber、bfq、none这组,说明是多队列;出现cfq、deadline、noop这组,说明是单队列,内核比较老。这个判断在你决定用什么调优手段之前必须先做,因为两套架构的参数路径和可用选项都不一样。

2.6 一张表看清怎么选

调度器核心机制适合设备适合负载主要代价
none只做基础合并,快速下发NVMe、虚拟盘、高速闪存高并发随机 IO、队列深度大的场景无超时保护,慢请求会拖累后续
mq-deadline按块号排序 + 读写截止时间通用 SSD、机械盘均可数据库、中间件、混合读写排序有少量 CPU 开销
bfq按预算轮转,进程级公平机械盘为主桌面交互、多进程竞争、cgroup 隔离CPU 开销较高,高队列深度表现一般
kyber令牌桶限流,逼近延迟目标高速闪存后端有瓶颈、需要压平延迟曲线峰值吞吐受限,顺序写收益为负

这张表只是起点,实际选型还得看你的设备队列深度、业务 IO 模型、以及监控里反映出来的瓶颈位置。我建议的做法是:先按表选一个候选,用 fio 做基准对比,再上业务压测验证,最后才改生产配置。跳过中间步骤直接改线上,出了问题很难归因。

3. 从查看现状到在线切换的完整实操

3.1 先确认设备走的是单队列还是多队列

动手之前先摸底。第一步看设备名,lsblk或者ls /sys/block/列出所有块设备,注意区分物理盘和逻辑卷,dm-开头的是设备映射器(LVM、加密卷等),md开头的是软 RAID,nvme0n1是 NVMe,sda、vdb这类可能是 SATA/SAS 或虚拟盘。

第二步看队列架构。执行ls /sys/block/<dev>/mq/,如果这个目录存在并且里面有内容,说明是多队列架构;如果报"No such file",那多半是单队列。另一个更直接的判断是看scheduler文件里的候选列表,前面提过了,候选里带mq-前缀的就是多队列。

第三步看设备类型。cat /sys/block/<dev>/queue/rotational,1 是旋转设备,0 是固态或虚拟设备。这里要留个心眼:云主机的虚拟盘经常报 1,但它底下可能是全闪存存储池,按机械盘去调反而错。这种情况我一般会结合实测延迟判断——如果随机 4K 读的延迟在几百微秒量级,那基本可以当固态盘对待。

第四步看队列深度。cat /sys/block/<dev>/queue/nr_requests是软件队列能挂的请求数,cat /sys/block/<dev>/queue/max_sectors_kb是单次请求的最大扇区数。这两个值会影响调度器的发挥空间,后面调参时会用到。

3.2 查看当前调度器与可选列表

查看命令就一条:

cat /sys/block/sda/queue/scheduler

输出长这样:

[mq-deadline] kyber bfq none

方括号包着的就是当前生效的那个,其余是这块设备支持的可选项。注意每块设备的候选列表可能不一样,NVMe 盘上有时只有[none] mq-deadline,因为内核在编译时或者驱动层做了裁剪,不是所有调度器都对所有设备开放。

如果你想一次性看全机器上所有块设备的情况,可以跑:

for d in /sys/block/*/queue/scheduler; do echo -n "$d: "; cat $d; done

输出会很直观地告诉你哪块盘用了什么。我在接手一台新机器的时候习惯先跑这一条,经常能发现同一台机器上盘和盘之间配置不一致的情况,比如系统盘是 none、数据盘是 mq-deadline,这种不一致本身不一定是错,但至少要知道为什么。

3.3 在线切换与验证

切换也是写文件,把目标调度器的名字写进去就行:

echo mq-deadline > /sys/block/sda/queue/scheduler

不需要重启,不需要卸载文件系统,对正在跑的 IO 也基本无感(切换瞬间队列里的请求会被重新归类,可能有极短暂的抖动,但通常感知不到)。切完再cat一次确认方括号位置变了。

这里有几个实操细节值得说。第一,切换需要 root 权限,普通用户没戏。第二,某些设备不允许在线切换,会报 "Invalid argument",常见于虚拟化环境或者驱动不支持的情况,这时候别硬试,先确认设备是否真的支持。第三,切换之后调度器参数会重置为默认值,如果你之前调过iosched/下的参数,切完要重新设一遍。第四,容器里改这个文件改的是宿主机的配置(如果挂载了/sys),要特别小心,别在容器里手滑把宿主机全改了。

验证是否真的生效,除了看scheduler文件,还可以观察iostat的行为变化。切换前后各跑一段相同压力的测试,对比r_await、w_await、aqu-sz这几个指标,如果有明显差异,说明切换确实起作用了。如果数字纹丝不动,那要么是压力太小调度器没发挥作用,要么是切换没成功。

3.4 参数调优:以 mq-deadline 为例算一遍

调度器选对了只是第一步,参数调对了才算完整。以 mq-deadline 为例,参数在/sys/block/<dev>/queue/iosched/下面:

ls /sys/block/sda/queue/iosched/ # fifo_batch front_merges read_expire write_expire writes_starved

read_expire和write_expire是读写请求的截止时间,单位毫秒,默认 500 和 5000。这两个值怎么定?我的思路是先看业务对延迟的容忍度。假设你的接口 SLA 是 P99 小于 50ms,那读请求在队列里停留超过几十毫秒就已经危险了,read_expire可以往下压到 100 到 200 之间。但压太低会导致频繁插队,排序效果变差,吞吐下降,所以要试探着来。

fifo_batch是每次从排序队列里批量取出多少个请求下发,默认 16。这个值越大,排序越充分(因为一次扫描的窗口更大),但单批延迟也越高。机械盘上可以适当加大到 32 甚至 64,让磁头扫描更连续;固态盘上保持默认或者调小都行,因为排序本身收益不大。

writes_starved控制写请求能被读请求饿多久,默认 2,意思是连续处理两批读之后必须放一批写。这个参数在写多读少的场景下要留意,如果写请求长期被压制,可能导致脏页堆积,反过来触发更猛烈的回写风暴。我遇到过写延迟突然飙高的情况,查下来就是writes_starved设得太激进。

front_merges是前向合并开关,1 开 0 关。一般保持开启,除非你发现合并带来的 CPU 开销超过了收益(极少数高 IOPS 场景)。

举个具体的计算例子。假设你的存储是 NVMe,实测 4K 随机读在队列深度 32 下的平均延迟是 200 微秒,P99 是 800 微秒。业务希望 P99 不超过 2ms,那么请求在软件队列里最多能等的预算就是 2ms 减去硬件处理时间,大概 1.2ms 左右。这时候read_expire设成 500ms 明显过于宽松——请求在队列里等 500ms 才被优先处理,早就超过 SLA 了。合理值应该在 1ms 到 5ms 这个量级,也就是设成 1 或 5。这就是为什么默认值不能照搬:默认值是面向"通用机械盘 + 中等压力"设计的,跟你的硬件和 SLA 未必匹配。

反过来,如果是一块机械盘,单次寻道就要 8ms,那把read_expire压到 1ms 毫无意义,因为物理上就做不到。这时候反而应该放宽到 100ms 以上,给排序留出足够空间。

注意:调参数之前先记录基线,调完对比,一次只改一个变量。同时改三个参数然后说"效果变好了",你根本不知道是哪个起的作用,下次出问题也没法回滚。

3.5 持久化:udev 规则与内核参数两条路

echo进去的配置重启就没了,必须做持久化。现在主流做法是 udev 规则,因为内核启动参数elevator=在较新的内核里已经被移除或者行为变了,靠不住。

新建/etc/udev/rules.d/60-ioschedulers.rules:

# 机械盘用 bfq ACTION=="add|change", KERNEL=="sd[a-z]*|vd[a-z]*", ATTR{queue/rotational}=="1", ATTR{queue/scheduler}="bfq" # 固态盘用 mq-deadline ACTION=="add|change", KERNEL=="sd[a-z]*|vd[a-z]*", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="mq-deadline" # NVMe 用 none ACTION=="add|change", KERNEL=="nvme[0-9]*n[0-9]*", ATTR{queue/scheduler}="none"

写完执行:

udevadm control --reload-rules udevadm trigger --type=devices --action=change

然后验证一下规则有没有生效,cat一遍所有设备的scheduler看看。如果没生效,先检查规则文件的语法(逗号、引号容易写错),再看udevadm test /sys/block/sda的输出有没有报错。

这条路的优点是灵活,可以按设备类型、按厂商、按容量做不同配置;缺点是规则写错不容易发现,而且依赖 udev 服务正常运行。在极简容器镜像或者某些定制系统里,udev 可能没跑,这时候就得考虑用 systemd 的 tmpfiles 或者启动脚本兜底。

如果你维护的是大量同构机器,还可以把这份规则纳入配置管理,配合开机的自检脚本,每次启动后自动核对一遍,避免有人手工改过之后忘了改回来。

3.6 用 fio 做 A/B 对比验证

改了配置不验证等于没改。我习惯用 fio 做对比测试,因为它的负载模型可控、结果可复现。下面这组命令可以覆盖两个典型场景:

# 随机写,模拟数据库落盘 fio --name=randwrite --ioengine=libaio --direct=1 --rw=randwrite \ --bs=4k --iodepth=32 --numjobs=4 --size=1G --runtime=60 \ --group_reporting --filename=/data/fiotest # 顺序读,模拟大文件扫描 fio --name=seqread --ioengine=libaio --direct=1 --rw=read \ --bs=128k --iodepth=16 --numjobs=1 --size=4G --runtime=60 \ --group_reporting --filename=/data/fiotest

测试文件要放在你要评估的那块盘上,大小至少是内存的两倍,避免全落进页缓存导致测的是内存速度。--direct=1绕过页缓存,测的是真实设备性能。

对比的时候重点看三个数:iops、lat (p99)、clat。切换调度器前后各跑一遍,把结果记到表格里。我在一块企业级 SSD 上做过类似的对比,随机写场景下 none 的 IOPS 比 mq-deadline 高约 8%,但 P99 延迟也高了约 15%;换成顺序读场景,两者差距在 2% 以内。这个结果说明对于这块盘,随机写场景下选谁取决于你更在意吞吐还是延迟,而顺序读场景下选谁都行。

测试完记得删掉测试文件,rm /data/fiotest,别占着空间忘了。

4. 按业务场景给出可落地的配置建议

4.1 数据库与日志型写入

数据库是典型的"混合读写 + 对延迟敏感"负载,我一般直接上 mq-deadline,然后把read_expire收到 100 到 200ms,write_expire保持 3000 到 5000ms。理由是数据库的读通常是同步的、在等结果,不能让它在队列里泡太久;写虽然大多异步,但也不能无限拖,否则检查点和刷脏的时候会集中爆发。

日志型写入(比如日志采集、时序数据)的特点是写多读少、写入量大且相对连续。这类负载下我倾向于把writes_starved调大一点(比如 4),避免写请求被读饿死导致缓冲区堆积;fifo_batch可以适当加大,让连续写合并得更充分。如果你用的是时序数据库,它的写入模式往往是批量追加,顺序性很好,那调度器的作用空间反而不大,甚至可以考虑 none 简化链路。

有个细节值得单独提:数据库的 WAL 写入通常带O_DIRECT或者定期fsync,这类写入的延迟直接影响事务提交时间。如果你发现事务提交的尾延迟很高,先别急着调调度器,先看iostat里w_await的分布——如果w_await本身很低,但事务延迟高,那问题可能在刷盘策略或者存储后端,不在调度器。这个区分很重要,能省下不少瞎调的时间。

4.2 容器与虚拟化宿主机

这一层的坑最多,因为 IO 路径被切成了多层:容器里的进程 → 容器文件系统 → 宿主机文件系统 → 宿主机块设备 → 物理盘。调度器在每一层都可能存在,你改哪一层的效果完全不同。

对于容器场景,我建议把注意力放在宿主机层,容器内一般不需要单独调调度器(很多容器运行时甚至不给你改的权限)。宿主机上用 mq-deadline 或 none 都行,取决于盘的类型。如果要按容器做 IO 隔离,那不是调度器的活,应该用 cgroup 的 IO 控制器(io.max 限制带宽/IOPS,io.latency 做延迟保护),这两者是互补关系而不是替代关系。

虚拟化场景要更小心。客户机看到的虚拟盘,它的 IO 最终还是会落到宿主机的物理盘上,所以客户机里改调度器只影响虚拟队列的排序,对物理盘的行为影响有限。我的做法是:客户机内统一用 none 或 mq-deadline(减少一层软件开销),把优化重点放在宿主机侧的物理盘调度和存储池配置上。如果用的是直通设备,那客户机里的配置就跟物理机一样对待。

还有一点容易被忽略:虚拟盘的rotational值经常报得不准,导致 udev 规则把它们分到错误的组里。稳妥的做法是给虚拟盘单独写一条规则,按设备名前缀匹配,而不是依赖rotational属性。

4.3 桌面与开发机

桌面和开发机的负载特征跟服务器完全不同:总 IO 量不大,但极度碎片化,随时可能冒出小请求,而且人对卡顿的感知非常敏锐。这种场景下 bfq 的优势就体现出来了,它能让后台的编译、下载、备份不影响到前台编辑器的响应。

具体配置上,如果系统盘是机械盘,直接 bfq 不用犹豫;如果是固态盘,可以试试 bfq 和 mq-deadline 哪个体感更顺。我自己在一台固态盘开发机上对比过,差别不明显,但跑大项目编译的时候 bfq 下的鼠标拖动会更跟手一些,这可能跟它愿意给交互进程让路有关。

桌面场景还有个小技巧:配合ionice给后台任务降优先级。比如跑全量备份的时候用ionice -c 3(idle 级别),这样它只在磁盘空闲时才拿带宽,不会跟你抢。这个手段在 bfq 下效果最明显,因为 bfq 真的会认这个优先级;在 none 下基本无效,因为 none 压根不看优先级。

4.4 备份节点与大文件顺序读

备份、冷数据归档、大文件传输这类场景,IO 模式是超大的顺序读写,块大小往往是 MB 级别。这种负载下,调度器能做的优化非常有限,因为顺序 IO 本身就是对硬件最友好的形态,几乎不需要重排。

我的做法是直接用 none 或者 mq-deadline 的默认参数,不做额外调优,把精力放在别的地方:比如加大read_ahead_kb预读窗口,让顺序读能提前把数据拉进内存;或者调整文件系统的挂载参数,减少元数据开销。这些手段对顺序吞吐的提升往往比换调度器明显得多。

如果备份节点用的是机械盘阵列,那排序还是有点价值的,可以考虑 mq-deadline 并把fifo_batch加大,让一次下发的请求块更大,减少磁头启停次数。但收益上限也就是百分之几,别期待质的飞跃。

5. 常见问题与排查实录

5.1 改了调度器但读出来没变

这是最高频的问题。原因通常有这么几个:一是改错了设备,机器上盘多,sda和sdb搞混了,尤其是热插拔之后设备名会漂移,建议用/dev/disk/by-id/下的稳定路径来确认;二是写文件失败但没看返回值,权限不够或者设备不支持都会静默失败(其实是会报错的,但你如果用了2>/dev/null就看不到了);三是 udev 规则在开机后又把它改回去了,这种情况要检查规则文件的匹配条件是不是写宽了;四是容器环境,改的是容器内的/sys视图,实际没落到宿主机。

排查顺序我一般是这样:先cat确认当前值,再echo一次看有没有报错,再cat确认是否变化,最后重启一次看是否被覆盖。四步走完基本能定位。

5.2 换了之后吞吐反而掉

调优调出负优化很常见,尤其是从 none 换到 bfq 或者从 bfq 换到 none 的时候。如果换完吞吐掉了,先看是不是调度器的 CPU 开销变大了——用top看%sys有没有明显上升,bfq 在高 IOPS 下确实会吃掉不少 CPU。

另一个原因是排序反而打乱了原本的顺序。有些业务提交的 IO 本身就是有序的(比如顺序写日志),调度器再排一遍并不会更好,反而因为排队引入了额外延迟。这种情况换回 none 就对了。

还有一种隐蔽的情况:切换调度器导致请求合并行为变化,进而影响了文件系统的预读效果。表现出来就是吞吐下降但延迟没变。这种只能靠对比测试发现,没法靠推理。

5.3 磁盘 util 很高但应用不慢,要不要管

iostat里%util接近 100% 经常吓人,但对固态盘来说这个指标的参考价值很低。固态盘的并行度高,%util反映的是"设备有多少时间在处理请求",而不是"设备饱和了"。一块 NVMe 在%util100% 的时候可能还有大量余力。

真正该看的是aqu-sz(平均队列长度)和await(平均延迟)。如果aqu-sz持续增长而await也在涨,说明确实在排队,该考虑扩容或者优化 IO 模式了;如果%util高但await稳定在低位,那只是设备在忙,不影响业务,不用管。

这个认知差异在新手和老手之间特别明显。我见过有人为了把%util压下去,疯狂调调度器参数,结果延迟反而变差了。指标要看,但得看对指标。

5.4 问题速查表

现象最可能的原因排查动作
改了 scheduler 文件无变化设备名错、权限不足、被 udev 覆盖确认设备路径、检查写操作返回值、查规则文件
切换后 IOPS 下降调度器开销大、破坏原本顺序看%sysCPU、对比切换前后 fio 结果
切换后延迟变差参数没跟着调、截止时间不匹配 SLA检查iosched/下参数、按 SLA 重算
重启后配置丢失没做持久化或规则未生效补 udev 规则、udevadm test验证
容器内改了没效果改的是容器视图或权限受限到宿主机确认、检查挂载方式
%util高但业务正常固态盘指标特性,非饱和看aqu-sz和await而非%util
写延迟周期性飙高writes_starved过小、脏页集中回写调大该参数、检查脏页比例和回写策略

6. 我在实际项目里踩过的几个坑

6.1 别在压测中途切调度器

有一次做性能验证,为了省时间,我在 fio 还在跑的时候直接echo切了调度器,想看看"实时效果"。结果那一轮数据完全没法用:切换瞬间队列里的请求被重新归类,延迟曲线出现一个巨大的毛刺,后面几分钟的数据也受残留影响,整体抖动比正常情况大得多。更麻烦的是,我没法判断这个抖动是切换本身造成的,还是新调度器的稳态表现。

正确做法是每轮测试独立进行:停掉压力、清空队列(等几秒让在途请求排空)、切换配置、重新起压、跑够时长、收集数据。虽然麻烦,但数据可信。压测的价值在于可比性,省下来的那点时间远不如数据可靠性重要。

6.2 云主机上很多参数是"看起来能改"

在云环境里,块设备往往是虚拟化出来的,/sys/block/vda/queue/下面一堆文件看起来都能写,但写进去未必有效果。我遇到过scheduler文件只有[none]一个选项的情况,也遇到过能切到 mq-deadline 但实际行为毫无变化的情况——因为底层的虚拟化层根本没把这个配置透传下去。

判断办法是做对比测试:切换前后跑同样的 fio,如果结果差异在噪声范围内,那这个参数对你就是无效的。与其在无效参数上折腾,不如把精力放到能真正影响性能的地方:实例规格、云盘类型、文件系统参数、应用层的 IO 模式优化。这些的收益通常比调度器大一个数量级。

另外,云主机的设备名在不同重启后可能变化,写 udev 规则的时候要用稳定的标识(比如/dev/disk/by-id/下的路径)做匹配,别直接写vdb,不然换一台机器规则就失效了。

6.3 监控口径要对齐

最后一个坑比较隐蔽:不同工具报出来的延迟数字对不上。iostat的await包含了排队时间和服务时间,fio 报的clat是完成延迟,应用层看到的延迟又包含了系统调用和上下文切换的开销。三者层层叠加,数值自然不同。

我一开始没注意这一点,拿iostat的await去跟应用的 SLA 对比,得出的结论是"延迟严重超标",然后一通猛调。后来把口径对齐了才发现,await里包含了大量排队时间,而应用的 SLA 是从发起到返回的端到端时间,两者根本不是一个东西。真正该对比的是应用侧埋点数据和应用侧埋点数据。

做 IO 调优的时候,先明确你要优化的是哪个口径的指标,然后用对应的工具去测。口径不对齐,调得再认真也是白费。

这套东西我前后在十几台机器上折腾过,最大的体会是:调度器不是一个"设了就一直对"的配置项,它跟硬件、内核版本、业务负载都在动态变化。今天合适的选择,半年后业务涨了、盘换了,可能就不合适了。所以与其找一套"最优配置"抄下来,不如把查看、切换、验证这套流程固化成脚本或者检查项,定期跑一遍,让配置跟着实际情况走。

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

无线传输标准协议怎么选:从链路预算到空口效率的工程账本

1. 从"信号格满格却丢包"说起&#xff1a;无线传输标准协议到底在解决什么几年前接手一个仓储场景的采集项目&#xff0c;设备装完之后现场反馈"信号显示满格&#xff0c;但数据就是断断续续"。我带着笔记本去现场蹲了一整天&#xff0c;最后发现问题跟信号…

作者头像 李华
网站建设 2026/10/27 14:51:21

AI模型推理中的GPU资源调度优化策略

1. AI模型推理中的GPU资源调度挑战在AI模型推理的实际部署中&#xff0c;GPU资源调度是个让人又爱又恨的话题。我经历过凌晨三点被报警电话叫醒&#xff0c;只因GPU利用率突然暴跌&#xff1b;也见过因为调度策略不当&#xff0c;导致价值百万的GPU集群只能发挥30%的计算能力。…

作者头像 李华
网站建设 2026/10/27 14:49:30

SciPy实战指南:工业级数值计算与数据分析核心引擎

1. 这不是“又一个Python库”&#xff0c;而是你处理真实数据时绕不开的底层引擎 很多人第一次听说 SciPy&#xff0c;是在学完 NumPy 后被教程里一句“它和 NumPy 配合使用”轻轻带过&#xff1b;也有人在用 Matplotlib 画完图后&#xff0c;看到文档里写着“拟合曲线推荐用 …

作者头像 李华
网站建设 2026/10/27 14:49:26

CAN数据采集实战:从物理层配置到ASC文件生成

1. 项目概述&#xff1a;为什么CAN数据采集是自动驾驶开发绕不开的第一道门槛 在自动驾驶系统开发的实际工作流中&#xff0c;我见过太多团队卡在“数据没采到”或“采到了但用不了”这一步。所谓“自动驾驶下CAN数据的采集&#xff08;asc文件&#xff09;”&#xff0c;说白了…

作者头像 李华
网站建设 2026/10/27 14:49:24

Marketingskills的本质是用户问题定义与验证系统

1. “Marketingskills”不是技能清单&#xff0c;而是商业生存的底层操作系统 你搜“marketingskills”&#xff0c;页面弹出一堆“30天速成”“爆款文案模板”“流量密码大全”——但真正干过三年以上市场的人&#xff0c;第一反应是&#xff1a;这词根本不是技能集合&#xf…

作者头像 李华
网站建设 2026/10/27 14:47:53

Zed编辑器实现OAuth 2.0设备认证流程的技术实践

1. 项目背景与核心需求 在开发工具领域&#xff0c;Zed作为新兴的代码编辑器正快速崛起。它采用Rust构建&#xff0c;以高性能和协作功能著称。最近我在为ACP&#xff08;Application Control Platform&#xff09;Server开发插件时&#xff0c;遇到了一个典型的企业级需求&…

作者头像 李华