上周三凌晨两点半,值班手机把我叫醒:业务接口的 P99 从平时的 30 毫秒直接涨到了 3 秒多。查了一圈,应用进程没变、CPU 也才跑了两成多,最后定位到那台机器上同时在跑的全量数据库备份任务它把整块盘的 I/O 队列拉到了几千深,线上库的每一次随机写都在后面排队。那天我才真正搞明白,磁盘算力明明还很闲,慢的并不是磁盘本身,而是排在磁盘门口的那条队伍。
如果你手上的服务器正被备份、压测、日志归档这类任务拖累,可以先看看这几家在存储和硬盘层面的方案:hosteons(服务器与主机,纯 SSD RAID 10 硬盘 KVM VPS) 和 onidel(全球云服务器,NVMe 块存储 VPS)。硬件层面换一块更顺手的盘能缓解一部分,但队列调度这层,还是得靠系统自己管。

大部分人排查磁盘慢,第一反应是跑个 fio 压一下看 IOPS,结果压出来的数字很漂亮,可线上业务还是卡。这时候问题往往不在盘的性能上限,而在于同一时刻有太多请求在排队。用 iostat -x 1 持续观察,重点看这几列:r/s 和 w/s 是每秒读写次数,aqu-sz 是平均队列深度,await 是请求从进队列到拿到结果的总耗时,%util 则是设备繁忙百分比。
关键的一个判断方法是:%util 只有二三十,但 await 涨到几百毫秒、aqu-sz 上千,那就说明设备没吃满,纯粹是排队时间被拉长了。这种场景下,再好的 NVMe 也救不了你,因为大家都在同一个队列入口互相踩。

Linux 有一套优先级体系来分配 I/O 请求的通行顺序,命令就叫 ionice。它属于 blkio 这套子系统,管的是「请求进队列之后按什么顺序被设备取走」,而不是「谁可以发起请求」。这点特别容易搞混:nice 管的是 CPU 时间的分配,ionice 管的是磁盘队列里的先后次序,两个是完全独立的维度。
ionice 有三个调度类。第一个是 1 号 realtime,最高优先级,直接抢队,业务关键写入可以用,但一不小心就能把普通任务饿死。第二个是 2 号 best-effort,也是默认值,优先级从 0 到 7,数字越小越优先。第三个是 3 号 idle,只有当队列里没有任何 normal 或 realtime 请求时才被执行,相当于「闲得慌才轮到我」。
日常运维用得最多的就是 best-effort 里面挑个靠后的数字,把备份、rsync、日志归档这类能等的任务挪到后面去。命令很简单:ionice -c 2 -n 7 -p 12345 就是把 12345 这个进程改到 best-effort 的 7 优先级;ionice -c 3 nice -n 19 tar -czf /backup/data.tgz /data 则是让这个压缩任务在整个盘空下来之前坚决不插队。

这是我当晚浪费了半小时的原因。我给备份脚本的父 shell 设了 ionice -c 3,跑起来看 ionice -p 也确实变了,结果 iostat 里的队列一点没降。后来才明白,tar、gzip 这些是 shell 拉起来的子进程,子进程会重置回默认优先级,除非显式指定 --ionice 参数(GNU coreutils 从 8.3 开始支持)或者让整个进程组一起改。
更省事的做法是直接对已经跑起来的进程下手:ionice -p 12345 能立刻生效,不用重启服务,也不用改脚本。改完用 ionice -p 12345 反查一下确认落地了。
现在的新机器默认都是 cgroup v2,容器场景下更明显。原因是 cgroup v2 用统一的 io.max 接口来限制设备带宽和 IOPS,块层的调度类优先级(也就是 ionice 那套 ioprio)被合并进统一的 ioprio 字段,容器里的进程想改自己的 I/O 优先级,多半会被权限直接拒掉,实测报的是 Operation not permitted。
容器场景下正确的做法是在 cgroup 层面用 io.max 写规则,例如 io.max rbps:254:0 wbps:50M 给某个 cgroup 限速,再配合 io.weight 给它一个相对权重,让它在和其他 cgroup 抢同一个块设备时排得靠后一点。io.weight 是比例,不是绝对值,这点比 ionice 的静态优先级灵活,也更符合现在的容器调度习惯。

最后一个容易踩的点。如果你用 systemd 托管备份服务,单元文件里的 Nice= 是有效的,但 IOSchedulingClass= 和 IOSchedulingPriority= 这两个字段在多数发行版上并不真的生效因为内核的 bfq 调度器没被启用,默认的 none/mq-deadline 调度器压根不读这两个值。写上去不报错,但什么也不会发生,非常有欺骗性。
更靠谱的做法是在 ExecStart= 前面加一段 ExecStartPre=/bin/sh -c 'ionice -c 3 -p $$PPID' 之类的处理,或者干脆包一层 nice -n 19 ionice -c 3 your-command,用最朴素的方式把优先级真正落到进程上。
改完别急着收工,一定要三件事一起做:一是跑 iostat -x 1 盯着 aqu-sz 和 await,确认线上库那部分的请求延迟回落了;二是用 cat /proc/<pid>/io 看看目标进程的读写字节数确实在推进,别出现「优先级降了,任务却卡住不动」的情况;三是看 cat /proc/pressure/io 里的 some avg60,这个指标专门统计因为 I/O 阻塞而卡住的任务比例,比 %util 更能反映真实的体感。
我现在的固化做法是:所有定时备份一律用 ionice -c 3 nice -n 19 包装,容器内的任务统一在 cgroup 上加 io.max 限速,业务关键的服务单独拆一块盘。这样做之后,凌晨跑备份的时段接口 P99 从 3 秒回到了 60 毫秒以内,整整一个量级的差别,而整套改动只花了不到十分钟。