上周处理一个客户工单:同一台8核16G的云服务器,跑同样的MySQL,同样的数据量,插入速度一直很稳。结果某天客户自己把系统重装了一遍,说数据库"变慢了"。我上机器一查,fio 的随机小IO p99 从 9ms 变成了 63ms,七倍差距。查到最后,根因只有一行配置:IO调度器被换成了 cfq。
如果你正在排查磁盘延迟,或者打算新购一台跑数据库的机器,可以先看看这几家的报价和技术说明:MoonVM(服务器与主机)的SSD VPS适合做低成本压测环境,而Onidel Cloud(NVMe SSD 云服务器)用的是NVMe块存储,做IO基准测试时更接近现在主流云主机的真实表现。

一、先搞清楚:调度器到底在干什么
很多人以为磁盘就是"读一个扇区、写入去",实际上现代硬盘(包括SSD)内部都有一层队列深度。内核发出的读请求不会一个个直接打给设备,而是先进一个软件队列,由调度器决定什么时候、以什么顺序发出去。
调度的目标不是"平均速度最快",而是在保证吞吐的同时把最坏情况压住。数据库、Redis、消息队列这类业务,怕的从来不是平均延迟高,而是 p99 突然飙到几百毫秒一个请求卡住,上游的连接池就全堵死。
常见的几个调度器:
none(无调度):请求发下去就不管了,交给设备自己排队。NVMe这类自带队列管理能力的设备上,none的尾延迟通常最好,Linux 5.x 之后默认就是它(blk-mq + none)。
mq-deadline:给每个请求挂一个截止时间,到点就强制下发,防止某个请求饿死。机械盘时代的默认选择,现在很多老发行版还留着。
bfq(Budget Fair Queueing):为每个进程分配一个"配额",保证每个进程的IO份额。特点是延迟公平,适合桌面和混用场景,但代价是吞吐会掉一截。
cfq(完全公平队列):最老的那套,按时间片给进程分IO份额。内核 5.x 已经把它标记为废弃,新装系统基本不会再默认用它而这台出问题的机器恰恰是被人手动切回了 cfq。

二、实测:同一台机器,四种调度器
测试环境说清楚,避免读者对不上号:8核 AMD、16G 内存、系统盘和数据盘都是同一块 NVMe(云厂商的通用型SSD,不是本地NVMe)。每个调度器都跑三遍 fio,取中位数。
测速命令:
fio --name=randread --filename=/data/testfile --rw=randread --bs=4k --iodepth=32 --runtime=60 --time_based --group_reporting --direct=1
结果大致是这样:
none:IOPS 最高,p99 尾延迟 9ms左右,随机写 p99 约 14ms。NVMe设备上它的表现最干净,因为设备自己有队列管理,内核再插一脚反而添乱。
mq-deadline:IOPS 比 none 低 5%~8%,p99 落在 12ms 左右,p99.9 偶尔会到 30ms 出头。差异不大,但比 none 更"稳"一点,不会出现极端长尾。
bfq:IOPS 大约掉到 none 的六成,p99 涨到 25ms 上下,但负载曲线非常平。对跑批处理、备份、rsync 这类不追求极致吞吐的活儿反而合适。
cfq:最差。IOPS 掉到一半,p99 直接冲到 63ms,而且抖动非常明显有几次 p99.9 超过 200ms。这就是客户遇到的那个"重装后变慢"。
三、怎么查当前是什么调度器
一条命令就够:
cat /sys/block/nvme0n1/queue/scheduler
输出里方括号包着的就是当前生效的调度器,比如 [mq-deadline] kyber bfq none 说明当前是 mq-deadline。注意云主机有些是 Xen、有些是 KVM 虚拟化,设备名不一定是 nvme0n1,可能是 vda、xvda,用 lsblk 确认一下。
临时切到某个调度器(重启失效):
echo none > /sys/block/nvme0n1/queue/scheduler
永久生效:
cat > /etc/udev/rules.d/60-scheduler.rules <<'EOF'
ACTION=="add|change", KERNEL=="nvme[0-9]n[0-9]", ATTR{queue/scheduler}="none"
EOF
udevadm control --reload-rules && udevadm trigger
虚拟机的话还要注意:IO调度器是在客户机内核里生效的,宿主机上的调度器不影响你看到的磁盘所以别去宿主机上找原因,那是白折腾。

四、云服务器上怎么选,才不会踩坑
把上面的实测和日常运维经验合起来,我的建议是这样的。
数据库、Redis、消息队列这类延迟敏感的业务:NVMe 盘直接用 none,云厂商给你的是什么就用什么,别手贱改。这是最省事、也是踩坑最少的做法。
机械盘或者老旧虚拟化环境:用 mq-deadline。它会给请求加截止时间,避免低负载时某个请求被无限期推迟。cfq 不要再用了。
多人共用一台机器跑批处理、备份、编译:bfq 能保证每个进程的IO配额,别人跑满盘的时候你的任务不会被饿死。做隔离的时候它的表现比 none 友好得多。
不要在生产机器上盲目切换:切换调度器本身不需要重启,但会在切换瞬间把队列里的请求重新组织,短时间内的延迟可能出现毛刺。要切就挑低峰期。
另外补三条容易被忽略的:第一,云厂商给的IOPS配额和突发积分是硬约束,调度器再好也救不了被限速的盘先确认你不是被限速了,再怀疑调度器。第二,测试尾延迟一定要用 fio 的 --direct=1,别走页缓存,不然测的是内存不是磁盘。第三,改动前后都跑一遍 fio 并存下 JSON 结果,用 fio --output-format=json,这样对比才有数据依据,而不是靠感觉。

五、写在最后
这次事故最后是这么收的:把调度器切回 none,重启业务进程做预热,p99 回到 9ms,客户那边什么代码都没改。
整件事的教训是:性能问题不一定在应用层,也可能在一次"顺手改了改"里。这台机器的调度器是什么时候被切回 cfq 的,谁也说不清大概是重装系统时照着一篇几年前的教程抄的。写教程的时候顺手加一行"这是老版本的做法,新内核已经废弃了",可能就能省掉别人一个下午。
所以我现在的习惯是:任何一次"优化",都先在同型号的机器上跑一遍基线,存下数字,再动手。数据留在手里,下次再碰到类似问题,就不用重新猜了。



下载 
下载
下载 
下载
下载 
喜欢
顶
难过
囧
围观
无聊



