工业边缘现场的"设备卡顿"经常最后落到 I/O 上:日志同步写爆了 eMMC,时序数据库在做 compaction,容器内应用频繁写小文件,或者脏页回写造成周期性抖动。iotop 是一个很好的入口,但它只回答"哪个进程或线程在做 I/O",不能单独回答"设备是否已经饱和、延迟卡在块层还是硬件、容器是否被 cgroup 限流"。
这篇文章按工程排查顺序整理一套工具链:iostat 看设备层,iotop / pidstat 看进程与线程,blktrace / btt 拆块层延迟,fio 做受控压测,再结合日志、数据库和容器场景定位根因。
一、先建立 I/O 排查框架
推荐按四层收集证据:
| 层次 | 关注问题 | 常用工具 | 能回答 |
|---|---|---|---|
| 块设备层 | 设备吞吐、延迟、队列、繁忙程度 | iostat |
/dev/sda 或 /dev/nvme0n1 是否异常 |
| 进程 / 线程层 | 谁在读写在做什么 | iotop、pidstat |
哪个进程或线程贡献了主要 I/O |
| 系统与文件层 | 缓存、脏页、容量、文件句柄 | df、du、/proc/meminfo、lsof |
是否为空间、回写或删除文件未释放 |
| 块层细节 | 队列、调度、设备完成耗时 | blktrace、blkparse、btt |
延迟主要花在排队还是设备完成 |
不要只看单个工具。一个常见误判是:iotop 里没有明显业务进程,但 iostat 显示设备很忙,这时要考虑回写线程、镜像构建、备份任务、虚拟设备聚合,或容器 PID 命名空间导致的信息缺失。
二、先留一份现场基线
排障前先确认路径、设备和调度器。同一个目录在根盘、独立 SSD、eMMC 或 NFS 上,结论完全不同。
bash
df -hT /var
findmnt -T /var
lsblk -o NAME,TYPE,SIZE,ROTA,MOUNTPOINTS
DEV=$(findmnt -n -o SOURCE -T /var)
echo "I/O device: $DEV"
如果 DEV 是分区,例如 nvme0n1p2,可以先找底层盘,再查看调度器:
bash
DISK=$(lsblk -ns -o PKNAME "$DEV" | tail -n 1)
cat "/sys/block/$DISK/queue/scheduler"
再记录缓存和脏页状态:
bash
free -h
grep -E '^(Dirty|Writeback):' /proc/meminfo
sysctl vm.dirty_ratio vm.dirty_background_ratio
第一轮数据至少保存 3 到 5 分钟。工业现场负载往往有周期性,只看 10 秒容易把正常批处理误判为故障。
三、iotop:看进程和线程的实际 I/O
1. 运行前提
iotop 依赖 Linux 内核的任务 I/O 统计能力,通常要求内核开启 CONFIG_TASK_DELAY_ACCT、CONFIG_TASK_IO_ACCOUNTING、CONFIG_TASKSTATS 等选项。较新的内核中,task_delayacct 可能默认关闭:
bash
sudo sysctl kernel.task_delayacct=1
iotop 一般用 root 运行。非 root 场景可以通过 CAP_NET_ADMIN 授权,但要遵循安全基线,不要无条件放大能力。
2. 常用命令
bash
# 只显示正在做 I/O 的进程,按进程聚合线程
sudo iotop -o -P -d 2
# 累计模式:显示自进程或 iotop 启动以来的累计量
sudo iotop -a -o -P -d 2
# 指定用户
sudo iotop -u myapp -P -d 2
# 指定进程或线程
sudo iotop -p 12345 -d 2
# 批处理落盘,便于和 iostat、应用日志按时间对齐
sudo iotop -b -o -P -t -k -n 10 -d 2 | tee iotop.log
参数含义:
| 参数 | 含义 | 注意点 |
|---|---|---|
-o |
只显示有 I/O 的任务 | 排障时通常先加 |
-P |
按进程聚合线程 | 查线程级问题时去掉 -P |
-a |
显示累计 I/O | 不是系统启动以来的总流量 |
-b |
批处理模式 | 适合采集和归档 |
-d |
采样间隔 | 默认 1 秒 |
-n |
采样次数 | 常与 -b 配合 |
-t |
输出时间戳 | 隐含批处理模式 |
-k |
固定用 KB 显示 | 便于脚本解析 |
3. 输出怎么读
iotop 的 DISK READ / DISK WRITE 表示进程或线程与内核块设备子系统之间的 I/O。由于 page cache、请求合并和内核重排的存在,进程侧速率与磁盘硬件瞬时速率不一定相同。Total DISK READ/WRITE 与 Current DISK READ/WRITE 在同一时刻也可能不同。
因此要看三类信息的组合:
- 进程读写量:定位主要来源;
IO或GRAPH列:任务等待 I/O 的时间比例;SWAPIN:是否存在换入干扰。
iotop 不知道业务语义,也不知道具体文件路径。如果只看到 kworker 或 jbd2 活跃,还要继续查脏页、文件系统日志、容器、镜像构建和后台任务。
四、iostat:先判断设备层状态
iostat 适合先看设备,再回头找进程:
bash
iostat -xz 2 5
第一份报告通常是系统启动以来的平均值,后续报告才是近实时采样。分析时建议跳过第一份,并记录同一时间窗口的 iostat 与 iotop 输出。
关键字段
| 字段 | 含义 | 工程判断 |
|---|---|---|
r/s、w/s |
每秒完成读 / 写请求数 | 小块高频写对 eMMC 伤害更大 |
rkB/s、wkB/s |
每秒读 / 写数据量 | 与业务上传、归档节奏对齐 |
rrqm/s、wrqm/s |
每秒合并请求数 | 合并多说明请求相邻或碎片化 |
areq-sz / 旧版 avgrq-sz |
平均请求大小 | 新旧版本单位可能不同 |
await、r_await、w_await |
平均 I/O 等待 + 服务耗时 | 读延迟和写延迟应分开看 |
aqu-sz / 旧版 avgqu-sz |
平均队列长度 | 队列高通常说明请求在堆积 |
%util |
设备有 I/O 请求的时间比例 | 对并行 SSD / NVMe 不能单独下结论 |
特别注意:svctm 在新版本 sysstat 中已经废弃,不能作为服务时间或容量判断依据。现代设备可以并行处理请求,%util 接近 100% 不一定代表已经达到性能极限;反过来,低 %util 也不排除个别请求延迟很高。
常见模式
| 现象 | 可能解释 | 下一步 |
|---|---|---|
await 高、aqu-sz 高 |
请求排队,设备或上游路径饱和 | 查进程、cgroup、调度器和设备健康 |
await 高、队列低 |
单次设备完成耗时长 | 查盘健康、固件、虚拟化底层或远程存储 |
| 写入呈周期性突发 | 脏页回写或批量任务 | 查 Dirty/Writeback 和任务计划 |
| 小块写为主 | 日志、数据库 WAL、指标碎片化 | 查落盘策略、批量大小和 compaction |
| 读高且缓存命中率低 | 冷数据反复读取 | 查查询模式、缓存和文件布局 |
阈值必须来自自己的设备基线。同样的 await=20ms,在机械盘、SATA SSD、NVMe、eMMC 和 NFS 上意义完全不同。
五、pidstat:适合采集进程与线程趋势
pidstat 的 I/O 输出没有 iotop 的交互视图,但更适合定时采集和告警归因:
bash
# 每 2 秒输出一次,共 5 次
pidstat -d 2 5
# 指定进程
pidstat -d -p 12345 2 5
# 线程级统计
pidstat -d -t 2
重点字段:
kB_rd/s:任务每秒从磁盘读取的 KiB;kB_wr/s:任务每秒造成或将写入磁盘的 KiB;kB_ccwr/s:被取消写入的 KiB;iodelay:任务等待块设备 I/O 完成的延迟,单位为 clock ticks。
iotop 适合现场交互定位,pidstat 适合长期采集。两者与 iostat 的时间戳必须一致,否则很难区分"谁先触发"和"谁被拖慢"。
六、blktrace:拆块层延迟
当 iostat 只能告诉你"设备慢",但不知道慢在排队、调度还是设备完成阶段时,可以使用 blktrace。
bash
# 采集 30 秒;生产环境先评估 CPU 与 trace 文件体积
sudo blktrace -d /dev/sda -w 30 -o edge-trace
# 合并各 CPU 的原始 trace,输出二进制文件
sudo blkparse -i edge-trace -d edge-trace.bin -q
# 用 btt 分析块层阶段耗时
sudo btt -i edge-trace.bin
不要只拿 edge-trace.blktrace.0 分析单核数据就下结论,多 CPU 系统上完整 trace 会分布在多个文件里。blkparse 负责解析与合并,btt 再输出各阶段延迟统计。
常见阶段可以粗略理解为:
| 阶段 | 含义 | 异常时看什么 |
|---|---|---|
Q2I |
请求进入块层到插入队列 | 请求合并、分配、调度器行为 |
I2D |
请求插入到下发驱动 | 队列堆积、调度策略、cgroup 限制 |
D2C |
下发设备到完成 | 设备自身响应能力、固件、底层虚拟化 |
Q2C |
块层总耗时 | 与 iostat await 和应用延迟互相印证 |
blktrace 采样本身有开销,不要在已经接近饱和的产线控制器上长时间盲跑。先限定设备、时间窗口和过滤条件,采集前后同步保存 iostat 与应用指标。
七、fio:做受控基准,不要先压生产盘
fio 用来验证设备在特定块大小、队列深度和读写混合比下的能力。必须使用专用测试目录和可删除文件,不要直接压根分区或生产数据盘。
bash
mkdir -p /mnt/io-bench
# 顺序读
fio --name=seq-read \
--filename=/mnt/io-bench/seq-read \
--size=2G \
--bs=1M \
--rw=read \
--direct=1 \
--iodepth=32 \
--runtime=60 \
--time_based \
--group_reporting
# 70% 读 + 30% 写的随机混合负载
fio --name=rand-rw \
--filename=/mnt/io-bench/rand-rw \
--size=2G \
--bs=4k \
--rw=randrw \
--rwmixread=70 \
--direct=1 \
--iodepth=64 \
--runtime=60 \
--time_based \
--group_reporting
压测时同步采集:
bash
iostat -xz 2 60 | tee fio-iostat.log
pidstat -d 2 60 | tee fio-pidstat.log
几个注意点:
--direct=1绕过 page cache,否则测到的是缓存和回写混合行为;- 测试文件大小要小于可用空间,避免把生产盘写满;
- eMMC / SD 卡有写入寿命,随机写压测要控制时长;
- 测试结果必须与生产观测值对比,不能直接照搬厂商峰值;
- 测试后清理文件:
rm /mnt/io-bench/seq-read /mnt/io-bench/rand-rw。
八、完整排障流程
第一步:确认设备是否异常
bash
iostat -xz 2 5
记录设备名、r/s、w/s、rkB/s、wkB/s、await、aqu-sz、%util。如果所有设备都正常,而应用仍感觉慢,应转向 CPU、锁、网络、数据库等待或调度问题。
第二步:定位进程和线程
bash
sudo iotop -b -o -P -t -k -n 5 -d 2 | tee iotop.log
pidstat -d -t 2 5 | tee pidstat.log
如果业务进程写入量不高,但 kworker、jbd2、数据库后台进程或容器运行时活跃,要继续查脏页、文件系统日志、镜像构建、备份和数据库维护任务。
第三步:确认文件与容量
bash
df -hT
du -xhd 1 /var | sort -h
find /var/log -xdev -type f -printf '%s %p\n' | sort -nr | head -n 20
journalctl --disk-usage
sudo lsof +L1 | head -n 30
du 使用 -x 避免跨挂载点把统计范围扩大。lsof +L1 用于发现已被删除但仍被进程打开的文件,这类文件在 df 里仍占空间,但 du 按目录可能找不到。
第四步:下探系统调用和代码路径
bash
sudo strace -c -f -p "$PID" -e trace=read,write,fsync,fdatasync
sudo perf record -F 99 -g -p "$PID" -- sleep 30
sudo perf report --stdio
strace 对高吞吐进程有明显开销,只适合短窗口确认行为,不适合长期挂在产线关键进程上。重点看是否出现频繁 fsync、小块 write、同步网络存储访问或重复读同一批文件。
第五步:确认设备健康
bash
sudo dmesg -T | grep -iE 'i/o error|ata error|nvme|mmc|reset|timeout'
sudo smartctl -a /dev/sda
sudo nvme smart-log /dev/nvme0
eMMC / SD 设备未必完整支持 SMART,需要借助厂商工具或 mmc 工具读取 extCSD 信息。设备错误、重置、超时通常比单一性能指标更有定性价值。
九、三个典型场景
场景 1:日志把边缘盘写满
典型表现:
df快速上涨;wkB/s持续偏高;iotop中业务进程或日志 agent 写入量大;- 设备
w_await抖动。
排查:
bash
du -xhd 1 /var/log | sort -h
find /var/log -xdev -type f -printf '%s %p\n' | sort -nr | head -n 20
journalctl --disk-usage
处理方向:
- 降低无效 debug 日志等级;
- 控制日志大小、保留天数和压缩策略;
- 用异步缓冲替代每条日志
fsync; - 检查
logrotate是否生效:
bash
logrotate -d /etc/logrotate.conf
如果必须收紧 systemd journal,先确认留存要求,再执行:
bash
sudo journalctl --vacuum-size=500M
场景 2:数据库写入慢
先分清是数据库自己慢、磁盘慢,还是等待其他资源:
bash
iostat -xz 2 5
sudo iotop -p "$(pgrep -o postgres)" -P -d 2
PostgreSQL 可以查看当前等待事件:
sql
SELECT
pid,
state,
wait_event_type,
wait_event,
now() - query_start AS query_age,
query
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY query_start;
如果启用了 pg_stat_statements,再看 Top SQL:
sql
SELECT
query,
calls,
mean_exec_time,
total_exec_time
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
处理方向包括减少无效查询、调整批量写入、检查 WAL 与检查点策略、确认表空间所在设备,以及避免把数据库与高频日志放在同一张 eMMC 上。
场景 3:容器 I/O 归因
只拿容器主进程 PID 可能遗漏子进程和线程。先看容器进程树:
bash
CONTAINER_PID=$(docker inspect --format '{{.State.Pid}}' mycontainer)
pstree -ap "$CONTAINER_PID"
cgroup v2 下更稳妥的是直接看容器 cgroup 的 I/O 统计:
bash
CID=$(docker inspect --format '{{.Id}}' mycontainer)
CGROUP=$(find /sys/fs/cgroup -type d -name "docker-${CID}.scope" -print -quit)
cat "$CGROUP/io.stat"
cat "$CGROUP/io.max"
io.stat 常见字段包括 rbytes、wbytes、rios、wios,具体依赖内核版本。io.max 能看出是否配置了读写带宽或 IOPS 限制。Kubernetes、containerd、podman 的 cgroup 路径不同,但思路一致:先定位 cgroup,再看统计与限制。
十、工业边缘特有的判断点
1. 小块随机写比大流量更伤 eMMC
eMMC / SD 的写入寿命和写放大与擦除块大小相关。长期 4KB 以下碎片写、频繁 fsync、数据库小事务和日志刷盘,可能比短时间大文件顺序写更危险。
优化方向:
- 日志按大小或时间批量刷盘;
- 指标先内存聚合,再批量落盘;
- 数据库合理设置 checkpoint 和 WAL 策略;
- 把高频写数据与系统盘分离;
- 监控设备健康指标和预留空间。
2. 脏页回写会造成周期性抖动
应用写入 page cache 时看似很快,真正的磁盘写入可能延后发生。观察:
bash
watch -n 1 'grep -E "^(Dirty|Writeback):" /proc/meminfo'
如果 Dirty 长期抬升、随后出现写爆发,要结合 vm.dirty_ratio、vm.dirty_background_ratio、业务批量策略和设备写入能力评估。调内核参数前先做可回滚测试,不要直接改产线配置。
3. 远程与虚拟存储要区分层次
NFS、iSCSI、overlay、device mapper、RAID 和虚拟化磁盘都会改变 I/O 路径。上层看到的 /dev/vda 可能不是真正承担擦写和排队的物理设备。排查时要记录映射关系,避免只对虚拟设备下结论。
4. CPU iowait 不是唯一证据
iowait 受时钟节拍、多核和空闲统计方式影响,只能作为辅助线索。设备延迟、队列深度、进程 I/O 等待和应用请求完成时间要一起看。
十一、长期监控建议
| 指标 | 采集建议 | 告警思路 |
|---|---|---|
| 设备读写速率 | iostat / node exporter |
与同时间业务周期对比 |
await、aqu-sz |
按设备和读写方向拆分 | 持续高于基线才告警 |
| 进程读写速率 | pidstat 或 taskstats |
Top 进程异常增长 |
| 脏页与回写 | /proc/meminfo |
周期性爆发和持续累积 |
| 文件系统水位 | df |
预留安全余量 |
| 设备错误 | dmesg / SMART /厂商工具 |
任何持续错误都高危 |
| 容器 I/O | cgroup 统计 | 超额或被限流 |
| 日志增长 | 日志目录大小 | 增长率异常 |
告警不要只设"绝对值超标",要设"相对基线偏离 + 持续时间 + 业务影响"。例如写速率升高本身不是故障,若同时数据上传积压、请求延迟上升或磁盘水位快速上涨,才需要升级处理。
十二、常见坑
坑 1:把 %util 当唯一饱和指标
并行 SSD / NVMe 可以同时处理多个请求,%util 高不一定代表性能耗尽。应结合 await、aqu-sz、吞吐、IOPS 和应用延迟。
坑 2:继续使用 svctm
svctm 已废弃且不可靠。服务阶段耗时应优先用 blktrace / btt 的阶段数据分析。
坑 3:容器里只看容器主 PID
主 PID 不等于容器内所有进程和线程。需要结合进程树、cgroup I/O 统计或进入正确的命名空间采集。
坑 4:误解 iotop -a
-a 是自进程或 iotop 启动以来的累计值,不是系统启动以来的累计值,也不适合直接当实时带宽看。
坑 5:fio 压测生产系统盘
fio 会制造真实写入,可能占满容量、放大延迟并消耗 eMMC 寿命。必须在专用测试目录和变更窗口执行。
坑 6:只看 du 不看已删除文件
du 找不到已被删除但仍被进程持有的文件空间,需要用 lsof +L1 交叉验证。
坑 7:没有基线
没有正常负载曲线时,很难判断 20ms、50ms 或 100ms 是异常还是设备特性。上线初期就应采集不同班次、不同工况和不同采集周期的 I/O 基线。
十三、落地清单
iostat -xz 2 5确认设备和队列状态;iotop -oP -d 2找主要进程;- 需要线程归因时用
iotop -o -d 2或pidstat -d -t; - 用
df、du -x、journalctl --disk-usage排查容量; - 用
/proc/meminfo判断脏页回写; - 用
blktrace/btt拆排队与设备耗时; - 用受控
fio建立设备基线; - 容器场景必须核对 cgroup 统计与限制;
- 所有输出统一时间戳并归档;
- 将结论回写为阈值、巡检项和应急 SOP。
在 Zenova EdgeOS 的边缘运维链路中,这类 I/O 证据通常被纳入统一的运行时观测与告警体系,让现场人员不只看到"磁盘忙",而能同时看到设备、进程、容器、容量和业务指标之间的关联。
TL;DR
iotop 负责回答"谁在做 I/O",iostat 回答"设备层发生了什么",pidstat 适合长期采集,blktrace / btt 拆解排队与设备完成耗时,fio 用于建立受控基线。工业边缘场景要额外关注 eMMC 寿命、小块随机写、脏页回写、容器 cgroup、日志容量和远程存储映射。判断饱和不要只看 %util,也不要再依赖已废弃的 svctm;最终要以同一时间窗口内的设备、进程、容量、健康状态和应用影响共同定位。