Linux服务器iowait偏高与磁盘IO打满的排查方法(iostat与iotop定位进程、await队列分析与脏页回写参数调整)

业务反馈接口慢,机器上看CPU一大半是空的,top里wa那一项长期在30以上,load average却高得离谱。这种组合基本就是磁盘IO拖住了。先别急着换盘,得确认到底有没有IO在排队、是谁在排。

之前遇到过一台4核机器,wa显示45%,第一反应是磁盘快坏了,换盘之后指标一点没变,真正的原因是上面跑着的日志采集进程在疯狂追加写。所以顺序要固定:先确认IO确实堵了,再找进程,最后才动内核参数。

先看vmstat的b列,别急着上iostat

vmstat 1的输出里,procs下面b那一列是处于不可中断睡眠状态的进程数,也就是正卡在IO上等着的那批。它持续大于0,才说明真有人在排队;如果只是wa高而b一直是0,那多半只是并发IO多,谁也没被卡住,业务慢的锅不在磁盘。

$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2 14      0  312440 1245688 13024560  0    0    120 48213 1204  3  2 12 42  0

上面这份里b是14,bo(写块数)四万多,wa是42,属于典型的写堵住了。顺手也要瞄一眼si和so两列,只要它们不是0,说明在换页,那wa高跟匿名内存被换出去有关,问题根源在内存而不是盘。

关于wa有个反直觉的地方:它统计的是CPU空闲且有IO在飞行的时间占比。也就是说CPU被计算任务吃满的时候,wa反而会显得很低,磁盘其实已经很慢了。多核机器上wa又是所有核的平均值,一个核全程等IO、其余七个核闲着,wa看起来也只有十来个点,容易被忽略。

用iostat确认是哪块盘、堵到什么程度

iostat要带-x和间隔跑,而且第一屏输出是自开机以来的累计平均,基本没有参考价值,一定要丢掉、看第二屏往后。

$ iostat -xdm 1
Device            r/s     w/s     rkB/s   wkB/s  rrqm/s  wrqm/s  %rrqm  %wrqm r_await w_await aqu-sz rareq-sz wareq-sz  svctm  %util
vda              0.00  320.00      0.00 48240.00   0.00   12.00   0.00   3.61    0.00  168.40  53.92     0.00   150.75   3.12 100.00

读这份输出的顺序是固定的:先看r/s和w/s知道请求有多密,再看r_await和w_await知道单次请求要等多久,最后看aqu-sz(老版本叫avgqu-sz)知道队列堆了多长。w_await到168毫秒、队列长度五十多,机械盘上这已经是完全堵死的状态了。

两个字段要留个心眼。一是svctm,它的手册里从很早开始就写着不要相信这个值,因为统计是在块层做的,内核并不知道驱动什么时候真正开始处理请求,sysstat 11.7.3、12.0.0、12.2.0、12.5.4这几个版本里它还在输出,到sysstat 12.7.2、12.7.9、12.8.0这一代已经不再拿它当判据。二是%util,在NVMe和RAID这类能并行处理请求的设备上,它到100%只是说采样期间一直有IO在飞,离真正的带宽上限还远着呢,这时候要看的是await和带宽数字。

机器上有多块盘、或者想按挂载点看的时候,加个-j ID或-j LABEL用持久化名字输出,免得重启之后sda和sdb对调导致看错对象;LVM上再加-N,看到的才是dm-设备对应的逻辑卷名。

定位到进程:iotop、pidstat和/proc

确认了盘之后就要找是谁在写。iotop -o -P最直观,只显示当前真正在做IO的进程,按进程聚合而不按线程刷屏。

$ iotop -oPa
Total DISK READ :       0.00 B/s | Total DISK WRITE :      47.20 M/s
  PID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN      IO    COMMAND
18342 be/4  www          0.00 B     47.1 M   0.00 %  89.12 %  java -jar app.jar

容器里跑iotop经常什么都刷不出来,那是内核没开CONFIG_TASK_DELAY_ACCT和CONFIG_TASK_IO_ACCOUNTING,两个/proc接口读不到数。这种情况退一步用pidstat -d 1,或者干脆直接读/proc/PID/io。

$ cat /proc/18342/io
rchar: 1843204712
wchar: 39120847120
read_bytes: 4096000
write_bytes: 40239104000

这里rchar和read_bytes的差距很关键:rchar是进程通过read这类系统调用读到的字符总数,中间会经过页缓存;read_bytes才是真正下沉到块设备层的字节数。一个进程rchar很大、read_bytes几乎是0,说明它读的东西全命中缓存了,压根没碰磁盘;两边都大,那就是实打实的IO。

