咱们直接聊正题。做后台开发或者运维的同学,大概率都经历过这种场景:系统CPU不高、内存充足,但接口就是慢,数据库查询就是卡,页面转圈圈。查了半天,最后定位到磁盘I/O上——要么是大量随机读写把磁盘拖垮了,要么是某个进程疯狂刷日志把带宽吃满,要么是数据库的redo日志跟数据文件抢地盘。I/O瓶颈不像CPU满载那么直观,但它的破坏力一点不小,而且排查起来往往更隐蔽、更容易踩坑。
这篇东西是我在实际项目中排查、优化I/O问题的一些经验总结,从“怎么判断瓶颈真的在磁盘”到“用哪些工具盯指标”,再到“调度器、文件系统、数据库层分别怎么调优”,最后用一个完整的排查案例把这些串起来。适合正在被性能问题折磨的后端开发、运维、DBA,也适合想系统了解I/O优化思路的架构师。
1. 先搞清楚瓶颈到底在哪
1.1 磁盘I/O问题的典型表现
I/O瓶颈的症状其实很好认,难的是确认它。常见表现有这么几类:
第一类是响应时间飙高。接口平时30毫秒返回,突然变成1秒、2秒,而且越积越多,像滚雪球一样。这种情况后端日志里通常看不到报错,就是单纯的慢。
第二类是吞吐量骤降。原来每秒能处理2000个请求,现在掉到500,怎么压都上不去。这时候CPU和内存往往还有富余,看起来很奇怪。
第三类是具有明显的周期性波动。比如整点时报表任务一跑,线上接口就卡;或者每天晚上同步任务启动,系统就像被人掐住了喉咙。这种周期性现象往往跟I/O资源争抢有直接关系。
第四类最迷惑人——单个操作很快,但整体就是慢。单条SQL执行只要几十毫秒,但整个事务提交要好几秒。这种往往跟fsync刷盘、日志落盘机制有关。
1.2 判断I/O瓶颈的三个维度
判断是不是I/O问题,我习惯从三个维度去看:
看等待。用top命令看,进程状态是D(uninterruptible sleep)还是R。如果一堆进程卡在D状态,几乎可以断定是在等磁盘I/O。D状态意味着进程在等待I/O完成,这种状态下kill不掉,只能等它自己超时。我曾经见过一个生产环境,某个进程卡在D状态超过10分钟,排查下来是磁盘阵列在进行RAID重建,把整个I/O路径堵死了。
看负载。负载高不代表CPU忙,也可能是R进程和D进程共同堆积的结果。如果一个16核的机器负载到了30多,CPU使用率却只有30%,那多出来的部分大概率是D状态进程在排队。
看链路耗时。从应用层日志、数据库慢查询、操作系统层面的I/O等待时间,逐层往下拆。如果SQL执行时间只有10毫秒,但从请求到响应却花了500毫秒,中间的时间就消耗在连接等待、锁等待或者I/O排队上了。
1.3 容易误判的两个场景
我见过不少团队在I/O问题上栽跟头,最容易误判的是这两个场景:
场景一:把CPU上下文切换过高当CPU瓶颈。网上有大量案例教你看vmstat里的cs列,上下文切换太高就以为是CPU不够,然后疯狂加CPU核数。但实际情况往往是磁盘I/O太慢,导致大量进程在等待唤醒,频繁切换上下文。这种时候用vmstat看wa(wait)列,如果iowait持续偏高,问题就在I/O侧,CPU是被拖累的。
场景二:内存不足引发的“假I/O瓶颈”。内存压力大时,系统会频繁触发swap换页,而swap本身就落在磁盘上,于是I/O量暴涨。这时候看起来是磁盘繁忙,实际上根因是内存不够用或者代码内存泄漏。所以看到I/O高,先确认一下swap使用情况,别一上来就优化磁盘。
2. 监控工具的正确打开方式
2.1 iostat:最基础也最核心的指标解读
排查I/O问题,第一个上场的一定是iostat。我常用的命令是:
iostat -x 1 5这条命令每秒采样一次,连续输出5次,重点看-x扩展字段。核心关注这几个值:
%util是设备忙碌百分比。很多人把这当磁盘“使用率”,其实严格说它表示设备处理请求的时间占比。SSD时代这个指标容易误导人——很多SSD的队列很深,%util打到100%不代表磁盘到极限了,可能只是队列在排队。
await是平均I/O响应时间(包括排队时间)。机械盘一般在10毫秒以下算正常,超过20毫秒就需要警惕。SSD的await通常小于2毫秒。如果看到await高达几十毫秒,大概率发生了排队。
svctm是平均服务时间,也就是真正在硬件上执行的时间。这个指标在新版iostat里已经不再统计了,因为现代磁盘尤其是NVMe SSD的svctm已经失去参考意义。但你可以通过await - 排队时间来间接判断。
w_await和r_await要分开看。只做读密集的操作时,写等待可能完全正常,混合读写场景下两个值都会涨。发现w_await特别高而r_await正常,说明写路径有问题,可能跟缓存策略、日志落盘有关。
2.2 pidstat与iotop:定位到具体进程
iostat告诉你磁盘很忙,但没告诉你是谁搞出来的。这时候用pidstat按进程维度看:
pidstat -d 1 5这个命令能看到每个进程每秒的读写块数,直接锁定元凶。我之前接管过一个系统,磁盘I/O经常周期性飙高,用pidstat一查,发现是一个日志压缩的定时任务在跟数据库抢I/O,整个业务响应直接劣化。
再配合iotop看实时的进程I/O排行,效果更好:
iotop -o -P-o只显示实际有I/O操作的进程,-P按进程而不是线程汇总。iotop的输出非常直观,谁在猛读猛写一目了然。
2.3 sar的长期趋势与排队模型
单次的采样只能看到当前瞬间的状态,要做趋势分析,还得靠sar。它是sysstat包自带的,通常已经随系统装了:
sar -d 1 3数据记录功能可以回溯历史,比如:
sar -d -f /var/log/sysstat/sa22查看某一天的磁盘统计。结合历史数据,你能判断当前的高I/O是常态增长还是突发故障。
另外,sar -q看运行队列和平均负载,sar -w看上下文切换和任务创建速率,这些指标配合磁盘数据,能帮你建立整体视图。比如运行队列很高但磁盘I/O很低,问题可能偏向CPU调度而非I/O。
3. 指标背后的逻辑:哪些数值才值得优化
3.1 响应时间与吞吐量的平衡
I/O调优的核心目标是在响应时间和吞吐量之间找平衡点,而不是单纯追求某一项最优。
看一个实际换算:一块典型的7200转SATA盘,随机读写IOPS大约是100~150,顺序读写可以达到150MB/s。假设你有一个业务每天产生50GB日志,机械盘的顺序写入可以应付,但如果业务模型是大量小文件随机写入(比如消息队列的commit log),IOPS就会成为第一瓶颈。100IOPS看起来不小,但遇到几千TPS的业务,再分摊到多个文件上,立即就不够用了。
SSD就不同了。普通的SATA SSD读取IOPS普遍在5万以上,NVMe SSD能打到几十万。但要注意SSD有写放大和磨损问题,随机写入频繁时实际性能和寿命都会打折扣。所以选择存储介质前,先把业务模型搞清楚:是读多写少、写多读少,还是随机小IO为主?
3.2 队列深度的应用层视角
应用层感知到的I/O延迟,很大程度受设备队列深度影响。Linux块设备层的队列深度是有限的,超过上限后请求只能排队等待,这时await会明显增加,但svctm保持稳定。你可以做一个简单测试来验证:
fio --name=randwrite --rw=randwrite --bs=4k --size=4G --numjobs=16 --runtime=30用fio并发打随机写,观察iostat -x里的await和%util变化。队列深度上来后,await会成倍增长。
应用层对应的优化思路是限制并发。数据库连接池大小、线程池大小都要做合理配置。很多团队把线程池调得很大,觉得并发越高越好,实际上超过设备处理能力后,多余的线程只是在排队,还白白增加上下文切换成本。
3.3 常见误区:关注%util的陷阱
前面提到过,%util接近100%时不一定代表磁盘到了极限。尤其SSD的并发能力很强,可以同时处理很多请求。在我的经验里,更可靠的判断逻辑是:
先看await是否持续走高。如果await稳定而%util高,说明设备还吃得下,是应用层的并发度问题。如果await和%util双双走高,说明设备真的忙不过来了。这就是STFW(顺序转随机)的问题——固态盘的随机读写性能再好,也有物理极限。
另一个误区是把iowait直接当成I/O瓶颈的证据。iowait高说明CPU有大量时间在等待I/O完成,但等待的进程可能只有一个,其他CPU核心都在正常干活。单核机器上iowait高是灾难,但48核机器上iowait到30%可能根本不算事。
4. 优化手段:从调度器到文件系统的作战地图
4.1 IO调度器的选择与切换
Linux的I/O请求从应用层生成后,会先进入块设备层的I/O调度器,由它决定请求的排队和合并策略。不同调度器对性能影响很大。
noop(现多映射为none):先来先服务,基本不做合并和排序。适合纯SSD环境或底层存储已经做了智能处理的情况。NVMe SSD建议使用这个。deadline:给每个请求设置超时时间,保证读请求不被写请求饿死。适合机械盘环境,数据库场景也常用。它兼顾了低延迟和吞吐量。cfq:完全公平队列,按进程分组分配I/O带宽。传统机械盘多进程场景适用,但高并发下公平性损耗性能,现在新内核已经移除。mq-deadline:多队列版deadline,适合多核机器和NVMe盘。
查看和修改当前调度器:
cat /sys/block/sda/queue/scheduler echo mq-deadline > /sys/block/sda/queue/scheduler如果是NVMe SSD,配上none基本上是最优解。之前有团队用默认的cfq跑NVMe,性能比换成none差了30%不止。
4.2 文件系统层面的几个关键参数
文件系统对I/O性能的影响往往被低估。常见的几个优化点:
noatime挂载参数。Linux默认记录文件访问时间,每次读文件都要更新atime元数据,产生额外写I/O。生产环境可以安全地关闭它:
mount -o remount,noatime /data或者在/etc/fstab里持久化配置。
nodelalloc参数(ext4)。ext4默认开启延迟分配(delalloc),数据先缓存在内存中,攒到一定量再落盘,减少写入次数。但对数据库这类要求事务持久性的场景,延迟分配可能造成数据块和元数据块更新顺序异常,增加崩溃后的检查时间。数据库数据目录建议关闭delalloc:
mount -o nodelalloc /dev/sdb1 /data如果是XFS文件系统,也有类似参数需要按场景取舍。日志型文件系统(ext4、XFS等)的journal记录元数据变化,每次写操作都要先落日志再更新数据,I/O成本高。高可靠场景可以考虑external journal,把journal放到独立磁盘或者SSD上。
4.3 数据库层面的I/O优化实践
数据库是I/O消耗大户,优化空间也最大。
以常见的MySQL InnoDB为例。脏页刷盘、redo log落盘、binlog写入,三者共同决定了一个事务提交需要多少次I/O。innodb_flush_log_at_trx_commit设置为1时,每次事务提交都要fsync到磁盘,数据最安全但性能最差;设置为2时每秒刷一次,性能提升明显但崩溃可能丢1秒数据。要看业务对数据丢失的容忍度来定,不适合所有场景都一刀切。
另一块是存储布局。当年我接手的一个交易系统,数据文件、redo log、binlog、系统盘全部放在同一块盘上,业务量和I/O量上去后冲突剧烈。后来拆成三块:数据文件放主盘,redo log单独放一块SSD,系统的binlog放另一块。整体性能好了不是一点半点。
库表层面,分区表能有效减少单次查询扫描的数据量。定期归档旧数据、冷热分离也是好办法——一张100GB的表,查询再快也在走I/O,把活跃数据放SSD、冷数据放HDD,等于从源头降低热点盘的负载。
5. 实操案例:一次真实的I/O瓶颈排查实录
5.1 现象描述与初步排查
某业务系统反馈,每天下午3点到4点间,核心接口响应时间从50毫秒飙升到2秒,持续大约40分钟后自动恢复。因为时间点固定,最初大家以为是业务高峰,但翻监控发现这段时间的请求量并没有明显增长。
我接手的排查从基础监控开始。先用top查看系统状态,CPU使用率只有25%,但load average达到8.6(机器是8核)。然后跑vmstat 1 10,看到wa列持续在40%~60%波动,si和so两个swap指标基本为0。到这里基本确定有I/O问题,但还需要锁定位到设备和进程。
5.2 用iostat定位设备
执行:
iostat -x 1 5结果中sda设备的%util在90%以上,await高达35毫秒,w_await更是到了60毫秒。很快注意到wr_s(每秒写请求数)达到了5000多,而r_s不到500——这是一台写密集型的压力特征。
结合业务场景,这个时段系统正在执行每日结算批处理,大量写交易流水。再跑pidstat -d 1,发现两个进程的写I/O特别大:一个是批处理程序自身,一个是日志框架的异步写线程。
5.3 逐层优化与效果验证
第一层优化:调度器。原系统用的是cfq,对于高并发随机写,cfq的公平队列机制反而制造了不必要的请求重排。切换为deadline后,await从35毫秒降到28毫秒,有一定改善但还不理想。
第二层优化:文件系统参数。检查/etc/fstab,数据盘挂载时没开noatime。remount后少了一大量atime更新产生的随机写,w_await降到40毫秒左右。
第三层优化:隔离。批处理程序和数据库共用同一块盘是核心矛盾。我们用一块空闲的SSD挂载为批处理的输出目录,同时把日志输出改为定时批量刷盘,避免实时小量写入频繁唤醒设备。
这轮操作下来,await稳定在8毫秒以下,接口响应时间恢复到60毫秒左右。全程没有改一行业务代码。
5.4 常见问题速查表
| 症状 | 可能原因 | 快速排查命令 | 解决办法 |
|---|---|---|---|
| await高,%util中高 | 设备处理能力不足 | iostat -x | 换SSD、拆分目录 |
| %util高,await正常 | 并发请求过多 | pidstat -d | 限制进程并发、检查连接池 |
| iowait高,%util不高 | 单点I/O等待 | vmstat 1 | 查看D状态进程、优化锁等待 |
| 周期性I/O高峰 | 定时批处理与在线业务争抢 | sar -d | 错峰调度、目录隔离 |
| 磁盘有空间但写入失败 | inode耗尽 | df -i | 清理旧文件/小文件 |
5.5 关于I/O监控的经验技巧
监控I/O不能只看瞬时值,要建立基线。我习惯在生产环境保留至少一个月的sar历史数据,这样出现异常时可以对比同一时段的正常曲线,快速找出变化点。
还有一个容易忽略的点:监控虚拟化环境的I/O要特别注意宿主机的影响。同一台物理机上其他虚拟机的I/O风暴,可能让你的虚拟机出现莫名其妙的性能下降。如果是云环境,配合厂商提供的底层监控数据会更准确。
最后,别把所有I/O问题都归结为硬件。很多时候是存储布局不合理、日志策略太激进、并发控制没做好。硬件升级是最简单的解法,也是最贵的解法。先做软件层面的优化,让系统负载先掉下来,你会发现其实很多情况根本不用加钱买盘。我之前遇到过一台老机器,调整了文件系统挂载参数和调度器之后,I/O瓶颈下降了近一半,省下了换存储阵列的预算。