用iostat一看,磁盘那行%util直接飙到95%以上,甚至常年99%,系统响应慢得跟放幻灯片一样,top里一排排D状态进程,这种情况遇到过一次就忘不掉。更折磨人的是,你很难说清楚到底是业务量真的大、还是哪块配置出了问题、还是磁盘本身就快不行了——三者表现出来的症状几乎一模一样。
这篇文章我不打算讲教科书理论,就结合我自己在服务器上、虚拟化环境里、还有数据库机器上处理这类问题的实际经历,把从“看到%util高”到“定位根因再到收尾”的完整流程和思路拆开聊。说实话,这套方法论到现在我还在用,大部分环境下的I/O问题都能套进去。
1. 先把指标体系张罗起来,再判断那块盘是真有罪,还是替罪羊
1.1 单看%util很容易被误导,必须把几个关键指标一起看
很多新手第一次用iostat,看到%util这一列数值很高,脑子里的第一反应就是“磁盘要不行了”“要扩容了”。这个理解其实不完全对。%util的含义是设备在采样周期内处理I/O请求所占用的时间百分比,它反映的是“设备的忙碌程度”,但这个“忙碌”背后的原因可能完全不同,必须结合其他指标来交叉判断。
我自己的习惯是,拿到iostat输出后,至少同时看这几列:
%util:设备忙碌度,超过80%基本可以判定为繁忙;r/s和w/s:每秒读请求数和写请求数,用来判断是读多还是写多;rkB/s和wkB/s:每秒读写数据量,用来判断请求大小;await:平均I/O响应时间(包括队列等待时间),这个是最直观的体验指标;svctm:平均服务时间(早期版本有这个字段,新版本已去掉,但概念仍然值得参考)。
你会发现一个很有意思的现象:有些情况下%util已经99%了,但await并不高,只有十几毫秒;另一种情况是%util只有30%,await却到了几百毫秒。这两种情况的处理思路是完全不同的。
1.2 读懂“高%util”背后的两种真实场景
用生活化的方式来理解:去银行办业务,排队窗口只有一个,%util就是窗口工作人员的忙碌度,它永远是接近100%的,因为只要还有业务,他就不停手。此时系统快不快,取决于一笔业务要办多久,也就是await。
场景一就是典型的“窗口效率低,但业务量并没有爆炸”。%util高、await也很高。这种情况说明磁盘本身处理请求的速度跟不上了,可能是磁盘硬件老化、接口速率限制、或者请求模式太碎片化。
场景二是“业务量真的太大了,窗口忙不过来还排长队”。%util高、await升高得更明显,甚至出现大量“D状态”进程(不可中断睡眠,等待磁盘I/O),这时通常是业务并发已经超出了磁盘的承受能力。
这两种场景的处理方式,一个偏重“调优”,一个偏重“扩容或拆分”,方向完全不同。所以先别急着动,把指标证据集齐了再说话。
2. 观测手段别只用iostat一次,要养成“动态观测”的习惯
2.1 跑一轮连续采样,把峰值、持续性和平均线都看在眼里
很多人排查I/O就执行一条iostat命令,看完一个数字就下结论。但I/O是一个波动性很强的指标,有时候刚好赶上业务高峰,看一眼当然是高的;有时候刚好在凌晨备份窗口,你看到的数值其实不代表常态。在生产环境里做判断,最忌讳用瞬时数据代表全部情况。
我的习惯是先做一轮iostat -x 1 5,每秒一条、连续采集5次,看趋势而不是看单点。如果每次输出的%util都在高位且稳定,说明是持续性的高负载;如果只在某些次采样中很高,其他时候降下来,说明是间歇性抖动,要结合业务时间窗口去分析是不是有定时任务、备份脚本、日志切割在作怪。
2.2 补充pidstat、iotop、dstat这些帮手,从“设备视角”落到“进程视角”
iostat告诉你的是“这个设备有多忙”,但它没办法告诉你“是谁让它这么忙”。要回答这个问题,需要用到另外一套工具。
pidstat -d可以按进程维度查看I/O统计信息,包括每秒读写的KB数和总的I/O操作数;iotop则像一个“top版”的I/O监控,能实时显示每个进程的I/O读写速度。dstat可以组合查看CPU、磁盘、网络、内存的联动情况,特别适合排查“是不是某个组件拖累了整个系统”的关联问题。
我自己在排查时会按这个顺序来:先说“设备有问题”,再说“是读还是写”,最后说“是哪个进程挑起来的”。这三步缺一不可,直接跳到排查进程会容易漏掉中间环节。
3. 从读/写模式入手,判读出I/O特征再决定调优方向
3.1 读多写少与写多读少,完全是两个世界的处理思路
判断出是读多还是写多,能让排查方向立刻缩小一大半。最简单的方法是看iostat输出里的rkB/s和wkB/s,如果读的量远超写的量,那问题大概率出在“数据没进内存缓存”或者“业务本身要频繁读取大量冷数据”上。
读多写少的场景,优先考虑的思路包括:应用层有没有做缓存、数据库的buffer pool是不是太小、文件系统层有没有使用适当的预读机制、是不是存在大量随机读小文件的情况。
写多读少的场景则更麻烦一些,因为写I/O往往比读I/O更难被缓存吸收。写多的时候要先确认是不是存在频繁的fsync操作(比如数据库的redo log、binlog刷盘)、有没有日志写得太频繁太碎的问题、文件系统的日志模式是不是过于保守(比如ext4默认的data=ordered模式)。
3.2 随机I/O和顺序I/O的判断,直接决定了硬件选型和参数调整
另外一个特别重要的区分维度是“随机”还是“顺序”,这个特征其实藏在单次请求的数据量里。观察rkB/s和r/s这两个值,如果每次读请求平均只有4K、8K这种小数据量,那基本就是随机读,比如数据库按主键查询;如果单次请求是几百KB甚至1MB以上,那通常是顺序读,比如全表扫描、视频流读取、大文件拷贝。
随机I/O和顺序I/O对存储设备的考验完全不同。机械硬盘的随机I/O能力很弱,顺序I/O却很可观;SSD虽然两者都强,但在随机写场景下也存在写放大和垃圾回收的问题。如果机器上是机械盘,%util常年居高不下,先看看磁盘的读写模式,基本上可以八九不离十地猜到结果。
3.3 一个实操案例:从iostat四条数据实现对思路的完整落地
我处理过一个比较典型的案例,一台跑着MySQL的物理服务器,每到业务高峰就卡顿,iostat显示sda的%util到了98%,rkB/s约15000,r/s却高达3000多,单次读大小只有5KB左右,这说明业务在做非常密集的小数据量随机读。
当时MySQL配置的innodb_buffer_pool_size只有2GB,而实际数据量到了30GB,缓存命中率很低,大量查询直接穿透到磁盘。在业务无法立刻扩容的情况下,先调整了buffer pool到可用内存的70%左右,同时开启了innodb_adaptive_flushing,减少刷脏页带来的额外I/O压力。
这一步操作后,%util从98%下降到40%左右,起码让系统先喘过气来。这个案例想表达的是:%util高不是终点,它背后一定有一个业务模式和配置之间的错配点,找到那个点才是关键。
4. 别放过系统层面的“隐形黑手”,D状态进程和日志刷盘是重灾区
4.1 用D状态进程定位到具体的等待者,再顺藤摸瓜找源头
top命令里偶尔能看到一些进程的状态是D,这是“不可中断睡眠”,通常意味着进程正在等待I/O完成。D状态进程过多,说明有大量线程在I/O上排队——但注意,它们只是受害者,真正的元凶要让pidstat出来指认。
pidstat -d输出的KB_rd/s、KB_wr/s和cswch/s(上下文切换数)很有参考价值。当某个进程的写I/O特别大、而且上下文切换也非常频繁时,基本可以判断它正在制造大量并发写操作,把磁盘队列塞满。
高并发写还有一个容易被忽视的地方:它会拖累所有其他进程。因为磁盘设备的队列是全局共享的,一个进程疯狂写I/O,其他进程的读请求也要排队等待,这时候你看到系统“莫名很卡”但其实没有明显的CPU瓶颈,七成是这么来的。
4.2 数据库的刷盘频率和日志落盘策略,通常是压垮磁盘的最后一根稻草
数据库类应用是磁盘I/O问题最集中的领域,因为这个群体对I/O的依赖度极高。MySQL的doublewrite、redo log刷盘、binlog同步,PostgreSQL的WAL写入,这些机制在保证数据安全性的同时,都消耗着大量写I/O。
如果你用的是MySQL,这几处配置值得仔细过一遍:
innodb_flush_log_at_trx_commit:默认是1,代表每次事务提交都刷盘,安全性最高,但也最费I/O;设置为2可以让OS缓存每次帮忙缓冲,性能显著提升但掉电会丢1秒内的数据;sync_binlog:同样是1最安全也最费I/O,和上面那个值配合使用时,可以形成一个安全性的组合拳,同时也可以考虑折中设置为0或N;innodb_doublewrite:为保证数据页写入的原子性,每次写操作会先写doublewrite buffer再落到实际表空间,这个机制在普通机械盘上成本很高,需要谨慎评估。
曾经有一台机器,磁盘%util常年80%,但业务量并不大。后来排查发现,是某业务系统在代码里做了极度频繁的小事务提交,每次都触发刷盘操作。这种“频繁的小事务”模式对I/O的杀伤力,远大于偶尔的大事务,因为它让磁盘的队列一直处于被占用的状态。
4.3 日志应用的write和fsync,可能比业务本身更烧磁盘
如果说数据库刷盘是明面上的消耗大户,那么日志系统就是暗处的消耗大户。很多应用的日志框架,每写一条日志就调用一次write甚至fsync,高层应用可能感受不到差别,但内核和磁盘却在承担极其沉重的压力。
对于这种场景,处理思路一般是:提升日志批量写入能力,减少直接落盘频率;把日志写到独立的磁盘分区上,避免和业务I/O争抢;如果条件允许,把日志目录放到SSD上;如果用的是rsyslog或syslog-ng这类系统日志服务,确认它们的异步写入配置是否合理。
之所以要单独说日志这块,是因为日志通常是“杀死磁盘I/O的最隐蔽凶手”。我曾经遇到一台服务器,磁盘%util很高但业务进程的I/O看起来都很正常,最后用lsof挨个排查打开了哪些文件,才发现是某个容器的stdout日志量太大了,数据卷把所有I/O都吃掉了。
5. 深入硬件与底层机制,识别“隐形降速”与“假性繁忙”
5.1 机械盘、SSD、网络存储,不同介质的%util高含义完全不同
同样是%util飙升,机械盘上的含义很可能与SSD上的含义大相径庭,因为它们的底层工作原理完全不同。
机械硬盘的磁头在盘片上寻道、旋转,随机小I/O的寻道时间占了响应时间的大头,所以当它%util很高时,通常是真的到了性能极限。SSD没有机械寻道,%util反映的是控制器处理请求的时间占比,高%util可能是因为垃圾回收(GC)在后台运行,或者是写放大效应(实际写入的数据量远大于逻辑写入量)。网络存储(如NFS、iSCSI)的%util还包括了网络延迟,所以%util高时第一怀疑对象可能是网络链路而不是远端存储。
如果判断出底层是SSD,并且写I/O很多,可以考虑检查一下固态盘的剩余空间和磨损情况。SSD在剩余空间不足时,垃圾回收会变得非常频繁,这会让I/O性能大幅下降,表现出来就是%util异常升高但实际业务请求量并没有增加。
5.2 RAID降级、坏道重映射、接口降速,硬件层故障往往以“%util高”方式呈现
有一类%util升高是硬件健康问题带来的,这类问题最容易被忽视,因为常规监控不会直接报错。
RAID阵列中的一块盘掉线、进入降级模式后,整个阵列的写性能会显著下降,因为需要实时计算校验数据;机械盘出现大量坏道时,重映射操作会让I/O响应时间瞬间暴涨,导致%util飙升;SATA/SAS链路如果出现松动或信号问题,接口会不断重试,表现出来也是I/O变慢。
所以在排查%util高的问题时,我通常会顺手做两件事:一是查看dmesg里有没有磁盘相关的报错信息,比如I/O error、reset、CRC错误等;二是查看RAID卡的状态,确认阵列是否处于正常状态。这一步虽然简单,但往往能帮你躲开一大段无效排错。
5.3 I/O调度器和文件系统挂载参数,内核层的小配置能带来大差别
Linux内核的I/O调度器负责决定磁盘请求的处理顺序,这直接决定了随机读写的排队方式。不同调度器在不同场景下表现差异明显,比较常见的几种:
noop:最简单的FIFO队列,适合纯SSD或者底层存储已经做了大量调度的场景;deadline:为每个请求设置截止时间,重读轻写优先保证读请求,传统机械盘场景表现不错;mq-deadline:多队列版本的deadline,现代内核默认之一,适用面比较广;cfq:完全公平队列,通过时间片方式让所有进程获得比较均衡的I/O机会,但高并发下延迟可能偏高,新内核已逐步弃用。
修改I/O调度器的方法很简单,临时生效用echo mq-deadline > /sys/block/sda/queue/scheduler,永久生效需要配置内核启动参数或者在udev规则里设置。
文件系统挂载参数同样值得检查。比如ext4/nfs常常会根据挂载选项决定是否启用atime更新,relatime和noatime可以显著减少读操作触发的元数据写操作;barrier=1(或者默认的data=ordered模式)保证顺序性但也会增加写成本。这些细节单个看起来影响不大,但在高并发下积少成多,完全可能从量变引起质变。
6. 常规“三板斧”不够用了,再讲几个压箱底的针对性方案
6.1 应用层优化:把I/O次数降下来,才是根本出路
无论底层设施怎么调,如果应用层没有减少不必要的I/O调用,那么一切优化都是治标不治本。应用层优化最常见的方向是缓存和批量。
缓存的意义在于,把多次重复读变成一次读。数据库的查询缓存、前端应用的本地缓存、搜索引擎的缓存,都是一样的原理。批量化的意义在于,把多次小I/O合并成一次大I/O,比如SQL里本来一次提交一条insert,改成一条insert里插入多行;日志本来是逐条写,改成攒够一定条数再刷盘。
有数据支撑地讲:一次写8KB和八次写1KB,后者对磁盘造成的压力大约是前者的好几倍,因为每次写I/O都有传输命令、寻址、校验等固定开销。用小事务批量刷新代替高频小事务,是数据库场景里性价比极高的优化。
6.2 系统层调优:内存换I/O、缓存换延迟的操作清单
系统层能做的调优,本质上是用计算资源和内存资源去换磁盘I/O资源。常见的几种成熟做法包括:
- 加大文件系统缓存:把脏页比例调高,让写操作在内存中多待一会再落盘;
- 调整内核的
vm.dirty_ratio和vm.dirty_background_ratio:前者是脏页达到多少比例时强制同步写,后者是达到多少比例时后台开始写,适当调大可以让突发写更平滑; - 为数据库等应用预留足够大的buffer pool,让热点数据尽量留在内存中;
- 对于读多的场景,合理调整
vm.vfs_cache_pressure参数,避免内核过早回收目录项和inode缓存。
这里要提醒一点:内存换I/O是有限度的,如果内存本身就紧张,强行调高缓存比例可能导致swap使用急剧上升,反而出现新的性能问题。
6.3 架构层拆分:把I/O压力分摊到多块磁盘、多台机器
当单台设备的I/O能力已经到达物理极限,再怎么调优都只是杯水车薪,接下来就轮到架构层面的工作了。思路无非两种:拆盘和拆机器。
拆盘就是把不同业务的I/O分散到不同的物理设备上。比如把数据库的数据目录、日志目录、系统目录放在不同的磁盘上,把WAL日志和热数据分开存储,避免一份盘上的I/O峰值影响其他模块。拆机器则是按业务维度横向扩展,比如把读多写少的服务拆出去做读写分离,用多台机器分担读压力,或者在中间加一层缓存服务来挡住读I/O。
从成本角度看,拆盘是最便宜高效的手段,如果你手头有闲置的磁盘,先做这一步。拆机器需要考虑的维度更多,还涉及到数据一致性、负载均衡、故障转移等问题,适合在拆盘仍不能满足需求时推进。
6.4 如果已经“救不回来”了,提前建立I/O性能基线和容量预警才有退路
真实运维场景中,经常有“问题已经发生再开始排查”的被动情况。几个主动运维的建议值得记住:一是平时定期记录iostat的常规读数,形成性能基线,将来出现问题时才能知道偏离了正常范围多少;二是监控告警不能只设置%util>90%这一条,还要配合await上升幅度、队列长度(avgqu-sz)变化来综合判断;三是重要业务尽量在条件允许时提前做高I/O压力测试,观察它在I/O密集时的表现。
我之前在给一台备用服务器做例行巡检时,发现它的%util有段时间一直异常偏高,但因为不是线上业务所以没人care。后来这个“备用机”真的被顶上生产时,第一波流量就把磁盘打满了。这个教训让我很感慨:I/O性能基线这个东西,平时花十分钟记录,关键时刻能顶一轮大排查。
7. 排查时候的思维框架,比技巧本身更值得沉淀
7.1 按顺序排查,少走弯路:设备层→系统层→应用层→架构层
I/O问题排查最怕一上来就怀疑某一方面,然后根据不完整的信息乱试。我踩过好多次“反过来排查”的坑后,逐渐形成了一个固定的排查顺序,分享出来仅供参考:
先确认硬件层有没有问题,包括磁盘健康状态、RAID状态、接口速率,这一步可以用dmesg、smartctl、RAID卡管理工具快速检查;再确认系统层的配置是否合理,包括I/O调度器、挂载参数、脏页比例;接着才看应用层的I/O模式,找出哪个进程、哪类操作在制造压力;最后才是架构层面的评估,判断是否需要扩容或者拆分。
这个顺序符合“从底层到顶层”的排查逻辑,每层都有相对清晰的判别标准,不容易漏掉关键信息,也不容易在一个方向上钻牛角尖。
7.2 不同业务的I/O陡增特征不同,要结合高峰时段一起分析
同样是一台机器I/O %util飙升,出现在双十一大促期间和出现在凌晨三点,含义完全不同。前者可能是预期内的业务峰值,后者则多半有异常因素。
我习惯在做I/O分析时,把时间因素一起拉进来思考:%util高的时间段是否与业务高峰一致?是否与定时任务窗口一致?是否与备份作业或日志切割窗口一致?如果各项都对齐了,那很可能是负载到了一定量级之后超出磁盘能力;如果时间上对不上,就要优先怀疑某个进程异常失控。
有个典型的例子:一台服务器白天%util正常,每晚10点开始飙高到90%,持续半小时后回落。排查后发现是某个定时脚本在这个时间点做全量文件扫描和索引重建,但脚本的执行频率和业务访问高峰撞在一起了。解决方式很简单,把定时任务错峰执行,%util就下来了。
7.3 长期高%util环境下的“降载”思路:削峰填谷,给系统留出喘息机会
在I/O资源长期吃紧的环境里,除了被动扩容之外,还可以主动做一些“削峰填谷”的操作。思路和城市交通错峰是一个道理:把密集的I/O请求尽量打散到不同的时间段执行。
具体做法包括:重I/O的任务错峰执行,比如备份、批量导入、索引重建不要挤在同一个时间窗口;在代码层面控制并发度,比如限制同时运行的异步任务数量,避免一瞬间启动大量线程产生I/O雪崩;利用消息队列将突发写入削峰,让系统以可控的速率慢慢消费积压的数据。
我见过很多系统在高峰期I/O很紧张,但非高峰时磁盘却很空闲,这种情况下优先考虑削峰,要比直接买新硬件更划算。说到底,I/O问题很多是“模式问题”,不完全是“容量问题”。
8. 最后再分享一把排查利器和一个压箱底的冷门技巧
8.1 一个非常实用的快捷排查组合命令
每次排查I/O问题,我总会在最开始执行这样一条命令,它能把当前I/O状况和进程占用情况一次性展现出来:
iostat -x 1 3 && pidstat -d 1 3 && top -bn1 | head -30iostat -x 1 3先看设备层状态,pidstat -d 1 3再看进程层统计,最后top看一眼整体负载和D状态进程数量。如果这几条命令的输出里已经能明显看出问题,那就不用启动一堆重型监控工具了,快速定位、快速止血是最重要的。
8.2 使用磁盘延迟直方图观察长时间趋势,比看平均值更有效
很多人在看iostat时只关注平均值,其实那只是“整体感受”,很难反映出问题的分布特征。后来我开始关注biostat(某些发行版自带,需要额外安装)或者内核提供的延迟统计信息,用直方图的形式观察I/O延迟的分布情况。
通过直方图能够看到有多少I/O落在1ms内、多少落在10ms内、多少卡在100ms以上,这比单纯看平均延迟有意义得多。因为一个系统里可能90%的I/O非常快,只有10%的I/O慢到拖垮整体,平均值可能看起来还行,但实际用户体验已经在崩溃边缘了。这种“长尾延迟”问题,看平均值看不出来,看直方图一目了然。
我见过一台数据库机器,平均await只有20ms,看起来非常正常,但用直方图一看,有约2%的I/O等待超过了500ms。正是这2%的长尾产生了大量的慢查询和锁等待,业务端实际感受是“系统经常卡顿”。这个问题后来通过对热点表分区、改善索引命中率解决掉了,如果只看平均值,可能排查方向完全被带偏。
8.3 为什么这篇文章没直接给你一个“标准答案”
经常有人问我,磁盘I/O %util特别高,到底该怎么解决?说句实话,真没有一个适合所有场景的统一答案。不同硬件、不同应用、不同业务模式,解决路径可能完全不同。我能给出的是完整的分析框架和实操思路,而不是一个“万能脚本”。
你真正要做的,是花一点时间搞清楚自己系统的I/O特征,是读多还是写多、是随机还是顺序、是高并发还是定时突发,然后把上面的方法按顺序套一套。思路对了,工具只是放大器;思路错了,再强大的监控也只能让你看到问题依然存在。
我自己的习惯是把每次I/O问题排查的过程记录成一份文档,包括现象、指标数据、诊断过程、最终方案和效果验证。遇到类似问题时直接翻历史记录,能省掉大量重复排查时间。希望这个习惯对你也有启发,也欢迎在评论区交流你遇到过的I/O疑难杂症。