还有一类进程是iotop抓不到但确实卡住的,状态是D。用下面这条把它们筛出来,再顺着/proc/PID/fd看它手上攥着哪些文件:

$ ps -eo stat,pid,comm,wchan:30,args --sort=-time | awk '$1 ~ /D/'
D    18342 java            wait_on_page_writeback  java -jar app.jar

$ ls -l /proc/18342/fd | grep -v socket | head -20

现场见过wait_on_page_writeback这个wchan,进程卡在等脏页回写完成,往前一步查就是刷脏页的参数问题,换盘一点用都没有。

想挖到具体文件和单次延迟,就得上bcc那一套

进程级的数字只能告诉你谁在写,想知道写的是哪个文件、每次请求延迟多少,用bcc-tools里的biosnoop和opensnoop。biosnoop按请求打印,带进程名、设备、扇区、大小和延迟,延迟那一列动辄几百毫秒的话,盘的问题就坐实了。

$ biosnoop
TIME(s)     COMM           PID    DISK    T  SECTOR     BYTES   LAT(ms)
12.401331   java           18342  vda     W  13041664   4096    214.82
12.401902   java           18342  vda     W  13041728   4096    221.05

$ blktrace -d /dev/vda -o - | blkparse -i -

blktrace那一条会把块层的事件完整抓下来,再用btt出一份各阶段耗时报告,Q2G、G2I、I2D、D2C分别对应入队、合并、派发、完成,看哪一段在涨就知道瓶颈在调度还是在设备。这套工具开销不小,别长时间开着。

给盘打个基线,别凭感觉判断快慢

排查到这儿如果还不确定盘本身够不够快,就用fio跑一次。fio 3.35、3.38、3.40、3.41这几代命令参数没变,随机写基线这么跑:

$ fio --name=randwrite --ioengine=libaio --direct=1 --rw=randwrite \
      --bs=4k --numjobs=1 --iodepth=32 --size=1G --runtime=60 \
      --time_based --group_reporting --filename=/data/testfile

--direct=1必须带上,否则测的是页缓存的速度,得到的数字毫无意义。dd也一样,要加oflag=direct才是真的在测盘。跑出来的IOPS如果跟云厂商标称值差一个数量级,那就不是应用的问题了,直接提工单。

刷脏页那几个参数是最常见的放大器

大量的写会被页缓存攒着,攒到一定量才真正落盘。控制这个行为的几个值默认值如下:vm.dirty_background_ratio是10,超过就开始后台回写;vm.dirty_ratio是20,超过之后写进程会被卡住同步回写;vm.dirty_expire_centisecs是3000,也就是30秒,脏页活过这么久就会被回写;vm.dirty_writeback_centisecs是500,回写线程每5秒醒一次。vm.swappiness默认60。

$ sysctl -a | grep -E 'dirty|swappiness'
$ echo 'vm.dirty_background_bytes = 268435456' >> /etc/sysctl.d/99-io.conf
$ echo 'vm.dirty_bytes = 536870912' >> /etc/sysctl.d/99-io.conf
$ sysctl --system

内存大于32G的机器上,那两个百分比换算出来的绝对值太大了,几百G内存按20%算就是几十G脏页,一旦开始同步回写,业务会被整体卡住好一阵子。这种情况改用dirty_bytes和dirty_background_bytes给绝对值,比如分别给512M和256M,把回写摊平到平时。

调度器那头也值得看一眼。cat /sys/block/vda/queue/scheduler会列出当前支持的,方括号里的是生效的那个,多半是mq-deadline。NVMe上换成none通常能拿到更低的延迟,机械盘用bfq能改善多进程抢IO时的公平性。read_ahead_kb默认128,顺序读为主的业务调到512甚至1024会有变化,随机读为主则应该调小,白预读的扇区也是要花钱买的带宽。改动之前先用fio各跑一遍记下数字,不然没法判断方向对不对。

临时给某个吃IO的任务让路,可以ionice -c 3 -p PID把它标成idle。不过它只在bfq这类支持优先级的调度器上有效,mq-deadline和none不认这个标记,别白忙一场。

回头看这次的现场:b列14、w_await 168毫秒、java进程独占47M/s写、wchan是wait_on_page_writeback,最后落在日志框架没做按大小滚动,单个日志文件写到了四十多G。改完logrotate配置并给dirty_bytes设了上限,wa从42掉到4。盘其实是好的,换盘纯属折腾。

文中参数默认值核对于2026年10月。