iostat里%util 100% 意味着磁盘满了吗?IO慢到底慢在哪一步?
先说结论
%util 在 SSD/NVMe 时代已经不能单独看。它只反映'设备至少有一个 IO 在处理的时间占比',对并发能力强的 SSD/NVMe 完全失真。判断 IO 饱和要看 `await`(平均等待时间)、`avgqu-sz`(平均队列长度)、`aqu-sz/nr_requests` 比值。
IO 慢要分层定位:① 文件系统层(fsync、sync、write back);② 块设备层(队列、merge);③ 物理设备层(磁盘/SSD 本身)。不同层的慢用不同工具:iostat 看块设备、iotop/pidstat -d 看进程、blktrace 看详细 IO 轨迹、biosnoop(eBPF)看单个 IO。
大数据场景的 IO 瓶颈多在写:Kafka/HDFS/Flink state 大量 sequential write,瓶颈一般是带宽;RocksDB/MySQL 是 random write,瓶颈是 IOPS 和 write amplification。面试时能说出 sequential vs random 的区别和各自瓶颈,就不一般了。
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
iostat 的 %util 是被误解最严重的一个指标。新人一看到 %util 100% 就说 '磁盘饱和了',但这只在老的机械盘上成立。SSD 和 NVMe 的并发模型完全不一样,%util 100% 可能只用了 20% 的实际带宽。真正反映 IO 饱和的是 `await`、`avgqu-sz`、`r_await/w_await`,还有 NVMe 的并发队列深度。能把这些说清楚的候选人,面试官立刻知道你不只是会背命令。
业务场景:Flink RocksDB state backend 写入慢
某元宇宙社交应用的实时数仓有个大状态作业,用 RocksDB state backend。上线初期偶发 checkpoint 超时,看监控是 state size 增长,checkpoint 的 sync 阶段慢。ssh 到 TaskManager 机器看 IO:
# iostat 每秒采样一次,看扩展指标
iostat -x 1
# Device r/s w/s rkB/s wkB/s avgqu-sz await r_await w_await %util
# nvme0n1 45 3200 1800 850000 12.3 85.2 2.1 118.5 100.00
# 解读:
# - %util 100%:老的结论是饱和,但这是 NVMe,不一定
# - w/s 3200:每秒 3200 次写 IO
# - wkB/s 850000:每秒写 850MB(这个是真饱和 NVMe 带宽)
# - avgqu-sz 12.3:平均队列里有 12 个 IO 在排队
# - w_await 118ms:写平均等 118 毫秒,非常慢(NVMe 正常 <1ms)
# - r_await 2ms:读正常
# 结论:NVMe 写带宽打满了,写 IO 在排队
# 继续看是哪个进程在写
pidstat -d 1 | head -20
# UID PID kB_rd/s kB_wr/s kB_ccwr/s Command
# 1000 23451 0 845000 0 java ...TaskManager
定位清楚了:Flink TM 的 RocksDB 在写 Compaction(后台合并 SSTable),每秒 850MB 的写入,把本地 NVMe 带宽打满了。这台机器用的是某云厂商的中配 NVMe,标称 1GB/s 写带宽,实际 RocksDB compaction 叠加正常 state 写入已经到极限了。修复方案两路:① 换更高规格的 NVMe;② RocksDB 调 compaction 参数降低 write amplification(`level0_file_num_compaction_trigger` 调小、`max_background_jobs` 调大避免阻塞、`target_file_size_base` 调大减小 compaction 频率)。调参之后 checkpoint 稳了。
手段一:iostat -x 每个字段的准确含义
iostat 是 IO 分析的第一站,-x 扩展指标必须会看。
# iostat -x 1 # 每秒一次
# iostat -x -d 1 5 # 只看设备(不看 CPU),5 次
# Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
# 字段解读:
# r/s 每秒读次数(IOPS 读)
# w/s 每秒写次数(IOPS 写)
# rkB/s 每秒读字节数(KB)
# wkB/s 每秒写字节数(KB)
# rrqm/s 每秒合并读请求数(IO scheduler 合并后剩下的)
# wrqm/s 每秒合并写请求数
# %rrqm 读请求合并率
# %wrqm 写请求合并率
# r_await 读平均等待时间(ms)= 排队 + 处理
# w_await 写平均等待时间(ms)
# aqu-sz 平均请求队列长度(老版本叫 avgqu-sz)
# rareq-sz 读请求平均大小(KB)
# wareq-sz 写请求平均大小(KB)
# svctm 设备服务时间(已废弃,不要看)
# %util 设备繁忙时间占比
# 关键指标的正常范围(参考):
# HDD await: 5-20ms 正常,>50ms 慢
# SATA SSD await: <1ms 正常,>5ms 慢
# NVMe await: <0.5ms 正常,>2ms 慢
#
# 判断饱和:await 升高 + aqu-sz 持续 > 1
重点:`await` 才是最能反映 IO 是否慢的指标。await 包括 '排队时间 + 设备处理时间',如果 await 远高于设备正常值(比如 NVMe 上看到 100ms)就是慢了。%util 对现代 SSD/NVMe 不能单独判断,要结合 await 和 aqu-sz。
手段二:%util 在 SSD/NVMe 为什么失真
这是面试的加分点。很多人只知道 %util 100% 就是饱和,不知道其中的原理。
# %util 的定义:
# 过去一段时间内,设备处理 IO 的总时间 / 总时间
# 即 '设备至少有 1 个 IO 在处理' 的时间占比
# 问题:这个定义只在'单队列串行处理'的设备上有意义
# 老的 HDD、SATA SSD 都是单队列 + 1-32 队列深度
# 串行处理时,%util 100% 就代表满了
# NVMe 的现实:
# NVMe 支持 64K 队列 × 64K 深度
# 一次能并发处理几百几千个 IO
# 但 %util 100% 只要求 '至少有一个 IO 在处理'
# 实际可能只用了 10% 的并发能力
# 例子:
# NVMe 标称 500K IOPS
# 如果 iostat 显示 10K IOPS + %util 100% + await 0.3ms
# 这不是饱和,只是持续有 IO 但很快处理完
# 真正看 NVMe 饱和要看:
# 1. w/s + r/s 是否接近设备标称 IOPS
# 2. wkB/s + rkB/s 是否接近设备标称带宽
# 3. aqu-sz 是否持续大于 1-2
# 4. w_await 是否大于设备正常值
# 查 NVMe 硬件能力
nvme list
nvme id-ctrl /dev/nvme0 | grep -E 'sqes|cqes|nn|maxcmd'
手段三:iotop 和 pidstat -d 找 IO 大户
知道设备慢之后,下一步是找是哪个进程在 IO。iotop 和 pidstat -d 是两个常用工具。
# iotop 类似 top,实时看进程 IO
iotop -o # 只显示有 IO 的进程
iotop -oP # 只看进程(不看线程)
iotop -b -n 3 # 批处理模式,出 3 次采样
# 输出示例:
# Total DISK READ: 0 B/s Total DISK WRITE: 845 MB/s
# TID USER DISK READ DISK WRITE COMMAND
# 23451 flink 0 B/s 845 MB/s java ...TaskManager
# pidstat -d 更适合脚本化
pidstat -d 1 5 # 每秒 1 次,采 5 次
# kB_rd/s 每秒读
# kB_wr/s 每秒写
# kB_ccwr/s 每秒 cancelled write(write 被取消)
# iodelay IO delay 时间
# 按 IO 排序看前 10
pidstat -d 1 1 | awk 'NR>3 && $5+$6 > 0' | sort -k6 -rn | head
# /proc/<pid>/io 看进程总累计 IO
cat /proc/23451/io
# rchar 4567890 # 从用户态视角读
# wchar 8901234567 # 从用户态视角写
# read_bytes 1234567 # 从存储设备读(真实磁盘 IO)
# write_bytes 98765432 # 写磁盘
# cancelled_write_bytes 0
# read_bytes 是真实磁盘 IO,rchar 可能走了 page cache
# 两者差距 = page cache 命中
手段四:blktrace / biosnoop 追单个 IO
如果需要更细粒度(比如想知道某个 IO 慢在 '排队'还是'设备处理'),要用 blktrace 或者 eBPF 的 biosnoop。
# blktrace - 追踪块设备的每个 IO 事件
blktrace -d /dev/nvme0n1 -w 30 -o /tmp/trace
blkparse /tmp/trace.*
# 事件类型:
# Q queue IO 进入队列
# M merge 与其他 IO 合并
# I insert 插入请求队列
# D dispatch 下发到设备
# C complete 设备处理完成
# 关键:D 到 C 是设备处理时间,Q 到 D 是排队时间
# 如果 Q 到 D 长 = 软件层(队列、调度器)瓶颈
# 如果 D 到 C 长 = 硬件瓶颈
# btt 分析 blktrace 输出
btt -i /tmp/trace.blktrace.0
# eBPF biosnoop(更轻量,现代首选)
biosnoop
# TIME PID COMM DISK T SECTOR BYTES LAT(ms)
# 14:01:23.1 23451 java nvme0n1 W 1234 4096 0.45
# LAT 就是单个 IO 的总延迟
# 看 IO 延迟分布
biolatency 10 1
# usecs : count distribution
# 0 -> 1 : 0
# 2 -> 3 : 12
# ...
# 2048 -> 4095 : 4567 |**********
# 看文件系统级延迟
ext4slower 5 # ext4 操作慢于 5ms 的
xfsslower 5 # xfs
手段五:file system 层 vs 块设备层
很多 IO 慢问题并不是硬件慢,是文件系统同步机制慢。典型的是 fsync 慢、脏页回写慢、journal 阻塞。
# 查看脏页占用(pdflush 还没写回的)
cat /proc/meminfo | grep -i dirty
# Dirty: 1234 kB
# Writeback: 0 kB
# 脏页写回相关参数
sysctl -a 2>/dev/null | grep dirty
# vm.dirty_background_ratio 10 # 脏页达到 10% 时后台开始写
# vm.dirty_ratio 20 # 脏页达到 20% 时阻塞 write()
# vm.dirty_expire_centisecs 3000 # 30 秒后脏页必须写回
# vm.dirty_writeback_centisecs 500 # pdflush 每 5 秒检查一次
# Kafka/HDFS 这种大写入场景,如果 dirty_ratio 默认 20% 太大
# 会导致周期性阻塞 write() 调用(因为触发了写回)
# 调小这两个值,让写回更平滑
sysctl -w vm.dirty_background_ratio=5
sysctl -w vm.dirty_ratio=10
# ext4 的 journal 阻塞问题
# 可以挂载时加 data=writeback 减少 journal 开销(代价是掉电数据可能丢)
# 或者 barrier=0(只在有 UPS 的机器上)
# 看 fsync 延迟(需要 eBPF)
fsync_latency 5
IO 慢的分层定位
| 层 | 症状 | 工具 | 常见原因 |
|---|---|---|---|
| 应用 | 对某个文件的读写慢 | strace -T | 未用缓存、fsync 频繁 |
| VFS/FS | fsync 慢、write 阻塞 | ext4slower, fsync_latency | 脏页满、journal 阻塞 |
| 块设备层 | iostat await 高 | iostat -x, blktrace | 队列深度、调度器 |
| 物理设备 | 设备带宽打满 | iostat, 设备监控 | 硬件瓶颈 |
面试高频追问
追问一:SSD 的 '写放大' 是什么?对大数据系统有什么影响?
SSD 的物理特性:擦除是'块'粒度(一般 256KB 或 1MB),写是'页'粒度(一般 4KB),而且一个页必须先擦才能写。所以应用写 1KB 数据,SSD 内部可能要擦一整个块(256KB)再写回。比值就是写放大(Write Amplification Factor, WAF)。对大数据的影响:① RocksDB/LSM-Tree 结构本身就有 write amplification,加上 SSD 自带的 WAF,总放大能到 10-20 倍;② 频繁小写入会加速 SSD 磨损(P/E cycles 有限);③ Compaction 期间的 WAF 最高,是 state backend 延迟抖动的主要原因。优化方向:① 批量写入减少 compaction;② 调 RocksDB level0 文件数;③ 选 IOPS 和 endurance 更高的 SSD 型号(企业级 SATA SSD 或 NVMe 的 DWPD 指标)。我在某元宇宙社交应用调 RocksDB 参数时专门关注了 WAF 这一块。
追问二:iostat 看到 %util 100% 但 await 只有 0.5ms,是不是饱和了?
不一定。这是典型的'老指标看新硬件'误区。NVMe 支持高并发队列,%util 100% 只是说 '任意时刻都有至少一个 IO 在处理',不代表队列满或者带宽打满。判断是否饱和看两条:① await 是否远高于设备正常值(NVMe 正常 0.1-0.5ms,0.5ms 属于正常范围);② 带宽是否接近设备标称(跑 `nvme id-ctrl` 或 `fio` 基准测试看设备能到多少)。await 0.5ms + %util 100% 通常说明设备一直在忙但完全没瓶颈,可以继续加压。真正饱和的信号是 await 飙到设备正常值的 10 倍以上 + aqu-sz 持续增长。
追问三:Kafka 磁盘写慢,怎么排查?
Kafka 的写主要是 append 到 segment 文件(sequential write),理论上磁盘应该很快。如果慢,分几层查:① iostat 看 w_await 和 wkB/s,判断是不是物理设备瓶颈;② `iotop` 看是不是别的进程在抢 IO(比如同机的 log 聚合、备份任务);③ 看 Kafka 的 log.flush.interval 配置,如果配得过激(比如每 1000 条就 flush),fsync 频繁会让 sequential 变 random;④ 看 producer 的 acks=all 时,leader 等 follower 同步也会增加延迟,但这是网络不是磁盘;⑤ 如果是 Compaction topic,segment merge 会产生额外 IO。我在某元宇宙社交应用遇到过 Kafka 写慢,根因是同机部署了一个备份 rsync 任务每小时跑一次抢 IO,迁移之后问题消失。这题面试官喜欢问因为可以顺着往磁盘原理、Kafka 写机制、干扰进程三个方向挖。
实战踩坑:混部机器上的 IO 抢占
某短剧出海公司一台数据盘机器上同时部了 Kafka、Flink RocksDB、一个 MongoDB。业务早期 IO 都不大都相安无事,后来 Flink 作业状态增长,RocksDB compaction 开始抢 IO,Kafka 的 producer P99 延迟从 10ms 飙到 500ms。排查过程:
# 第一次看 iostat
iostat -x 1
# nvme0n1 w/s 8000 wkB/s 950000 await 15ms %util 100
# 写带宽接近打满了
# 看是哪些进程
pidstat -d 1 5
# UID PID kB_rd/s kB_wr/s Command
# 1000 12345 0 320000 java ...TaskManager <-- Flink
# 1000 23451 0 180000 java ...kafka
# 1000 34567 0 280000 mongod
# 三个进程分别吃 320/180/280 MB/s,合计接近 800MB/s
# RocksDB compaction 触发时峰值会翻倍,打满 1GB/s 带宽
# 临时方案:给不同服务上 cgroup v2 的 io.max
echo '259:0 wbps=200000000' > /sys/fs/cgroup/flink/io.max # 限制 Flink 写带宽 200MB/s
# 根本方案:把三个服务拆到不同机器
# Kafka 独占一批盘,Flink state 独占一批盘
# MongoDB 是历史遗留,迁走
教训:IO 密集型服务一定要独占磁盘。大数据机器最常见的坑就是'看着磁盘空间够就往上堆服务',然后 IO 带宽成瓶颈。企业级 NVMe 单盘 1-2 GB/s 带宽看着很充裕,但 3-4 个大作业一起来就爆。做容量规划要按 IO 带宽算,不能光看空间。我后来在所有涉及 state backend 的设计里都把 '磁盘 IO 带宽' 列为一级约束。
总结
iostat 的 %util 在 NVMe 时代不能单独看了,真正判断 IO 是否饱和要看 await 和 aqu-sz 组合。IO 慢分四层:应用、文件系统、块设备、物理设备,每层都有对应的工具。大数据系统的 IO 瓶颈多在 RocksDB compaction 的写放大、Kafka segment flush、HDFS 大块 sequential write,能把这些具体场景讲清楚的面试表现就非常立体了。
下一篇开始聊 Shell 和文本处理:Shell 脚本里的 $? $$ $! 到底是什么?调度脚本报错怎么优雅捕获?
你们线上 IO 瓶颈最典型的场景是什么?评论区聊聊~
关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长
Linux 面试通关手册
第11-15篇:Shell / 文本 / 权限 / Cron / SSH
Wood聊大数据
9年大数据老兵,聊技术、聊职场、聊成长
本册导读
第 1-5 篇讲的是'线上出问题先看哪里',第 6-10 篇讲的是'进程 / 网络 / IO 怎么深挖'。
到第 11-15 篇,要讲的是每个大数据人每天都在用、但几乎从来没有系统学过的那一层——Shell 脚本、文本处理、权限体系、定时任务、SSH 会话。这些东西看起来'不够高级',但如果你写过凌晨批跑脚本、给生产机做过权限整改、排过 crontab 为什么不跑、调过跨区 SSH 登录慢,你就知道——真正决定你在生产上靠不靠谱的,恰恰是这一层的细节。
这一册的每一题都对应一个我真实踩过的坑:set -euo pipefail 没写被 tee 吞错,grep 30G 日志把服务器打爆,实习生 chmod -R 777 /usr 毁了机器,crontab 因为 PATH 没 source 静默失败了一周,ssh 登录因为 UseDNS + GSSAPI 卡了 30 秒。每一个坑都对应着一套长期生效的防复发方案。
看完这 5 篇,你会发现——所谓'Linux 熟手',其实就是'踩过这些坑、并且写出了防复发规范'的那个人。
— Wood,2026 年春