问题背景
线上服务器监控报警,CPU us 不高,但 iowait 高达 40%、50%,磁盘 util 100%。这时候工程师的第一反应往往是"磁盘坏了",但实际情况远比这复杂。iowait 高只是现象,背后可能藏着 MySQL 慢查询、Docker 日志风暴、Nginx 写 access log、文件系统碎片、Swap 滥用、甚至是内核调度问题。
这篇文章从 Linux I/O 栈的全貌出发,讲清楚 iowait 到底是什么、不是什么、怎么一步步定位根因,最后给出常见的故障复盘案例。
Linux I/O 栈全貌:从应用到磁盘
Linux 的 I/O 路径是一层一层串起来的,每一层都有自己的队列、缓冲和调度逻辑。理解这个栈,是排查 I/O 问题的前置条件。
第 1 层:应用层
应用通过系统调用发起 I/O 请求,最常见的是 read() 和 write()。应用程序本身不直接跟磁盘打交道,它只管写文件描述符。具体怎么写、写到哪块磁盘、由内核决定。
常见的 I/O 发起方:
- MySQL:InnoDB 的脏页刷新、binlog 写入、redo log 刷盘
- Nginx/Apache:access log、error log 写入
第 2 层:VFS(虚拟文件系统层)
VFS 是 Linux 内核提供的一层抽象,它统一了不同文件系统的接口。无论你用的是 ext4、XFS、NFS 还是 tmpfs,在应用层看来都是统一的 open()、read()、write() 接口。
VFS 的核心数据结构:
struct super_block:文件系统超级块
VFS 层还有一个关键机制:页缓存(Page Cache)。所有文件的读写都会经过页缓存。写操作默认是"写回"(write-back)模式:数据先写入页缓存,之后由内核线程异步刷到磁盘。这意味着 write() 系统调用通常会立即返回,但数据还没真正落盘。
第 3 层:具体文件系统(ext4/XFS/btrfs)
文件系统层负责把文件操作翻译成对底层块设备的请求。它要管理:
不同文件系统在 I/O 调度策略、日志模式、空间分配方式上有显著差异,这会直接影响 I/O 性能表现。
第 4 层:通用块设备层(Block Layer)
这是 I/O 栈中最复杂的一层。Block Layer 接收来自文件系统的块请求(bio),并负责:
- I/O 调度:把多个相邻扇区的请求合并,减少磁盘寻道次数
- 调度算法选择:CFQ、Deadline、NOOP、MQ-Deadline(blk-mq)
Linux 4.13 之后默认使用 mq-deadline,之前默认是 CFQ。调度算法的选择对 I/O 延迟影响很大。
关键数据结构:
struct request:经过调度器合并后的磁盘请求struct request_queue:请求队列
第 5 层:设备驱动层
设备驱动把块请求翻译成针对具体硬件的操作指令。机械硬盘(HDD)走 SCSI/SATA 协议,SSD 走 NVMe 协议,虚拟化环境走 virtio-blk 或 NVMe 模拟。
设备驱动的性能差异:
- HDD:受寻道时间和转速限制,顺序读尚可,随机 I/O 极差
- SATA SSD:受 SATA 通道带宽限制,顺序读约 550MB/s
- NVMe SSD:走 PCIe 通道,延迟低、并发强,顺序读可达数 GB/s
- 虚拟磁盘:受宿主机 I/O 队列和物理磁盘双重影响
第 6 层:物理磁盘
最终的存储介质。机械硬盘有磁头寻道、盘片旋转的物理限制;SSD 有读写放大、NAND 颗粒寿命的问题。
iowait 到底是什么
iowait 是 top 和 vmstat 输出中的一个指标,全称是 "I/O wait"。它表示 CPU 处于空闲状态,但有未完成的 I/O 请求正处于不可中断的等待状态。
理解 iowait 有几个关键点:
iowait 不等于磁盘 I/O 繁忙
iowait 高不等于磁盘 util 高。如果只有 1 个 CPU 核心的 iowait 高,而其他核心忙碌,整体 iowait 会被均摊,看起来不高但磁盘已经很繁忙。反过来,iowait 高也不一定是磁盘慢——可能是 NFS、tmpfs、内存分配等待等场景。
iowait 是 CPU 级别的指标
iowait 是从 CPU 视角看的指标。具体来说:
iowait = CPU 在非空闲状态(user/nice/system)之外,剩余时间中花在 I/O 等待上的比例
如果你有 8 核 CPU,其中 1 核 100% iowait,剩下 7 核 0%,top 显示的整体 iowait 大约是 12.5%。所以看到整体 iowait 20% 时,实际上可能是 1 核 100% iowait(4 核机器)或者 2 核全满(8 核机器)。
iowait 高的本质是 CPU 饿着
iowait 高的意思是:CPU 想干活,但活被 I/O 卡住了,只能等。CPU 本身没有损坏,但利用率上不去,系统吞吐量下降。
iowait 和 CPU idle 的区别
- iowait:CPU 没事干,但有 I/O 请求正在等待
两者在 top 里都显示为 idle,但含义完全不同。一个是系统负载极低,一个是系统被 I/O 阻塞了。
排查工具链:从宏观到微观
排查 iowait 问题的工具有好几个,每个工具关注不同的层级。
1. vmstat:看整体趋势
# vmstat 1:每秒采样一次vmstat 1 10
输出示例:
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 2 3 0 812340 123456 4567890 0 0 1200 500 3000 4500 5 3 0 92 0
关键列:
b:不可中断睡眠状态的进程数(通常是被 I/O 阻塞)wa(wait I/O):iowait 占用的 CPU 百分比
重点关注:若 b 列长期大于 0,说明有进程被阻塞在 I/O 上。若 wa 长期高于 30%,说明 iowait 是主要瓶颈。
2. iostat:看磁盘 I/O 细节
# 安装 sysstat 包# CentOS: yum install sysstat# Ubuntu: apt install sysstat# 基本用法:-x 显示扩展信息,-k 以 KB 为单位,1 表示每秒iostat -xzk 1 5
输出示例:
Linux 5.4.0-xxxx (hostname) 05/19/2026 _x86_64_ (8 CPU)avg-cpu: %user %nice %system %iowait %steal %idle 3.21 0.00 1.45 45.23 0.00 50.11Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %utilsda 0.00 12.00 0.00 120.00 0.00 16384.00 272.73 45.00 375.00 0.00 375.00 8.26 99.20
重点字段解读:
%util:设备利用率,接近 100% 说明磁盘已经饱和。这是判断磁盘是否是瓶颈的最直接指标。await:平均 I/O 响应时间(毫秒),包括排队时间和实际服务时间。avgqu-sz:平均队列深度,磁盘请求的排队长度。如果持续高于 4(机械硬盘)或者高于 32(SSD),说明 I/O 超过了磁盘处理能力。r_await / w_await:读写分离的平均响应时间。svctm(service time):平均服务时间,已经废弃(不可靠),仅做参考。rrqm/s / wrqm/s:每秒合并的读写请求数。合并越多,效率越高。
实战判断:
%util 接近 100% 且 await 很高 → 磁盘本身是瓶颈%util 接近 100% 但 await 很低 → 磁盘性能很好但请求太多,块层排队严重%util 不高但 iowait 高 → 可能是 NFS、内存压力、或 CPU 层面的等待
3. iotop:定位具体进程
# 需要 root 权限iotop -o -b -n 2 -d 5
参数说明:
输出示例:
Total DISK READ: 0.00 B/s | Total DISK WRITE: 16.39 G/sActual DISK READ: 0.00 B/s | Actual DISK WRITE: 16.39 G/s TID PRIO USER DISK READ DISK WRITE SWAPIN IO COMMAND18123 be/4 mysql 0.00 B/s 16.39 G/s 0.00 % 99.99 % mysqld18234 be/3 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % kworker/u256:2
重点:IO 列显示该进程的 I/O 占用百分比。如果 MySQL 的 IO 列接近 100%,基本可以确定是数据库的 I/O 导致的问题。
4. pidstat:进程级别 I/O 统计
# 查看每个进程的 I/O 统计,每秒一次pidstat -d 1 5
输出示例:
Linux 5.4.0-xxxx (hostname) 05/19/2026 _x86_64_ (8 CPU)03:15:10 PM UID PID kB_rd/s kB_wr/s iodelay command03:15:11 PM 0 18123 0.00 16384000.00 0 mysqld03:15:11 PM 0 18234 0.00 120.00 0 rsyslogd
iodelay:I/O 延迟(以时钟周期计),反映进程等待 I/O 的时间
iodelay 越大,说明进程花在等待 I/O 上的时间越多。
5. /proc/diskstats:原始磁盘统计
cat /proc/diskstats
这是 iostat 数据的来源。如果需要自定义监控或者写脚本采集,可以用这个接口。
输出字段(按顺序):
- Weighted I/O time(累计 I/O 时间,含排队)
计算平均 I/O 响应时间:
(总 I/O 时间 / (读完成数 + 写完成数)) = 平均每次 I/O 的毫秒数
6. blktrace:追踪每个 I/O 请求
如果普通工具不够用,需要看每个 I/O 请求的细节,用 blktrace:
# 安装:yum install blktrace 或 apt install blktrace# 对特定设备追踪 10 秒blktrace -d /dev/sda -o /tmp/blktrace -w 10# 分析结果blkparse -i /tmp/blktrace -d | head -50
blktrace 可以看到每个 I/O 请求从应用发出、到 VFS、到块设备层的完整耗时分布。这个工具一般用于深度性能分析,生产环境慎用,会产生大量数据并影响性能。
7. free 和 /proc/meminfo:看内存压力
free -hcat /proc/meminfo | grep -E "^(MemTotal|MemFree|MemAvailable|Cached|Buffers|SwapTotal|SwapFree|SwapCached)"
内存压力会导致两个 I/O 相关问题:
- Swap 使用:如果
SwapFree 持续下降,说明系统在使用 Swap,而 Swap 在磁盘上,会产生大量 I/O - Page Cache 回收:内存紧张时,内核会回收 Page Cache,导致原本可以通过缓存满足的读 I/O 变成磁盘直接读
检查 Swap 是否在大量使用:
# 看 Swap 使用情况vmstat 1 10 | awk '{print $2,$3,$4}'# 如果 si(swap in)和 so(swap out)长期不是 0,说明在换页
常见根因与排查路径
根因一:MySQL InnoDB 脏页刷盘
MySQL InnoDB 有自己的缓存池(Buffer Pool),数据页在内存中修改后变成脏页,由后台线程定期刷到磁盘。如果脏页积累过多或者磁盘写入速度跟不上, InnoDB 的 page_cleaner 线程会产生大量 I/O。
排查步骤:
pidstat -d 1 5 -p $(pgrep -x mysqld)
-- 登录 MySQLmysql -u root -p-- 查看脏页比例和刷新状态SHOWENGINEINNODBSTATUS\G-- 关注以下指标:-- Pages made flushd:累计页刷新数-- InnoDBBufferPool 的脏页比例
-- 查看关键参数SHOWVARIABLESLIKE'%innodb%flush%';SHOWVARIABLESLIKE'innodb_max_dirty_pages_pct';SHOWVARIABLESLIKE'innodb_buffer_pool_pages_dirty';
关键参数说明:
innodb_max_dirty_pages_pct:脏页比例上限,默认 75(MySQL 5.6),超过这个比例会强制刷新innodb_io_capacity:InnoDB 能承受的 I/O 吞吐量上限,默认 200(机械硬盘),SSD 应该设置更高innodb_flush_method:刷新方式,O_DIRECT(绕过 OS 页缓存)或 fsync(通过 OS 页缓存)
修复方案:
# 方法1:调整 innodb_io_capacity(如果是 SSD)# 在 my.cnf 中添加或修改:# innodb_io_capacity = 2000 # SSD 推荐 2000-10000# innodb_io_capacity_max = 4000# 方法2:增加 Buffer Pool 大小,减少磁盘访问# innodb_buffer_pool_size = 16G # 建议设置为可用内存的 60-80%# 方法3:调整脏页刷新策略# innodb_max_dirty_pages_pct = 50 # 降低阈值,更频繁刷新
风险提醒:修改 innodb_flush_method 或增大 innodb_io_capacity 可能导致 I/O 峰值更高,短期更卡。生产环境建议在低峰期操作,并提前备份配置。
根因二:Nginx/Apache 日志写入
每个 HTTP 请求都会写入 access log,如果 QPS 很高(几千甚至几万),日志写入 I/O 会非常频繁。
排查步骤:
pidstat -d 1 -p $(pgrep -x nginx | tr '\n'',') 2 5
# -c 只显示变化的字节,-a 显示属性变化,-f 持续监控tail -f /var/log/nginx/access.log | pv -rate > /dev/null
# nginx.conf 中查看access_log /var/log/nginx/access.log combined buffer=16k flush=5s;error_log /var/log/nginx/error.log warn;
修复方案:
# 方案1:关闭 access log(不推荐生产环境,但临时救火有效)access_log off;# 方案2:降低日志级别,只记录 erroraccess_log /var/log/nginx/access.log error;# 方案3:开启日志缓冲,减少系统调用access_log /var/log/nginx/access.log combined buffer=64k gzip=4;# 方案4:日志写入 tmpfs(内存文件系统),避免磁盘 I/O# 在 /etc/fstab 中添加:# tmpfs /var/log/nginx tmpfs defaults,size=512m 0 0# 注意:重启后日志会丢失,需要定期同步到磁盘# 方案5:使用 syslog 协议将日志发送到远程日志服务器access_log syslog:server=192.168.1.100:514,facility=local7,tag=nginx,severity=info combined;
验证方式:
# 重载 nginx 配置nginx -s reload# 确认配置生效nginx -t# 之后观察 iostat,看 %util 是否下降
根因三:Docker 容器日志
Docker 容器的日志默认由 dockerd 接收并写入 /var/lib/docker/containers/<container-id>/<container-id>-json.log。如果容器 stdout 输出很多,日志文件会快速膨胀,撑爆磁盘并产生大量 I/O。
排查步骤:
find /var/lib/docker/containers -name "*-json.log" -exec ls -lh {} \; | sort -k5 -h | tail -20
pidstat -d 1 -p $(pgrep -x dockerd) 2 5
# 看容器最近 100 行日志的行数增长速率watch "docker logs --tail 100 <container-id> 2>&1 | wc -l"
修复方案:
# 方案1:限制容器日志大小(在 docker-compose.yml 中)# docker-compose.ymllogging: driver: "json-file" options: max-size: "50m"# 单个日志文件最大 50MB max-file: "5"# 最多保留 5 个文件# 方案2:限制容器日志直接写入系统日志(降低 dockerd 压力)# /etc/docker/daemon.json{"log-driver": "json-file","log-opts": {"max-size": "50m","max-file": "5" },"storage-driver": "overlay2"}# 修改后重启 dockerdsystemctl restart dockerd# 方案3:手动清理历史日志(高风险,需确认容器正常运行)# 先停止容器日志写入# truncate -s 0 /var/lib/docker/containers/<id>/*-json.log# 方案4:改用 journald 日志驱动# /etc/docker/daemon.json{"log-driver": "journald","log-opts": {}}# 之后重启 dockerdsystemctl restart dockerd
风险提醒:修改 dockerd 配置会重启 Docker 服务,导致所有容器停止。生产环境需要在维护窗口操作,提前通知用户,并确认容器支持重启后自动拉起(使用 restartpolicy)。
根因四:文件系统碎片与日志模式
ext4 文件系统在频繁的小文件写入后会产生碎片,导致文件读取时磁头移动次数增加,I/O 延迟上升。
排查步骤:
# 安装 e2fsprogs 包yum install e2fsprogs -y# 查看 ext4 文件系统碎片e2fsck -n /dev/sda1 2>&1 | grep -i fragment# 或者用 debugfs 查看debugfs -R "frag /" /dev/sda1 2>/dev/null
dumpe2fs /dev/sda1 | grep -i "journal mode"tune2fs -l /dev/sda1 | grep "Journal"
ext4 的日志模式:
journal:所有数据写入前先写日志,最安全但最慢ordered(默认):只记录元数据日志,数据写入在元数据提交之后writeback:不记录数据,只记录元数据,最快但不安全
修复方案:
# 方法1:在线调整 ext4 日志模式为 writeback(提升写入性能)tune2fs -o journal_data_writeback /dev/sda1tune2fs -O "^has_journal" /dev/sda1tune2fs -O "has_journal" /dev/sda1# 注意:调整为 writeback 后,如果突然断电可能丢失数据# 确保有 UPS 和硬件 RAID# 方法2:对于 XFS 文件系统,看是否可以优化# XFS 日志默认在外置设备上,性能更好# 方法3:定期碎片整理(需要卸载文件系统或者单用户模式)# CentOS 7 以后:umount /dataxfs_frags /dev/sda2 # 检查碎片xfs_fsr /dev/sda2 # 碎片整理,可能需要数小时mount /data
风险提醒:修改文件系统参数和碎片整理都需要谨慎操作。生产环境建议先在测试环境验证,并确保有完整备份。碎片整理期间性能会严重下降。
根因五:Swap 使用
当物理内存耗尽,系统会把不活跃的内存页换出到 Swap 空间。如果 Swap 所在的磁盘是机械硬盘,大量的换入换出会导致严重的 I/O 风暴。
排查步骤:
# 确认哪个设备是 Swapswapon -s# 确认 Swap 使用量和换入换出速率vmstat 1 10# 看具体哪些进程在换入换出cat /proc/$(pgrep -x mysqld)/status | grep -i swap
# 每秒采样一次,监控 si(swap in)和 so(swap out)列vmstat 1 | awk '{print $3,$4,$7,$8}'
# 看哪些进程占用的内存最多ps aux --sort=-%mem | head -20
修复方案:
# 方案1:临时关闭 Swap(仅限内存充足时)swapoff -a # 关闭所有 Swapswapon -a # 重新开启# 方案2:降低 Swap 优先级(让系统尽量用物理内存)# 在 /etc/sysctl.conf 中添加:vm.swappiness = 10 # 默认 60,值越低越少使用 Swap# 立即生效:sysctl -p# 方案3:把 Swap 放到 SSD 上# 创建一个 SSD 上的 Swap 文件fallocate -l 8G /mnt/ssd/swapfilechmod 600 /mnt/ssd/swapfilemkswap /mnt/ssd/swapfileswapon /mnt/ssd/swapfile# 在 /etc/fstab 中添加:# /mnt/ssd/swapfile none swap sw 0 0# 方案4:确认 MySQL 的内存配置是否合理# MySQL 5.7: 确保 innodb_buffer_pool_size <= 物理内存 * 0.8# 避免所有进程内存之和超过物理内存
根因六:批量写入任务
运维过程中常见的定时任务:备份脚本、rsync 同步、日志切割(logrotate)、数据库全量导出、大文件压缩等。这些任务往往在半夜或高峰期跑,产生大量 I/O 把正常业务拖垮。
排查步骤:
# 高频观察whiletrue; doecho"=== $(date) ==="; ps aux --sort=-%mem | awk '{print $2,$3,$4,$11}' | head -15; sleep 3; done
# 看 crontabcrontab -lcat /etc/crontabls -la /etc/cron.d/
# 找出最近 1 分钟内写入超过 100MB 的文件find / -type f -mmin -1 -size +100M 2>/dev/null
修复方案:
# 方案1:使用 ionice 限制 I/O 优先级# cron 任务中使用 ionice 限制0 2 * * * ionice -c 3 -n 7 /backup/backup.sh# ionice 参数说明:# -c 3:空闲类(idle),只有磁盘空闲时才执行# -c 2:最佳努力类(best effort),可以设置 -n 优先级(0-7,越低越优先)# -c 1:实时类(real time),最高优先级,生产环境慎用# 方案2:使用 cgroups 限制 I/O# 创建 cgroup 限制写入带宽mkdir /sys/fs/cgroup/blkio/limitedecho"8:0 1048576" > /sys/fs/cgroup/blkio/limited/blkio.throttle.write_bps_deviceecho $(pgrep -f backup.sh) > /sys/fs/cgroup/blkio/limited/tasks# 方案3:使用 rsync 的限速参数rsync -avz --bwlimit=10240 /source/ /dest/# 方案4:调整 logrotate 时间# /etc/logrotate.conf 中把 daily 改成 weekly 或 monthly# 减少日志切换频率# 方案5:备份任务安排到低峰期# 安排在凌晨 3-5 点,业务最空闲的时段0 3 * * * ionice -c 3 /backup/backup.sh >> /var/log/backup.log 2>&1
实战案例:从 iowait 高到定位 MySQL 脏页刷新
案例背景
某台 16 核 64GB 内存的物理机,运行 MySQL 5.7.30,数据库大小约 300GB。最近一周监控显示:
- CPU iowait 从平时的 5% 上升到 35-45%
- iostat 显示
sda 的 %util 持续在 95% 以上 - 数据库查询延迟明显上升,从 10ms 上升到 500ms+
第 1 步:初步判断
先用 top 看整体 CPU:
top -b -n 1
%Cpu(s): 3.2 us, 1.5 sy, 0.0 ni, 58.2 id, 37.1 wa, 0.0 hi, 0.0 si, 0.0 st
37% 的 iowait 确认了问题。再用 iostat 看磁盘:
iostat -xzk 1 5
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %utilsda 0.00 12.00 0.00 120.00 0.00 16384.00 272.73 45.00 375.00 0.00 375.00 8.26 99.20
%util 99.20%,w/s 120 次/秒,写入 16MB/s,avgqu-sz 45,说明队列严重积压,写 I/O 响应时间高达 375ms。
第 2 步:定位进程
用 iotop 找元凶:
iotop -o -b -n 3 -d 3
Total DISK READ: 0.00 B/s | Total DISK WRITE: 16.39 G/s TID PRIO USER DISK READ DISK WRITE SWAPIN IO COMMAND18123 be/4 mysql 0.00 B/s 16.39 G/s 0.00 % 99.99 % mysqld
mysqld 进程占了 99.99% 的 I/O 带宽,写入速度 16.39 G/s(这个数字异常大,说明单位可能是块设备报的累计值,实际写入速率需要用 pidstat 确认)。
第 3 步:分析 MySQL I/O 来源
登录 MySQL 查看状态:
mysql> SHOWENGINEINNODBSTATUS\G
找到关键段落:
---LOG---Log sequence number 28495678912Log flushed up to 28495456789Pages flushed up to 28494000000Last checkpoint at 28493000000100 pending log writes, 200 pending chkp writes
100 个待写入的日志写操作,200 个待检查点的脏页刷新。这就是 I/O 压力的来源。
再看 InnoDB 配置:
mysql> SHOWVARIABLESLIKE'innodb_%flush%';+---------------+-------+| Variable_name | Value |+---------------+-------+| innodb_flush_log_at_trx_commit | 1 || innodb_flush_method | O_DIRECT |+---------------+-------+mysql> SHOWVARIABLESLIKE'innodb_io_capacity%';+------------------------+-------+| Variable_name | Value |+------------------------+-------+| innodb_io_capacity | 200 || innodb_io_capacity_max | 2000 |+------------------------+-------+mysql> SHOWSTATUSLIKE'Innodb_buffer_pool_pages_dirty';+------------------------------+-------+| Variable_name | Value |+------------------------------+-------+| Innodb_buffer_pool_pages_dirty | 20480 |+------------------------------+-------+
发现问题:
innodb_io_capacity 只有 200,这个值是给机械硬盘设计的,对于 NVMe SSD 来说严重偏低Innodb_buffer_pool_pages_dirty 有 20480 个脏页,每个 16KB,总共约 320MB 脏页等待刷新innodb_flush_log_at_trx_commit = 1,每次事务提交都会刷 redo log,I/O 压力会比较大
第 4 步:确认根因
综合以上信息,根因分析:
innodb_io_capacity = 200 严重偏低,后台刷新线程每次只能处理很少的脏页- 脏页积累速度(业务写入) > 脏页刷新速度(
innodb_io_capacity 限制) - 脏页堆积到
innodb_max_dirty_pages_pct 阈值后,InnoDB 被迫强制刷新,阻塞前台查询 - 大量写 I/O 积压在块层队列中,
avgqu-sz 达到 45,await 高达 375ms
第 5 步:修复方案
风险评估:修改 innodb_io_capacity 是在线参数,可以动态调整,不需要重启数据库,风险可控。但调整后短时间内刷新速度加快,磁盘 I/O 会更集中。
操作步骤:
-- 先备份当前配置mysqld --help --verbose | grep my.cnf-- 备份 my.cnfcp /etc/my.cnf /etc/my.cnf.bak.$(date +%Y%m%d)-- 动态调整参数(当前 session 生效)SETGLOBAL innodb_io_capacity = 2000;SETGLOBAL innodb_io_capacity_max = 4000;-- 确认生效SHOWVARIABLESLIKE'innodb_io_capacity%';-- 动态调整 innodb_max_dirty_pages_pct(MySQL 5.7 可在线调整)SETGLOBAL innodb_max_dirty_pages_pct = 60;-- 写进配置文件,永久生效-- 在 [mysqld] 段添加:-- innodb_io_capacity = 2000-- innodb_io_capacity_max = 4000
# 编辑 my.cnfvim /etc/my.cnf# 在 [mysqld] 段添加或修改[mysqld]innodb_io_capacity = 2000innodb_io_capacity_max = 4000innodb_max_dirty_pages_pct = 60
第 6 步:验证效果
修改后观察:
# 1. 监控 iowait 是否下降vmstat 1 30# 2. 监控磁盘 util 是否下降iostat -xzk 1 30# 3. 监控脏页是否在正常下降mysql -u root -p -e "SHOW STATUS LIKE 'Innodb_buffer_pool_pages_dirty';" every 10s
预期效果(5-10 分钟后):
- iowait 从 35-45% 下降到 5-10%
await 从 400ms 下降到 10-30msInnodb_buffer_pool_pages_dirty 稳定在 2000 以下
第 7 步:回滚方案
如果调整后出现其他问题(比如 I/O 峰值把磁盘带宽占满影响其他服务),立即回滚:
SETGLOBAL innodb_io_capacity = 200;SETGLOBAL innodb_io_capacity_max = 2000;
同时还原 my.cnf:
cp /etc/my.cnf.bak.$(date +%Y%m%d) /etc/my.cnfsystemctl restart mysqld # 需要在维护窗口操作
诊断流程图:iowait 排查 7 步法
┌─────────────────────────────────────────────┐│ 1. vmstat 1 10 ││ 发现 b 列 > 0 或 wa 列持续 > 20% │└──────────────────┬──────────────────────────┘ │ ▼┌─────────────────────────────────────────────┐│ 2. iostat -xzk 1 5 ││ 判断 %util 是否 > 80% ││ 判断 avgqu-sz 是否过高 ││ 判断是读还是写为主 │└──────────────────┬──────────────────────────┘ │ ▼┌─────────────────────────────────────────────┐│ 3. iotop -o -b -n 3 ││ 找出具体是哪个进程在产生 I/O │└──────────────────┬──────────────────────────┘ │ ▼┌─────────────────────────────────────────────┐│ 4. pidstat -d 1 -p <PID> ││ 确认进程的 I/O 读写速率和延迟 │└──────────────────┬──────────────────────────┘ │ ▼┌─────────────────────────────────────────────┐│ 5. 进一步分析: ││ - MySQL: SHOW ENGINE INNODB STATUS ││ - Nginx: access log 配置检查 ││ - Docker: docker logs / 容器日志大小 ││ - Swap: free + vmstat 看 si/so ││ - 定时任务: crontab -l │└──────────────────┬──────────────────────────┘ │ ▼┌─────────────────────────────────────────────┐│ 6. 确认根因后,制定修复方案 ││ - 调整参数(在线 or 维护窗口) ││ - 关闭/限流/迁移 I/O 来源 ││ - 升级硬件(SSD) │└──────────────────┬──────────────────────────┘ │ ▼┌─────────────────────────────────────────────┐│ 7. 验证效果 ││ - 监控 iostat / vmstat 趋势 ││ - 业务延迟是否恢复正常 ││ - 准备回滚方案 │└─────────────────────────────────────────────┘
高风险操作汇总
内核参数调优:控制 I/O 行为
Linux 内核提供了大量可调参数(sysctl),可以控制 I/O 行为。合理调整这些参数,可以从系统层面改善 I/O 性能或限制 I/O 滥用。
1. 调整内核 I/O 调度器
调度器决定了请求如何排序和合并。不同调度器适合不同场景:
# 查看当前调度器cat /sys/block/sda/queue/scheduler# noop [deadline] cfq 输出中 [] 包围的是当前值# deadline 调度器:适合数据库、SSD、随机读写场景# cfq 调度器:适合桌面系统和通用 Linux,对实时性要求高的场景表现差# noop 调度器:适合 SSD、虚拟机、RAID 卡带缓存的场景# mq-deadline 调度器:blk-mq 版本的 deadline,更好的并发支持# 临时修改(重启后失效)echo deadline > /sys/block/sda/queue/scheduler# 永久修改(在 udev 规则或启动脚本中)# /etc/udev/rules.d/60-io-scheduler.rulesACTION=="add|change", SUBSYSTEM=="block", KERNEL=="sd[a-z]", ATTR{queue/scheduler}="deadline"
调度器选择建议:
- NVMe SSD:noop 或 mq-deadline
- 虚拟化环境:noop(virtio-blk 或 pvscsi 本身有调度)
- 数据库服务器:deadline(避免 cfq 的"公平"调度导致的延迟不稳定)
2. 调整块设备队列深度
# 查看队列深度cat /sys/block/sda/queue/nr_requests# 默认 128,适合机械硬盘# SSD 和 RAID 卡可以设置为 512-1024# 临时调整echo 512 > /sys/block/sda/queue/nr_requests# 永久修改(在 /etc/rc.local 或 systemd service 中)# echo 512 > /sys/block/sda/queue/nr_requests
队列深度过小会导致高并发下请求排队不足,队列深度过大会增加延迟。生产环境建议根据磁盘数量和并发连接数调整。
3. 调整 read_ahead_kb(预读)
# 查看预读大小(单位 KB)cat /sys/block/sda/queue/read_ahead_kb# 默认 128KB# 临时调整(适合顺序读多的场景,如数据仓库)echo 256 > /sys/block/sda/queue/read_ahead_kb# 永久修改# echo 256 > /sys/block/sda/queue/read_ahead_kb# 适合随机读多的场景(如数据库)可以降低预读echo 16 > /sys/block/sda/queue/read_ahead_kb
预读的作用是:当内核读取一个扇区时,提前把相邻的后续扇区也读入缓存。如果业务是大量顺序读(如备份、ETL),增大预读能显著提升吞吐。如果是随机读(如数据库),增大预读只会浪费 I/O 带宽。
4. 调整内核脏页刷新参数
内核的内存管理子系统会定期把脏页(已修改但未写回磁盘的页)刷新到磁盘。相关参数:
# 查看当前值cat /proc/sys/vm/dirty_background_ratio # 脏页占可用内存的比例,默认 10cat /proc/sys/vm/dirty_ratio # 强制同步的脏页比例,默认 20cat /proc/sys/vm/dirty_expire_centisecs # 脏页被认为是"可刷新"的时间,单位是 0.01 秒,默认 3000(30 秒)cat /proc/sys/vm/dirty_writeback_centisecs # 后台刷新线程运行间隔,默认 500(5 秒)# 临时调整(降低脏页比例,适合数据库服务器)echo 5 > /proc/sys/vm/dirty_background_ratioecho 10 > /proc/sys/vm/dirty_ratio# 永久修改(在 /etc/sysctl.conf 中)# vm.dirty_background_ratio = 5# vm.dirty_ratio = 10# vm.dirty_expire_centisecs = 3000# vm.dirty_writeback_centisecs = 500
重要:这些参数直接影响数据库的 I/O 模式。如果 dirty_background_ratio 太高,后台刷新线程会在磁盘已经很繁忙时继续往磁盘写数据,导致 I/O 拥塞。如果 dirty_ratio 太高,当进程写入大量数据时,可能在最糟糕的时机触发强制刷盘(此时进程被阻塞,用户请求积压)。
数据库服务器的推荐配置(MySQL/ PostgreSQL):
dirty_background_ratio = 5(更积极的后台刷新)dirty_ratio = 10-15(更低的强制刷盘阈值)dirty_expire_centisecs = 500(脏页 5 秒后就可被刷新,更快响应)
5. 开启 I/O 统计并实时监控
# 开启 I/O 延迟统计(需要内核支持 blk_mq)echo 1 > /sys/block/sda/queue/iotail_latency# 实时监控 I/O 延迟分布# 安装 bpftrace(高级工具,需要 root)# bpftrace -e 'kprobe:blk_account_io_start { @ = lhist(args->bytes, 0, 4096, 512); }'# 或者用 iostat 持续监控并记录iostat -xzk 1 >> /var/log/iostat.log &# 注意:这个日志会持续增长,需要 logrotate 处理
6. 限制单进程的 I/O 带宽
如果某个进程(如备份脚本)占用了过多 I/O,影响正常业务,可以用 cgroups v2 限制:
# 创建 IO 限制组(cgroups v2)mkdir -p /sys/fs/cgroup/io/limited# 限制 /dev/sda 的写入带宽为 50MB/secho"8:0 wbps=52428800" > /sys/fs/cgroup/io/limited/max.bps.write# 限制 IOPS 为 1000echo"8:0 wiops=1000" > /sys/fs/cgroup/io/limited/max.iops.write# 把备份进程加入限制组echo $(pgrep -f backup.sh) > /sys/fs/cgroup/io/limited/tasks# 监控限制效果cat /sys/fs/cgroup/io/limited/io.stat
实战案例 2:Nginx 日志写入导致的 iowait 飙升
案例背景
某台 4 核 8GB 内存的 Web 服务器,运行 Nginx,每天 QPS 约 2000 次。最近发现 iowait 经常在业务高峰期(上午 10-12 点)飙升到 30-40%,CPU idle 掉到 20%,导致部分 HTTP 请求超时。
第 1 步:初步排查
# 1. 看 CPU 状态top -b -n 1 | head -20# us 40%, sy 10%, wa 35%, id 15%# 2. 看磁盘 I/Oiostat -xzk 1 3# %util 接近 100%,w/s 200+,写为主# 3. 找进程iotop -o -b -n 5 -d 2
输出显示:Nginx 主进程的 IO 占比 80% 以上。
第 2 步:分析日志配置
# 看 Nginx 日志配置nginx -T | grep -A5 "access_log"
配置是:
access_log /var/log/nginx/access.log combined;error_log /var/log/nginx/error.log warn;
没有缓冲、没有压缩、没有限时刷新。每次请求都同步写日志。
第 3 步:修复方案
# 修改 nginx.confhttp { # 开启日志缓冲,减少系统调用 log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; # access log 加缓冲,16KB 缓冲区,5 秒刷新一次 access_log /var/log/nginx/access.log main buffer=16k flush=5s; # error log 用 info 级别,减少写入量 error_log /var/log/nginx/error.log info; # 开启异步日志写入(需要配置缓冲) # 注意:异步日志在进程崩溃时可能丢失最后几秒的日志}
第 4 步:验证
# 重载配置nginx -s reload# 观察 iostatiostat -xzk 1
重载后观察:
- HTTP 请求延迟明显改善,P99 从 800ms 降到 150ms
第 5 步:更激进的方案(可选)
如果业务能接受日志丢失(短暂的服务中断不丢即可),可以写入 tmpfs:
# 在 /etc/fstab 中添加tmpfs /var/log/nginx tmpfs defaults,size=256m 0 0# 或者直接挂载mount -t tmpfs -o size=256m tmpfs /var/log/nginx# 将现有日志迁移cp /var/log/nginx/access.log /tmp/backup_access.log> /var/log/nginx/access.log # 清空现有日志# 定期同步到磁盘(每分钟)# /etc/cron.d/sync-nginx-logs* * * * * root rsync -av /var/log/nginx/ /backup/nginx_logs/ >> /var/log/sync-nginx.log 2>&1
风险:tmpfs 中的数据在重启后会丢失。需要确保有定期同步机制,且在日志同步的窗口期内能接受最多 1 分钟的日志丢失。
实战案例 3:系统升级 SSD 后的 I/O 优化
背景
一台运行 3 年的物理服务器,从机械硬盘(7200 转 SATA)升级到 NVMe SSD。升级后运维团队期望 I/O 问题彻底消失,但实际观察发现 iowait 仍然存在,%util 仍然偏高。
问题分析
升级 SSD 后出现新问题,说明瓶颈从"磁盘物理速度"转移到了"其他层级"。可能的原因:
调度器仍然是 cfq:cfq 的设计基于"磁盘有寻道时间"的假设,SSD 没有寻道延迟,cfq 的公平调度策略反而增加了不必要的上下文切换和延迟。
队列深度不够:机械硬盘的队列深度通常很低,SSD 支持更高的队列深度,但内核默认值可能没有充分利用。
文件系统日志模式不合适:ext4 默认的 ordered 模式每次写数据前要先写日志,对于 SSD 这种快速设备,日志写入产生的额外 I/O 仍然占比不小。
诊断步骤
# 1. 确认 SSD 是否被识别为旋转设备cat /sys/block/sda/queue/rotational# 1 = 机械硬盘,0 = SSD# 2. 查看当前调度器cat /sys/block/sda/queue/scheduler# 如果是 cfq,SSD 也可能变慢# 3. 查看队列深度cat /sys/block/sda/queue/nr_requests# 默认 128,SSD 可以开到 512# 4. 查看 I/O 合并情况cat /sys/block/sda/queue/nr_requestsiostat -x 1 | grep sda# 如果 %util 仍然高但 r/s + w/s 不高,说明瓶颈在别处
优化方案
# 1. 切换到 noop 调度器(NVMe SSD 最适合)echo noop > /sys/block/sda/queue/scheduler# 2. 增大队列深度echo 512 > /sys/block/sda/queue/nr_requests# 3. 调整脏页刷新参数(对所有类型磁盘都有效)echo 5 > /proc/sys/vm/dirty_background_ratioecho 10 > /proc/sys/vm/dirty_ratioecho 1000 > /proc/sys/vm/dirty_writeback_centisecs# 4. 如果用 ext4,可以考虑切换到 XFS# XFS 在高并发写入时性能更好,日志管理更高效# 但迁移文件系统需要备份数据、重新格式化
# 永久化这些配置:/etc/rc.local 或 systemd servicecat > /etc/systemd/system/tune-ssd.service <<'EOF'[Unit]Description=Tune SSD I/O settingsAfter=local-fs.target[Service]Type=oneshotExecStart=/bin/bash -c 'echo noop > /sys/block/sda/queue/scheduler'ExecStart=/bin/bash -c 'echo 512 > /sys/block/sda/queue/nr_requests'ExecStart=/bin/bash -c 'echo 5 > /proc/sys/vm/dirty_background_ratio'ExecStart=/bin/bash -c 'echo 10 > /proc/sys/vm/dirty_ratio'[Install]WantedBy=multi-user.targetEOFsystemctl enable tune-ssd.servicesystemctl start tune-ssd.service
验证优化效果
# 用 fio 做基准测试yum install fio -y # CentOS# apt install fio # Ubuntu# 顺序写测试(64KB 块,4 线程,1GB 数据)fio --name=seqwrite --filename=/tmp/fio_test --size=1G \ --rw=write --bs=64k --numjobs=4 --iodepth=32 \ --ioengine=libaio --direct=1 --runtime=30 --time_based=1# 随机读测试(4KB 块,16 线程,1GB 数据)fio --name=randread --filename=/tmp/fio_test --size=1G \ --rw=randread --bs=4k --numjobs=16 --iodepth=64 \ --ioengine=libaio --direct=1 --runtime=30 --time_based=1# 查看结果中的 iops 和 lat 指标# NVMe SSD 在优化后,随机读 iops 应该达到 10 万以上# 延迟(clat)P99 应该低于 1ms
优化后实际效果:
- %util 从 80-90% 降到 20-30%(消除了不必要的调度开销)
- 延迟稳定性提升,P99 延迟从 5ms 降到 0.8ms
- 吞吐量提升约 40%(队列深度增加后并发能力增强)
监控体系:从被动告警到主动预防
建立 I/O 基线
在系统正常时建立 I/O 基线,便于在异常时快速对比判断。
# 建立基线脚本(每日定时执行)#!/bin/bash# /usr/local/bin/io-baseline.shDATE=$(date +%Y%m%d_%H%M%S)OUTPUT_DIR="/var/log/io-baseline"mkdir -p $OUTPUT_DIR# 采集当前 I/O 状态{echo"=== $(date) ==="echo"--- vmstat ---" vmstat 1 5echo"--- iostat ---" iostat -x 1 5echo"--- disk usage ---" df -hecho"--- inodes ---" df -iecho"--- mount ---" mount | grep "^/dev"echo"--- loadavg ---" uptime} > $OUTPUT_DIR/baseline_$DATE.txt# 只保留最近 30 天find $OUTPUT_DIR -name "baseline_*.txt" -mtime +30 -delete
Prometheus + node_exporter 监控指标
在 Prometheus 中配置 node_exporter,可以采集以下 I/O 相关指标:
# prometheus.yml 中添加 node_exporterscrape_configs:-job_name:'node_exporter'static_configs:-targets:['localhost:9100']
关键查询:
# 平均 I/O 等待时间百分比(每台机器)100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)# 磁盘使用率node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_free_bytes{mountpoint="/"}# 每台机器的 I/O util(需要 node_exporter 0.18+)rate(node_disk_io_time_seconds_total[5m]) * 100# 每台机器的写入吞吐量rate(node_disk_written_bytes_total[5m])
Grafana 面板配置
推荐的 Grafana 面板布局(从左到右,从上到下):
第一行:CPU 使用率分解(user、system、iowait、idle)第二行:磁盘 util(%util)和队列深度(avgqu-sz)第三行:读写吞吐量(r/s、w/s)和带宽(rkB/s、wkB/s)第四行:I/O 响应时间(await、r_await、w_await)第五行:内存使用率和 Swap 换入换出速率
告警规则配置:
# alertmanager.rulesgroups:-name:io_alertsrules:# iowait 持续 5 分钟高于 30%-alert:HighIOWaitexpr:100-(avgby(instance)(rate(node_cpu_seconds_total{mode="idle"}[5m]))*100)>30for:5mlabels:severity:warningannotations:summary:"High iowait on {{ $labels.instance }}"description:"iowait is {{ $value }}% for more than 5 minutes"# 磁盘 util 持续 5 分钟高于 90%-alert:DiskUtilHighexpr:rate(node_disk_io_time_seconds_total[5m])*100>90for:5mlabels:severity:criticalannotations:summary:"Disk {{ $labels.device }} utilization is critical"
总结
iowait 只是一个症状指标,它告诉你"CPU 在等 I/O",但不会告诉你"谁在产生 I/O"。排查 iowait 的核心思路是:
先确认是否真的是磁盘问题:用 iostat 看 %util,如果磁盘利用率很低但 iowait 高,可能是 NFS、网络 I/O 或 CPU 层面的等待。
找到 I/O 产生者:用 iotop 和 pidstat 定位具体进程,不要凭感觉猜测。
分析 I/O 类型:是读还是写,是顺序还是随机,是元数据还是数据,这决定了修复方向。
理解每层 I/O 栈的特性:VFS 的页缓存、文件系统的日志模式、块层的调度算法、设备的物理限制,每一层都可能成为瓶颈。
修复时考虑全局:调高 InnoDB 的 I/O 容量可能解决了 MySQL 问题,但如果这台机器还有其他服务,可能把整台机器的 I/O 带宽占满,反而更糟。
永远准备回滚方案:改配置之前先备份,改完之后监控效果,如果变差立即回滚。
建立监控基线:在系统正常时建立 I/O 基线,异常时快速对比,缩短故障定位时间。
内核参数不是万能药:调度器、队列深度、脏页参数等系统级调优能提升性能,但无法替代应用层的 I/O 优化(如缓存、异步写入、批量操作)。
iowait 排查没有银弹,每一次都需要结合业务场景、硬件配置、负载特征综合判断。工具只是手段,工程师的分析思路才是核心。