Linux 磁盘延迟升高,别只看 iostat 的 util

139 次浏览8 条回复

适用于 Linux 5.4+、sysstat 12.2+。要在故障窗口执行,并确认当前账号能读取进程和块设备统计;示例路径要换成实际业务路径。先把文件系统映射到块设备,再连续采样:

date -u
findmnt -T /path/to/data -o TARGET,SOURCE,FSTYPE,OPTIONS
lsblk -o NAME,KNAME,TYPE,PKNAME,SIZE,ROTA,MOUNTPOINTS
iostat -xzy 1 10
pidstat -d 1 10
cat /proc/pressure/io

await 包含排队和设备处理时间,aqu-sz 看平均队列长度;两者一起升高,才更像 I/O 路径正在积压。util 接近 100% 只表示采样周期内设备几乎一直有请求,对 RAID、device-mapper 和并行度高的 SSD/NVMe,不能单凭它认定设备已经跑满。

如果 findmnt 指向 LVM、加密卷或容器存储,记得同时看映射设备和下层物理盘。pidstat -d 能找出主要读写进程,但页缓存回写可能落在内核线程上,所以进程写入速率和设备写入速率不一定同步。PSI 的 some/full 更适合判断任务是否真的因 I/O 停顿,单次 avg10 快照仍要结合持续采样。

调整并发、队列或存储后,用相同采样间隔和相近负载复测;除了 await、队列和 PSI,还要确认业务吞吐、尾延迟及错误率没有变差。

还要小心把 mapper 设备和底层盘的吞吐直接相加:同一批请求可能在 dm-0nvme0n1 各记一次。逐层对照有用,但不能拿各层数值求业务总 I/O。另一个坑是 await 是已完成请求的平均值,快慢请求混在一起时,少量高延迟会被稀释;sysstat 12.2+ 可以先分开看 r_awaitw_await 和请求大小,再把采样窗口与应用 p99 对齐,避免“平均正常”但尾延迟已经抬头。

如果主机上混跑多个服务,主机级 pidstat/proc/pressure/io 还可能把归因混在一起。使用 cgroup v2、systemd 245+ 时,可以先拿到目标服务的 cgroup,再看该组自己的累计 I/O 和压力:

CG=$(systemctl show app.service -p ControlGroup --value)
cat "/sys/fs/cgroup${CG}/io.stat"
cat "/sys/fs/cgroup${CG}/io.pressure"

io.stat 是累计计数,最好按固定间隔取两次做差;其中设备号可以用 lsblk -o NAME,MAJ:MIN 对上实际块设备。这样能区分“整机 I/O 忙”和“这个服务确实在产生 I/O 或被 I/O 阻塞”。前提是目标服务确实处在独立 cgroup,并且内核启用了 PSI。

还有个边界:findmnt 如果返回 nfsnfs4,本机 iostat 看到的是客户端本地块设备,不能代表 NFS 服务端存储。装有 nfs-utils 时,可以在同一故障窗口补一组:

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

nfsiostat 里的重传、平均 RTT 和平均执行时间能帮助判断客户端到服务端这段路径,但仍看不到服务端块设备内部的排队。还需要按同一 UTC 时间窗对照服务端的块设备指标和业务尾延迟;本机 await 正常,不能据此排除 NFS 后端慢。

再补一个容易漏掉的情况:await 主要反映已经完成请求的平均耗时。请求卡住还没完成时,它可能暂时不升,但队列已经在积压。Linux 5.4+ 可以对实际叶子设备看当前未完成请求,并同时查块层超时或复位日志:

DEV=nvme0n1   # 换成 lsblk 对应的实际设备名
cat "/sys/class/block/$DEV/inflight"
journalctl -k --since '-15 min' --no-pager \
  | grep -Ei 'timeout|reset|I/O error|blk_update_request|nvme'

inflight 要连续采样才有意义;如果它持续不归零、aqu-sz 上升而完成量下降,就比单看 await 更像请求卡在块层。内核日志需要相应权限,设备名也别直接套用示例。

calmvaleLv1#4

再补一个容易漏掉的情况:await 主要反映已经完成请求的平均耗时。请求卡住还没完成时,它可能暂时不升,但队列已经在积压。Linux 5.4+ 可以对实际叶子设备看当前未完成请求,并同时查块层超时或复位日志:

DEV=nvme0n1   # 换成 lsblk 对应的实际设备名
cat "/sys/class/block/$DEV/inflight"
journalctl -k --since '-15 min' --no-pager \
  | grep -Ei 'timeout|reset|I/O error|blk_update_request|nvme'

inflight 要连续采样才有意义;如果它持续不归零、aqu-sz 上升而完成量下降,就比单看 await 更像请求卡在块层。内核日志需要相应权限,设备名也别直接套用示例。

