磁盘与 IO 排查:iostat、inode、句柄
磁盘问题有两种“满”,报错几乎一样但处理方式完全不同:空间满了要删文件,inode 满了删小文件才有用。IO 问题则更隐蔽:%util 看着不高,延迟却已经翻十倍。本章把磁盘空间、inode、IO 延迟、文件句柄四条线一次讲清。
第一步:两种“满”必须分开看
df -h # 空间使用率
df -i # inode 使用率
| 情况 | 报错关键字 | 判断命令 | 处置 |
|---|---|---|---|
| 空间满 | No space left on device | df -h 显示 100% | 清理大文件、扩容、轮转日志 |
| inode 满 | 同样是 No space left on device | df -h 正常、df -i 显示 100% | 清理海量小文件(缓存、session、临时文件) |
| 句柄耗尽 | Too many open files | lsof -p <pid> | wc -l 接近 ulimit -n | 提高上限并修连接未释放的代码 |
只看 df -h 是最常见的低级失误:磁盘还有 30% 空间,服务却已经写不进任何文件,原因就是 inode 用完了。
找出到底是谁占的
# 1. 逐层下钻目录占用
du -sh /var/* 2>/dev/null | sort -rh | head -10
du -sh /var/log/* 2>/dev/null | sort -rh | head -10
# 2. 找大文件
find / -xdev -type f -size +500M -exec ls -lh {} \; 2>/dev/null | head
# 3. 关键:找出已删除但仍被进程占用的文件(空间不释放的元凶)
lsof +L1 2>/dev/null | head -20
lsof -nP 2>/dev/null | grep -i deleted | awk '{print $1,$2,$7,$9}' | sort -k3 -rn | head
# 4. 找小文件多的目录(inode 满时用)
find /var -xdev -type f 2>/dev/null | awk -F/ '{print $2"/"$3"/"$4}' | sort | uniq -c | sort -rn | head
| 现象 | 结论 | 处理 |
|---|---|---|
du 统计的总量远小于 df 显示 | 有已被删除但仍被进程持有的文件 | 重启或让进程重新打开日志文件,用 truncate 清空而非 rm |
| 日志目录占大头 | 日志未轮转或级别过高 | 立即轮转 + 降级别 |
大量 .tmp、.part 文件 | 上传或任务失败残留 | 清理并加清理任务 |
| 容器可写层巨大 | 应用往容器里写数据 | 挂载 volume,别写容器层 |
| inode 满但数据量不大 | 小文件过多(session、缩略图、缓存) | 删小文件或换文件系统方案 |
iostat 关键列判读
iostat -x 1 5
# Device r/s w/s rkB/s wkB/s r_await w_await await aqu-sz %util
| 列 | 含义 | 判读阈值与结论 |
|---|---|---|
r/s、w/s | 每秒读写次数(IOPS) | 与设备上限对比;HDD 通常几百,SSD 可达数万 |
rkB/s、wkB/s | 每秒读写吞吐(KB) | 与带宽上限对比,大文件场景看这两列 |
await | 平均每次 IO 的总等待时间(毫秒,含排队) | 明显高于平时即变慢,HDD 常见 < 20ms,SSD 应 < 5ms |
r_await / w_await | 分开看读与写的等待时间 | 写慢常见于 fsync 与脏页回写 |
aqu-sz | 平均请求队列长度(旧版本叫 avgqu-sz) | 持续 > 1 说明设备有排队,IO 越积越多 |
%util | 设备至少有 1 个请求在处理的时长占比 | 接近 100% 表示忙,但多队列 SSD 上 100% 不等于饱和,要结合 await |
%iowait(top/vmstat) | CPU 等 IO 的时间占比 | 高即磁盘瓶颈,但虚拟机上会被 steal 干扰 |
一句话判读法:先看 await 是否升高,再看 aqu-sz 是否排队,最后才看 %util。只盯 %util 会漏掉“队列很长但设备偶尔空闲”的情况。
# 谁在制造 IO:进程级定位
pidstat -d 1 5 # kB_rd/s 与 kB_wr/s,找出读写最多的进程
iotop -oPa # 交互式,看累计写入量(需 root)
# IO 压力(PSI),比 %util 更贴近“应用是否被拖慢”
cat /proc/pressure/io
# some avg10=12.34 total=... :some 表示至少一个任务等 IO 的时间占比,超过 10% 就明显影响延迟
IO 慢的常见根因
| 根因 | 特征 | 处置 |
|---|---|---|
| 日志同步写 | w_await 高,sy 高,栈里是 fsync | 改异步日志、降级别、分离日志盘 |
| 大查询扫盘 | 读 IOPS 高,DB 慢查询日志有记录 | 加索引、加缓存、限制并发 |
| 脏页集中回写 | %util 周期性冲到 100%,延迟尖刺 | 调 dirty_ratio、提高回写频率 |
| 数据盘与日志盘共用 | 任何写入互相干扰 | 物理分离或至少逻辑分离 |
| 快照/备份任务 | 每天固定时间点 IO 高峰 | 错峰执行,限制备份速率 |
| 云盘 IOPS 额度用尽 | await 高但 %util 不高 | 升配云盘或降低写入量 |
句柄与磁盘的联动
句柄泄漏往往同时表现为磁盘问题:文件没关,删除后空间不释放,日志还继续写。
cat /proc/sys/fs/file-nr # 已分配 / 未使用 / 系统上限
ulimit -n # 当前 shell 的软限制
cat /proc/<pid>/limits | grep -i 'open files'
ls /proc/<pid>/fd | wc -l
lsof -p <pid> | wc -l # 与上限对比,看余量
lsof -nP -p <pid> | grep -c REG # 打开了多少普通文件
| 现象 | 结论 |
|---|---|
| 句柄数随 QPS 线性上涨、回落时不降 | 连接或文件未释放,典型泄漏 |
句柄数接近 ulimit -n | 马上会出现 Too many open files,先提上限再修代码 |
大量 deleted 文件 | 空间不释放,重启该进程或 truncate 释放 |
file-nr 第一项接近第三项 | 系统级句柄也将耗尽,需调 fs.file-max |
处置动作
# 1. 清空正在被写入的日志文件:用 truncate 而不是 rm
truncate -s 0 /var/log/myapp/app.log
: > /var/log/myapp/app.log # 等价写法
# 2. 立即轮转并压缩
logrotate -f /etc/logrotate.d/myapp
# 3. systemd-journald 占用过大时限制总量
journalctl --vacuum-size=500M
# 4. 清理已删除但仍被占用的空间:重启持有该句柄的进程
lsof +L1 | awk '{print $2}' | sort -u
处置模板:先 truncate 让服务立刻恢复写入,再定位是哪个进程、哪个目录、哪类文件在涨,最后改轮转策略或拆盘,不要只删一次就走。
预防与巡检
- 空间与 inode 都要设告警,阈值 80% 预警、90% 告警。
- 日志强制轮转(按大小与天数双条件),并保留压缩归档。
- 把日志目录与数据目录放在不同分区,避免日志写满拖垮数据库。
- 关键服务限制单日日志上限,超出直接丢弃并统计,宁可少日志也不要挂服务。
- 云盘注意 IOPS 与吞吐额度,容量升配不等于性能升配。
- 巡检脚本每天统计
await、aqu-sz、%util峰值,形成基线后才能快速判断“这次是否异常”。
小结:df -h 与 df -i 必须同时看,空间满与 inode 满的报错相同但处置不同;IO 判读按 await、aqu-sz、%util 的顺序看,进程级定位用 pidstat -d 与 iotop,压力水平用 /proc/pressure/io;被写入的日志用 truncate 清空而不是 rm,最后把日志与数据分盘、轮转与句柄上限都纳入日常巡检。