Linux I/O 延迟升高:区分设备排队、脏页回写与存储错误

6 次浏览1 条回复

适用于 Linux 服务延迟升高、请求卡顿或写入耗时波动,且怀疑块存储 I/O 的场景。命令基线为 Linux 5.4+、util-linux 2.36+、procps-ng 3.3+;iostatpidstat 来自 sysstat 12.2+,属于可选工具。读取其他进程、cgroup 和完整内核日志通常需要 root。示例路径 /path/to/data 与单元 app.service 必须替换为实际值。先保留故障窗口,不要一开始就清缓存、调整写回参数或在线跑写压测。

1. 固定实际文件系统与设备链路

date -u
uname -r
findmnt -T /path/to/data -o TARGET,SOURCE,FSTYPE,OPTIONS
lsblk -o NAME,KNAME,TYPE,PKNAME,MAJ:MIN,FSTYPE,SIZE,MOUNTPOINT
df -hT /path/to/data
df -ih /path/to/data

先确认应用路径落在哪个挂载点。LVM、device-mapper、软件 RAID、虚拟磁盘和云盘会形成多层设备,挂载源不一定就是最终承载 I/O 的物理设备;容器中的 overlayfs 也不能直接代表宿主机下层存储。采集命令应在故障发生的挂载命名空间执行,并同时保留宿主机视角。

df -hdf -i 用于先排除空间或 inode 耗尽。它们不能证明设备延迟正常。

2. 同时观察任务等待、PSI 与块设备统计

vmstat 1 10
cat /proc/pressure/io
iostat -xz -y 1 10

vmstatwa 是采样期间 CPU 处于 I/O wait 的比例,不是单次请求延迟;b 增加说明不可中断睡眠任务增多,但不能单独归因到某块盘。I/O PSI 的 some 表示至少有任务因 I/O 停顿,full 表示所有非空闲任务同时停顿,它能反映工作负载受影响程度,却不指出具体设备。

iostat 中重点看读写吞吐、IOPS、队列深度和 await 的连续变化。await 包含排队与设备服务时间;单看一次尖峰不足以定性。%util 在传统单队列设备上可辅助判断忙碌程度,但对 NVMe、多队列、RAID 与虚拟设备不能简单解释为“达到 100% 才饱和”。第一份自启动以来的累计报告容易稀释故障,因此示例用 -y 跳过它。

3. 把设备压力关联到进程或服务

pidstat -d -p ALL 1 10

systemctl show app.service -p MainPID -p ControlGroup
stat -fc %T /sys/fs/cgroup
CG=$(systemctl show app.service -p ControlGroup --value)
cat "/sys/fs/cgroup${CG}/io.stat"
sleep 10
cat "/sys/fs/cgroup${CG}/io.stat"

最后四条只适用于 cgroup v2;stat 输出应为 cgroup2fs,并且目标单元的 ControlGroup 必须非空。io.stat 是按设备主次设备号累计的字节数和操作数,应比较固定时间间隔内的增量,再用 lsblkMAJ:MIN 映射设备,不能直接比较不同服务的累计总数。

pidstat 可能漏掉生命周期很短的进程。缓冲写入还可能先记在应用,稍后由内核写回线程落盘,所以“看到内核线程写盘”不等于根因在内核线程;应与应用请求量、批处理任务和 cgroup 增量对齐。

4. 检查脏页回写是否造成周期性停顿

grep -E '^(Dirty|Writeback|WritebackTmp|NFS_Unstable):' /proc/meminfo
sysctl vm.dirty_background_bytes vm.dirty_background_ratio \
       vm.dirty_bytes vm.dirty_ratio \
       vm.dirty_expire_centisecs vm.dirty_writeback_centisecs

普通 write(2) 往往先写入页缓存,真正的设备 I/O 稍后发生;达到脏页阈值后,应用自身也可能参与回写并出现延迟尖峰。应连续采样 DirtyWriteback、设备队列与应用延迟,观察它们是否同步增长和回落。

dirty_bytes 非零时会取代对应的 ratio 口径;比例阈值还会随可用内存规模变化。不要仅凭一个峰值修改这些主机级参数,调整前需要核对同机工作负载和持久化语义,尤其是依赖 fsync、数据库 WAL 或日志落盘的服务。

5. 排除设备超时与文件系统错误

journalctl -k --since '-30 min' --no-pager -o short-iso \
  | grep -Ei 'I/O error|blk_update_request|buffer i/o|reset|timeout|nvme|ext4-fs error|xfs.*(error|corrupt)|read-only file system'
findmnt -T /path/to/data -o TARGET,SOURCE,FSTYPE,OPTIONS

若出现设备 reset、超时、介质错误、文件系统损坏或只读重挂载,优先保留完整内核日志、设备映射和存储平台事件;不要继续用压测扩大故障。主动 fio 测试必须在隔离、可丢弃且明确指定的测试卷上进行,不能把生产文件或裸设备当作临时验证目标。

6. 按同一口径闭环

修复后在代表性但受控的负载窗口重复采集:应用端延迟与错误率、iostat -xz、I/O PSI、目标 cgroup 的 io.stat 增量,以及内核日志。闭环标准不是某个指标瞬时下降,而是原入口延迟恢复,设备队列与 await 不再持续增长,脏页能够稳定回落,且没有新增超时或文件系统错误。

若瓶颈来自突发并发,应优先限制无界并发、建立背压或错峰;若是写回波动,应结合持久化要求和内存预算调整写入节奏;若底层存储持续报错,则应转入设备或平台故障处置,而不是只调应用超时。

可以在第 2、3 节之间补一组按服务范围采样 PSI,用来判断全局 I/O 停顿是否真的落在目标 cgroup 上。前提是 cgroup v2、内核启用 CONFIG_PSI,并且当前用户可读取该服务的 cgroup 文件:

CG=$(systemctl show app.service -p ControlGroup --value)
P="/sys/fs/cgroup${CG}/io.pressure"
test -n "$CG" && test -r "$P" || exit 1

cat "$P"
sleep 10
cat "$P"

最好与同一 10 秒窗口内的 io.stat 增量及应用延迟一起保存。判断时比较 somefull 行中 total 的增量;total 单位是微秒,avg10/avg60/avg300 是滚动平均比例,不能做简单差分。

如果 /proc/pressure/io 明显上升,而目标 cgroup 的 io.pressure 基本不变,就不应把主机级停顿直接归因给该服务,应继续查看同层其他 cgroup 或宿主机任务。反过来,目标 cgroup 的 full 上升也只说明该 cgroup 的非空闲任务同时因 I/O 停顿,仍不能单独证明某块设备已经饱和;需要再与设备映射、await、队列和应用入口延迟对齐。