上个月接手一台数据库服务器,业务方反馈接口响应越来越慢,晚间高峰时段经常超时。我上去一看,CPU、内存都挺空闲,load average却异常高,top里wa那一栏直接飙到40%以上。再跑iostat -x 1一看,磁盘%util已经满格,await数字看得人血压升高。这种典型的磁盘IO延迟过高问题,恰恰是Linux服务器性能排查里最容易让人迷惑的一类:表面上是业务变慢,实际上是磁盘子系统扛不住了,而磁盘本身又不一定是硬件故障。这篇文章就围绕磁盘IO延迟过高这个场景,把我实际做过的排查思路、IO调度器调整、文件系统挂载参数优化、脏页回写参数调节完整复盘一遍,给遇到类似问题的朋友一个可以直接照着走的路径。文章涉及的命令和配置都是通用层面的Linux运维操作,不依赖特定发行版,无论是云服务器还是物理机都能参考。
1. 延迟高的第一步不是调参,而是用iostat把病看准
很多人一听说磁盘性能差,第一反应就是“换个SSD”“加内存做缓存”,或者直接去改内核参数。这种操作不能说错,但很容易治标不治本。磁盘IO延迟高的原因千差万别,可能是硬件故障、可能是设备队列堆积、可能是文件系统配置不合理、也可能是应用层产生的IO模式有问题。先确认瓶颈到底在哪一层,再动手改配置,这是我一直强调的排查顺序。
1.1 业务报障与现象初判
那台数据库服务器的情况很典型:CPU使用率只有20%左右,内存充足,Swap基本没用,但业务接口的响应时间从几十毫秒涨到了2秒多。一开始我们怀疑是SQL慢查询,后来发现所有查询都变慢,包括最简单的select 1。这就基本排除了应用层问题,把矛头指向了通用组件——网络、磁盘或者内核锁。
网络排查很快做完,延迟和丢包都正常。剩下最大的嫌疑就是磁盘IO。用top确认load average和wa状态之后,我做了几件事:先确认磁盘类型和挂载方式,再看分区使用率,最后用iostat看具体指标。这里有个经验,磁盘使用率到达100%不代表磁盘满了,很可能是IO请求排队排满了,所以在看df之前,更应该先看IO负载状况。
1.2 iostat指标解读:await、svctm、%util到底谁说了算
iostat是分析磁盘IO最直接的工具,命令就是iostat -x 1,每秒刷新一次扩展指标。我截取一段当时看到的输出,字段含义值得展开讲清楚,避免新手被误导。
code复制Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
sda 12.00 145.00 240.00 4600.00 0.00 12.00 0.00 7.64 110.00 130.00 18.40 20.00 31.70 21.60 100.00
这里几个指标是最关键的:
- %util:设备忙闲程度,接近100%表面上看是“满负荷了”,但这个指标只代表采样周期内有IO请求的时间占比。对于传统机械盘,它和吞吐瓶颈高度相关;对SSD尤其是NVMe,很可能出现%util到100%但延迟依然很低的情况,因为固态盘内部是并行处理的。
- await:包括排队时间和服务时间在内的平均IO响应时间。当时r_await超过100ms,对数据库这种场景已经是灾难级别,正常应该稳定在20ms以内才算健康。
- svctm:平均服务时间,反映硬件处理一个IO请求本身要多久。如果svctm很高,例如超过50ms,说明盘本身响应很慢,可能硬件有故障;如果svctm不高但await很高,比如svctm只有10ms,await却到100ms,说明IO请求在队列里大量积压,问题更多出在排队而不是硬件。
- aqu-sz:平均队列长度。数值越大,说明积压的请求越多,排队时间自然也就越长。
那次看到的情况是svctm在20ms左右,但await到了120ms,说明设备本身没完全坏,但请求积压严重。这种问题光靠硬件排查解决不了,重点要看软件层面怎么把请求调度得更平滑、更高效。
1.3 先排除存储侧故障,再谈软件调优
软件调优做得再漂亮,也救不了一块快报废的硬盘。我个人的习惯是,在动任何内核参数之前,先做一轮存储健康检查。
- 看系统日志:
dmesg | grep -i error,如果有大量的I/O error、disk error、ata error,基本可以断定硬件层面出了问题。 - 看S.M.A.R.T.信息:机械盘用smartctl,SSD也支持。重点关注Reallocated_Sector_Ct、Pending_Sector、CRC_Error_Count这些关键属性,出现阈值告警就要考虑换盘而非调优。
- 确认线缆、RAID卡状态:物理机要检查硬盘灯是否异常,RAID卡日志是不是有磁盘掉线记录;云服务器则要关注宿主机是否做了热迁移、是否出现了存储降级。
我见过一个典型案例:一台机器刷了各种调度器参数、改了一堆文件系统选项,延迟纹丝不动,最后发现是SAS线缆老化导致传输层重传,换线之后问题立刻消失。所以,软件层面的优化一定要建立在硬件健康的前提之上,这不是保守,而是避免浪费时间。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. IO调度器怎么选:先搞清楚内核在排队什么
确认硬件正常之后,下一步该看内核IO路径的“排队策略”了。Linux通过IO调度器决定请求以什么顺序下发到底层设备。调度器选得不合适,哪怕硬件再好,延迟也会被无谓的排队拉高。
2.1 调度器到底干了什么活:一个食堂打饭的比喻
把磁盘想象成一个食堂打饭窗口,应用产生的IO请求就是来打饭的同学。IO调度器就是站在窗口前的秩序管理员,负责决定谁先打饭、怎么并桌、怎么减少窗口空闲。不同调度器的区别在于“公平优先”还是“延迟优先”,而数据库这类业务往往需要的是“关键请求不能等太久”,而不是“每个人都要绝对公平”。
传统内核里常见的调度器有三种:
- noop:只做简单的请求合并和排序,基本不干预,把底层排序逻辑交给硬件。适合SSD、NVMe这类寻址无成本、内部已经自带智能队列的设备。
- deadline:给每个请求设置截止时间,保证不会饿死某个方向的请求。对机械盘上的数据库负载来说,deadline往往比cfq表现更好,因为读写请求都有明确的时效保障。
- cfq:完全公平队列,按照进程优先级分配IO带宽,理论上非常公平,但公平的代价是延迟不可控。高并发下会出现大量请求排队等待,反而不适合延迟敏感型业务。
到了多队列内核时代,调度器演变成了none、mq-deadline和bfq三足鼎立。none基本就是传统noop的继承者,把调度工作完全下放给硬件;mq-deadline则是deadline思想在多队列模型下的实现,兼顾延迟上限和全局吞吐;bfq更强调交互式场景下的响应延迟,桌面环境用得多,服务器场景我不太推荐。
2.2 三种主流调度器的适用场景和我的选择标准
给出我的通用选择逻辑,纯属个人经验,但实测过不少机器:
| 设备类型 | 推荐调度器 | 原因 |
|---|---|---|
| 企业级NVMe SSD | none | 硬件内部队列足够智能,内核层干预反而增加开销 |
| 普通SATA SSD | mq-deadline | 可以保证请求时效,又能适当合并小IO |
| 机械盘数据库服务器 | mq-deadline | 读写混合场景下延迟可控,不易饿死 |
| 机械盘文件服务器 | bfq或cfq | 多用户共享场景下希望带宽分配更均衡 |
| 虚拟化宿主机/云主机 | none | 让宿主机和虚拟化层统一处理IO更高效 |
这里要提醒一个容易踩的坑:看到SSD就无脑选none不一定对。老一些的内核里none调度器对请求合并非常少,大量4K随机小IO直接穿透到硬件,某些型号SSD在这种情况下反而会触发内部读改写放大,延迟不降反升。建议在相同负载下分别用deadline和none做一轮对比压测,再决定最终方案。
2.3 临时切换和永久配置的操作细节
查看当前调度器:
bash复制cat /sys/block/sda/queue/scheduler
输出类似:
code复制[mq-deadline] none bfq
方括号里就是当前生效的调度器。临时切换可以直接写sysfs接口,无需重启:
bash复制echo mq-deadline > /sys/block/sda/queue/scheduler
问题是:这个操作重启就丢了。要永久生效,我习惯用udev规则,而不是在rc.local里硬写,因为udev规则在设备出现的瞬间就会执行,通用性更强。创建一个规则文件:
bash复制vim /etc/udev/rules.d/60-iosched.rules
写入如下内容:
code复制ACTION=="add|change", KERNEL=="sd*", ATTR{queue/scheduler}="mq-deadline"
保存后执行udevadm control --reload重载规则。之后新接入的磁盘都会自动使用这个调度器。注意,如果机器上有多块盘且类型不同,这个规则就太粗糙了,需要用ATTRS{model}或BY-ID匹配具体设备,比如:
code复制ACTION=="add|change", KERNEL=="nvme*", ATTR{queue/scheduler}="none"
ACTION=="add|change", KERNEL=="sd*", ATTR{queue/rotational}=="1", ATTR{queue/scheduler}="mq-deadline"
rotational为1的就是机械盘,0就是SSD,这个属性判断非常靠谱,比用盘符判断稳多了。
2.4 一个容易忽略的前提:硬件队列与多队列块层
改调度器之前,最好确认一下内核是否启用了多队列块层。可以通过lsblk -d -o name,model,queue-depth查看设备队列深度,观察/sys/block/sda/queue/nr_requests和/sys/block/sda/queue/max_sectors_kb等属性。多队列块层下,每个硬件队列有独立的中断和提交路径,调度器的行为和应用层观察到的延迟特征与传统单队列有很大区别。如果你的内核版本较老,比如还在3.10,那套调度器选择逻辑完全是另一个世界,大多数场景下deadline是相对安全的默认项。新内核我建议优先考虑none和mq-deadline的组合,别再拿老经验套新问题。
3. 文件系统与VFS回写参数:解决“静默的写入放大”
IO调度器搞定了设备侧的排队策略,但很多延迟其实来自文件系统层。文件系统决定了数据以什么格式落盘、要不要先写日志、要不要更新时间戳。这些看起来微小的行为,在高IO负载下会被成倍放大。尤其对于数据库服务器,文件系统层一个不合理配置,可能让写入量翻倍,延迟自然压不下来。
3.1 noatime到底省了多少IO
Linux文件系统每次读取文件时,默认会更新文件的访问时间(atime)。这个更新看起来无所谓,但它是一次元数据写入操作,在机械盘上就意味着多一次寻道。数据库场景下,数据文件会被反复读取,每次读取都要写atime,写入放大效应非常明显。
传统noatime完全关闭访问时间更新。更折中的是relatime,只在访问时间比修改时间旧时才更新一次,Linux在2.6.30之后默认就是relatime。但我的习惯是数据库服务器和文件服务器都直接上noatime,除非有审计需求明确依赖atime。nodiratime则是关闭目录访问时间更新,配合使用效果更好。
挂载参数修改可以这样操作:
bash复制mount -o remount,noatime,nodiratime /data
要永久生效,需要修改/etc/fstab,把挂载选项加到对应行:
code复制/dev/mapper/vg_data-lv_data /data ext4 defaults,noatime,nodiratime 0 2
改完fstab之后一定要执行mount -a验证语法是否正确,否则下次重启可能进不了系统。这也是我一直强调的:根文件系统的挂载参数尤其要谨慎,改坏了可能有起不来的风险。
3.2 data=writeback与日志模式的取舍
ext4文件系统有三种日志模式:journal、ordered、writeback。默认是ordered,也就是数据先写盘、再写日志,保证文件内容与元数据的一致性。但这个“先写盘”的过程会带来额外的IO等待,对写密集场景就是灾难。
writeback模式只对元数据进行日志记录,数据内容什么时候落盘不保证顺序。性能提升非常明显,但代价是掉电或系统崩溃时,可能发生文件内容与元数据不一致的情况,最典型的表现是掉电后文件大小为0。对于数据库这类应用,数据库自己已经有WAL日志来保证崩溃恢复,文件系统层再叠一层ordered模式的保护其实是浪费,所以我个人在数据库场景会改成writeback模式,但不能不看场景盲目改。
挂载方式:
bash复制mount -o remount,data=writeback /data
fstab对应参数:
code复制/dev/mapper/vg_data-lv_data /data ext4 defaults,noatime,nodiratime,data=writeback 0 2
XFS文件系统没有直接对应的data模式,但也可以通过allocsize、logbsize等参数控制预分配大小和日志缓冲区,对性能也有明显影响。选择文件系统时,我建议新项目直接上XFS,尤其适合大文件、高并发场景。
3.3 barrier开关:性能与掉电安全的天平
barrier是文件系统层保证数据写入顺序的一种机制,防止掉电时日志越过数据产生错乱。在传统机械盘时代,barrier会强制刷写磁盘缓存,导致写性能明显下降。很多老教程在高性能调优时建议关闭barrier,例如ext4挂载参数barrier=0,或者XFS的nobarrier。
但我要给一个明确的提醒:**现代存储环境,特别是带有BBU(电池备份单元)的RAID卡或企业级SSD,本身已经具备掉电保护能力,关闭barrier收益不大,风险却不小。**如果服务器使用的是普通SSD且没有掉电保护,突然断电可能导致文件系统损坏,这种事故的代价远超那一点点性能提升。所以我现在的做法是默认保留barrier,只有在压测确认瓶颈确实在barrier刷写、且硬件有明确掉电保护时才考虑关闭。
其实多数情况下,先用noatime和writeback模式调一轮,性能就已经能上来不少,没必要一上来就动barrier这个最敏感的参数。
3.4 脏页回写水位:调节sync对业务延迟的影响
VFS层的“脏页回写”是另一个经常被忽略的点。Linux会把应用写数据先放到page cache里,标记为脏页,再由内核pdflush/flusher线程异步刷到磁盘。这个设计本来是为了让写入看起来“很快”,但回写水位设置不合理时,延迟问题非常隐蔽。
关键参数是:
vm.dirty_ratio:脏页占全部内存的百分比达到这个值时,进程自己的写操作会被阻塞,强制触发回写。vm.dirty_background_ratio:脏页达到这个比例时,内核后台线程开始刷盘,不阻塞应用。vm.dirty_writeback_centisecs:后台线程唤醒周期,单位是百分之一秒。vm.dirty_expire_centisecs:脏页在内存中最多待多久,到期必须刷盘。
系统默认值通常是dirty_ratio=20、dirty_background_ratio=10。在大内存机器上,这两个值意味着应用可以写满几个GB甚至几十个GB的脏页才触发回写。一旦触发,会产生大量突发IO,导致磁盘延迟瞬间飙升。更麻烦的是,如果应用执行sync、fsync操作,内核需要立刻把这些脏页刷下去,延迟直接体现在应用侧。
我的调整思路是适当调低后台回写水位,让刷盘更平滑:
bash复制sysctl -w vm.dirty_background_ratio=5
sysctl -w vm.dirty_ratio=15
sysctl -w vm.dirty_writeback_centisecs=500
sysctl -w vm.dirty_expire_centisecs=3000
持久化写入/etc/sysctl.conf:
code复制vm.dirty_background_ratio = 5
vm.dirty_ratio = 15
vm.dirty_writeback_centisecs = 500
vm.dirty_expire_centisecs = 3000
这里要特别说明:dirty_ratio调太低会让写性能大跌,因为应用很快就会被强制刷盘阻塞住。具体数值需要根据业务写密度和内存大小来试,经验上8到16之间是比较常见的区间,不要照搬。
4. 一次完整调优实录:从改动到验证的每一步
理论说了一堆,关键是看实际操作。下面是我在这台数据库服务器上做的完整调优过程,每一步都带着命令和验证结果,方便对照复现。场景是CentOS系统,内核版本4.19,数据库运行在独立数据分区上。
4.1 目标机器的初始状态
机器配置是两颗16核CPU、128GB内存、一块SATA SSD作为系统盘、一块2TB机械盘作为数据库数据盘。数据盘挂载在/data,文件系统是ext4。数据库高峰期写入量大,但读请求更多,属于典型的读写混合随机IO。
初始状态:
code复制调度器:cfq
挂载参数:defaults
dirty_background_ratio=10
dirty_ratio=20
压测或业务高峰时的表现:%util经常到100%,await在80到120ms之间波动,应用侧查询延迟明显升高。
4.2 逐项调整的具体操作
第一步调整调度器。因为数据盘是机械盘,我先把调度器从cfq切到mq-deadline:
bash复制echo mq-deadline > /sys/block/sdb/queue/scheduler
第二步调整挂载参数,把noatime、nodiratime、data=writeback加上:
bash复制mount -o remount,noatime,nodiratime,data=writeback /data
同时修改/etc/fstab,把参数固化下来。
第三步调整脏页回写参数:
bash复制sysctl -w vm.dirty_background_ratio=5
sysctl -w vm.dirty_ratio=15
sysctl -w vm.dirty_writeback_centisecs=500
4.3 对比数据与验证方法
调整完成后再跑一轮业务压力,观察iostat输出:
code复制Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
sdb 80.00 200.00 1800.00 6200.00 5.00 30.00 5.88 13.04 12.00 18.00 4.20 22.50 31.00 11.20 90.00
调整前后的关键指标对比如下:
| 指标 | 调整前 | 调整后 | 说明 |
|---|---|---|---|
| r_await | 110ms | 12ms | 读延迟大幅下降 |
| w_await | 130ms | 18ms | 写延迟明显好转 |
| %util | 100% | 90% | 仍有压力,但不再满负荷卡死 |
| aqu-sz | 18.4 | 4.2 | 队列积压显著减少 |
| 应用响应时间 | 2000ms+ | 350ms | 业务侧感知明显 |
从数据看,延迟大幅下降,虽然%util仍然偏高,但等待时间已经降到可接受范围。这个结果说明问题根源确实在排队和调度策略,而不是硬件完全故障。
4.4 回滚预案与长期观察
任何调优都要有回滚预案。我当时把每一步改动都记录在案,方便出问题时快速还原。调度器和挂载参数是立即生效的,回滚只需几秒钟:
bash复制echo cfq > /sys/block/sdb/queue/scheduler
mount -o remount,defaults /data
sysctl参数回滚也简单,写回默认值即可。长期观察方面,我让监控系统持续采集iostat和业务响应时间,观察一个星期,确认延迟没有反弹。如果调优后延迟问题缓解了几天又重新升高,那就要考虑是不是数据量上涨导致IOPS需求超限,或者机械盘本身的IOPS天花板到了,这时候就需要考虑把数据迁移到SSD或扩展集群了。
5. 调优之外:几个决定成败的周边细节
调度器和文件系统参数是主角,但实际工作中,我发现有几个周边因素对磁盘延迟影响非常大,如果忽略了,前面做的优化效果很容易被抵消。
5.1 swappiness不调,调了也白调
很多Linux服务器内存明明还很充足,却一遇到IO高峰就频繁换页。原因就是vm.swappiness默认值是60,内核在内存压力不大时也可能把不常访问的页面交换到swap分区,而swap分区通常就在机械盘上,一旦发生换页,磁盘IO压力雪上加霜。
对数据库服务器,我一般把swappiness调到10以下:
bash复制sysctl -w vm.swappiness=10
写入/etc/sysctl.conf持久化。如果确定swap完全不用,甚至可以直接改为0,不过要谨慎,有些环境依赖swap处理突发内存需求。调整swappiness后你会发现一个神奇的现象:磁盘IO负载下降,CPU的wa比例也肉眼可见地降低。
5.2 让iostat跑成定时任务,问题提前暴露
磁盘IO问题最怕的是事发后排查,因为现场已经过去了,很多有价值的线索都丢了。我建议把性能数据常态化采集起来,最简单的办法是用crontab跑一个定时任务,把iostat输出落盘:
bash复制*/5 * * * * /usr/bin/iostat -x 1 60 >> /var/log/iostat_$(date +\%Y\%m\%d).log 2>&1
这样每5分钟采集一次60秒的详细数据。等哪次出问题时,翻日志就能看到问题出现前的完整走势。有条件的还可以接入prometheus+node_exporter,用Grafana画图,磁盘延迟这类指标一眼就能看出规律。
5.3 从架构上减少随机IO,才是终极解法
改参数能改善延迟,但改不了硬件的物理上限。如果业务增长到一定程度,机械盘的IOPS天花板就摆在那里,再怎么调调度器、调文件系统参数,延迟也降不下来。这种时候就要考虑架构层面的动作了:
- 热点数据进缓存:用Redis等内存缓存扛住高频读,把绝大多数读请求挡在磁盘之前。
- 大事务拆小事务:减少单次事务持有的IO资源时间,让请求更均匀地分布在时间轴上。
- 读写分离:把读流量分散到从节点,主节点专注写,降低单盘压力。
- 硬件升级:数据库这类随机IO型负载,换NVMe SSD的收益远远大于任何软件调优。
就我个人体会而言,软件调优是在现有硬件上“压榨潜力”,架构调整才是根本解决。但在还没到架构升级那一步时,把IO调度器、文件系统参数、脏页回写这几个关键点调好,你会有一种“这台机器换了一副面孔”的感觉。尤其是老旧的机械盘数据库服务器,先试这些改动,成本极低、风险可控,却常常能解决大部分延迟问题。
