业务反馈接口慢,机器上看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月。