一、现象与影响面:机器变卡但 CPU 不忙
症状很有辨识度:应用响应出现尖刺,但 CPU 使用率不高;ssh 登录比平时慢半拍;命令行 tab 补全卡一下;top 一看,%wa 一栏居高不下。wa(iowait,等待 IO 的 CPU 时间占比)高说明 CPU 没病,它在等磁盘——瓶颈在存储侧。
反模式一:一卡就重启,一见大文件就 rm。rm 掉几个 G 的日志本身就是一次重 IO,会让已经打满的盘雪上加霜;重启则丢掉全部现场。IO 问题的证据都在实时计数器里(iostat、/proc),动手前先采一轮。
排查主线四层:确认是不是 IO(iostat)→ 找进程(iotop/pidstat)→ 找文件(fd 与 fatrace)→ 分层治法。所有命令默认在宿主机上跑;容器本质上就是宿主机进程,宿主机视角最全。
二、第一层:确认”是不是 IO”——iostat -x 怎么读
# 宿主机上;sysstat 包自带,没有就装:apt install sysstat 或 yum install sysstat
iostat -x 1 5 # 每 1 秒采一次、共 5 组;第一组是开机以来的均值,判断看后几组
vmstat 1 5 # 复核:si/so 持续非零说明在换页;b 列(不可中断进程)持续大于 0 与 iowait 互相印证
关键列速查表(量级为经验参考值,非实测):
| 列 | 含义 | 参考量级 |
|---|---|---|
r/s、w/s |
每秒读/写请求数 | 无绝对标准,看基线 |
rkB/s、wkB/s |
每秒读/写吞吐 | 机械盘上限约百 MB/s 级,SSD 再高一个量级以上 |
r_await、w_await |
单次读/写平均耗时(毫秒,含排队时间) | SSD 亚毫秒到 2ms;机械盘 5-15ms;持续远超基线即异常 |
aqu-sz |
平均队列长度(老版本 sysstat 列名是 avgqu-sz) |
持续高于设备并发能力即堆积 |
%util |
设备忙于处理 IO 的时间占比 | 机械盘逼近 100% 即接近饱和 |
组合判读:
| 读数组合 | 解读 | 下一步 |
|---|---|---|
| %util 高 + await 高 | 设备真饱和,请求在排队 | 进第三节找进程 |
| %util 高 + await 低 | 持续小 IO 但没排队,未必是病 | 对照历史基线再定 |
| await 正常、应用仍慢 | 慢在别处 | 转延展阅读的 CPU 分诊一篇 |
| w_await 高、r_await 正常 | 写路径堵:日志与刷盘类根因嫌疑大 | 第四节重点看写文件 |
两个必要提醒。其一,%util 对 NVMe 这类多队列设备会骗人——设备能并行处理多路 IO,%util 100% 不代表饱和,判断以 await 为准。其二,”异常”必须有基线才成立:第一次排查顺手把正常时段的 iostat 存档,以后所有判断都有参照。
三、第二层:找进程——iotop 与 pidstat
# 宿主机上,需要 root
iotop -oPa # -o 只看有 IO 的,-P 按进程聚合(不拆线程),-a 按累计量排序
pidstat -d 1 10 # 每秒采一次进程级读写 kB/s,共 10 轮,适合留档对比
cat /proc/<PID>/io # 单进程精确读数:write_bytes 是真正落盘的字节,wchar 只是请求量
1. 容器环境多一步认领:宿主机 PID 对回容器名
docker ps -q | xargs docker inspect --format '{{.Name}} {{.State.Pid}}'
1. /app 12345 ← PID 12345 就是 app 容器的主进程
容器里再看反而片面(PID namespace 只显示自己那一截),宿主机定位、必要时才 docker exec 进去复核——与内存排障同一思路。这层锁定嫌疑人(比如 mysqld、某个 node 进程、rsyslogd),进第三层找它写的是哪个文件。
四、第三层:找文件——谁在写、写哪个文件
# 宿主机上,需要 root
ls -l /proc/<PID>/fd | head -20 # 进程当前打开的文件,一眼看到在写哪个日志
lsof -p <PID> | grep REG # 同样信息的 lsof 视角,可再 grep log 过滤
fatrace | grep -i mysql # 实时流:哪个进程在动哪个文件(fatrace 需单独安装)
1. mysqld[1234]: W /var/lib/mysql/ib_logfile0
1. Docker 日志专项体检:json-file 驱动默认不带轮转,会无限长
du -sh /var/lib/docker/containers/*/*-json.log 2>/dev/null | sort -rh | head -5
dmesg -T | grep -iE 'i/o error|blk_update' # 出现 I/O error 则有硬件层嫌疑,见根因表末条
fatrace 的输出把进程和文件对上了,这是多数根因的最后一环:日志风暴会看到某应用高频 W 同一个 log 文件;数据库刷盘会看到 redo 日志或 RDB 临时文件;备份任务会看到 tar/gzip 的读与目标文件的写。
五、根因清单:按出现频率排序
| 根因 | 特征 | 一行验证 |
|---|---|---|
| 日志风暴(debug 刷屏、错误循环重试) | 某应用 write_bytes 疯涨、w_await 高 | /proc/PID/io 对照日志文件增长速度 |
| 容器日志无轮转 | /var/lib/docker/containers 下巨型 json.log | 第四节的 du 一条命令 |
| swap 抖动 | si/so 持续非零,多见于内存紧张的机器 | vmstat 1 看 si/so;内存侧排查见延展阅读 OOM 一篇 |
| 数据库刷盘 | 定点尖峰,与 checkpoint/RDB 保存点时刻吻合 | 尖峰时间对数据库日志 |
| 备份、压缩任务撞业务高峰 | 定点出现,与 crontab 对得上 | crontab -l 对照时间戳 |
| 页缓存反复回收 | 内存 available 低、回写频繁 | free -h 看 available |
| 硬件衰减 | dmesg 有 I/O error,await 全线抬高 | dmesg;进一步用 SMART 工具查健康度,以厂商工具为准 |
六、治法:分层下药
| 层 | 招 | 代价与边界 |
|---|---|---|
| 应用层 | 日志降到合适级别、错误重试加退避、高频日志改采样 | 治本,但要改代码 |
| 配置层 | 日志轮转全覆盖;Docker 配 max-size/max-file;定时任务错峰 | 轮转本身占额外磁盘,配监控别转出新的”磁盘满” |
| 系统层 | swappiness 调小减少换页;cgroup io 限额给容器划边界 | 调脏页刷新参数(vm.dirty_*)会放大掉电丢数据的窗口,慎动 |
| 硬件层 | 机械盘换 SSD、加内存扩页缓存 | 最贵,但加内存同时缓解 IO 与 OOM 两种病 |
配置层的 Docker 日志轮转给个可抄的样板:
# compose 里给单个服务配;全局则在 /etc/docker/daemon.json 配 log-driver 与 log-opts
services:
app:
logging:
driver: json-file
options:
max-size: "50m"
max-file: "3"
网上常见”用 ionice 让备份让路”的招,注意它只在 CFQ/BFQ 这类调度器下有效,NVMe 默认的 none 调度器会无视 ionice——用了没效果先查调度器(cat /sys/block/<盘>/queue/scheduler)。
反模式二:把别人的治法当配置包抄。看到”iowait 高就调 vm.swappiness”就抄——swap 抖动导致的 IO 高,病根在内存预算,内存够用的机器根本不该有大 swap 流量;不确认根因先调参,等于闭眼吃药,参数还留在系统里继续起作用。
七、预防
| 措施 | 做什么 | 边界 |
|---|---|---|
| 基线与告警 | 定期采 iostat 存档,await 与 %util 超基线告警 | 没有基线的阈值告警全是误报 |
| 轮转巡检 | cron 巡检 docker 日志目录与应用日志大小 | 新上线的服务也要纳入,漏一个就白巡 |
| 任务错峰表 | 备份、压缩、批量任务登记时间窗,避开业务高峰 | 跨团队任务要有共同的排期视图 |
| 容量与内存规划 | 内存富余则页缓存足,IO 压力天然小 | 与内存限额策略通盘考虑,别按下葫芦浮起瓢 |
这套走法收拢成一句:先确认(await 才是判决,%util 会骗人),再找进程(iotop/pidstat),再找文件(fd 与 fatrace),最后分层治(应用 → 配置 → 系统 → 硬件)。每一步都有明确的”看到什么才进下一步”,这比记住任何根因清单都重要。
延展阅读:内存侧的孪生问题(swap 抖动、页缓存压力的另一半真相)见《OOM 与内存泄漏的定位》;应用”慢”但 IO 正常时先做 CPU 侧分诊,见《CPU 100% 的定位路径》;本文命令的日常基本功见《Linux 常用命令》。
版本核实(以官方文档为准):iostat 列名随 sysstat 版本有 aqu-sz 与 avgqu-sz 之别,列含义以 sysstat 手册为准;%util 在多队列设备上的局限、ionice 依赖 CFQ/BFQ 调度器为社区长期共识;Docker json-file 默认无轮转与 logging options 为长期稳定文档。文中量级数字为经验参考值,非实测数据。