补个采样选项的坑:iostat -xzy 1 10 里的 -y 会跳过启动以来的首个平均值,-z 会隐藏本轮没有 I/O 的设备。排查 dm-crypt/LVM 时,如果想逐层对照,建议在故障窗口另跑一次不带 -z 的采样,并保留设备映射;否则某层暂时空闲时,可能误以为它没参与路径。各层的 await 仍要分别解读,不能跨层相加。

再补一个“盘不忙但服务慢”的方向:cgroup v2 的 io.max 可能给单个服务设了固定上限。这时整机 utilawait 都可能不高,但服务仍在等 I/O。Linux 5.4+、systemd 245+ 可以只读核对:

CG=$(systemctl show app.service -p ControlGroup --value)
test -r "/sys/fs/cgroup${CG}/io.max" || echo 'io controller unavailable'
cat "/sys/fs/cgroup${CG}/io.max"
cat "/sys/fs/cgroup${CG}/io.stat"
cat "/sys/fs/cgroup${CG}/io.pressure"
lsblk -o NAME,MAJ:MIN

io.max 按设备主次编号列出限制;对应设备的 rbpswbpsriopswiops 不是 max,且固定间隔采样的实际速率贴近该值时,才比较像命中限流。io.pressure 升高只能说明组内任务在等待,不能单独证明是这个上限造成的。示例单元要替换,读取文件失败时也别直接当成“未限流”,还要确认目标服务的 cgroup 路径和 I/O 控制器是否可用。

Sam_NorthLv1#6

再补一个“盘不忙但服务慢”的方向:cgroup v2 的 io.max 可能给单个服务设了固定上限。这时整机 utilawait 都可能不高,但服务仍在等 I/O。Linux 5.4+、systemd 245+ 可以只读核对:

CG=$(systemctl show app.service -p ControlGroup --value)
test -r "/sys/fs/cgroup${CG}/io.max" || echo 'io controller unavailable'
cat "/sys/fs/cgroup${CG}/io.max"
cat "/sys/fs/cgroup${CG}/io.stat"
cat "/sys/fs/cgroup${CG}/io.pressure"
lsblk -o NAME,MAJ:MIN

io.max 按设备主次编号列出限制;对应设备的 rbpswbpsriopswiops 不是 max,且固定间隔采样的实际速率贴近该值时,才比较像命中限流。io.pressure 升高只能说明组内任务在等待,不能单独证明是这个上限造成的。示例单元要替换,读取文件失败时也别直接当成“未限流”,还要确认目标服务的 cgroup 路径和 I/O 控制器是否可用。

这里还要查祖先 cgroup。cgroup v2 的 I/O 限制按层级生效:服务自己的 io.max 没有条目,不代表一定未限流,system.slice 或更上层 slice 的限制仍可能约束它。Linux 5.4+ 可以只读逐级核对:

CG=$(systemctl show app.service -p ControlGroup --value)
p="/sys/fs/cgroup${CG}"
while [ "$p" != /sys/fs/cgroup ]; do
  printf '\n[%s]\n' "$p"
  cat "$p/io.max" 2>/dev/null || echo 'io.max unavailable'
  p=${p%/*}
done

同一设备在多层都有限制时,实际会受路径上更严格的那层约束;设备号仍要用 lsblk -o NAME,MAJ:MIN 对照。若某层文件不存在,还要确认父 cgroup 是否启用了 I/O 控制器,不能只凭叶子文件得出结论。示例单元需要替换。

阿雨天中Lv1#7

这里还要查祖先 cgroup。cgroup v2 的 I/O 限制按层级生效:服务自己的 io.max 没有条目,不代表一定未限流,system.slice 或更上层 slice 的限制仍可能约束它。Linux 5.4+ 可以只读逐级核对:

CG=$(systemctl show app.service -p ControlGroup --value)
p="/sys/fs/cgroup${CG}"
while [ "$p" != /sys/fs/cgroup ]; do
  printf '\n[%s]\n' "$p"
  cat "$p/io.max" 2>/dev/null || echo 'io.max unavailable'
  p=${p%/*}
done

同一设备在多层都有限制时,实际会受路径上更严格的那层约束;设备号仍要用 lsblk -o NAME,MAJ:MIN 对照。若某层文件不存在,还要确认父 cgroup 是否启用了 I/O 控制器,不能只凭叶子文件得出结论。示例单元需要替换。

再补个写路径的错位:应用 write() 返回快,不代表数据已经落盘;fsync/fdatasync 或数据库 WAL flush 卡住时,业务 p99 可能先抬,而 pidstat -d 里的写入量未必异常。sysstat 12.2+ 的 iostat -x 可以关注对应设备的 w_await,支持时再看 f_await,并把采样窗口和应用侧 fsync/WAL 等待时间对齐;f_await 只说明块层 flush 请求耗时,不能单独证明盘坏或缓存不安全。若怀疑写回压力,可同时连续记录 /proc/meminfo 里的 Dirty/Writeback/proc/vmstatnr_dirty/nr_writeback,不要拿单点值下结论。