1. 为什么IO是Linux性能优化里最难啃的骨头
做Linux性能优化这些年,我越来越觉得IO这块是最容易让人翻车的。CPU飙高你能一眼看出来,内存不够系统会OOM告诉你,但IO出问题的时候,现象往往特别隐蔽——应用响应慢、数据库查询突然变卡、日志写入延迟,你上去一看CPU不高、内存也够,就是不知道哪里出了问题。这种时候十有八九是IO在作怪。
IO性能优化之所以难,是因为它涉及的链路特别长。从应用程序调用write()开始,数据要经过VFS层、文件系统层、块设备层、IO调度层,最后才落到物理磁盘上。中间任何一层出问题,表现出来的症状都可能是“IO慢”,但根因完全不同。你要是只会看iostat里那个%util,基本上就是在盲人摸象。
这篇内容适合谁看?如果你已经会用top、free这些基础命令,但对IO相关的指标一知半解,遇到IO瓶颈不知道怎么下手,那这篇就是写给你的。我会从IO的基本概念讲起,把整个IO栈拆开揉碎,然后讲清楚每个关键指标到底在说什么、什么情况下该看哪个指标、看到了异常又该怎么往下追。整个过程我会用实际案例串起来,保证你看完能直接上手排查问题。
需要提前说明的是,IO优化这个话题很大,一篇文章不可能覆盖所有细节。这篇主要聚焦在磁盘IO的性能分析与优化思路上,网络IO和内存映射这些内容我会在后续单独展开。先把磁盘这块吃透,已经能解决日常工作中80%以上的IO问题了。
2. 先把IO的基本盘搞清楚:从一次write调用说起
2.1 一次写操作到底经历了什么
很多人对IO的理解停留在“写文件就是往磁盘上写数据”这个层面,实际上远没有这么简单。我拿一次普通的write()调用来举例,带你走一遍完整的数据路径。
当你的程序调用write(fd, buf, count)的时候,数据并不是直接写到磁盘上的。它首先会从用户空间的buf拷贝到内核空间的页缓存(Page Cache)里。这个拷贝过程是CPU参与的,也就是所谓的“CPU拷贝”。拷贝完成后,write()调用就返回了,你的程序可以继续往下跑。但这时候数据还在内存里,并没有真正落到磁盘上。
内核会在合适的时机把这些脏页(Dirty Page)刷到磁盘上。这个“合适的时机”由几个因素决定:一是脏页的比例超过了内核参数dirty_ratio或dirty_background_ratio设定的阈值;二是脏页在内存里待的时间超过了dirty_expire_centisecs设定的时间;三是应用程序主动调用了fsync()或sync()。
数据从页缓存到磁盘的过程,又要经过文件系统层。文件系统负责把文件逻辑块映射到磁盘的物理块,然后把这些块组织成bio(Block IO)请求发给块设备层。块设备层再通过IO调度器把这些请求合并、排序,最后发给磁盘驱动,驱动再操作硬件完成真正的写入。
你看,一次简单的write背后有这么多环节。任何一个环节出现瓶颈,都会导致IO性能下降。而且不同环节的瓶颈,排查方法和优化手段完全不一样。
2.2 机械硬盘和固态硬盘的IO特性差异
聊IO优化绕不开存储介质本身的特性。机械硬盘(HDD)和固态硬盘(SSD)在IO表现上差异巨大,理解这些差异才能做出正确的优化决策。
机械硬盘的核心问题是寻道时间。数据存在旋转的盘片上,读写磁头需要先移动到正确的磁道,然后等盘片旋转到正确的位置才能读写。这个物理过程决定了HDD的随机IO性能很差,因为每次随机读写都要重新寻道。典型的7200转HDD,随机IOPS大概只有75-150左右,但顺序读写能到150MB/s以上。
固态硬盘没有机械结构,随机读写和顺序读写的差距没那么大。一块普通的SATA SSD,随机IOPS能到几万甚至十几万,NVMe SSD更是能到几十万甚至上百万。但SSD有自己的问题:写放大和垃圾回收。SSD不能直接覆盖写,必须先擦除整个块再写,这导致实际写入量可能远大于应用层看到的数据量。而且垃圾回收过程会占用SSD内部带宽,导致性能波动。
这个差异直接影响了优化策略。对HDD来说,核心思路是减少随机IO,尽量把随机IO变成顺序IO。对SSD来说,核心思路是减少写入量,避免不必要的写操作。后面讲具体优化手段的时候,我会反复回到这个基本盘上来。
2.3 页缓存:Linux IO性能的第一道防线
Linux的页缓存设计是IO性能优化的基石。理解页缓存的工作机制,很多IO现象就能解释通了。
页缓存本质上就是内核用空闲内存缓存了磁盘上的数据。读操作的时候,如果数据在页缓存里,直接返回,不需要访问磁盘,这就是所谓的“缓存命中”。写操作的时候,数据先写到页缓存,标记为脏页,后续再异步刷回磁盘。这种设计让读写操作都能在内存速度下完成,极大提升了IO性能。
但页缓存也是一把双刃剑。如果脏页积累太多,一方面会占用大量内存,另一方面在刷盘的时候会产生IO尖峰,导致其他应用的IO请求被阻塞。更严重的是,如果系统在脏页还没刷完的时候断电或崩溃,数据就丢了。
所以内核提供了一系列参数来控制脏页的行为:
| 参数 | 含义 | 默认值 | 调优建议 |
|---|---|---|---|
| dirty_ratio | 脏页占总内存的最大比例,超过则同步刷盘 | 20% | 内存大的机器可适当降低 |
| dirty_background_ratio | 脏页占总内存的比例,超过则后台异步刷盘 | 10% | 通常设为dirty_ratio的一半 |
| dirty_expire_centisecs | 脏页在内存中的最长保留时间 | 3000(30秒) | 根据数据安全性要求调整 |
| dirty_writeback_centisecs | 后台刷盘线程的唤醒间隔 | 500(5秒) | 一般不需要改 |
这几个参数怎么调,取决于你的业务场景。如果是数据库服务器,数据安全性要求高,应该把dirty_ratio调低,让数据尽快落盘。如果是文件服务器,更看重吞吐量,可以适当调高,让内核批量刷盘减少IO次数。
注意:调整这些参数之前一定要先搞清楚业务对数据安全性和延迟的要求。盲目调大dirty_ratio可能会导致系统在内存压力大的时候出现长时间的IO卡顿。
3. 核心指标拆解:iostat里每个数字到底在说什么
3.1 最容易被误解的%util
iostat是排查IO问题最常用的工具,但里面的%util指标被误解得最厉害。很多人看到%util到了100%就认为磁盘饱和了,实际上这个理解并不准确。
%util的计算方式是:在统计周期内,磁盘有IO请求在处理的时间占比。注意,它统计的是“有没有IO在处理”,而不是“IO处理能力是否饱和”。对于机械硬盘来说,因为一次只能处理一个IO请求,%util确实能比较好地反映磁盘忙碌程度。但对于SSD、RAID阵列这些能并行处理多个IO的设备,%util到了100%并不意味着性能到顶了——设备可能还有余力处理更多请求。
我见过太多人盯着%util做优化决策,结果把好好的系统调出了新问题。正确的做法是结合await和aqu-sz(平均队列长度)一起看。如果%util高,await也高,aqu-sz也大,那说明确实有IO瓶颈。如果%util高但await很低,aqu-sz也很小,那可能只是IO请求比较密集,但每个请求处理得很快,不一定需要优化。
3.2 await和svctm的区别与联系
await和svctm这两个指标经常被搞混。简单来说:
- await:平均每次IO请求的等待时间,包括在队列中等待的时间和处理时间。单位是毫秒。
- svctm:平均每次IO请求的处理时间,不包括排队时间。单位是毫秒。
await减去svctm,大致就是平均排队时间。如果await远大于svctm,说明IO请求在队列里等了很久,瓶颈可能在IO调度层或者设备本身处理能力不足。如果await和svctm差不多,说明请求基本不需要排队,设备处理速度就是瓶颈。
不过要提醒一句,svctm这个指标在新版本的工具里已经被标记为不可靠了。因为它的计算方式假设IO请求是串行处理的,对并行处理的设备不准确。所以现在更推荐用await和aqu-sz来判断。
3.3 IOPS和吞吐量:什么时候该看哪个
IOPS(每秒IO请求数)和吞吐量(每秒传输的数据量)是两个不同维度的指标,反映的是不同场景下的性能表现。
IOPS适合衡量小文件随机读写的场景。比如数据库的OLTP业务,每次读写的数据量很小(几KB),但请求数量很大。这种场景下,磁盘的IOPS能力就是瓶颈。一块HDD大概能扛100 IOPS左右,SATA SSD能到几万,NVMe SSD能到几十万。
吞吐量适合衡量大文件顺序读写的场景。比如视频编辑、大数据处理,每次读写的数据量很大(几MB甚至几十MB),但请求数量不多。这种场景下,磁盘的顺序读写带宽就是瓶颈。HDD大概100-200MB/s,SATA SSD大概500MB/s,NVMe SSD能到几GB/s。
实际生产环境中,这两种场景经常混合出现。所以看iostat的时候,要同时关注r/s、w/s(每秒读写次数)和rkB/s、wkB/s(每秒读写数据量),结合业务特点来判断瓶颈在哪里。
3.4 队列深度:被忽视的关键指标
队列深度(aqu-sz)是我在排查IO问题时最看重的指标之一。它反映的是在统计周期内,平均有多少个IO请求在队列中等待处理。
队列深度高说明什么?说明IO请求的到达速度超过了磁盘的处理速度,请求在排队。这时候即使%util还没到100%,系统实际上已经在经历IO延迟了。用户体验就是应用响应变慢,因为每个IO请求都要等前面的请求处理完。
队列深度低说明什么?说明磁盘处理能力还有富余,IO请求来了就能马上处理。这时候即使%util很高,也不一定有性能问题。
所以我的经验是:%util高但aqu-sz低,不用太担心;%util不高但aqu-sz高,反而要警惕。后者说明IO请求的到达模式可能有问题,比如突发的大量小IO,虽然磁盘还没忙到100%,但请求已经在排队了。
4. 实操:用iostat和iotop定位IO瓶颈
4.1 iostat的正确打开方式
iostat的基本用法很简单,但要用好需要理解几个关键参数:
iostat -x -d 1 5这个命令的意思是:显示扩展统计信息(-x),只显示磁盘信息(-d),每1秒刷新一次,总共显示5次。
输出里字段很多,我挑几个最关键的来说:
- r/s和w/s:每秒读请求数和写请求数。这两个数字直接反映IO负载的类型。
- rkB/s和wkB/s:每秒读写的KB数。结合r/s和w/s可以算出平均每次IO的大小。
- await:平均等待时间。超过10ms就值得关注,超过50ms基本可以确定有性能问题。
- aqu-sz:平均队列长度。持续大于1就要注意,大于4说明排队严重。
- %util:磁盘忙碌时间占比。参考前面说的,要结合其他指标一起看。
我一般会先跑一轮iostat,看看哪个磁盘的await和aqu-sz最高,锁定问题设备。然后再针对这个设备做更细粒度的分析。
4.2 iotop:找到是谁在疯狂读写
iostat告诉你磁盘忙,但没告诉你谁在忙。这时候就需要iotop上场了:
iotop -o -d 1-o参数表示只显示有IO活动的进程,-d指定刷新间隔。输出里会列出每个进程的读写速率,一眼就能看出是哪个进程在疯狂读写。
iotop需要root权限才能运行。如果系统里没装,可以用pidstat -d 1替代,效果差不多。
我遇到过一个案例:某台服务器的磁盘IO一直很高,但业务量并没有明显增长。用iotop一看,发现是一个日志收集进程在疯狂写日志,原因是日志级别被误调成了DEBUG。这种问题用iostat只能看到磁盘忙,用iotop才能定位到具体进程。
4.3 用pidstat追踪单个进程的IO行为
当你锁定了一个可疑进程,想进一步分析它的IO模式时,pidstat是个好工具:
pidstat -d -p <pid> 1这个命令会每秒输出一次指定进程的IO统计,包括读写速率和IO次数。通过观察一段时间内的变化趋势,可以判断这个进程的IO是持续性的还是突发性的,是大块顺序IO还是小块随机IO。
如果是突发性的IO,可以结合业务逻辑看看是不是某个定时任务或者批量操作导致的。如果是持续性的高IO,那可能是程序设计有问题,比如频繁写小文件、没有使用缓冲等。
4.4 一个完整的排查案例
说一个我实际处理过的案例。某台数据库服务器,业务反馈查询变慢,但CPU和内存都正常。
第一步,跑iostat看磁盘状态:
iostat -x -d 1 3发现数据盘的await到了80ms,aqu-sz到了12,%util接近100%。很明显磁盘IO有严重瓶颈。
第二步,用iotop看是谁在读写:
iotop -o -d 1发现是数据库进程本身在大量写数据文件,写入速率大概50MB/s。
第三步,用pidstat追踪数据库进程的IO模式:
pidstat -d -p <db_pid> 1发现写入是持续性的,而且每次写入的数据块很小,大概4KB左右。这说明是随机小写入,不是批量顺序写。
结合这三点信息,基本可以判断:数据库在做大量的随机小写入,磁盘的IOPS能力跟不上。后来查下来是因为某个业务表缺少索引,导致大量全表扫描后的回写操作。加上索引后,IO量直接降了一个数量级。
这个案例说明,IO问题的排查需要层层递进:先确认磁盘有瓶颈,再找到是哪个进程,最后分析IO模式找出根因。
5. 常见IO性能问题的排查思路与解决方向
5.1 IO等待时间高但吞吐量低
这种情况通常说明磁盘在处理大量小IO请求,每个请求的数据量很小,但请求数量很多。机械硬盘遇到这种场景最吃亏,因为每次随机读写都要寻道。
排查方向:
- 用iostat看r/s和w/s是否很高,同时rkB/s和wkB/s很低
- 用iotop定位具体进程
- 分析进程的IO模式,看是否可以合并小IO为大IO
解决思路:
- 应用层加缓冲,把多次小写合并成一次大写
- 如果是数据库,检查是否有缺失的索引导致大量随机读
- 考虑使用SSD替换HDD,或者加一层缓存
5.2 吞吐量高但IO等待时间也高
这种情况说明磁盘在传输大量数据,但请求排队严重。通常出现在大文件读写或者备份场景。
排查方向:
- 用iostat看rkB/s和wkB/s是否接近磁盘的顺序读写上限
- 看aqu-sz是否持续很高
- 确认是否有多个进程在竞争同一个磁盘
解决思路:
- 错峰调度,避免多个大IO任务同时跑
- 如果是备份任务,考虑限速
- 考虑做磁盘分区隔离,把不同业务的IO分开
5.3 磁盘利用率不高但应用感觉IO慢
这种情况最隐蔽,因为从磁盘角度看一切正常,但应用就是觉得慢。可能的原因有几个:
一是文件系统层的问题。比如ext4的日志模式设置不当,每次写操作都要等日志落盘。可以检查挂载参数,看看是不是用了data=journal这种最安全的模式,换成data=ordered或者data=writeback能提升性能。
二是IO调度器不合适。Linux默认的IO调度器对SSD和HDD的优化策略不同。HDD适合mq-deadline或bfq,SSD适合none或kyber。用错了调度器会导致性能下降。
三是页缓存参数不合理。dirty_ratio设得太低会导致频繁刷盘,设得太高又会导致IO尖峰。需要根据业务特点调整。
排查方向:
- 检查文件系统挂载参数
- 检查当前IO调度器
- 检查脏页相关内核参数
5.4 常见问题速查表
| 现象 | 可能原因 | 排查工具 | 解决方向 |
|---|---|---|---|
| await高,IOPS高,吞吐低 | 大量随机小IO | iostat, iotop | 合并IO,加缓存,换SSD |
| await高,吞吐高 | 大IO排队 | iostat, pidstat | 错峰调度,限速,隔离磁盘 |
| %util高但await低 | IO密集但处理快 | iostat | 通常无需优化 |
| 应用慢但磁盘指标正常 | 文件系统/调度器问题 | mount, cat /sys/block/*/queue/scheduler | 调整挂载参数和调度器 |
| 写入延迟波动大 | 脏页刷盘策略问题 | /proc/sys/vm/dirty_* | 调整脏页参数 |
提示:排查IO问题时,一定要结合业务场景来看指标。同样的await值,对延迟敏感的在线业务可能是灾难,对离线批处理可能完全可接受。脱离业务谈性能指标都是耍流氓。
6. 几个容易被忽略的IO优化实操技巧
6.1 选对IO调度器能省一半事
IO调度器决定了IO请求怎么排队和合并。Linux提供了多种调度器,不同场景适合不同的选择:
- none:不做任何调度,直接发给设备。适合NVMe SSD这种本身处理能力很强的设备。
- mq-deadline:保证每个请求在截止时间内被处理。适合大多数通用场景,尤其是HDD。
- bfq:按进程分配带宽,保证公平性。适合桌面环境或者需要QoS的场景。
- kyber:针对快速设备优化,延迟低。适合高性能SSD。
查看当前调度器:
cat /sys/block/sda/queue/scheduler临时切换:
echo mq-deadline > /sys/block/sda/queue/scheduler永久生效需要改内核启动参数或者用udev规则。我一般建议数据库服务器用mq-deadline,NVMe盘用none,桌面环境用bfq。
6.2 文件系统挂载参数里的门道
挂载参数对IO性能影响很大,但很多人装完系统就不管了。几个关键参数:
- noatime:不更新文件的访问时间。默认情况下每次读文件都要更新atime,产生额外的写IO。对于读多写少的场景,加上noatime能明显减少IO量。
- data=writeback:ext4的日志模式。默认是data=ordered,保证数据在元数据之前落盘。writeback模式性能更好,但崩溃时可能丢数据。根据业务对数据安全性的要求选择。
- barrier=0:关闭写屏障。能提升性能,但同样有数据安全风险。只在有电池保护缓存的RAID卡上建议使用。
修改挂载参数需要编辑/etc/fstab,然后重新挂载或者重启。生产环境操作前一定要评估数据安全性。
6.3 用fio做IO性能基准测试
优化之前先要知道基线在哪里。fio是Linux下最专业的IO测试工具,可以模拟各种IO模式:
# 模拟随机读,4K块,队列深度32,跑60秒 fio --name=randread --ioengine=libaio --direct=1 --bs=4k --rw=randread --size=1G --numjobs=4 --iodepth=32 --runtime=60 --group_reporting这个命令会输出IOPS、吞吐量、延迟等详细指标。通过对比优化前后的测试结果,可以量化优化效果。
几个常用的测试场景:
- 随机读:模拟数据库查询
- 随机写:模拟数据库写入
- 顺序读:模拟大文件读取
- 顺序写:模拟日志写入
测试的时候要注意:一定要用direct=1绕过页缓存,否则测的是内存速度不是磁盘速度。另外测试会消耗大量IO,不要在业务高峰期跑。
6.4 监控IO的长期趋势比看瞬时值更重要
iostat和iotop适合排查即时问题,但要发现潜在的性能退化,需要长期监控。我一般会用sar来收集历史IO数据:
# 每10秒采集一次,保留30天 sar -d 10 1sar的数据可以用sadf导出成CSV,然后导入到监控系统里做趋势分析。通过观察await、aqu-sz这些指标的长期变化,可以在问题爆发之前就发现苗头。
比如你发现某个磁盘的await在过去一个月里从5ms慢慢涨到了15ms,虽然还没到报警阈值,但趋势已经不对了。这时候就可以提前介入,看看是数据量增长了还是有什么异常进程。
7. 我个人在IO优化上踩过的坑
说几个我实际踩过的坑,都是血泪教训。
第一个坑是盲目调大dirty_ratio。早期我觉得让数据多在内存里待一会儿,批量刷盘能提升性能,就把dirty_ratio调到了40%。结果有一次内存压力大的时候,内核要回收内存,发现大量脏页需要先刷盘才能回收,导致系统卡死了好几秒。后来我才明白,dirty_ratio不是越大越好,要根据内存总量和业务写入速度来算。一个简单的估算方法:dirty_ratio乘以内存总量,再除以磁盘写入速度,就是最坏情况下刷盘需要的时间。这个时间不能超过业务能容忍的延迟。
第二个坑是忽略了IO调度器的影响。有一次给一台NVMe SSD的服务器做优化,怎么调参数性能都上不去。后来发现IO调度器还是默认的mq-deadline,换成none之后IOPS直接翻倍。NVMe设备本身处理能力很强,不需要调度器做额外的合并和排序,直接发给设备反而最快。
第三个坑是只看平均值不看分布。iostat输出的await是平均值,但IO延迟的分布可能很不均匀。大部分请求很快,少数请求很慢,平均下来看着还行,但用户体验被那些慢请求拖累了。后来我学会了用iostat的百分位统计,或者用bpftrace直接追踪IO延迟的分布,才能看到真实情况。
第四个坑是在虚拟化环境里忽略了存储后端。有一次在虚拟机里排查IO问题,怎么调虚拟机内部的参数都没用。后来才发现瓶颈在宿主机共享存储上,虚拟机里怎么调都是白费劲。虚拟化环境里排查IO问题,一定要从宿主机到虚拟机自上而下地看,不能只盯着虚拟机内部。
这些坑让我明白一个道理:IO优化没有银弹,必须理解整个IO栈的工作原理,结合具体场景做针对性的调整。而且每次调整之后都要用数据验证效果,不能凭感觉。
这个内容后续还可以这样扩展:网络IO的性能分析与优化、内存映射IO的使用场景和注意事项、容器环境下的IO隔离和限制。每一个方向都值得单独展开讲,我会在后续的内容里陆续分享。