util 100% 不等于饱和, await 才是体感 — IO 分析三仪表: 延迟 await / 排队 aqu-sz / 参考 %util, 再用 pidstat -d 抓凶手
lsof +L1df -i, 容量没满也写不进磁盘 IO 像餐厅出餐: 应用点单 (write) 通常只要递进page cache这个传菜口就算完, 后厨 (磁盘) 异步慢慢做 — 所以平时飞快; 一旦后厨积压 (队列 aqu-sz 涨), 传菜口也开始堵, 顾客等待时间 (await) 才暴露真相。分析 IO 就是看三个仪表: 等了多久 (await)、排了多长 (aqu-sz)、设备忙不忙 (%util 仅参考), 然后顺着 vmstat → iostat → pidstat -d → iotop 四步把"哪口锅糊了"落实到"哪个厨师"。
-x 扩展统计 -z 隐藏空闲设备。第一行是开机以来均值, 判断要看后续逐行。 $ iostat -xz 1 # Device r/s w/s await aqu-sz %util # vda 2 840 320.5 82.3 100.0 ← 平均值无意义, 看下一行
# await 基线 1ms 的 SSD: # 突变到 200ms → 磁盘明显出事, 立刻查谁在读写
# aqu-sz 偶发 2 → 抖动正常 # aqu-sz 持续 80 → IO 在深度排队, 延迟必然爆炸
# NVMe: %util=100 但 await=0.3ms aqu-sz=0.5 # → 没饱和! 判饱和: await 突变 + aqu-sz 持续高
# 随机 4K: 10000 IOPS = 40MB/s # 顺序 1M: 500 IOPS = 500MB/s ← 同一块盘两种命
# wa=50% → 只能说"去查 IO" # 结论要到 iostat await/aqu-sz + pidstat -d 才能下
$ pidstat -d 1 # PID kB_rd/s kB_wr/s iodelay Command # 5678 0 409600 95 log-agent
-o 只显示有 IO 的进程, 磁盘突然忙时抓现行最快。 $ iotop -o -b -n 3 # 批模式打 3 轮, 可丢进日志 # 5678 be/4 root 380.5M/s 0.0B/s log-agent
dirty_ratio/dirty_expire 异步刷盘。突发写大文件时"积攒-集中刷盘"正是延迟毛刺来源。 $ grep -E "Dirty|Writeback" /proc/meminfo # Dirty: 3.2GB ← 大量脏页待刷, 接下来会有回写风暴
# 每条 fsync: ~1000 条/s (机械盘受限明显) # 攒 100 条一次 fsync: ~5 万条/s ← WAL 的原理
$ lsof +L1 | grep deleted # app 1234 40G REG /var/log/app.log (deleted) $ : > /var/log/app.log # 截断释放, 不用重启
$ df -i /data # Filesystem Inodes IUsed IUse% Mounted # /dev/vdb 6.5M 6.5M 100% /data ← inode 满, block 才 40%
# 备份 (顺序大读) 撞上 OLTP (随机小 IO) # → 数据库 await 起飞; 正解: 错峰 + ionice/限速
tmp 系列经典案例的完整走法: CPU 正常先想"在等", 顺着 IO 链四步收网。
$ vmstat 1 # wa=65% b=20 ← 不是 CPU 空闲, 是在等 IO $ iostat -xz 1 # await=500ms aqu-sz=80 ← 设备排队爆炸 $ pidstat -d 1 | sort -k5 -rn | head -3 # 12:00 PID kB_wr/s Command # 5678 409600 log-agent ← 每秒 400MB 在写 # 结论: 日志 agent 本地缓冲配置错误, 全量直写 # 修复: 改批量+压缩, 写入限速 50MB/s → await 回落 2ms
问题从"接口怎么慢了"缩小成"磁盘被这个进程拖死", 这就是性能分析。
毛刺整点出现, iostat 历史能对齐时间, iotop 批模式守株待兔拿到进程名。
# 守在整点前 (cron 排查) $ echo "iotop -o -b -n 30 -d 2 | grep -v '^Total'" > /tmp/watch.sh $ at 09:58 < /tmp/watch.sh # 整点前后各抓 1 分钟 # 抓到: 09:59:58 backup.sh R 820M/s tar # 根因: 全量 tar 打到数据盘; 修复: # ionice -c3 nice -n 19 tar ... + 限速 rsync --bwlimit=50000
批处理与线上 IO 隔离后, 整点 P99 毛刺消失。
运维 rm 大日志"清空间", df 纹丝不动 — 文件还被进程握着。
$ df -h /var | tail -1 # /dev/vda1 100G 90G 88% /var $ du -sh /var/log # 50G /var/log ← 少了 40G, 差值在 deleted $ lsof +L1 | sort -k7 -rn | head -3 # app 1234 40G REG /var/log/app.log (deleted) # 不重启的解法: 截断 fd 对应文件 $ : > /proc/1234/fd/15 # 直接清空该 fd 指向的已删文件 # 根治: logrotate 配 copytruncate
从此大文件清理 SOP: 先 lsof 看引用, 再决定 truncate 还是重启。
缩略图服务把每张图拆成小分片, 两个月后 inode 先于容量阵亡。
$ df -h /data | tail -1 # /dev/vdb 500G 190G 40% /data ← 明明没满 $ df -i /data | tail -1 # Inodes 6.5M 6.5M 100% ← inode 烧光 # 找小文件大户: $ find /data -type f | sed 's|/[^/]*$||' | sort | uniq -c | sort -rn | head # 5900000 /data/cache/thumb # 修复: 分片合并存储 + 过期清理任务 → IUse% 回落到 12%
巡检加一条: block 使用率与 inode 使用率都进面板。
凌晨延迟起飞但自家流量平稳 — 云盘的突发额度用完了, 这是云环境特有病灶。
$ iostat -xz 1 | awk '$NF==100.0' # vdb await 200 util 100% 但 w/s 只有 300 $ 云监控: EBS/云盘 IO Credit Balance → 0 # 根因: 每天集中备份把 burst 额度烧光, 之后基线性能暴跌 # 修复: 升配到 io 型盘 / 备份限速到基线以内 / 错峰
云盘性能 = 基线 + burst, 容量规划要按"基线"算, 不按"峰值"。
"必须落盘才返回"被理解成"每条 fsync", 组提交才是正解。
// 错: 每条刷盘 for e := range ch { f.Write(e) f.Sync() // 每次 ~1ms, 吞吐锁死 1000/s } // 对: 攒批 + 单次 fsync (组提交) ticker := time.NewTicker(10 * time.Millisecond) for { select { case e := <-ch: buf = append(buf, e) case <-ticker.C: f.Write(bytes.Join(buf, nil)); f.Sync(); buf = buf[:0] } } // 吞吐: 1k/s → 48k/s, 最坏丢失窗口 = 10ms
取舍写进注释: 每秒最多丢 10ms 内的日志, 换 48 倍吞吐。
运营导出全表触发大范围顺序扫描, 预读把 buffer 池挤空, 在线随机读全部 miss 到盘。
$ pidstat -d 1 | grep mysqld # kB_rd/s = 80000 ← mysqld 在疯狂读盘 $ iostat -xz 1 # await 2ms → 45ms, aqu-sz 30 ← 数据盘排队 # 慢查询日志: SELECT ... LIMIT 100 OFFSET 8000000 # 修复: 导出走只读副本 + keyset 分页 (WHERE id > ? LIMIT 100) # 效果: await 回落 2ms, 主库 P99 恢复
离线大查询永远不进主库 — 这是 IO 层面的隔离, 比加索引更根本。
容量水位不该只看磁盘空间, IO 排队是更早的领先指标。
# node_exporter + Prometheus 规则 - alert: DiskQueueDepthHigh expr: rate(node_disk_io_now[5m]) > 1 for: 10m annotations: summary: "{{ $labels.device }} IO 持续排队" # 配套: await 突变告警 # expr: (await_now / await_baseline_7d) > 5 # 处置顺序: 找凶手进程 → 限速/挪盘 → 还没救回再扩容
两项规则上线后, 三起磁盘事故都在用户感知前处理掉了。
新同事看到 util 100% 要求扩盘 — 数据打脸: await 0.3ms, aqu-sz 0.4。
$ iostat -xz 1 # nvme0n1 r/s=8000 w/s=12000 await=0.3 aqu-sz=0.4 %util=100 # 解读: NVMe 多队列并行处理, "一直忙"≠"排长队" # 判饱和三板斧: # 1) await 是否相对基线突变 # 2) aqu-sz 是否持续 > 1 # 3) biolatency 看延迟分布 (P99 而非平均) # 结论: 不扩容, 关单
把"判饱和三板斧"写进容量评审 checklist, 杜绝 util 单指标决策。
事故已过去, sysstat 历史是唯一证人 — 前提是采集开着。
$ sar -d -f /var/log/sa/sa24 | awk '$1 ~ /^14:/' # 14:00 await aqu-sz %util dev # 14:10 180 65 100 vdb ← 尖峰窗口 $ sar -d -f /var/log/sa/sa24 | awk '$1 ~ /^14:1/ {print $6}' # 结合当时进程日志: 14:10 数据迁移脚本启动 # 修复: 迁移脚本加 --throttle, 排到 02:00 低峰
所有生产机开 sysstat (ENABLED="true"), 保留 30 天 — 成本几乎为零, 救命率极高。
await 突变 + aqu-sz 判饱和。 # 错: util 100% → 扩容工单 # 对: await=0.3ms aqu-sz=0.4 → 不饱和, 关单
biolatency 看延迟分布。 # 错: await 50ms → "还行" # 对: biolatency → P50=1ms, P99=1s → 有问题
# 错: "w/s 3000 很正常" # 对: 同图叠加 await 曲线
# 错: 面板只有 %util # 对: 加 aqu-sz, 持续 >1 告警
pidstat -d 找到人再说。 # 错: wa 60% → 采购 SSD # 对: pidstat -d → log-agent 直写 → 配置修复
lsof +L1 点名。 # 错: "du 少 40G, 大概是工具 bug" # 对: lsof +L1 → app.log (deleted) → 截断
: > file 截断或 logrotate copytruncate。 # 错: rm -f app.log (进程还在写) # 对: truncate -s 0 app.log
copytruncate。 # 错: 只配 daily rotate 30 # 对: copytruncate + compress + missingok
df -i 与 df -h 同巡检。 # 错: df -h 40% → 高枕无忧 # 对: df -i 100% 才是写不进的原因
# 错: 每条 200B 记录一次 write() # 对: 攒 64KB 或 10ms 批量落盘
# 错: open(..., O_DIRECT) 写普通日志 # 对: 走 page cache + 批量回写
# 错: for { write(); fsync() } → 1k/s # 对: 攒批 10ms 一次 fsync → 48k/s
# 错: echo none > scheduler 后性能更差 # 对: fio 基准 (randrw 4k / seq 1m) 对比后再定
# 错: "昨天还好好的, 今天磁盘坏了" # 对: 云监控 credit → 0 → 升配或限流备份
oflag=direct / 测前 drop cache。 # 错: dd if=test of=/dev/null bs=1M → 8GB/s 假象 # 对: dd if=test of=/dev/null bs=1M iflag=direct
# 错: 只看 iostat 数字 # 对: grep -i "i/o error" dmesg → 报修换盘
# 错: 白天 tar 全库到同盘 # 对: 02:00 + ionice -c3 + --bwlimit
blockdev --setra。 # 错: 导出按 hash 乱序读主键 # 对: ORDER BY id 扫描, 让预读连续命中
vmstat b + ps stat D 定位到挂载。 # 错: load 30 → 扩 CPU # 对: b=28, wchan=rpc_wait → NFS 挂了
# 错: iostat -xz 1 只看第一行 (均值) # 对: 看 2 行以后: await=500 在当下