一、问题背景
某天下午三点,监控系统推送了一条告警:数据库服务器磁盘 IO 使用率达到 95%,业务方几乎同时反馈订单系统出现大量接口超时。这是一次真实的生产事故复盘,从告警触发到最终定位根因、完成修复、验证效果,整个过程记录下来,作为磁盘 IO 类问题排查的参考案例。
磁盘 IO 问题相比 CPU 问题有一个特点:现象往往更隐蔽,很多时候 CPU 使用率看起来并不高,load average 却居高不下,业务响应却明显变慢,这种”看起来资源没占满但业务就是慢”的场景,恰恰是磁盘 IO 问题的典型表现。如果排查者没有磁盘 IO 相关的判断经验,很容易被 CPU 和内存的正常指标误导,浪费大量时间在错误的方向上。
本文按照真实排查的时间线,还原从告警触发到问题解决的完整过程,同时提炼出一套可以复用的磁盘 IO 排查方法论。
二、适用场景
本文适用于:
- 物理机、虚拟机、云服务器上出现磁盘 IO 使用率告警(IOPS 或者 IO 利用率超过阈值);
- 数据库服务器(MySQL、PostgreSQL、Redis 持久化场景)响应变慢,怀疑磁盘瓶颈;
- 应用日志写入、文件处理类服务出现性能下降;
- 容器化部署环境中,某个容器的磁盘 IO 异常影响宿主机整体性能;
- 需要区分”磁盘满了”(空间问题)和”磁盘 IO 打满了”(吞吐/IOPS 问题)这两类不同性质问题的场景。
本文不涉及磁盘空间占满(df 显示 100% 已用)的排查,这是另一类问题(存储容量问题),本文聚焦在磁盘的读写性能维度。
三、核心知识点
3.1 IOPS 与吞吐量(Throughput)是两个不同的维度
IOPS(Input/Output Operations Per Second)衡量的是每秒能处理的读写请求次数,吞吐量衡量的是每秒能传输的数据量(比如 MB/s)。这两个指标不是线性对应关系:如果都是小块随机 IO(比如数据库的随机读写场景),IOPS 容易被打满,但吞吐量可能远没有达到磁盘的理论上限;如果是大文件顺序读写(比如日志归档、备份任务),吞吐量容易被打满,但 IOPS 可能还有余量。
排查磁盘问题时,先要判断当前的压力是 IOPS 密集型还是吞吐量密集型,这决定了后续优化方向完全不同(IOPS 瓶颈通常要考虑升级到 SSD/NVMe 或者优化 IO 模式减少随机访问,吞吐量瓶颈通常要考虑网络存储带宽或者本地磁盘的顺序读写能力)。
3.2 %util 不能简单等同于”磁盘满了”
iostat 输出里的 %util 表示设备处理 IO 请求所占用时间的百分比。对于传统机械硬盘,%util 接近 100% 基本可以认为磁盘已经饱和;但对于 SSD 或者支持并行处理多个 IO 请求的存储设备(尤其是云盘、分布式存储),%util 达到 100% 不一定代表设备真的饱和了,因为这类设备可以并行处理多个 IO 队列,%util 的统计方式在这种场景下会有偏差。这也是为什么排查磁盘问题不能只看单一指标,需要结合 await(平均等待时间)、svctm(如果有)、队列深度等指标交叉判断。
3.3 await 与 svctm
await 表示每个 IO 请求的平均处理时间(包含在队列中排队等待的时间和设备实际处理的时间),单位是毫秒。这是判断磁盘是否”真的慢”最直接的指标:如果 await 数值远超磁盘正常基线(比如机械盘正常在几毫秒到十几毫秒,SSD 正常在1毫秒以内,如果飙升到几十毫秒甚至上百毫秒),说明请求排队严重或者设备本身响应变慢。
需要注意的是不同版本的 iostat 输出字段可能有差异,svctm 字段在较新版本的 sysstat 工具里已经被标记为不再准确并逐渐移除,重点关注 await、r_await、w_await(分别是读、写各自的平均等待时间,可以帮助区分是读慢还是写慢)。
3.4 队列深度(avgqu-sz / aqu-sz)
表示平均有多少个 IO 请求在排队等待处理。这个数值持续偏高(远超1),说明磁盘的处理能力跟不上请求提交的速度,请求在队列里堆积。结合 IOPS 和 await 一起看,能更准确判断瓶颈的严重程度。
3.5 顺序 IO 与随机 IO
数据库场景下,随机 IO 是最容易触发磁盘瓶颈的模式,比如索引扫描、大量小事务的随机写入。顺序 IO(比如大文件的连续读写、WAL 日志的追加写入)对磁盘更友好,即使是机械硬盘也能有不错的表现。判断当前的 IO 模式是随机还是顺序,可以结合业务场景推断,也可以用 blktrace/iostat 的细粒度数据辅助判断(相对专业,日常排查中更多是先靠业务场景推断,再针对性验证)。
3.6 Buffer/Cache 对磁盘 IO 的缓冲作用
Linux 内核会把最近读写过的文件数据缓存在内存里(Page Cache),大量读请求实际上会命中缓存,不真正触发磁盘 IO。当内存不足、缓存被大量淘汰,或者应用绕过缓存直接做(O_DIRECT)IO 时,磁盘的真实压力才会完全暴露出来。所以内存不足有时候会间接表现为磁盘 IO 问题,排查时不能完全孤立地看磁盘指标,需要结合内存使用情况(free -h 里的 available、buff/cache)一起判断。
四、整体排查思路
- 确认告警范围:是单台机器还是多台机器同时告警,是特定磁盘设备还是所有设备。
- 区分空间问题和性能问题:先用 df -h 确认不是磁盘空间耗尽,再进入性能维度排查。
- 用 iostat 看整体 IO 压力:重点看 %util、await、avgqu-sz、IOPS、吞吐量几个指标的组合表现。
- 用 iotop 定位具体进程:找出哪个进程在产生大量 IO。
- 结合业务场景判断 IO 类型:是数据库随机读写、日志写入、备份任务、还是其他批处理。
- 深入应用层排查:如果是数据库,看慢查询、看是否有全表扫描;如果是应用日志,看是否有异常的日志刷盘频率。
- 检查存储层本身:如果是云盘,检查是否触发了云盘的 IOPS/吞吐限额;如果是本地磁盘,检查是否有硬件层面的问题(SMART 状态、dmesg 报错)。
- 确认根因并制定修复方案。
- 执行修复并验证效果。
- 复盘并完善监控。
五、实战步骤
沿用文章开头的场景:数据库服务器磁盘 IO 告警,接口大量超时。
步骤一:确认告警范围和磁盘空间
df -h
预期输出:
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 50G 28G 22G 56% /
/dev/vdb1 500G 312G 188G 63% /data/mysql
判断逻辑:磁盘空间使用率都在正常范围内(56%、63%),排除”磁盘满了导致写入失败”这类空间问题,可以确定这是性能维度的 IO 问题,进入下一步。
步骤二:用 iostat 查看整体 IO 压力
iostat -x 1 5
-x 参数显示扩展统计信息,1 5 表示每 1 秒采样一次,共采样 5 次(第一次输出是系统启动以来的累计平均值,通常参考意义不大,重点看第二次及以后的数据,这反映的是实时状态)。
预期输出(节选,第二次采样):
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz %util
vdb 12.00 892.00 96.00 45620.00 0.00 120.00 0.00 11.85 2.10 68.40 15.32 98.50
判断逻辑:
- %util 达到 98.5%,说明该设备几乎处于持续繁忙状态;
- w_await 高达 68.4 毫秒,而 r_await 只有 2.1 毫秒,说明写请求的等待时间远超读请求,问题集中在写入路径;
- w/s(每秒写请求数)892,远超 r/s(每秒读请求数)12,进一步确认这是一个写密集型的压力场景;
- aqu-sz(平均队列长度)15.32,说明有大量写请求在排队等待处理。
这一步的结论:磁盘的写入路径存在明显瓶颈,需要进一步定位是哪个进程、哪类操作在产生大量写 IO。
步骤三:用 iotop 定位具体进程
iotop -o -b -n 5
-o 参数只显示实际产生 IO 的进程(避免看到一堆 IO 为 0 的进程干扰判断),-b 表示批处理模式(非交互,方便脚本采集或者记录到日志),-n 5 表示采样 5 次。如果系统没有安装 iotop,需要先安装(yum install iotop 或 apt install iotop),生产环境安装软件包属于变更操作,建议提前确认符合内部变更流程。
预期输出:
Total DISK READ: 0.20 K/s | Total DISK WRITE: 44.60 M/s
Actual DISK READ: 0.20 K/s | Actual DISK WRITE: 45.10 M/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
9201 be/4 mysql 0.00 B 43.80 M/s 0.00 % 92.30 % mysqld
判断逻辑:mysqld 进程(TID 9201)几乎独占了整台机器的磁盘写入带宽(43.8M/s,占总写入量的绝大部分),IO> 列显示 92.30% 说明这个进程绝大部分时间都在等待 IO 完成,这是磁盘写入压力的直接来源。这一步把范围从”整台机器”收窄到”MySQL 进程本身”。
步骤四:进入 MySQL 排查是什么操作在产生大量写入
先确认当前是否有异常的写入型 SQL 在执行:
SHOW FULL PROCESSLIST;
预期输出(节选):
Id User Host db Command Time State Info
8821 app 10.0.1.23:5566 orders Query 182 updating UPDATE order_items SET status=2 WHERE batch_id=88231
8822 app 10.0.1.24:5566 orders Query 175 updating UPDATE order_items SET status=2 WHERE batch_id=88232
8823 app 10.0.1.25:5566 orders Query 168 updating UPDATE order_items SET status=2 WHERE batch_id=88233
判断逻辑:有多个连接同时在执行结构相似的 UPDATE 语句,且执行时间(Time 列,单位秒)都已经超过了 160 秒,明显是慢操作。结合前面 iotop 看到的持续写入压力,初步怀疑这几条批量更新语句是磁盘写入压力的来源。
进一步查看这条 SQL 涉及的表结构和索引情况:
SHOW CREATE TABLE order_items;
发现 batch_id 字段上没有索引。用 EXPLAIN 验证执行计划:
EXPLAIN UPDATE order_items SET status=2 WHERE batch_id=88231;
MySQL 8.0 支持对 UPDATE/DELETE 语句直接使用 EXPLAIN 查看执行计划(MySQL 5.7 同样支持,语法一致)。输出显示:
+----+-------------+-------------+------+---------------+------+---------+------+---------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------------+------+---------------+------+---------+------+---------+-------------+
| 1 | UPDATE | order_items | ALL | NULL | NULL | NULL | NULL | 2843021 | Using where |
+----+-------------+-------------+------+---------------+------+---------+------+---------+-------------+
判断逻辑:type 为 ALL,说明这是全表扫描,rows 显示预计扫描 284 万行,而实际每次更新的目标记录可能只有几千行(同一个 batch_id 的记录)。全表扫描本身会产生大量的随机读 IO(读取数据页判断是否匹配条件),命中条件后还要写入更新后的数据页和对应的 redo log、undo log,多个并发的全表扫描更新叠加在一起,正是磁盘写入被打满的直接原因。
步骤五:确认触发条件(为什么这几个批量更新会同时执行)
查看这几条 SQL 从哪里发起,是否是同一个批处理任务:
grep "88231\|88232\|88233" /app/logs/order-batch-service.log | head -n 10
发现这是一个”批量标记订单状态”的定时任务,正常情况下应该按批次顺序串行执行,但因为任务调度配置的问题(可能是重试机制没有做好互斥控制),同一时间段内触发了多个批次的任务并发执行,多个未走索引的全表扫描 UPDATE 同时压向数据库,叠加放大了本来就存在的索引缺失问题。
步骤六:制定修复方案(分为紧急止血和根本修复两部分)
紧急止血:先终止掉当前正在执行的几条慢 SQL,避免持续占用磁盘 IO 影响其他正常业务:
KILL 8821;
KILL 8822;
KILL 8823;
风险提醒:KILL 会中断正在执行的事务,MySQL 会对已执行的部分做回滚,回滚过程本身也会产生额外的 IO 和 CPU 消耗(尤其是大事务回滚可能耗时不短),执行 KILL 前需要评估这一点,避免”为了止血反而制造了新的压力尖刺”。同时要和业务方确认这批订单状态更新任务被中断后,是否有幂等重试机制能安全地重新执行,不会导致数据不一致(比如重复标记、遗漏标记)。
根本修复:给 batch_id 字段补充索引,从根本上解决全表扫描的问题:
-- 生产环境执行前建议先在预发环境验证耗时和影响,大表加索引期间会消耗额外 IO
-- MySQL 5.7/8.0 InnoDB 引擎默认支持 Online DDL 方式添加二级索引,多数情况下不会长时间锁表,
-- 但仍会占用额外的临时空间和 IO 资源,建议低峰期执行
ALTER TABLE order_items ADD INDEX idx_batch_id (batch_id);
同时修复批处理任务的调度逻辑,增加互斥锁或者分布式锁,避免同一批次任务因为重试机制导致并发执行(这部分属于应用代码修复,需要和研发团队协作)。
步骤七:验证修复效果
索引生效后重新执行 EXPLAIN:
EXPLAIN UPDATE order_items SET status=2 WHERE batch_id=88231;
+----+-------------+-------------+------+---------------+---------------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------------+------+---------------+---------------+---------+-------+------+-------------+
| 1 | UPDATE | order_items | ref | idx_batch_id | idx_batch_id | 5 | const | 3200 | Using where |
+----+-------------+-------------+------+---------------+---------------+---------+-------+------+-------------+
type 变为 ref,rows 从 284 万降低到 3200,扫描范围大幅缩小。持续观察 iostat -x 1 的输出,确认 %util 回落到正常区间(30% 以下),w_await 回落到个位数毫秒,iotop 里也不再看到 mysqld 长时间占用大量写入带宽。
六、常用命令清单
# 确认磁盘空间是否耗尽(排除空间问题)
df -h
df -i # 查看 inode 使用率,避免 inode 耗尽导致的"无法写入但空间显示充足"问题
# 查看磁盘 IO 整体压力
iostat -x 1 5
iostat -d -x 1 5 # 只看设备 IO,不看 CPU 部分
# 定位具体进程的 IO 占用
iotop -o -b -n 5
iotop -o # 交互模式,实时观察
# 查看某个进程的 IO 详细统计
cat /proc/<PID>/io
# 查看系统内存和缓存情况(辅助判断是否是缓存不足间接导致 IO 压力)
free -h
# 查看具体挂载点对应的物理设备
lsblk
mount | grep /data
# 查看内核日志中是否有磁盘硬件相关报错
dmesg | grep -i "error\|fail" | grep -i "sd\|nvme\|vd"
# 查看磁盘 SMART 健康状态(物理机/托管云主机适用,部分云盘不支持)
smartctl -a /dev/sda
# MySQL 相关:查看当前会话和慢查询
SHOW FULL PROCESSLIST;
SHOW VARIABLES LIKE 'slow_query_log%';
SHOW VARIABLES LIKE 'long_query_time';
# 查看 InnoDB 状态(包含 buffer pool、IO 相关信息)
SHOW ENGINE INNODB STATUS;
七、配置示例
7.1 开启 MySQL 慢查询日志(用于长期发现潜在的全表扫描问题)
# /etc/my.cnf 或 /etc/mysql/mysql.conf.d/mysqld.cnf(路径因发行版和安装方式而异)
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
long_query_time = 1 表示执行超过 1 秒的查询记录到慢查询日志;log_queries_not_using_indexes = 1 会记录没有使用索引的查询(即使执行时间不长),有助于提前发现潜在的全表扫描风险,而不是等到数据量增长到引发严重问题才被动发现。
修改配置后需要重启 MySQL 服务才能生效(部分参数支持动态调整,不需要重启):
-- 动态调整方式,无需重启,适合临时开启排查
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;
风险提醒:直接修改配置文件的方式在下次重启后依然生效(持久化配置),而 SET GLOBAL 动态调整方式在 MySQL 进程重启后会丢失(除非同时写入配置文件或使用 MySQL 8.0 的 SET PERSIST语法)。生产环境建议两种方式配合使用:先用 SET GLOBAL 临时生效排查问题,确认需要长期保留后再写入配置文件。
7.2 调整 InnoDB 相关的 IO 参数
[mysqld]
innodb_flush_log_at_trx_commit = 1
innodb_flush_method = O_DIRECT
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000
innodb_buffer_pool_size = 8G
- innodb_flush_log_at_trx_commit = 1 是最安全的设置(每次事务提交都刷盘),如果业务对绝对不丢数据要求没有那么严格,可以调整为 2(提交时写入操作系统缓存,由操作系统决定何时刷盘,性能更好但有极小概率在系统崩溃时丢失最后一秒的事务),这个调整需要和业务方确认数据安全性要求后再决定,不能单方面为了性能牺牲数据安全;
- innodb_flush_method = O_DIRECT 让 InnoDB 绕过操作系统的 Page Cache 直接管理自己的 buffer pool,避免双重缓存浪费内存,这是大多数生产 MySQL 部署的推荐设置;
- innodb_io_capacity 告诉 InnoDB 后台任务(如脏页刷新)预期的磁盘 IOPS 能力,设置过低会导致脏页刷新跟不上导致积压,设置过高可能让后台刷盘任务过度占用 IO 资源影响前台业务查询,这个值应该参考实际磁盘的 IOPS 能力(云盘可以查看购买规格标注的 IOPS 上限)设置为一个合理比例(通常是磁盘标称 IOPS 的 50%-70%作为起点,再根据实际观察调整);
- innodb_buffer_pool_size 决定了有多少数据能被缓存在内存里减少磁盘读取,一般建议设置为物理内存的 50%-70%(专用数据库服务器),这个参数的调整需要重启才能生效(MySQL 5.7 及以上部分场景支持在线调整,但通常仍建议规划好维护窗口)。
修改这类核心参数后,需要执行:
sudo systemctl restart mysqld
风险提醒:重启 MySQL 会导致短暂的服务中断和所有连接断开,务必确认应用层有重连机制,且选择业务低峰期,并提前评估是否需要主从切换配合(如果是主库重启,业务流量能否临时切到从库承接,避免重启期间业务完全不可用)。
7.3 Linux 内核层面的 IO 调度器配置
对于使用机械硬盘或者部分虚拟化环境下的块设备,IO 调度器的选择会影响 IO 性能表现。查看当前调度器:
cat /sys/block/vda/queue/scheduler
输出类似:
[mq-deadline] kyber none
方括号内的是当前生效的调度器。对于数据库这类随机 IO 较多的场景,mq-deadline 或 none(配合 NVMe SSD,因为 SSD 本身没有寻道开销,调度器的排序优化意义不大)通常表现较好;kyber 更适合对延迟敏感的场景。临时调整(重启后失效):
echo mq-deadline > /sys/block/vda/queue/scheduler
风险提醒:直接调整调度器属于内核参数层面的变更,虽然理论上是安全的(不会导致数据损坏),但不同存储硬件和虚拟化环境下的实际效果差异较大,建议先在预发环境测试验证效果后再应用到生产环境,且要持久化配置(通过 udev 规则或者启动脚本),否则重启后会恢复默认值。
八、日志或指标观察方法
8.1 系统层面
- iostat -x 1:核心的磁盘 IO 观察工具,持续观察 %util、await、aqu-sz、IOPS、吞吐量;
- vmstat 1:观察 bi/bo(块设备读写的数据块数)辅助判断趋势;
- free -h:观察 available 是否偏低,如果内存紧张导致 Page Cache 被压缩,可能间接加重磁盘 IO 压力。
8.2 进程层面
- iotop:定位具体进程的 IO 占用;
- /proc/<PID>/io:查看单个进程更细粒度的读写字节数统计,字段包括 rchar、wchar、syscr、syscw、read_bytes、write_bytes,其中 read_bytes/write_bytes 更能反映真实的磁盘 IO(rchar/wchar 包含了命中缓存的部分,不完全等于磁盘层面的 IO)。
8.3 数据库层面
- 慢查询日志:识别哪些 SQL 执行时间长,是否伴随大量 IO;
- SHOW ENGINE INNODB STATUS:查看 InnoDB 内部状态,包含缓冲池命中率、脏页比例、当前 IO 相关统计,重点关注 BUFFER POOL AND MEMORY 部分的 Buffer pool hit rate(命中率过低说明大量请求要穿透到磁盘);
- Performance Schema(performance_schema 库,需要确认已开启对应的 instrument 和 consumer 配置,不同版本默认开启的监控项可能不同)中的 events_waits_summary_global_by_event_name 等表,可以观察 IO 相关等待事件的耗时分布,具体表结构和字段以实际部署版本为准。
8.4 云平台监控
如果是云服务器,云盘的 IOPS 和吞吐量通常有厂商规格限额,登录云控制台查看该云盘实例的监控面板,确认是否触发了限额(表现为 IOPS 或吞吐量曲线贴着规格上限出现平台状截断),这是本机 iostat 无法直接看出来的信息,需要结合云平台侧监控数据交叉验证。
九、排查路径(决策树)
磁盘 IO 告警触发
│
├─ df -h 确认不是空间耗尽问题
│
├─ iostat -x 1 查看整体压力
│ ├─ 读多写少 → 排查是否有大量缓存未命中的读请求(数据量过大、索引缺失导致的全表扫描读)
│ ├─ 写多读少 → 排查是否有大量写入型操作(批量 UPDATE/INSERT、日志刷盘、备份任务)
│ └─ 读写都高 → 排查是否是整体并发量上升或者存在混合型的批处理任务
│
├─ iotop 定位具体进程
│
├─ 结合进程类型深入排查
│ ├─ 数据库进程 → 查慢查询、查执行计划、查是否缺索引
│ ├─ 日志/文件写入进程 → 查日志级别是否过于详细、是否有同步刷盘的低效配置
│ └─ 备份/归档任务 → 查任务调度时间是否与业务高峰重合、是否可以调整到低峰期
│
├─ 检查存储层是否触发厂商限额(云盘场景)或硬件异常(物理机场景,dmesg/smartctl)
│
└─ 确认根因,制定紧急止血和根本修复两套方案
十、风险提醒
- KILL 数据库连接/查询会导致事务回滚,大事务回滚本身耗时且消耗额外资源,执行前评估影响,并确认应用层的重试和幂等机制;
- 大表加索引(ALTER TABLE … ADD INDEX)即使是在线 DDL,仍会消耗额外的 IO 和临时磁盘空间,且执行期间需要观察主从复制延迟,避免影响数据一致性,建议在低峰期操作并提前在预发环境验证耗时;
- 调整 IO 调度器、innodb_io_capacity 等内核和数据库参数需要结合实际硬件规格,不能凑一个”看起来合理”的数字,盲目调大 innodb_io_capacity 可能导致后台刷盘任务过度抢占前台查询的 IO 资源,反而拖慢正常业务;
- 重启 MySQL 应用配置会导致连接中断,务必评估是否需要主从切换配合,避免重启期间业务完全不可用;
- 清理日志、执行 truncate 类操作释放磁盘空间前,务必确认要清理的内容不是正在被写入或者其他进程正在读取的文件,truncate 一个仍被进程持有句柄写入的日志文件虽然不会报错(Linux 下可以截断正被打开的文件),但可能导致该进程后续的日志内容出现异常(比如稀疏文件、写入位置错乱),操作前建议先确认该文件没有进程正在活跃写入,或者用 logrotate 这类专门设计用于处理”截断正在写入文件”场景的工具,而不是手动 truncate。
十一、验证方式
- 指标层面:iostat -x 1 持续观察至少 30 分钟,确认 %util、await、aqu-sz 都回落到历史基线水平,且没有新的异常波动;
- 业务层面:核对订单相关接口的响应时间和超时率是否恢复正常,可以对比故障发生前一天同一时间段的历史数据作为基线参考;
- 数据库层面:重新执行 EXPLAIN 确认相关 SQL 已经命中新增的索引,SHOW ENGINE INNODB STATUS 里的缓冲池命中率恢复到正常水平(通常应维持在 99% 以上,具体基线需要参考该实例历史数据);
- 持续观察周期:数据库类问题的验证不能只看修复后立即的表现,建议持续观察到下一次相似的业务高峰(比如次日同一时间段的订单处理批次),确认没有复发。
十二、回滚方案
- 索引变更:如果加索引后发现写入性能出现超出预期的下降(索引维护本身有开销),可以用 ALTER TABLE order_items DROP INDEX idx_batch_id 回退,但这种情况较少见,通常需要先评估清楚是否真的是索引导致,还是有其他并发因素;
- 配置文件修改:修改前用 cp my.cnf my.cnf.bak.$(date +%Y%m%d_%H%M%S) 备份,如果调整后出现异常,恢复备份文件并重启服务;
- IO 调度器调整:调度器是运行时可切换的参数,直接 echo 回原来的调度器名称即可立即回退,无需重启;
- 批处理任务调度逻辑修复:如果修复后的互斥锁逻辑本身引入了新问题(比如任务因为锁竞争反而执行延迟),应保留旧版本代码的快速回滚能力(通过发布系统的版本回退功能),不要在生产环境直接调试新逻辑。
十三、生产环境注意事项
- 涉及数据库参数调整、索引变更、服务重启的操作,务必提前通知业务方,明确操作窗口和预期影响范围;
- 批量 KILL 数据库连接前,先确认清楚要终止的连接确实是问题源头,避免误杀正常业务连接,建议先用 SHOW FULL PROCESSLIST 或者查询 information_schema.processlist(不同版本字段可能略有差异,以实际输出为准)逐一确认再执行;
- 磁盘 IO 类问题的根因排查往往涉及跨团队协作(运维、DBA、研发),沟通时用具体的执行计划、慢查询日志、iostat 截图等证据说话,而不是笼统描述”数据库很慢”;
- 长期来看,应该建立慢查询和全表扫描的常态化监控,而不是等到 IO 告警触发才去排查,把被动响应转变为主动发现。
十四、总结
这次磁盘 IO 告警的排查路径是:确认不是空间问题 → iostat 确认是写密集型压力 → iotop 定位到 MySQL 进程 → SHOW FULL PROCESSLIST 定位到具体慢 SQL → EXPLAIN 确认全表扫描 → 结合应用日志确认是批处理任务调度异常导致并发执行 → 制定紧急止血(KILL慢查询)和根本修复(加索引 + 修复调度逻辑)两套方案 → 验证效果。
磁盘 IO 问题的排查难点在于它经常是多个因素叠加的结果(缺索引 + 并发触发 + 数据量增长),单一维度的指标很难直接给出答案,需要从系统层的 iostat 一路下钻到数据库层的执行计划,每一步都要有实际输出作为判断依据,这样才能给出经得起复盘检验的根因结论。
十五、案例二:日志级别配置不当引发的持续性 IO 压力
前面的案例是数据库层面的问题,这里补充一个更常见、更容易被忽视的场景:应用日志配置不当导致的磁盘 IO 长期偏高。
15.1 现象
某个网关服务所在的机器,iostat 显示磁盘 %util 长期维持在 40% 到 60% 之间(没有达到告警阈值,但明显高于同类机器的正常水平,大约是其他机器的 3 倍),业务方没有明确反馈问题,是在做容量规划巡检时发现的异常。
15.2 初步判断
没有业务方反馈明显故障,说明还没到严重影响的程度,但持续偏高的资源占用意味着这台机器的容量余量比看起来的要小,一旦流量突增,更容易率先触发瓶颈。这类”慢性病”式的问题同样值得排查。
15.3 命令检查
iotop -o -b -n 3
发现是网关应用进程本身在持续写入,而不是数据库或者其他中间件。查看该进程打开的文件句柄,确认具体在写哪个文件:
ls -la /proc/<PID>/fd | grep -i log
发现该进程正在写入一个访问日志文件,用 du 查看该日志文件的增长速度:
ls -la /app/logs/gateway-access.log
sleep 10
ls -la /app/logs/gateway-access.log
对比两次的文件大小差值,计算出每秒的写入速率,发现该文件每秒新增接近 2MB,对于该网关的实际请求量(每秒几百个请求)来说,这个日志体量明显偏大。
15.4 关键指标与根因定位
查看日志配置,发现日志级别被设置为 DEBUG,且每条访问日志里包含了完整的请求头和响应体内容:
grep -i "level" /app/gateway/config/logback.xml
<root level="DEBUG">
<appender-ref ref="FILE" />
</root>
DEBUG 级别在生产环境会输出大量非必要的调试信息,叠加记录完整请求/响应体的做法(尤其是响应体可能包含大段 JSON 数据),单条日志的体积和总日志量都被显著放大,磁盘写入压力持续偏高的根因就在这里。查看 Git 提交记录发现,这个 DEBUG 级别配置是上一次紧急排查问题时临时调整的,排查完成后忘记改回 INFO 级别。
15.5 修复方案
<root level="INFO">
<appender-ref ref="FILE" />
</root>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/app/logs/gateway-access.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>/app/logs/gateway-access.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>200MB</maxFileSize>
<maxHistory>14</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
同时补充了日志滚动策略:按大小和时间双重条件滚动、历史保留 14 天、总大小上限 10GB 并自动压缩旧日志(.gz),避免日志无限增长占满磁盘空间(这属于另一个维度的问题,但经常和 IO 压力问题一起出现,顺带一并处理)。
15.6 生效方式
Logback 支持配置热加载(如果配置文件里设置了 scan=”true”),修改后无需重启应用即可生效;如果没有开启热加载,需要重启应用服务。查看是否支持热加载:
grep "scan=" /app/gateway/config/logback.xml
如果输出为空说明没有开启,需要走正常的重启/重新部署流程。
15.7 验证结果
修改并生效后,持续观察日志文件的增长速率:
watch -n 5 'ls -la /app/logs/gateway-access.log'
确认增长速率从每秒 2MB 降低到每秒几十 KB 的水平(正常业务量下的合理量级)。同时观察 iostat -x 1 里该磁盘的 %util,从长期 40%-60% 回落到 10% 左右的正常基线。
15.8 复盘总结
这个案例的核心教训是:临时性的调试配置调整必须有明确的回退机制和提醒,不能依赖人工记忆。建议对这类临时调整建立工单跟踪机制,比如在变更记录里注明”预计恢复时间”,并设置提醒任务,到期后自动检查该配置是否已经改回,避免”临时”变成”永久”。日志相关的配置变更应该纳入代码审查,而不是可以随意在生产环境直接修改而不留痕迹的配置。
十六、案例三:备份任务与业务高峰重叠导致的 IO 争抢
16.1 现象
每天凌晨 2 点左右,多个业务系统同时出现短暂的响应变慢(持续 10 到 20 分钟),因为不在典型的业务高峰期,长期没有引起足够重视,直到某次促销活动延长了业务高峰时段,与这个时间窗口重叠,才真正造成了明显影响,升级为需要处理的问题。
16.2 初步判断
固定时间点出现的性能问题,第一反应应该是检查该时间点是否有定时任务(备份、归档、报表生成、日志切割等),这类任务通常在业务低峰期调度,一旦业务高峰时段发生变化或者延长,就会与任务窗口产生冲突。
16.3 命令检查
crontab -l -u root
cat /etc/cron.d/*
发现凌晨 2 点有一条数据库全量备份任务:
0 2 * * * /app/scripts/mysql_backup.sh >> /var/log/mysql_backup.log 2>&1
查看备份脚本的具体实现:
cat /app/scripts/mysql_backup.sh
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="/data/backup/mysql"
DATE=$(date +%Y%m%d)
mysqldump --single-transaction --quick -u backup_user -p"${MYSQL_BACKUP_PASSWORD}" \
orders > "${BACKUP_DIR}/orders_${DATE}.sql"
find "${BACKUP_DIR}" -name "*.sql" -mtime +7 -delete
16.4 关键指标
在下一次凌晨 2 点的时间窗口内蹲点观察:
iostat -x 1 60 > /tmp/backup_window_iostat.log
分析采样数据,发现从 2:00 到 2:18 期间,%util 从平时的 20% 左右跳升到 90% 以上,与备份任务的执行时间窗口高度重合。
16.5 根因定位
mysqldump 全量备份是一个典型的大量顺序读操作(读取全部表数据用于导出),虽然使用了 –single-transaction 保证一致性(不会阻塞写入,基于 InnoDB 的 MVCC 机制),但读取过程本身仍会对磁盘 IO 产生持续压力,尤其是当数据库规模增长后,备份耗时和 IO 压力都会同步增长。当业务高峰时段恰好与备份窗口重叠时,备份产生的大量读 IO 和业务本身的读写 IO 相互争抢磁盘带宽,导致业务响应变慢。
16.6 修复方案
方案一(短期):调整备份任务的执行时间,避开当前及可预见的业务高峰时段。
方案二(中长期,更根本):把全量备份任务迁移到从库执行,避免占用主库的 IO 资源:
#!/usr/bin/env bash
# mysql_backup.sh - 从从库执行备份,降低对主库的影响
set -euo pipefail
BACKUP_DIR="/data/backup/mysql"
DATE=$(date +%Y%m%d)
LOG_FILE="/var/log/mysql_backup.log"
REPLICA_HOST="10.0.2.15"
echo "$(date '+%Y-%m-%d %H:%M:%S') 开始备份,目标从库:${REPLICA_HOST}" >> "${LOG_FILE}"
# 备份前检查从库复制延迟,延迟过大则不执行备份,避免备份到不一致或过旧的数据快照
SECONDS_BEHIND=$(mysql -h "${REPLICA_HOST}" -u backup_user -p"${MYSQL_BACKUP_PASSWORD}" \
-e "SHOW REPLICA STATUS\G" | grep "Seconds_Behind_Source" | awk '{print $2}')
if [ -z "${SECONDS_BEHIND}" ] || [ "${SECONDS_BEHIND}" -gt 60 ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') 复制延迟异常(${SECONDS_BEHIND:-未知}秒),跳过本次备份并告警" >> "${LOG_FILE}"
exit 1
fi
mysqldump -h "${REPLICA_HOST}" --single-transaction --quick -u backup_user -p"${MYSQL_BACKUP_PASSWORD}" \
orders > "${BACKUP_DIR}/orders_${DATE}.sql"
echo "$(date '+%Y-%m-%d %H:%M:%S') 备份完成:${BACKUP_DIR}/orders_${DATE}.sql" >> "${LOG_FILE}"
# 清理超过 7 天的旧备份,find -delete 属于破坏性操作,执行前先 dry-run 确认范围
find "${BACKUP_DIR}" -name "*.sql" -mtime +7 -print
find "${BACKUP_DIR}" -name "*.sql" -mtime +7 -delete
脚本说明:
- SHOW REPLICA STATUS是 MySQL 8.0.22 及以上版本引入的新命令名(用于替代旧的 SHOW SLAVE STATUS,语义相同),MySQL 5.7 及更早版本需要使用 SHOW SLAVE STATUS,具体使用哪个命令要根据实际部署的 MySQL 版本确认,两者返回的字段基本一致但命令名不同;
- 备份前检查复制延迟(Seconds_Behind_Source 在新命令下的字段名,旧版本对应字段名为 Seconds_Behind_Master),避免在从库数据滞后过多的情况下备份出不一致的快照;
- find … -delete 属于破坏性操作,脚本里先用 -print 打印一次将要删除的文件列表(这一行在生产定时任务中通常会输出到日志,方便事后追溯是否有误删),再执行真正的 -delete,这是清理类脚本的标准做法;
- 密码通过环境变量 MYSQL_BACKUP_PASSWORD 传入,不写死在脚本里,实际调用时应该配合权限受控的环境变量文件(比如只有运维账号可读的 /etc/mysql_backup.env,权限设置为 600)或者密钥管理系统读取,避免明文密码出现在脚本或者进程列表中(ps aux 可能会暴露命令行参数中的密码,这也是为什么用环境变量而不是命令行参数传递密码的原因之一)。
16.7 验证结果
调整为从库备份并避开业务高峰时段后,连续观察一周的凌晨时段主库 iostat 数据,%util 保持在正常的 20% 左右波动范围内,没有再出现跳升现象,业务响应时间在这个时间窗口内也保持平稳。
16.8 回滚方案
如果从库备份方式出现问题(比如从库资源本身不足,备份反而影响了从库的只读查询服务),可以先临时切回主库备份,但要重新评估执行时间窗口,确保避开当前的业务高峰。备份脚本本身的修改建议保留在版本控制系统中(即使是简单的 Shell 脚本),方便追溯每次调整的历史和快速回退到上一版本。
16.9 复盘总结
固定时间点重复出现的性能问题,排查思路应该优先看该时间点是否有可预期的批处理任务,而不是每次都从零开始怀疑代码或者流量异常。这类问题的另一个启示是:运维层面的任务调度(备份、报表、归档)需要随着业务的变化(比如大促活动导致高峰时段延长)动态调整,不能”配置一次就永远不管”。
十七、Prometheus 磁盘 IO 相关监控配置参考
# /etc/prometheus/rules/disk_io_alerts.yml
groups:
- name: disk_io_alerts
rules:
- alert: HighDiskUtilization
expr: |
rate(node_disk_io_time_seconds_total[5m]) * 100 > 90
for: 10m
labels:
severity: warning
annotations:
summary: "设备 {{ $labels.device }} 磁盘 IO 利用率持续偏高"
description: "过去5分钟平均IO利用率超过90%,当前值:{{ $value | printf \"%.2f\" }}%,注意SSD/云盘场景该指标可能不完全反映真实饱和度,需结合await判断"
- alert: HighDiskAwait
expr: |
rate(node_disk_write_time_seconds_total[5m]) / rate(node_disk_writes_completed_total[5m]) * 1000 > 50
for: 5m
labels:
severity: critical
annotations:
summary: "设备 {{ $labels.device }} 写入平均等待时间异常"
description: "平均写等待时间超过50ms,可能存在IO瓶颈,当前值:{{ $value | printf \"%.2f\" }}ms"
说明:
- node_disk_io_time_seconds_total 和相关的 node_disk_write_time_seconds_total、node_disk_writes_completed_total 指标来自 node_exporter,具体指标名称和标签在不同版本的 exporter 中可能有细微差异,配置告警规则前建议先用 Prometheus 的表达式浏览器(Graph 页面)实际查询确认指标存在且数值符合预期,以实际 exporter 输出为准;
- 阈值(90%、50ms)需要结合具体磁盘类型(机械盘/SSD/云盘)和历史基线调整,不能直接套用到所有环境,比如高性能 NVMe SSD 的正常 await 可能常年在 1ms 以内,机械盘可能正常就在 10-20ms,套用同一个阈值会导致其中一类设备频繁误报或者另一类设备漏报;
- 修改规则文件后先用 promtool check rules 校验语法,再通过 curl -X POST http://localhost:9090/-/reload(需要 Prometheus 启动时开启 –web.enable-lifecycle)热加载生效。
十八、常见问答(FAQ)
问:%util 100% 就一定是磁盘瓶颈吗?
不一定。对于支持多队列并行处理的 SSD、NVMe 或者部分云盘、分布式存储,%util 的统计方式(基于设备”非空闲”的时间比例)在这些设备上可能不能准确反映真实饱和度,需要结合 await 和队列深度综合判断,如果 %util 很高但 await 很低(比如始终在 1ms 以内),设备本身可能还有余量,只是统计方式的局限性导致数字看起来吓人。
问:为什么磁盘空间还有很多,但写入报错说空间不足?
检查 inode 是否耗尽:df -i 查看 IUse% 列,如果这个数字接近 100%,说明文件数量(尤其是大量小文件场景,比如日志切割产生的海量小文件)已经耗尽了文件系统预分配的 inode 数量,即使剩余空间充足,也无法再创建新文件,这是与磁盘空间容量完全独立的另一种资源限制。
问:iostat 里同一块磁盘,为什么有时候能看到多个设备名(比如 sda 和 sda1)?
sda 是整块物理磁盘的统计,sda1 是该磁盘上某个分区的统计,如果磁盘只有一个分区且占满整块盘,两者的数据会非常接近;如果磁盘划分了多个分区,各分区的 IO 会分别统计,观察时要确认自己关注的是哪一层。
问:备份期间数据库变慢,是否可以完全避免?
很难完全避免(备份本身就需要读取大量数据),但可以通过以下方式缓解:迁移到从库执行备份、错开业务高峰时段、使用增量备份减少全量备份的频率和单次数据量、评估存储层是否可以为备份任务单独分配 IO 带宽配额(部分企业级存储和云盘支持 QoS 限速配置)。
问:iotop 显示不出任何进程的 IO 数据,是什么原因?
iotop 依赖内核的 IO 计费功能(CONFIG_TASK_IO_ACCOUNTING),部分精简内核或者容器环境(尤其是某些沙箱化的容器运行时)可能没有开启这个功能,导致数据采集不到。可以先执行 iotop 不带任何参数确认是否有报错提示,或者退而使用 /proc/<PID>/io 手动轮询对比多次采样的差值作为替代方案。
十九、附录:磁盘 IO 排查术语速查
| 术语 | 含义 |
|---|---|
| IOPS | 每秒读写请求次数 |
| Throughput | 吞吐量,每秒传输的数据量 |
| %util | 设备处理IO请求所占用时间的百分比 |
| await | IO请求的平均等待时间(排队+处理),单位毫秒 |
| aqu-sz / avgqu-sz | 平均IO请求队列长度 |
| Page Cache | 内核用于缓存文件数据的内存区域 |
| O_DIRECT | 绕过Page Cache直接对磁盘进行IO的标志位 |
| WAL | Write-Ahead Log,预写日志,数据库保证持久性的常见机制 |
| redo log | InnoDB的重做日志,保证事务持久性 |
| Online DDL | 在不长时间锁表的前提下完成表结构变更的机制 |
| QoS | Quality of Service,服务质量限速/保障机制,常见于存储和网络设备 |
二十、生产环境磁盘 IO 巡检建议清单
为避免类似问题反复发生,建议将以下检查项纳入日常巡检或自动化巡检脚本:
- 每日巡检 iostat -x 关键指标,与历史基线对比,发现偏离趋势及时跟进而不是等告警触发;
- 定期审查慢查询日志和 log_queries_not_using_indexes 输出,主动发现潜在的全表扫描风险;
- 审查所有定时任务(备份、归档、报表)的执行时间窗口,与业务高峰时段的变化保持同步更新;
- 巡检应用日志级别配置,确认生产环境没有遗留调试期间临时调整的 DEBUG 级别配置;
- 检查磁盘 inode 使用率,尤其是有大量小文件产生的业务场景(如日志、临时文件、缓存文件目录);
- 云盘场景下核对当前 IOPS/吞吐量监控曲线与购买规格的余量,提前规划升级,而不是等触发限额后被动处理。
这份清单的意义在于把”故障后排查”的经验转化为”故障前预防”的日常动作,磁盘 IO 类问题很多时候都有征兆可循,只是没有被日常巡检覆盖到,才演变成需要紧急处理的告警事件。
二十一、自动化巡检脚本示例
把上面的巡检清单转化为可执行的脚本,定期运行并把异常项输出到日志,方便快速回顾:
#!/usr/bin/env bash
# disk_io_healthcheck.sh - 磁盘 IO 日常巡检脚本
# 用法:./disk_io_healthcheck.sh
set -euo pipefail
LOG_FILE="/var/log/disk_io_healthcheck_$(date +%Y%m%d).log"
THRESHOLD_UTIL=80
echo "===== 磁盘 IO 巡检开始:$(date '+%Y-%m-%d %H:%M:%S') =====" | tee -a "${LOG_FILE}"
echo "[1] 检查磁盘空间使用率..." | tee -a "${LOG_FILE}"
df -h | tee -a "${LOG_FILE}"
echo "[2] 检查 inode 使用率..." | tee -a "${LOG_FILE}"
df -i | tee -a "${LOG_FILE}"
echo "[3] 采样磁盘 IO 利用率(10秒)..." | tee -a "${LOG_FILE}"
IOSTAT_OUTPUT=$(iostat -x 1 10 | tail -n +$(($(iostat -x 1 10 | wc -l) - 10)))
echo "${IOSTAT_OUTPUT}" | tee -a "${LOG_FILE}"
# 简单判断是否有设备利用率超过阈值,实际生产环境建议结合监控系统的历史基线而不是硬编码阈值
HIGH_UTIL_DEVICES=$(echo "${IOSTAT_OUTPUT}" | awk -v threshold="${THRESHOLD_UTIL}" 'NR>1 && $NF+0 > threshold {print $1, $NF}')
if [ -n "${HIGH_UTIL_DEVICES}" ]; then
echo "[警告] 以下设备IO利用率超过 ${THRESHOLD_UTIL}%:" | tee -a "${LOG_FILE}"
echo "${HIGH_UTIL_DEVICES}" | tee -a "${LOG_FILE}"
else
echo "[正常] 未发现IO利用率超过阈值的设备" | tee -a "${LOG_FILE}"
fi
echo "===== 巡检结束:$(date '+%Y-%m-%d %H:%M:%S') =====" | tee -a "${LOG_FILE}"
脚本说明:
- 使用 tee -a 同时输出到终端和日志文件,方便手动执行时实时查看,也保留历史记录用于追溯;
- 阈值判断只是简单示例(固定 80% 阈值),生产环境更严谨的做法是结合 Prometheus 等监控系统里保存的历史基线数据动态判断是否异常,而不是依赖脚本里硬编码的固定值;
- 脚本本身只做只读的信息采集和判断,不涉及任何修改或删除操作,可以安全地作为定时任务加入 crontab;
- 如果要加入 crontab 定期执行,建议先手动运行几次确认输出格式符合预期,再配置为每日或每小时执行一次,执行日志建议纳入日志轮转策略,避免日积月累占用过多空间。
二十二、与容量规划的关联
磁盘 IO 问题的很多根因最终都会指向容量规划层面的欠账:数据量增长超出了原始设计预期、业务并发量超出了原始规格的承载能力、备份等运维任务的资源消耗没有随着数据规模同步评估。
建议在完成故障复盘后,额外做一次容量视角的复盘:
- 当前数据库表的数据量相比一年前增长了多少倍,索引设计是否还匹配当前的查询模式;
- 当前磁盘/云盘的 IOPS 和吞吐量规格,相比业务增长曲线,还有多久会再次逼近瓶颈;
- 备份、归档任务的执行耗时趋势,是否需要考虑增量备份、异地容灾等更适合大数据量场景的方案替代当前的全量备份模式;
- 是否需要引入读写分离、分库分表等架构层面的调整,而不是持续依赖单机的性能优化。
这些问题不需要在一次故障复盘中全部回答,但应该作为后续容量规划会议的输入,让每一次故障排查真正转化为系统健壮性的长期提升,而不是”救火完就结束”。
二十三、结语
磁盘 IO 类问题的排查经常比 CPU 问题更容易被误判,因为现象和根因之间的因果链条更长(比如”日志级别配置不当”这种看似无关的配置项,最终体现为磁盘 IO 告警)。养成”先分清读写类型,再定位具体进程,再深入到应用/数据库层找具体操作,最后结合业务场景确认触发条件”的排查习惯,能够应对绝大多数生产环境中的磁盘 IO 类故障。工具命令是通用的,但每一次排查的具体路径都要基于实际观察到的指标数据来推进,而不是照搬上一次故障的结论。
二十四、案例四:Redis 持久化配置不当引发的周期性 IO 尖刺
除了数据库和应用日志,Redis 的持久化机制也是磁盘 IO 排查中经常遇到的场景,这里补充一个典型案例。
24.1 现象
某台缓存服务器每隔几分钟出现一次短暂的 IO 尖刺(%util 从平时的 5% 瞬间跳到 90% 以上,持续几秒到十几秒后回落),业务方反馈缓存偶尔出现请求超时的毛刺现象,但持续时间很短,不容易复现排查。
24.2 初步判断
周期性、短时的尖刺现象,结合这是一台 Redis 服务器,首先要怀疑是否是 RDB 快照或者 AOF 重写触发的持久化操作。Redis 的持久化机制本身会在特定条件下(比如 save 配置的规则触发,或者 AOF 文件增长到一定比例触发重写)产生批量的磁盘写入,这类操作天然是脉冲式的,而不是持续性的。
24.3 命令检查
登录 Redis 查看持久化相关配置和状态:
bashredis-cli CONFIG GET save
redis-cli CONFIG GET appendonly
redis-cli INFO persistence
预期输出(节选):
save 900 1 300 10 60 10000
appendonly no
rdb_changes_since_last_save:15234
rdb_bgsave_in_progress:0
rdb_last_save_time:1757570400
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:8
判断逻辑:save 900 1 300 10 60 10000 表示配置了三条自动触发 RDB 快照的规则,其中最激进的一条是”60 秒内有 10000 次写入变化就触发一次快照”。结合业务侧的写入量,如果这台 Redis 承载了较高频率的写操作(比如作为计数器、会话存储等高频写场景),很容易在 60 秒这个规则下频繁触发快照,每次触发快照时 rdb_bgsave_in_progress 会短暂变为 1,同时子进程执行 fork 并把内存数据写入磁盘,这个写入过程正是 IO 尖刺的来源。
进一步确认触发频率,持续观察一段时间的 rdb_last_save_time:
bashwatch -n 5 'redis-cli INFO persistence | grep rdb_last_save_time'
发现该时间戳大约每隔 90 到 120 秒就更新一次,与业务方反馈的”每隔几分钟”的现象基本吻合。
24.4 关键指标与根因定位
rdb_bgsave_in_progress 状态变化,配合 iostat -x 1 同时段观察,两者的时间点完全重合,确认根因是 RDB 快照的触发频率与当前写入负载不匹配——原本设计这条规则时业务写入量较低,60 秒内很难达到 10000 次变更,但随着业务增长,现在的写入量很容易在远小于 60 秒的时间内就触发这个阈值,导致快照触发频率远超预期,叠加每次快照本身的 IO 开销,形成了持续性的周期尖刺。
24.5 修复方案
方案一:调整 save 规则,降低触发频率,适配当前的写入量级:
redis-cli CONFIG SET save "3600 1 300 100 60 10000"
这条命令把最宽松的规则改为”3600 秒(1小时)内至少1次变更才触发”,同时保留了原有更严格规则作为兜底(依然存在 300 100 和 60 10000 两条规则,只是新增了更宽松的第一条,实际生效逻辑是三条规则任意一条满足即触发,所以这里的调整逻辑需要根据实际期望重新设计,不是简单加一条规则就能生效,需要确认最终期望的整体触发频率),实际调整时应该完整重新设计这三条规则组合,而不是简单叠加。
风险提醒:CONFIG SET 只是运行时临时生效,Redis 重启后会恢复配置文件里的设置,需要同步修改配置文件才能持久化:
# /etc/redis/redis.conf
save 3600 1
save 300 100
save 60 10000
方案二(更根本,评估后决定是否采用):如果业务对数据持久性要求不高(缓存类场景丢失部分数据可以接受),可以考虑完全关闭 RDB 自动快照,只在需要的时候手动触发或者依赖主从复制保证数据可用性:
redis-cli CONFIG SET save ""
风险提醒:完全关闭持久化意味着 Redis 进程重启或者宕机会丢失全部内存数据(除非配置了从库且能安全地做主从切换),这个决策必须和业务方确认数据丢失的可接受范围,不能运维单方面决定。对于将 Redis 用作持久化存储(而非纯缓存)的场景,不建议关闭持久化。
24.6 配置说明与生效方式
CONFIG SET 立即生效,不需要重启 Redis 进程,这是 Redis 相比很多其他数据库/中间件更友好的地方,很多参数支持不停机动态调整。但要注意 CONFIG SET 修改的是运行时内存中的配置,如果不同步修改配置文件或者执行 CONFIG REWRITE(把当前运行时配置写回配置文件),下次重启依然会加载旧配置:
redis-cli CONFIG REWRITE
风险提醒:CONFIG REWRITE 会重写整个配置文件,如果原配置文件里有大量注释和自定义格式,重写后这些格式可能会丢失或者被重新排列(不同 Redis 版本的行为略有差异),建议执行前先备份原配置文件:
cp /etc/redis/redis.conf /etc/redis/redis.conf.bak.$(date +%Y%m%d_%H%M%S)
24.7 验证结果
调整后持续观察 24 小时的 iostat 数据,确认 IO 尖刺的触发频率明显降低(从原来平均每 90-120 秒一次,降低到符合新规则预期的频率),业务方反馈的偶发超时毛刺现象消失。同时通过 redis-cli INFO persistence 确认 rdb_changes_since_last_save 的增长曲线和快照触发时间点符合新配置的预期。
24.8 回滚方案
如果调整后发现数据持久性不满足业务预期(比如故障恢复时丢失的数据量超出可接受范围),可以用备份的配置文件恢复:
cp /etc/redis/redis.conf.bak.20260911_160000 /etc/redis/redis.conf
redis-cli CONFIG SET save "900 1 300 10 60 10000"
redis-cli CONFIG REWRITE
风险提醒:Redis 场景下的配置回滚相对简单直接,不涉及数据结构变更,但如果这台 Redis 是集群或者主从架构的一部分,需要确认是否所有节点都需要同步调整(Redis Cluster 模式下每个节点的持久化配置是独立的,需要逐个节点确认和调整,不是修改一个节点就能全局生效)。
24.9 复盘总结
这个案例说明中间件自身的持久化、复制、压缩等后台机制,同样会产生不可忽视的磁盘 IO 压力,且这类压力往往是脉冲式、周期性的,不容易在长周期的平均值监控里被发现,需要用短周期高频采样(比如每秒一次的 iostat)配合中间件自身的状态指标(INFO persistence)交叉验证才能定位。日常运维中,对于 Redis、MySQL、Kafka 等有持久化机制的中间件,都应该主动了解其后台任务的触发条件和资源消耗特征,而不是当作黑盒使用,出问题了才第一次去看官方文档。
二十五、AOF 场景下的额外排查要点
如果 Redis 开启了 AOF(appendonly yes),除了 RDB 快照,还需要关注 AOF 重写(rewrite)过程的 IO 影响。
25.1 查看 AOF 相关状态
redis-cli INFO persistence | grep aof
预期输出(节选):
aof_enabled:1
aof_rewrite_in_progress:0
aof_last_rewrite_time_sec:12
aof_current_size:1048576000
aof_base_size:104857600
判断逻辑:aof_current_size 相对 aof_base_size 的比例,反映了 AOF 文件相对上次重写后的增长幅度,如果这个比例持续增大且触发了 auto-aof-rewrite-percentage 配置的阈值(默认通常是 100%,即文件大小相比上次重写增长了一倍),会自动触发 AOF 重写,这个过程同样涉及 fork 子进程和大量磁盘写入,原理和 RDB 快照类似,IO 影响特征也类似(脉冲式)。
25.2 AOF 重写相关配置
# /etc/redis/redis.conf
appendonly yes
appendfsync everysec
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
- appendfsync everysec 是性能和安全性折中的推荐配置(每秒刷盘一次),always 每次写入都刷盘,安全性最高但性能开销最大,no 交给操作系统决定刷盘时机,性能最好但安全性最低,生产环境极少使用;
- auto-aof-rewrite-percentage 和 auto-aof-rewrite-min-size 共同决定自动重写的触发条件,如果发现重写触发过于频繁导致 IO 压力,可以适当调大这两个值,降低触发频率,但会导致 AOF 文件相对更大、重启加载时间相对更长,需要权衡。
二十六、Kafka 场景下磁盘 IO 的特殊考量(补充说明)
如果排查对象涉及 Kafka 这类以磁盘顺序写为核心设计的中间件,判断思路会有一些不同,这里简要补充,供涉及消息队列运维的读者参考。
Kafka 的设计高度依赖磁盘的顺序写性能(消息以追加方式写入分区日志文件),正常情况下即使是机械硬盘也能提供不错的吞吐表现。如果 Kafka 所在节点出现磁盘 IO 瓶颈,通常需要重点排查:
- 是否存在大量的消费者重复消费导致的额外读取压力(正常的顺序读通常有 Page Cache 的良好命中,但如果消费者拉取的是较早、已经被缓存淘汰的历史消息,会转变为随机读,对磁盘更不友好);
- 分区数量和副本数量是否与磁盘的实际承载能力匹配,过多的分区会让原本的顺序写在同一磁盘上被拆分成多个并发的顺序写流,某种程度上退化为接近随机写的模式;
- flush.messages、flush.ms 等控制刷盘频率的参数配置是否合理(Kafka 默认依赖操作系统的后台刷盘机制,而不是每条消息都主动刷盘,这是其高吞吐的重要原因之一,如果误配置为过于激进的主动刷盘策略,会显著增加磁盘写入压力)。
这部分内容涉及消息队列运维的专项知识,本文不做过多展开,仅作为磁盘 IO 排查知识体系的关联提示,实际排查 Kafka 相关磁盘问题时,应结合 Kafka 自身的 JMX 监控指标(如 LogFlushRateAndTimeMs)和消费者组的消费延迟(Consumer Lag)指标综合判断。
二十七、跨团队沟通与文档沉淀建议
磁盘 IO 类问题的排查经常跨越运维、DBA、研发、甚至云平台支持团队,沟通效率直接影响故障处理时长,这里补充几点实践经验:
- 向云厂商提交工单前先自查:如果怀疑是云盘本身的性能问题(比如 SLA 内但实际表现不稳定),提工单前先准备好 iostat 的原始数据、时间范围、云盘规格型号,能大幅缩短对方定位问题的时间,避免多轮来回询问基础信息;
- 和 DBA 协作时说清楚是主库还是从库:主从架构下,同样的语句在主库和从库上的影响面完全不同,误报角色信息会导致排查方向出错;
- 记录每次持久化配置调整的业务影响评估:比如 Redis 关闭持久化、调整 save 规则这类决策,涉及数据安全性的取舍,应该有书面记录(哪怕只是一条内部工单或者聊天记录归档),避免后续出现数据丢失问题时,责任和决策依据无法追溯;
- 建立磁盘 IO 相关故障的案例库:按照”现象特征-常见根因-排查工具”的维度整理归档,比如”周期性尖刺优先怀疑定时任务/持久化机制”、“持续性偏高优先怀疑慢查询/日志配置”,这类经验性的归纳能大幅提升团队新人的排查效率。
二十八、最终总结
一次完整的磁盘 IO 排查,本质上是在”现象层”(告警、业务反馈)和”根因层”(具体的 SQL、具体的配置项、具体的定时任务)之间建立一条可验证的证据链条。这条链条上的每一环——iostat 的整体判断、iotop 的进程定位、应用或数据库层面的深入排查、业务日志的交叉验证——缺了任何一环,得出的结论都可能是似是而非的猜测,而不是站得住脚的根因。
磁盘 IO 问题相比 CPU 问题的特殊之处在于,它经常涉及多个系统组件的协同(应用、数据库、操作系统、存储设备本身),排查者需要具备跨层次的知识储备,同时保持”看数据不看感觉”的严谨态度——每一个判断都要有对应的命令输出作为支撑,而不是凭经验直接下结论。这也是本文反复强调的核心方法论,希望能帮助读者在面对下一次磁盘 IO 告警时,少走弯路,多一份底气。
二十九、附:容器化环境下磁盘 IO 的额外注意点
容器场景下的磁盘 IO 排查有一些额外的复杂性,这里补充说明。
29.1 容器读写层与数据卷的区分
Docker 容器默认的可写层(基于存储驱动,如 overlay2)和挂载的数据卷(-v 参数挂载的目录)在 IO 特征上可能完全不同。如果应用把大量数据写入容器内部未挂载数据卷的路径,这些写入实际发生在存储驱动的可写层,性能通常不如直接挂载的数据卷或者绑定挂载(bind mount)的宿主机目录。
查看容器的存储驱动类型:
docker info | grep "Storage Driver"
查看某个容器的挂载情况:
docker inspect order-service | grep -A 10 "Mounts"
判断逻辑:如果发现数据库、日志等 IO 密集型的数据被写入未挂载数据卷的容器内部路径,建议改造为挂载宿主机目录或者专用数据卷,一方面性能更可预期,另一方面容器重建或迁移时不会丢失数据。
29.2 容器 IO 限额
和 CPU 类似,容器也可以通过 cgroup 对 IO 进行限额,避免单个容器占满宿主机的磁盘带宽:
docker update --blkio-weight 500 order-service
–blkio-weight 设置的是相对权重(取值范围 10-1000,默认 500),用于在多个容器竞争 IO 资源时按权重分配,不是绝对的带宽上限。如果需要绝对限制,可以使用 –device-read-bps 和 –device-write-bps 参数(需要在容器创建时指定,无法用 update 动态调整):
docker run -d --name order-service \
--device-write-bps /dev/vda:50mb \
--device-read-bps /dev/vda:50mb \
order-service:1.8.2
风险提醒:设置过低的 IO 限额会导致容器内应用出现类似”假性变慢”的表现(类似前文 CPU throttling 的案例),排查这类问题时同样需要检查容器的 cgroup blkio 相关统计(/sys/fs/cgroup/blkio/ 路径下的文件,具体路径和文件名因 cgroup 版本(v1/v2)而异)。
29.3 Kubernetes 场景下的临时存储与 emptyDir
Kubernetes 中如果 Pod 使用了 emptyDir 类型的卷且未指定 sizeLimit,容器内的写入可能会持续消耗节点的本地磁盘空间和 IO,且没有明确的隔离边界。检查相关配置:
kubectl -n production get pod order-service-7d9f8c6b4-xk2p9 -o yaml | grep -A 5 "emptyDir"
建议为 emptyDir 设置合理的 sizeLimit,并结合节点的本地磁盘容量规划整体的 Pod 密度,避免多个 Pod 的临时存储需求叠加超出节点承载能力。
三十、案例五:文件系统层面的性能差异排查
有一类相对少见但值得了解的场景:同样的硬件、同样的应用,仅因为文件系统选择或挂载参数不同,IO 表现出现明显差异。
30.1 现象
新采购的一批服务器上部署同样的应用,压测时发现磁盘写入性能明显低于老服务器(旧服务器能达到预期吞吐,新服务器只有旧服务器的 60% 左右),硬件规格(磁盘型号、RAID 配置)基本一致。
30.2 命令检查
对比两台机器的文件系统和挂载参数:
mount | grep /data
cat /etc/fstab | grep /data
老服务器输出:
/dev/vdb1 on /data type ext4 (rw,noatime,nodiratime)
新服务器输出:
/dev/vdb1 on /data type ext4 (rw,relatime)
30.3 关键指标与根因定位
差异在于挂载参数:老服务器使用了 noatime,nodiratime,新服务器使用了默认的 relatime。atime 相关参数控制文件访问时是否更新”最后访问时间”元数据,relatime(现代 Linux 发行版的默认值)已经比传统的每次访问都更新(atime)有所优化,但对于 IO 密集、频繁读取大量小文件的场景(比如这里的应用需要频繁扫描和读取大量小文件),仍然会产生额外的元数据写入开销,noatime 完全禁用这类更新,能进一步降低不必要的写入。
30.4 修复方案
修改 /etc/fstab 增加挂载参数:
# /etc/fstab
/dev/vdb1 /data ext4 rw,noatime,nodiratime 0 2
风险提醒:noatime 会导致依赖文件访问时间做判断逻辑的程序失效(比如某些基于 atime 判断文件是否”最近被访问过”从而决定是否清理的缓存淘汰策略),在修改前需要确认业务应用没有依赖 atime 语义的逻辑,多数常规业务场景(数据库、日志写入、常规文件处理)不依赖这个语义,可以安全启用。
30.5 生效方式
/etc/fstab 的修改在下次挂载时生效,如果不想重启,可以对已挂载的文件系统执行重新挂载:
sudo mount -o remount,noatime,nodiratime /data
验证是否生效:
mount | grep /data
30.6 验证结果
重新压测,写入吞吐提升到接近老服务器的水平,差距从 40% 缩小到 5% 以内(剩余差距可能来自其他细微的硬件或驱动差异,属于合理误差范围)。
30.7 复盘总结
批量采购和部署新服务器时,应该建立标准化的初始化配置检查清单(挂载参数、内核参数、文件系统类型等),通过自动化工具(如 Ansible)统一应用,避免因为个别机器的初始化配置遗漏或者使用了不同的默认值,导致同一批次的机器出现不易察觉的性能差异。
三十一、Ansible 自动化巡检与配置基线参考
为避免上一个案例中”新旧服务器配置不一致”的问题重演,可以用 Ansible 建立配置基线检查:
# check_disk_mount_options.yml
---
- name: 检查磁盘挂载参数是否符合基线
hosts: app_servers
become: true
tasks:
- name: 获取当前挂载信息
command: findmnt -no OPTIONS /data
register: mount_options
changed_when: false
- name: 校验是否包含 noatime 参数
fail:
msg: "节点 {{ inventory_hostname }} 的 /data 挂载参数未包含 noatime,当前参数:{{ mount_options.stdout }}"
when: "'noatime' not in mount_options.stdout"
这个 Playbook 只做检查(fail 模块只是报告不合规项,不会自动修改系统配置),执行方式:
ansible-playbook -i inventory.ini check_disk_mount_options.yml
风险提醒:如果要扩展为自动修复(而不只是检查报告),涉及修改 /etc/fstab 和重新挂载操作,务必先在少量测试节点验证 Playbook 的正确性,再考虑批量应用到生产环境,批量修改系统级配置文件属于高风险操作,即使是通过 Ansible 这类自动化工具执行,也应该保留 –check(dry-run)模式先行验证,并控制 –limit 参数分批次、分节点执行,而不是一次性对全量节点生效。
三十二、写在最后
从最初的告警触发,到最终形成可以指导后续巡检和自动化配置的完整方法论,这中间经历的每一次案例排查,都是把一次性的故障处理转化为团队长期资产的过程。磁盘 IO 类问题看起来是一个基础的运维话题,但实际排查中涉及的知识面横跨操作系统、文件系统、数据库、中间件、容器编排等多个层次,这正是运维和 DevOps 工作的价值所在:不是记住几条命令,而是建立起一套能够应对各种变化场景的系统性排查能力。
三十三、附:不同磁盘类型的正常基线参考(供排查时对照,非绝对标准)
排查过程中经常需要判断”当前的指标数值算不算异常”,这需要有一个大致的基线概念。以下数值仅为经验参考,实际基线应以每台机器自身历史数据为准,不同厂商、不同型号、不同网络环境下的实际表现会有差异:
| 存储类型 | 典型顺序读写吞吐 | 典型随机 IOPS | 典型 await(正常水平) |
|---|---|---|---|
| 机械硬盘(SATA HDD) | 100-200 MB/s | 几十到一两百 | 5-20ms |
| SATA SSD | 400-550 MB/s | 数万 | 1ms 左右 |
| NVMe SSD | 1000MB/s 以上 | 数十万以上 | 远小于 1ms |
| 云盘(普通型) | 依厂商规格而定 | 依厂商规格而定 | 数毫秒,受网络因素影响 |
| 云盘(高性能型) | 依厂商规格而定,通常显著优于普通型 | 依厂商规格而定 | 亚毫秒到数毫秒 |
使用这张表时要注意:云盘的实际表现除了规格参数外,还受网络链路、后端存储集群负载等因素影响,不能完全等同于本地物理盘的表现特征;同时厂商规格也在持续更新迭代,具体数值应以当前采购的云盘产品文档为准,这里仅提供一个数量级的参考区间,帮助判断当前观察到的 await 或 IOPS 数值是”明显异常”还是”在正常波动范围内”。
三十四、附:排查过程记录模板
为了让每次排查都能沉淀为可复用的文档,而不是排查完就遗忘,建议使用统一的记录模板,示例如下:
## 故障记录:[简要描述,例如"订单服务磁盘IO告警"]
- 发生时间:2026-09-11 15:32
- 发现方式:Prometheus告警 / 业务方反馈 / 日常巡检
- 影响范围:[具体服务、具体接口、影响的用户规模]
- 现象描述:[尽量客观描述观察到的现象,附监控截图或者关键指标数值]
### 排查过程
1. [步骤一:执行的命令、观察到的输出、得出的判断]
2. [步骤二:...]
3. [步骤三:...]
### 根因结论
[明确的根因描述,附上支撑该结论的证据(命令输出、日志片段、执行计划等)]
### 修复方案
- 紧急止血:[具体操作]
- 根本修复:[具体操作,包含代码/配置变更的链接或commit]
### 验证结果
[修复后观察到的指标变化,验证周期]
### 回滚记录
[是否执行了回滚,回滚原因(如有)]
### 后续改进项
- [ ] 补充监控指标
- [ ] 调整告警阈值
- [ ] 代码/配置审查关注点更新
- [ ] 容量规划复核
这个模板的关键在于强制记录”证据”和”后续改进项”两部分,前者保证结论可信、可复查,后者保证故障处理不止步于”恢复正常”,而是真正转化为系统的长期改进。团队内统一使用这样的模板,几次故障积累下来就能形成一份宝贵的案例知识库,大幅降低新人上手排查同类问题的门槛。
三十五、附:常见错误操作对照与规避方式
结合本文所有案例,把排查过程中容易踩的坑汇总成对照表,供快速自查:
| 常见错误操作 | 潜在风险 | 正确做法 |
|---|---|---|
| 一发现慢查询就直接 KILL 而不核实来源 | 可能误杀重要的长事务,或者数据同步任务 | 先用 SHOW FULL PROCESSLIST 核实 User/Host/Info 字段确认来源再操作 |
| 直接在业务高峰期执行 ALTER TABLE 加索引 | 消耗额外 IO,可能拖慢业务高峰期的正常查询 | 低峰期执行,且提前在预发环境评估耗时 |
| 修改 redis.conf 后只用 CONFIG SET 不做持久化 | 进程重启后配置丢失,问题复发 | 同步执行 CONFIG REWRITE 或手动同步配置文件 |
| 清理磁盘空间时用 truncate 处理正被写入的日志文件 | 可能导致应用日志写入异常(取决于应用如何处理文件句柄) | 使用 logrotate 或者先确认没有活跃写入再操作 |
| 批量修改多台服务器配置时一次性全量执行 | 如果配置有误,影响面覆盖全部机器,难以快速止损 | 先在少量节点验证,分批灰度,保留回滚脚本 |
| 密码硬编码在备份脚本或者 Ansible Playbook 里 | 密码泄露风险,尤其是脚本被提交到代码仓库 | 使用环境变量、Vault、密钥管理系统等方式管理敏感信息 |
| 只看 %util 就判断磁盘饱和 | 在 SSD/云盘场景下可能得出错误结论 | 结合 await、队列深度、IOPS/吞吐量综合判断 |
三十六、全文回顾
这篇文章以一次真实的磁盘 IO 告警为主线,串联了五个具体案例(数据库全表扫描更新、应用日志级别配置不当、备份任务与业务高峰重叠、Redis 持久化配置不匹配、文件系统挂载参数差异),覆盖了磁盘 IO 问题在不同层次(数据库、应用、中间件、操作系统)可能出现的典型根因模式。每个案例都完整走过了”现象-初步判断-命令检查-关键指标-根因定位-修复方案-验证结果-回滚预案-复盘总结”的闭环,目的是让读者不仅学到具体的命令和配置,更能内化一套可以套用到新场景的排查框架。
磁盘作为计算机系统里相对”慢”的存储介质,天然是各类性能问题的高发地带,无论技术如何演进(从机械硬盘到 SSD 再到分布式存储),磁盘 IO 的排查方法论——先看整体压力、再定位具体进程、再深入应用或中间件层面找具体操作、最后结合业务场景确认触发条件——这条主线始终适用,值得作为运维知识体系里的基础能力长期积累和打磨。
三十七、附:磁盘 IO 相关面试/自查常见问题
作为知识体系的补充,列出几个团队内部技术分享或者新人自查时经常会遇到的问题,附上简要回答思路:
Q: iostat 里的 %util 是 100% 就一定代表磁盘已经饱和吗?
A: 不一定,尤其是在 SSD 或者云盘这类支持较高并发的存储介质上,%util 反映的是”设备处理请求的时间占比”,即使设备仍有能力并行处理更多请求,这个值也可能显示为接近100%。判断是否真正饱和,需要结合 await(平均等待时间)和实际的 IOPS/吞吐量是否已经接近云盘/磁盘的规格上限来综合判断。
Q: 为什么有时候 iostat 显示磁盘压力不大,但应用还是很慢?
A: 可能的原因包括:应用层面的锁等待(不是磁盘IO慢,而是并发访问同一份数据被阻塞)、网络延迟(分布式存储场景下IO请求要经过网络)、应用自身的CPU或内存瓶颈、数据库执行计划问题(扫描了不必要的数据但IO本身还没有达到瓶颈)。需要结合应用层的监控指标一起判断,不能只看磁盘IO这一个维度。
Q: 发现磁盘IO告警,第一步应该做什么?
A: 先看告警的具体内容和触发时间,区分是”持续性”还是”突发性”问题;然后用 iostat -x 或者类似工具确认当前是否仍在发生,如果已经恢复,重点分析告警期间的历史监控数据;如果仍在发生,按照”整体压力判断-定位具体进程-深入分析该进程的具体操作”的顺序逐层排查,避免在还没搞清楚现象之前就直接执行修复操作。
Q: 生产环境该不该开启文件系统的 noatime 挂载选项?
A: 对绝大多数应用场景是可以的,因为大部分应用不依赖文件的访问时间戳,关闭它可以减少每次读操作附带的一次元数据写入,从而降低IO压力。但如果有依赖 atime 的功能(某些邮件服务器、文件访问审计场景),需要提前确认清楚,避免关闭后影响到依赖这个特性的功能。
三十八、写在最后(终)
本文从一次真实的磁盘 IO 告警展开,系统梳理了排查思路、常用工具、典型案例和预防机制。希望读者不只是记住了具体的命令,更能建立起”现象-证据-根因-修复-验证-复盘”这套通用的故障排查框架,在面对新的、本文未覆盖的具体场景时,依然能够有条不紊地推进排查工作。

本文链接:https://www.yunweipai.com/archives/49405




网友评论comments