凌晨两点被电话叫起来,说一台机器的业务写不进去了。登上去df -h一看,根分区还剩三十多个百分点,第一反应是对方看错了。把lsblk的完整设备树拉出来对照,才发现问题根本不在空间上——新加的一块逻辑卷压根没挂到业务目录,应用一直在往一个二十来 G 的临时卷里写。类似的事情我碰上过不止一次:真正把问题定下来的,往往不是df、top这些天天敲的命令,而是lsblk、iostat这类偏底层、平时容易被忽略的工具。这篇就把这三块内容串起来讲清楚:lsblk到底能看出什么块设备拓扑,CPU 使用情况该怎么读才不会被单一数字骗到,以及iostat的输出字段背后对应着磁盘的什么行为。不管你是刚接手 Linux 服务器的新手,还是已经在线上排查过几轮抖动的老手,下面这些内容应该都能直接拿去用。
1. 从块设备拓扑看起:lsblk 到底在告诉你什么
lsblk的名字来自 list block devices,它读的是/sys/block和/sys/class/block下内核暴露的信息,不依赖挂载表,所以哪怕某个设备根本没挂载、甚至文件系统已经损坏,它照样能列出来。这一点和df有本质区别:df是站在文件系统视角统计剩余空间的,没挂载的文件系统它压根看不见;而lsblk站在内核设备层,把整棵设备树以层级方式铺开。很多“空间去哪了”“这个盘到底是什么”的问题,靠的就是这个视角差。
你随手敲一个lsblk,默认输出大概是这么几列:NAME、MAJ:MIN、RM、SIZE、RO、TYPE、MOUNTPOINT。看起来简单,但每一列都有它的意义。NAME是内核里的设备名,sda这类传统命名和nvme0n1这类新命名会同时出现;MAJ:MIN是主次设备号,排查设备节点丢失、mknod重建时要用;RM为 1 表示可移动设备,插着 U 盘的时候一眼能看出来;RO为 1 说明设备只读,有时候磁盘故障被内核降级成只读,这一列就是最先给出信号的;TYPE区分disk、part、lvm、crypt、loop、rom,层级缩进关系就是真实的设备依赖关系;MOUNTPOINT为空,意味着这个设备或分区当前没有被挂载——这一条经常是排查的突破口。
1.1 TYPE 列里的层级关系,比想象中重要
实际生产里最常见的一棵树,是物理盘 → 分区 → LVM 逻辑卷 → 文件系统这种结构。比如sda是disk,下面挂sda1、sda2两个part,sda2又被做成 PV,上面再叠vg-root、vg-data这类lvm卷,最后才是挂载点。你在lsblk里看到的缩进不是排版好看,而是真实的父子关系。如果某天你发现业务目录对应的lvm卷不见了,但sda和sda2还在,那基本可以判断是 LVM 元数据或者激活状态出了问题,而不是磁盘物理损坏。
再往深一层,crypt类型是加密卷,loop是回环设备,rom是光驱。现在容器和镜像场景多了,loop设备出现得越来越频繁,snap包、某些镜像挂载、mount -o loop都会产生 loop 设备。有次一台机器df显示某个挂载点空间异常,lsblk一看是个loop0挂着一个几个 G 的镜像文件,问题一下就清楚了:镜像文件放在根分区上,挂载点又占了一份,等于同一块空间被算了两次。
1.2 -f、-o、-J 这三个参数覆盖了大部分场景
默认输出不带文件系统信息,想知道某个分区是什么格式、UUID 是多少、用了多少,加-f。输出里会多出FSTYPE、FSVER、LABEL、UUID、FSAVAIL、FSUSE%、MOUNTPOINTS几列。UUID在改/etc/fstab的时候是刚需,用设备名写挂载配置,磁盘顺序一变就挂不上,用 UUID 稳得多。FSUSE%则是把df的用量信息直接并到设备树里,省得两边对着看。
-o是自定义列,格式是逗号分隔的列名,比如lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT。我个人的习惯是把它和-p一起用,-p会输出/dev/sda1这种完整路径,避免在脚本里还要手动拼/dev/前缀。脚本化场景最值得记的是-J,输出 JSON;老一点的系统上可能只有-P,输出KEY="value"的键值对格式,用awk或者while read解析都方便。-J配合jq基本可以替代掉一大半手工解析。
-d这个参数也值得单独说一句。默认lsblk会把分区、逻辑卷全列出来,输出很长;加-d只显示磁盘本身,不显示从属设备,快速看清机器上到底有几块物理盘时很省事。另外-t能带出TRAN(传输类型,比如sata、nvme、usb)、VENDOR、MODEL、SERIAL,机房里有几十台机器、盘位又多的时候,靠这一列核对硬盘型号和序列号,比进带外管理界面快。
1.3 一个真实场景:磁盘满了,但 df 看不出来
回到开头那个案例。业务报写入失败,df -h显示根分区有空间,du -sh /*扫一遍也没找到异常大目录。这时候正确的动作是先lsblk看设备树:
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT输出里如果出现一个lvm类型的卷,SIZE显示 500G,但MOUNTPOINT那一列是空的,而业务配置里写的路径又恰好应该挂在这个卷上,那问题就基本确定了——卷建好了但没挂载,或者挂载配置写错了,应用实际写的是根分区下的同名空目录。这种情况df完全不会报警,因为从文件系统角度看,那个空目录确实还有空间,只是它不在你以为的地方。
还有一种变体是mount --bind或者容器挂载导致的“同一块空间被多处引用”。lsblk里同一个设备可能对应多个MOUNTPOINT,-f的FSUSE%也会重复显示。这时候不要急着删文件,先用findmnt把挂载关系理清楚,再决定从哪个路径下手。
1.4 lsblk 和 df、findmnt 的分工
这三者的关系我一般这么理解:lsblk回答“机器上有哪些块设备、它们怎么层层叠起来的”;findmnt回答“当前挂载树长什么样、哪个挂载点覆盖了哪个”;df回答“每个已挂载文件系统还剩多少”。排查设备相关问题时,顺序应该是lsblk→findmnt→df,先看设备,再看挂载,最后看用量。反过来先看df,很容易被“已挂载文件系统”这个前提限制住视野,漏掉没挂上的卷。
提示:
lsblk读的是内核信息,不需要 root 也能执行,但某些字段(比如部分设备的SERIAL)可能需要提权才能看到。脚本里如果要判断设备是否存在,用lsblk -n -o NAME拿纯名字列表比ls /dev/sd*可靠,后者会把分区和整个盘混在一起。
2. CPU 使用率不是一个数字:拆解那些容易看错的口径
很多人看 CPU 就盯top第一行那个百分比,看到 80% 就觉得 CPU 忙、看到 20% 就觉得 CPU 闲。这个判断在单核时代勉强成立,在多核、多线程、NUMA 架构普及之后基本不靠谱。CPU 使用情况的真实含义,要结合us、sy、wa、st这几个分项一起看,还要区分“整体平均”和“单核峰值”。
top第一行那串数字,含义分别是:us用户态占用,sy内核态占用,ni调整过优先级的用户进程占用,id空闲,wa等待 I/O 完成的时间,hi硬中断,si软中断,st被虚拟化层偷走的时间。这几个加起来接近 100%。真正需要关注的是比例关系:us高说明应用在算,可能是业务量上来了,也可能是代码里有死循环;sy高说明系统调用频繁,常见于大量小文件读写、频繁创建销毁进程、网络包处理密集的场景;wa高说明 CPU 在等磁盘,这时候 CPU 本身并不忙,瓶颈在存储;st高在云主机上很常见,说明宿主机把物理核分给别人了,你这边看到的“慢”不是自己造成的。
2.1 整体使用率会掩盖单核打满
一台 32 核的机器,某一个核被单线程应用跑满,整体使用率只有 3% 左右,top第一行看起来风平浪静,但那个线程的响应时间可能已经飙到几百毫秒。这种情况必须用mpstat -P ALL 1来看每个核的分布。输出里%usr一列如果有某个 CPU 长期接近 100%,而其他核都在个位数,那基本就是单线程瓶颈,加机器没用,要么改多线程模型,要么做多实例部署。
mpstat属于sysstat包,很多精简镜像里默认没装,得先补上。它的-P ALL是关键,不加这个参数默认只输出平均值,等于白看。采样间隔建议至少 1 秒,次数给 3 到 5 次,单次采样受瞬时波动影响太大。如果是排查间歇性卡顿,可以用sar -u 1 60连续采一分钟,事后回看哪几秒出现尖峰。
2.2 load average 高不等于 CPU 忙
uptime和top右上角那三个数字,是 1 分钟、5 分钟、15 分钟的平均负载。很多人把它直接等同于 CPU 使用率,这是个流传很广的误解。Linux 的 load average 统计的是处于运行态(R)和不可中断睡眠态(D)的进程数量,D 状态主要就是等 I/O。所以一块慢盘能把 load 拉到几十,而 CPU 其实闲得很。
判断方法很简单:load 高的时候看一眼wa和vmstat的b列。vmstat 1输出里r是等待运行的进程数,b是阻塞进程数,如果b明显大于 0 而r很小,说明进程都卡在 I/O 上;反过来r很大、b接近 0,那才是真的 CPU 排队。我见过最典型的一次误判,是一台机器 load 到 40,值班同事直接扩容 CPU,扩完一点没好转,最后iostat一跑发现是某块盘await到了几百毫秒,一批批处理任务全堵在 D 状态。
2.3 看 CPU 要带上下文
单独一个 CPU 百分比说明不了什么,必须结合业务形态。Web 服务us偏高是正常的,但sy突然从 5% 涨到 30%,往往意味着系统调用变多了,可能是连接数暴涨、也可能是某个日志组件在疯狂刷盘。数据库场景wa高要重点看,但也要区分是数据盘慢还是日志盘慢,iostat分设备看就能区分开。
再补一个常被忽略的点:%si软中断高。网络收包密集的机器上,ksoftirqd会吃掉不少 CPU,表现出来就是sy和si一起高。这时候单纯看应用进程的 CPU 排名是找不到元凶的,得用top按1展开每个核,或者直接看/proc/softirqs的增长情况。
注意:
top默认刷新间隔 3 秒,抓瞬时尖峰容易漏。排查抖动时先按d把间隔改成 1 秒,或者干脆用pidstat -u 1看进程级明细,比盯着top靠谱。
3. iostat 的参数体系与输出字段实战解读
iostat和mpstat同属sysstat包,它的数据来源是/proc/diskstats和/proc/stat,本质是把内核维护的累计计数器做差分。理解这一点非常关键,因为它直接决定了“第一次采样为什么不能信”这个经典问题。
3.1 第一次采样必须扔掉
内核里的 I/O 计数器是从系统启动开始一直累加的,iostat第一次输出的时候,没有“上一个时间点”可以做差,只能拿累计值除以开机时长,算出一个自开机以来的平均值。这个值对排查当前问题几乎没有意义——一台开机 200 天的机器,早上那次备份产生的 I/O 早就被平均得看不见了。
正确做法是加-y参数跳过第一次输出,或者手动忽略第一段。比如iostat -x 1 3会输出四段(第一段是平均值加三段采样),实际只看后三段。更稳妥的写法是iostat -x -y 1 3,直接跳过第一段,输出干干净净。这个细节看起来小,但每年都能看到有人拿着第一段数据下结论。
3.2 -x 扩展模式下的关键指标
不带-x的iostat只有tps、kB_read/s、kB_wrtn/s这些粗粒度数据,排查瓶颈基本不够用。加上-x之后会多出一批关键列,我按重要程度整理成下面这张表:
| 指标 | 含义 | 怎么看 |
|---|---|---|
r/s、w/s | 每秒读、写请求数(IOPS) | 和设备的额定 IOPS 对比,机械盘通常几百,SSD 可以到几万 |
rkB/s、wkB/s | 每秒读、写字节数(吞吐) | 和带宽上限对比,SATA SSD 大约 500MB/s,NVMe 可以到 GB 级 |
rareq-sz、wareq-sz | 平均每次请求的大小 | 小于 8KB 偏随机小 IO,大于 128KB 偏顺序大 IO |
aqu-sz | 平均请求队列长度 | 持续大于 1 说明有排队,机械盘上明显大于 1 就要注意 |
await | 平均每次 I/O 的等待时间(毫秒) | 包含排队加服务,机械盘 10ms 以内正常,SSD 应在 1ms 量级 |
r_await、w_await | 分别统计读写等待 | 写慢读快,往往是刷盘策略或写放大导致 |
%util | 设备忙碌时间占比 | 机械盘接近 100% 基本饱和,SSD 上参考价值有限 |
老版本sysstat还有个svctm列,表示平均服务时间,后来因为计算方式在不同设备上失真严重,已经被官方移除。如果你在旧系统上看到这一列,记住它不能直接和await相减来算排队时间,那个算法在并发场景下不成立。
3.3 await 高但 %util 低,到底发生了什么
这个组合是很多人困惑的地方。%util的定义是设备处于忙碌状态的时间比例,它统计的是“有没有请求在设备上”,而不是“设备能不能处理更多请求”。现代 SSD 和 RAID 控制器内部有很深的并行度,一块盘可以同时处理几十个请求,%util很容易被打到 100%,但这不代表它到了瓶颈;反过来,如果await很高而%util只有 20%,说明请求在队列里等的时间长,但设备本身并没有满负荷运转,问题可能出在上层——比如文件系统锁竞争、块层调度、或者虚拟化层的队列限制。
判断饱和的正确姿势是看aqu-sz和await的变化趋势。随着压力增加,await持续上升、aqu-sz持续大于 1,那才是真的排队。如果await上去了但aqu-sz一直贴着 0,多半不是磁盘本身的锅。我以前遇到过一次云盘await长期 20ms 以上,%util只有 15%,最后查出来是云厂商的存储网关在那段时间做了限流,跟实例内部配置毫无关系。
3.4 -z、-t、-p、-g 这几个参数的实际用途
-z隐藏没有活动的设备。一台机器挂了几十块盘、大部分是空闲的时候,不加-z输出能刷好几屏,加了之后只看有流量的,排查效率高很多。
-t在每行前面加时间戳。做长时间采样、需要和日志时间对齐时必加,否则事后根本对不上哪一段对应哪次告警。
-p可以指定设备或分区,比如iostat -x -p sda 1。注意-p后面跟分区时,不同版本行为略有差异,有的会同时输出整盘和分区的统计,看的时候要分清。
-g是分组统计,可以把多块盘归成一组看汇总。做 RAID 或者分布式存储时,单盘数据看着都不高,汇总起来才发现整体吞吐已经接近上限,这个视角挺有用。-N则用于显示 LVM 的逻辑卷名而不是dm-0这种设备名,可读性好不少。
4. 把三个工具串成一条排查链:一次磁盘 IO 抖动的定位过程
工具本身不难,难的是出问题的时候知道先敲哪个、后敲哪个。我拿前段时间处理的一次接口超时来说明整条链路,场景是一台应用服务器,白天偶发接口响应从 50ms 涨到 2s,持续几十秒又自己恢复。
4.1 第一步:先确认设备和挂载关系
登录后第一件事不是看 CPU,而是lsblk -f把设备树和挂载点对一遍。原因是这类偶发问题经常和存储层有关,先确认业务数据目录到底落在哪块盘上,后面所有iostat的观察才有目标。那次输出显示/data挂在一个独立的lvm卷上,底层是sdb,系统盘是sda,两块盘分开,这个信息很关键——如果数据和系统在同一块盘上,日志刷盘就可能干扰业务 IO,排查思路完全不一样。
同时看一眼mount参数,确认有没有开noatime、discard之类的选项。那次发现/data是默认挂载,每次读文件都会更新访问时间,虽然影响不大,但在高频小文件场景下也是一笔额外写入。
4.2 第二步:iostat 锁定哪块盘在抖
确认设备后,直接iostat -x -y 1 30 sdb连续采 30 秒,把输出重定向到文件,事后再分析。这一步的要点是“持续采样 + 落盘”,因为问题只持续几十秒,人盯着屏幕很容易错过。采完之后按await排序,找峰值那几秒。
那次的数据里,正常时sdb的w_await在 2ms 左右,aqu-sz接近 0;抖动期间w_await冲到 300ms 以上,aqu-sz涨到 8 左右,wkB/s反而没有明显上升。这个组合说明不是流量涨了,而是单次写的延迟变大了,请求在队列里堆积。同期%util维持在 40% 上下,进一步排除了磁盘吞吐打满的可能。
4.3 第三步:用 CPU 的 wa 和 load 交叉验证
同时段vmstat 1的输出里,wa从 1% 涨到了 25%,b列有几个进程,r列不高。这印证了前面的判断:CPU 没忙,进程都堵在 I/O 上。如果这时候只看top的整体 CPU 使用率,会看到 70% 以上的id,很容易得出“机器很闲”的错误结论。
load average的 1 分钟值在那几十秒里从 2 涨到了 15,如果只看这个数字,第一反应会是 CPU 不够。但结合wa和b列就清楚了,涨的是 D 状态进程。这个对比我在很多次排查里都用得上,几乎是区分“CPU 瓶颈”和“IO 瓶颈”最快的办法。
4.4 第四步:落到具体进程
设备层面确认是写延迟问题后,下一步是找谁在写。pidstat -d 1能按进程列出每秒读写字节数,iotop -o更直观一些,只显示有 IO 的进程。那次定位到一个日志采集组件,它在每分钟的整点会做一次批量刷盘,刷的时候单次写入量不大但请求很碎,正好撞上了存储层的某个限制。
顺带说一句,pidstat -d在某些内核上需要 root 权限才能看到完整数据,普通用户跑出来可能全是 0,别以为是没 IO。
5. 几个容易踩的坑和我的习惯用法
工具用久了,总会积累一些文档里不会写的经验。下面这几条都是实际踩过之后才记住的。
5.1 容器里看到的设备名和宿主机对不上
在容器里跑lsblk,看到的往往是宿主机透传进来的设备,名字可能是sdb也可能是dm-3,取决于运行时怎么配的。更麻烦的是iostat,容器里如果没有挂载/proc/diskstats,输出可能是空的或者只有宿主机的一部分设备。排查容器内 IO 问题时,稳妥的做法是回到宿主机上看,用docker inspect或者crictl inspect找到容器的挂载映射,再对着宿主机的设备名去查。
还有一种情况是容器用了overlay文件系统,lsblk里看不到业务目录对应的独立设备,因为写入都落在宿主机的某个目录下。这时候要靠df的挂载点反查,或者直接findmnt看 overlay 的底层目录在哪块盘上。
5.2 SSD 和 RAID 上 %util 会骗人
前面提过%util在 SSD 上的局限性,这里再强调一次。机械盘时代,%util到 100% 基本等于饱和,这个经验被写进了很多监控模板,导致现在还有人拿它做告警阈值。SSD 和 RAID 阵列内部并行度高,%util长期 90% 以上但await依然很低的情况很常见,这时候设备其实还有余力。反过来,某些云盘的%util计算方式和物理盘不同,参考价值更低。
我的做法是把告警阈值从%util换成await和aqu-sz的组合:await超过基线三倍、同时aqu-sz持续大于 2,才认为有问题。基线要按设备类型分别定,机械盘、SATA SSD、NVMe 的正常await差着一个数量级,用同一套阈值必然误报。
5.3 我日常用的几条命令组合
排查问题时我不太喜欢现敲长命令,容易漏参数,平时就把几条固定组合记在笔记里:
# 设备树加文件系统信息,看挂载关系 lsblk -o NAME,SIZE,TYPE,FSTYPE,FSUSE%,MOUNTPOINT # 完整设备信息,含传输类型和序列号 lsblk -d -o NAME,SIZE,TRAN,VENDOR,MODEL,SERIAL # 单核 CPU 分布,采样 5 次 mpstat -P ALL 1 5 # 进程级 CPU,按占用排序 pidstat -u 1 5 | sort -k8 -rn | head -20 # 磁盘扩展统计,跳过第一次采样,只看有活动的设备 iostat -x -y -z 1 10 # 指定设备持续采样并落盘 iostat -x -y -t 1 60 sdb > /tmp/iostat_sdb.log最后一行那个-t加时间戳的习惯,是从一次跨天排查里养成的。当时问题发生在凌晨,日志时间戳和监控时间对不上,事后分析多花了一个多小时。从那以后,凡是超过 10 秒的采样,我一律加-t并重定向到文件,宁可多占点磁盘,也别在关键时刻缺数据。
再分享一个小技巧:iostat的输出如果直接贴到聊天窗口,列会挤在一起看不清,用column -t过一遍会整齐很多。命令是iostat -x 1 1 | column -t,对宽屏终端尤其友好。