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

19 次浏览3 条回复

适用于 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、队列和应用入口延迟对齐。

还建议在第 1 节明确一个分流条件:findmnt 若显示的是 nfs/nfs4cifs、CephFS 或其他网络文件系统,就不要再把本机 iostat 当成端到端延迟。它最多说明客户端本地块设备的情况,远端服务排队、RPC 重传和网络抖动都可能完全不体现在目标盘的 await 中。

以 NFS 为例,先用实际挂载点确认客户端参数;nfsstatnfsiostat 来自发行版的 nfs-utils(Debian/Ubuntu 通常由 nfs-common 提供),后者需要目标挂载仍然可访问:

findmnt -T /path/to/data -o TARGET,SOURCE,FSTYPE,OPTIONS
nfsstat -m
nfsiostat 1 10 /actual/mountpoint

采样时可把 retransavg RTTavg exerpc bklog 与应用延迟放在同一时间窗口观察。avg RTT 上升更偏向网络或服务端响应变慢;avg exe 明显高于 avg RTT、同时 backlog 增长时,还要检查客户端 RPC 排队与并发限制。它们仍是相关性证据,不能仅凭一个字段断定服务端存储故障。

如果实际是本地 ext4/XFS,但下层为云盘、虚拟盘或网络块设备,本机 iostat 能看到的是来宾系统视角;此时还应按同一 UTC 窗口核对平台侧卷延迟、队列和限流事件,避免把平台配额或宿主机问题误判成脏页回写。

还可以补一个内存回收与换页分流:同一块设备上的队列和 await 升高,不一定来自目标数据文件,也可能是匿名页换出、换入或文件页重大缺页触发的 I/O。沿用主题的 Linux 5.4+、util-linux 2.36+ 与可选 sysstat 12.2+ 前提,可在同一故障窗口采集:

swapon --show --output=NAME,TYPE,SIZE,USED,PRIO
vmstat -w 1 10
pidstat -r -p ALL 1 10
cat /proc/pressure/memory

grep -E '^(pswpin|pswpout|pgmajfault|allocstall_|pgscan_|pgsteal_)' /proc/vmstat
sleep 10
grep -E '^(pswpin|pswpout|pgmajfault|allocstall_|pgscan_|pgsteal_)' /proc/vmstat

/proc/vmstat 是启动以来的累计计数,应比较两次采样的增量;vmstat 第一行同样是累计口径,重点看后续样本。若 si/so 持续非零,pswpin/pswpout 与目标设备队列在同一窗口增长,说明换页至少贡献了部分 I/O。pidstat -rmajflt/s 可帮助定位发生重大缺页的进程,但读取其他用户进程通常需要更高权限;重大缺页也可能来自文件映射,不应一律解释为 swap。

若要把现象收敛到 systemd 服务,在 cgroup v2 且内存控制器可读时,可再比较服务范围内的计数:

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

grep -E '^(pgfault|pgmajfault|pgscan|pgsteal)' "$M/memory.stat"
test -r "$M/memory.swap.current" && cat "$M/memory.swap.current"
sleep 10
grep -E '^(pgfault|pgmajfault|pgscan|pgsteal)' "$M/memory.stat"
test -r "$M/memory.swap.current" && cat "$M/memory.swap.current"

不同内核暴露的 memory.stat 子项会有差异,缺少某个匹配项不等于没有回收。memory.swap.current 是当前占用量而非换页速率,单独不构成归因;仍需与 io.stat 增量、块设备映射和应用延迟对齐。确认是内存压力后,也不宜先关 swap 或清缓存,应继续查工作集增长、cgroup 内存上限和直接回收,再在原入口复测。