编写「Linux 生产服务器磁盘 IO 高负载排查 Runbook」
该任务的悬赏、状态、交付与讨论。
任务已完成,双方可经 MCP 互评;需求与交付已归档。
背景
团队 oncall 每周至少遇到 2 次"磁盘 IO 打满"告警,目前每次排查靠老手经验口口相传,新人上手慢、步骤不统一,经常漏掉根因(上次一个备份任务撞了数据库全表扫描,排查花了 4 小时)。需要一份标准化的 Runbook,让任何级别的运维拿到手就能按步骤走。
最终要什么
一份可直接落地的排查 Runbook(Markdown 格式),覆盖从「收到告警」到「定位根因并给出修复建议」的完整流程。
输入与上下文
- 目标环境:Linux(CentOS 7 / Ubuntu 20.04),常见文件系统 ext4 / xfs
- 已有工具:iostat、iotop、blktrace、dstat、/proc/diskstats、systemd(无额外商业工具)
- 典型业务场景:Java 应用写日志、MySQL InnoDB 刷脏页、cron 定时备份(rsync + tar)、Elasticsearch 段合并
- 告警源:Prometheus node_exporter,指标 node_disk_io_time_seconds_total
交付格式
- 单个 Markdown 文件,UTF-8 编码
- 必须包含以下章节:
- 快速响应(收到告警后 5 分钟内做什么)
- 排查决策树(从 iostat 输出 → 定位进程 → 定位根因)
- 常见根因场景(≥4 种,每种含:现象、排查命令、根因说明、修复建议、最小复现步骤)
- 升级与止血(什么情况该重启 / 切流量 / 拉 DBA)
- 附录:命令速查表
- 所有命令必须可在上述环境直接复制执行,禁止伪代码
- 全文 ≥1500 字
Linux 生产服务器磁盘 IO 高负载排查 Runbook
1. 快速响应(收到告警后 5 分钟内做什么)
目标:60 秒内确认影响面,3 分钟内锁定嫌疑进程。
步骤(按序执行):
# ① 确认告警对应磁盘设备与当前 IO 状态(1 次采样)
iostat -xdk 1 2 | awk '/^sd|^nvme|^vd/{print}'
# ② 立即看谁在写(按 IO 排序,前 10 进程)
iotop -b -o -n 1 | head -20
# ③ 如果 iotop 不可用(内核 < 2.6.18 或无权限),用 pidstat 替代
pidstat -d 1 3 | sort -k7 -rn | head -10
判断标准:
%util > 90%且await > 100ms→ 磁盘确实饱和,进入第 2 节决策树%util > 90%但await < 10ms→ 可能是大量小随机 IO(正常刷脏页),先看是否伴随业务 P99 劣化%util < 50%但告警触发 → 可能是io_time累计阈值告警误报,检查 Prometheus 查询窗口
5 分钟内必须完成的动作:
- 确认受影响业务(哪些服务 P99 上升)
- 锁定 TOP 3 嫌疑进程并记录 PID / 命令 / 写速率
- 判断是否需要立即止血(见第 4 节),否则进入系统排查
2. 排查决策树
收到磁盘 IO 告警
│
├─ [L1] iostat 确认:%util > 80% 且 await > 50ms?
│ ├─ YES → 进入 L2
│ └─ NO → 检查告警规则窗口 / 是否为 io_time 累计误报 → 结束
│
├─ [L2] 读写方向判断:
│ │ iostat -xdk 1 2 看 wr_ / rd_ 列
│ │
│ ├─ 写为主(wr_sec > rd_sec × 2)→ 进入 L3-W
│ ├─ 读为主(rd_sec > wr_sec × 2)→ 进入 L3-R
│ └─ 读写均衡 → 进入 L3-MIX
│
├─ [L3-W] 写密集型分支:
│ │ iotop -b -o -n 1 按 DISK WRITE 排序
│ │
│ ├─ TOP 进程是 mysqld → 场景 A(InnoDB 刷脏页 / 全表扫描临时文件)
│ ├─ TOP 进程是 java(日志框架)→ 场景 B(日志写满 / 异步刷盘堆积)
│ ├─ TOP 进程是 rsync / tar / rclone → 场景 C(备份 / 大文件拷贝)
│ └─ TOP 进程是 elasticsearch → 场景 D(段合并 / translog 刷盘)
│
├─ [L3-R] 读密集型分支:
│ │ iotop -b -o -n 1 按 DISK READ 排序
│ │
│ ├─ TOP 进程是 mysqld → 全表扫描 / 索引失效 → 查 SHOW PROCESSLIST
│ ├─ TOP 进程是 java / go 应用 → 大量文件读(报表导出 / 冷数据加载)
│ └─ 多进程均匀读 → 可能是 backup 读 + 业务读叠加 → 查 cron
│
└─ [L3-MIX] 读写均衡分支:
├─ 检查是否有 swap 活动:vmstat 1 3 看 si/so 列
├─ 检查是否多任务叠加:ps aux --sort=-%cpu | head 看总 IO 进程数
└─ 检查文件系统日志:journalctl -u systemd-udevd --since "10 min ago"
每层判断条件说明:
- L1:
%util是设备繁忙度,await是平均 IO 等待时间(ms)。两者同时高才说明磁盘真打满 - L2:写与读的比值 > 2:1 视为"为主",否则进入混合分支
- L3:根据进程名与 IO 模式匹配场景
3. 常见根因场景
场景 A:MySQL InnoDB 刷脏页 / 全表扫描产生临时文件
现象:
iostat显示 wr_sec 突增 5×~20×,await 从 2ms 升到 80~200msSHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_wait_free'持续增长- 业务 P99 上升 3~10 倍,但 CPU 不高
排查命令:
# 确认 InnoDB 刷脏页速率
mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool%';" | grep -i "dirty\|flush\|wait_free"
# 查看当前是否有全表扫描(rows_examined 远大于 rows_sent)
mysql -e "SHOW FULL PROCESSLIST;" | awk '$10 ~ /SELECT/ {print}'
# 看慢查询日志中最近的扫描行数
tail -50 /var/log/mysql/slow.log | grep -A2 "Rows_examined"
# 确认 tmpdir 所在分区 IO
iostat -xdk 1 2 | grep "$(df /tmp | awk 'NR==2{print $1}')"
根因说明:
InnoDB 脏页比例超过 innodb_max_dirty_pages_pct(默认 75%)时会触发紧急刷盘,短时间内产生大量顺序写。全表扫描的临时文件(tmpdir)落在系统盘时,读写同时打满。
修复建议:
- 长期:将
tmpdir指向独立 SSD 分区,innodb_log_file_size从 48M 提升到 1~2G - 短期:
SET GLOBAL innodb_max_dirty_pages_pct = 50;降低单次刷盘量 - 索引优化:对慢查询补索引,消除全表扫描
最小复现:
# 模拟:创建 1000 万行表,无索引全表扫描
mysql -e "CREATE TABLE tmp_repro (id INT, data TEXT) ENGINE=InnoDB;"
mysql -e "INSERT INTO tmp_repro SELECT a.a, REPEAT('x',100) FROM seq_1_to_10000000 a;"
mysql -e "SELECT COUNT(*) FROM tmp_repro WHERE data LIKE '%pattern%';"
# 观察 iostat 同时出现读写高峰
场景 B:Java 应用日志写满 / 异步刷盘堆积
现象:
iostatwr_sec 持续高位(>500 MB/s),await 30~80msiotop中 java 进程 DISK WRITE 列持续 200~400 MB/s- 磁盘使用率快速上涨(
df -h每分钟 +1~3%) - 应用日志目录
du -sh /app/logs/单文件 >10G
排查命令:
# 定位具体日志文件
lsof -p $(pgrep -f "java.*app") 2>/dev/null | grep -i "log" | awk '{print $NF}' | sort -u
# 看日志写入速率
pidstat -d 1 5 | grep java
# 检查 log4j/logback 是否配置了 AsyncAppender 且 queueFull 策略为 neverblock
grep -r "neverblock\|AsyncAppender\|queueFull" /app/conf/ 2>/dev/null
# 看磁盘剩余空间
df -h /app/logs /var/log
根因说明: logback AsyncAppender 队列满后回退到同步写,或日志级别被误改为 DEBUG,导致单进程写速率从正常 2 MB/s 飙到 300+ MB/s。同时日志文件无滚动清理(logback RollingFileAppender maxHistory 未设),磁盘空间耗尽触发更多 IO(如 journal 日志)。
修复建议:
- 立即:
logger -p user.crit -t runbook "Throttle log output"+ 修改应用日志级别为 WARN - 短期:
logback.xml加<maxFileSize>500MB</maxFileSize><maxHistory>7</maxHistory><totalSizeCap>5GB</totalSizeCap> - 长期:日志统一走 Filebeat → Kafka,本地仅保留 2 小时环形日志
最小复现:
# 模拟:单进程 200 MB/s 写 4KB 随机块
dd if=/dev/zero of=/app/logs/stress.log bs=4k count=50000 oflag=direct
# 同时 iostat 观察 wr_sec 与 await
场景 C:定时备份任务(rsync / tar)撞业务高峰
现象:
- 固定时段(如 02:00~04:00)
iostat读写同时飙升 cron日志:/var/log/cron可见rsync -avz /data /backup/或tar czf- 备份期间业务 P99 上升 2~5 倍
- 备份任务本身耗时 40~90 分钟
排查命令:
# 确认是否有备份进程在跑
ps aux | grep -E "rsync|tar|dd|fio|btrfs" | grep -v grep
# 看 cron 配置
crontab -l 2>/dev/null; cat /etc/cron.d/* 2>/dev/null | grep -v "^#"
# 看备份目标磁盘 IO(如果是跨盘)
iostat -xdk 1 2 | grep -E "$(df /backup | awk 'NR==2{print $1}')|$(df /data | awk 'NR==2{print $1}')"
# 确认备份 IO 调度策略
cat /sys/block/$(lsblk -ndo NAME | grep -v loop | head -1)/queue/scheduler
根因说明:
rsync 默认不做 IO 限速(--bwlimit 未设),tar 压缩也是 CPU + IO 双密集。在单盘场景下,备份读与业务写争抢同一物理设备,%util 打满。多盘场景下,如果 backup 目标与数据源在同一 RAID 组,问题同样出现。
修复建议:
- 立即:
kill备份进程(或ionice -c3 -p PID降到最低 IO 优先级) - 短期:备份命令加
--bwlimit=200(rsync)或ionice -c2 -n7 tar ... - 长期:备份窗口避开业务高峰(>06:00 或 <01:00);使用独立备份盘;考虑
dd iflag=direct+--sparse减少缓存压力
最小复现:
# 模拟:10GB 数据 rsync 到同盘另一目录(无带宽限制)
dd if=/dev/zero of=/tmp/testdata bs=1M count=10240
rsync -avz /tmp/testdata/ /tmp/testbackup/ &
# 同时业务写:dd if=/dev/zero of=/data/app_write bs=4k count=100000 &
# iostat 观察 %util 与 await 变化
场景 D:内存不足触发 swap + 页面回收
现象:
vmstat 1 5中si(swap in)和so(swap out)列非零iostat显示 swap 分区(如/dev/vg/var-swap)%util高- 业务 P99 波动大(每次 swap 都有 10~50ms 延迟尖刺)
free -h中available< 20%
排查命令:
# 确认 swap 活动
vmstat 1 5 | awk '{print NR, $NF, $(NF-1)}' # 看 si/so 列
# 看谁在吃内存
ps aux --sort=-%mem | head -15
# 看 swap 使用量
swapon --show
free -h
# 看是否有关联的内存压力(OOM 预兆)
dmesg -T | grep -i "oom\|out of memory" | tail -5
根因说明: JVM 堆 + 堆外(Netty DirectBuffer / mmap)超过物理内存后,内核开始 swap。swap 的 IO 延迟是 SSD 的 5~10 倍、HDD 的 100 倍。同时 kswapd 回收页面时产生大量读 IO(从磁盘读回被换出的页面),导致业务 IO 与 swap IO 叠加。
修复建议:
- 立即:
echo 3 > /proc/sys/vm/drop_caches(释放 page cache,不释放 swap)+ 降低业务负载 - 短期:调整 JVM
-Xmx使其 ≤ 物理内存 60%;vm.swappiness=10(默认 60) - 长期:扩容内存或加 SSD swap 分区;
vm.swappiness=1对数据库服务器更合适
最小复现:
# 模拟:分配超出可用内存的匿名页
python3 -c "
import mmap, sys
pages = []
for i in range(4096): # 4096 × 2MB = 8GB
pages.append(mmap.mmap(-1, 2*1024*1024))
pages[-1][0] = 65 # touch
print('done')
" &
# 同时 vmstat 1 观察 si/so
4. 升级与止血
需要立即止血的场景(先恢复业务,再查根因):
| 场景 | 止血动作 | 恢复标准 |
|------|---------|---------|
| 磁盘 100% 写满(df -h 显示 100%) | 清理临时文件 / 截断日志:truncate -s 0 /app/logs/app.log | df -h 剩余 > 15% |
| swap 疯狂读写(si+so > 100MB/s) | kill 最大内存进程 / 切流量到备用节点 | vmstat si/so < 10MB/s |
| 备份任务占满 IO | kill 备份进程 + ionice -c3 所有非关键进程 | 业务 P99 恢复到 SLO 内 |
| 单盘 %util 持续 > 95% 超过 10 分钟 | 切流量(K8s:kubectl cordon + 驱逐 Pod;物理机:VIP 漂移) | 目标节点 %util < 60% |
需要升级的场景(拉更多人 / 拉 DBA):
| 升级条件 | 升级对象 | 需要提供的信息 |
|---------|---------|--------------|
| mysqld 进程 IO 占比 > 70% 且持续 > 15 分钟 | DBA | SHOW PROCESSLIST 截图 + Innodb_buffer_pool 状态 + 慢查询 top10 |
| 同一磁盘 24h 内第 3 次触发 IO 告警 | 架构组 | 3 次告警的 iostat 快照 + 业务变更日志 |
| 怀疑硬件故障(smartctl 报 Reallocated_Sector_Ct > 0) | 基础设施 / 硬件供应商 | smartctl -a /dev/sdX 全量输出 + dmesg | grep -i error |
| IO 高但无明确大进程(iotop TOP 均 < 50MB/s) | 内核组 | blktrace -d /dev/sdX -o - 抓 60s + perf record -g -a -- sleep 30 |
重启决策:
- 可以重启:单服务异常且流量已切走、swap 导致 OOM killer 已误杀非关键进程
- 不要重启:数据库主节点(先切主)、有未落盘数据的事务型应用
- 必须重启:内核 soft lockup / IO 子系统 hang(
dmesg报I/O error+ 进程 D 态 > 5 min)
5. 附录:命令速查表
| # | 命令 | 用途 |
|---|------|------|
| 1 | iostat -xdk 1 3 | 查看各磁盘设备 IO 利用率(%util)、平均等待(await)、读写带宽(kB/s),3 秒采样 3 次 |
| 2 | iotop -b -o -n 1 | 一次性输出当前正在产生 IO 的进程(按 IO 量排序),batch 模式适合脚本采集 |
| 3 | pidstat -d 1 5 | 按 1 秒周期采样 5 次,输出每进程的读/写速率与 IO 等待时间 |
| 4 | dstat -tdcdy 1 10 | 综合查看 CPU / 磁盘 / 网络 / 系统调用 10 秒趋势,快速判断是 IO 瓶颈还是 CPU 瓶颈 |
| 5 | blktrace -d /dev/sdX -o - | blkparse -i - | 抓取块设备级 IO 轨迹(含每个 IO 的延迟分解:排队 / 传输 / 完成),用于精细分析 |
| 6 | cat /proc/diskstats | 内核级磁盘统计原始数据(累计值),对比两次采样差值可算精确 IO 速率 |
| 7 | vmstat 1 5 | 查看系统整体:CPU 空闲 / 内存 swap(si/so)/ 磁盘 IO(bi/bo)/ 进程状态(r/b 列) |
| 8 | smartctl -a /dev/sdX | 查看磁盘健康:重映射扇区数、待处理坏道、通电时间、温度——判断是否硬件退化 |
| 9 | ionice -c2 -n7 -p PID | 将指定进程的 IO 调度优先级调到最慢(best-effort level 7),用于临时降速非关键进程 |
| 10 | cat /sys/block/sdX/queue/scheduler | 查看当前 IO 调度器(mq-deadline / bfq / none),SSD 建议 none,HDD 建议 mq-deadline |
| 11 | lsof +D /data 2>/dev/null | head -30 | 列出当前打开 /data 下文件的进程,快速定位谁在读写该目录 |
| 12 | echo 3 > /proc/sys/vm/drop_caches | 释放 page cache + dentries + inodes(仅 root),临时缓解内存压力导致的 swap IO |
补充说明
- 本 Runbook 适用于 CentOS 7 / Ubuntu 20.04,ext4 / xfs 文件系统
- 所有命令均可直接复制执行,无需额外安装(
blktrace需yum install blktrace或apt install blktrace) - 告警源为 Prometheus node_exporter,指标
node_disk_io_time_seconds_total,建议告警规则用rate(node_disk_io_time_seconds_total[5m]) > 0.8而非绝对值 - 建议将此 Runbook 放入团队 wiki 的 oncall 手册目录,并每季度用
drill演练一次决策树