前言:网站变卡,先别急着怪 CPU
很多个人站长都有过这样的经历:服务器负载不算高,CPU 使用率只有百分之二三十,可网站就是卡顿,数据库查询偶尔超时,页面加载要等好几秒。这时候如果只盯着 CPU 和内存看,很容易走弯路。真正拖后腿的,往往是磁盘 IO。磁盘 IO 是服务器里最慢的一环,CPU 是纳秒级、内存是微秒级,而机械硬盘的随机读写是毫秒级,两者差了好几个数量级。一旦磁盘成为瓶颈,整个服务链路上的所有程序都会跟着变慢。这篇文章就从一个真实排障案例出发,把 Linux 磁盘 IO 性能排查的完整流程讲清楚,从工具使用到常见原因再到解决方案,希望能给正在被卡顿问题困扰的站长一些实在的参考。
第一步:确认是不是 IO 问题
排查的第一步不是急着装工具,而是先确认问题到底出在哪一层。打开终端,依次执行两个命令:先用 uptime 看系统负载,再用 top 看整体状态。top 输出的第二行里有一个 wa 字段,全称 iowait,表示 CPU 等待 IO 完成的时间占比。如果 wa 长期在百分之二三十以上,基本可以断定磁盘 IO 在拖后腿。另外还要看 load average,如果负载很高而 CPU 使用率不高,说明大量进程在等 IO,而不是在跑计算。有个简单的经验法则:CPU 忙说明是计算瓶颈,CPU 闲而负载高说明是 IO 瓶颈,两者都高说明资源整体紧张。记住 wa 只是一个参考指标,有些情况下内核线程的 IO 等待不计入 iowait,所以还要配合下面的工具交叉验证。
第二步:用 iostat 看磁盘到底有多忙
确认方向之后,就用 iostat 来量化磁盘的忙碌程度。大多数发行版需要先安装 sysstat 软件包:Debian 和 Ubuntu 用 apt install sysstat,CentOS 用 yum install sysstat。安装完成后,用下面这条命令实时观察所有磁盘的 IO 情况:
iostat -x 1 5参数 -x 表示显示扩展信息,1 是采样间隔一秒,5 是采样五次。输出里每个磁盘一行,重点看这几个字段:%util 表示磁盘在采样周期内的忙碌百分比,接近 100% 说明磁盘已经饱和;await 表示 IO 请求从发出到完成的平均等待时间,机械硬盘正常应该在 10 毫秒以内,SSD 应该在 1 毫秒左右,如果 await 飙升到几百毫秒,说明请求在排队;r_await 和 w_await 分别表示读和写的等待时间,可以帮我们判断是读多写多的问题还是读写都有问题;svctm 是设备自身的服务时间,现在很多版本的 iostat 已经不推荐看这个字段了,以 await 为主。另外 rkB/s 和 wkB/s 是吞吐量,对判断业务特征也有帮助。要注意 %util 对 SSD 和 NVMe 这类支持并行处理的设备参考价值有限,因为多队列设备可以同时处理多个请求,%util 显示 100% 也不一定真的饱和,这时候更要结合 await 和队列长度来判断。
第三步:用 iotop 揪出是哪个进程在疯狂读写
iostat 告诉我们磁盘很忙,但没告诉我们是谁在忙。这时候用 iotop 来定位具体的进程。iotop 需要 root 权限运行,效果类似于 top,会按 IO 读写量给进程排序:
iotop -o -P参数 -o 表示只显示正在做 IO 的进程,-P 表示以进程而不是线程为单位显示。输出中 TID 列是进程号,IO 列显示实时的读写速率,COMMAND 列是进程名。如果看到 mysqld 占着大量写入,多半是数据库刷盘太频繁;如果看到 php-fpm 在疯狂读文件,可能是程序有死循环读文件或者日志写得太狠;如果看到 rsync 或者备份脚本在跑,那就属于计划内的正常 IO,可以等它跑完再观察。除了 iotop,pidstat 的 -d 参数也能按进程统计磁盘 IO,适合在脚本里定时采集:
pidstat -d 1 5它会输出每个进程的读写速率和总 IO 量,配合 iotop 交叉验证,定位结果就很可靠了。
第四步:用 sar 查历史,找规律
实时工具只能看到当下,但网站卡顿往往是间歇性的,比如每天早上八点流量高峰卡一次。这种问题就需要历史数据来还原现场。sar 同样是 sysstat 包提供的工具,只要系统开了 sysstat 服务,它就会定时把数据写入 /var/log/sysstat 目录。用下面命令查看当天某个时段的磁盘情况:
sar -d -f /var/log/sysstat/sa$(date +%d) | grep -v '^$'输出里每一行对应一分钟的采样,DEV 列是设备名,await 和 %util 两列能清楚显示卡顿发生的时间点。把卡顿时间和网站访问日志、数据库慢查询日志对照起来看,往往能直接锁定元凶。如果服务器没有开启 sysstat 采集,现在补上也不晚:编辑 /etc/default/sysstat 把 ENABLED 改成 true,重启 sysstat 服务,以后就有历史数据可查了。
常见原因:为什么磁盘会突然变成瓶颈
定位到具体进程之后,还要想明白根本原因,否则重启一下过几天又复发。根据个人经验,个人站长服务器最常见的磁盘 IO 瓶颈有以下几类。第一类是 Swap 抖动,物理内存不够时系统把内存页换到 Swap 分区,Swap 在磁盘上,频繁换页等于频繁随机读写,IO 压力立刻拉满,典型表现是 wa 很高、内存占用接近满。第二类是数据库刷盘策略太激进,MySQL 的 innodb_flush_log_at_trx_commit 如果设为 1,每次事务提交都要把日志刷到磁盘,写入频繁时磁盘压力巨大。第三类是日志写入失控,Nginx、PHP-FPM、MySQL 的错误日志如果不做轮转,单个日志文件越来越大,定位和写入都变慢,logrotate 没配好还会导致日志无限增长。第四类是云服务器的突发 IO 额度耗尽,很多低价云盘有 IO 突发限制,平时用着没事,一旦持续高 IO 就会被限速,表现为 iostat 显示设备并不忙但实际读写速度骤降。第五类是机械硬盘本身老化或者 RAID 正在重建,数据盘在重建期间性能会明显下降。
解决方案:对症下药才有效
针对上面的原因,解决方案也各不相同。Swap 抖动的问题,优先考虑加内存或者把 Swap 的 swappiness 调低,让系统尽量少用 Swap:
sysctl vm.swappiness=10
echo 'vm.swappiness=10' >> /etc/sysctl.conf数据库刷盘的问题,如果业务能接受轻微的数据丢失风险,可以把 innodb_flush_log_at_trx_commit 从 1 调整为 2,性能提升非常明显;不能接受风险的话,就考虑把数据库的数据目录和日志目录放到不同的磁盘上,把随机写压力分散开。日志失控的问题,配置好 logrotate 按天轮转并压缩旧日志,同时限制 PHP-FPM 的慢日志和错误日志大小。云盘限速的问题,先确认是不是 IO 突发额度用完,可以在控制台看 IO 监控图,确认后要么升级云盘类型,要么给业务加缓存层减少重复读盘。最后,如果预算允许,把系统盘和数据盘换成 SSD 是最直接的解决办法,SSD 的随机读写性能比机械硬盘高一个数量级,很多 IO 瓶颈换盘之后立刻消失。
进阶:用 fio 摸清磁盘的真实能力
判断磁盘是不是真的达到性能上限,光靠生产环境的观察还不够,因为生产负载不稳定。可以找业务低谷期用 fio 做一次基准测试,先拿到磁盘的理论性能基线,以后排障就有对比依据。比如测试 4K 随机读:
fio --name=randread --ioengine=libaio --iodepth=32 --rw=randread \
--bs=4k --size=1G --numjobs=1 --runtime=60 --time_based \
--group_reporting --direct=1测试完成后重点关注 IOPS 和平均延迟。如果测出来的 IOPS 远低于同类型云盘的标称值,说明磁盘本身有问题,该提工单找云厂商了;如果测出来很高但生产环境还是卡,那问题多半不在磁盘硬件,而在应用层刷盘太频繁或者队列深度设置不合理。注意测试前先确认数据盘上没有重要数据,fio 的写测试会覆盖文件内容,一定要指定测试文件而不是整个盘。
总结:IO 排障要形成自己的套路
磁盘 IO 排查说到底是三板斧:先用 top 和 uptime 判断方向,再用 iostat 量化磁盘压力,然后用 iotop 和 pidstat 定位进程,最后结合 sar 历史数据找规律、结合业务特征想原因。把这套流程走熟之后,遇到网站卡顿就不会再一头雾水了。另外也建议把 iostat 和 pidstat 的采样写成一个小脚本配合 crontab 定期记录,出问题的时候有数据可查,比事后拍脑袋强得多。希望这篇文章能帮你在下次服务器卡顿的时候,快速找到那块真正拖后腿的磁盘。