第 7 章给出 fd 到 inode 的结构,第 9 章给出块层路径;本章回答现场真正要紧的三个问题:盘满在哪、I/O 慢在哪、谁在读写。

学习目标

读完本章,你应当能够:

  1. 说出 ext4 三种日志模式(journal/ordered/writeback)各自保证什么、崩溃后会看到什么形态的坏数据,并在持久性与写吞吐之间取舍。
  2. 解释 xfs 的延迟分配为什么让大文件更连续、inode 为什么按需分配,以及 ext4 的 inode 数量为什么在 mkfs 那一刻定死。
  3. 在 df -h 有余量而 df -i 满时,用可直接执行的命令序列定位到堆积小文件的具体目录。
  4. 给出 du 与 df 不一致的三类根因与排查命令链,并说清稀疏文件属于哪一组"数字对不上"。
  5. 计算 RAID 0/1/5/6/10 的容量利用率与随机写惩罚,解释数据库为什么几乎只用 RAID 10。
  6. 说清报 IOPS 时必须同时给出哪几个口径,用 fio 复现可比的数字,并判断该先看 await 还是 %util。
  7. 按三层结构排查 too many open files,并在磁盘写满时按正确顺序处置而不是乱删。

前置知识检查

你需要知道在哪一节补齐
fd 表 → struct file → inode 三层结构、硬链接与软链接、page cache 与 O_DIRECT见第 7 章 7.1~7.3 节、7.6 节
块层 I/O 栈、bio 与请求、I/O 调度器、fsync 保证了什么见第 9 章 9.9 节
iostat 的 %util/await/aqu-sz 字段读音,以及 %util 为什么不可靠见第 11 章 11.3 节
page cache 回收、脏页回写与 dirty_ratio见第 12 章 12.2 节

13.1 ext4 与 xfs:三处分歧决定了选型

“xfs 更适合大文件"只是现象。两者的真正分歧只有三处:日志模式的设计、空间分配时机、inode 分配方式。

13.1.1 ext4 的三种日志模式:保证什么、放弃什么

ext4 的日志由 jbd2 实现,是环形日志。元数据(inode、目录项、块映射)永远先写日志再落最终位置——这是所有模式的共同点,区别只在数据怎么办。

模式数据是否进日志数据与元数据的先后崩溃后可能的状态写吞吐
journal(data=journal)是,数据与元数据都先进日志不涉及先后,两者都在日志里内容与元数据自洽,要么全新要么全旧最低:数据写两次,日志回收受 checkpoint 约束
ordered(默认)否,数据直接写最终位置数据块落盘先于引用它的元数据事务提交不会"新元数据指向旧块内容”;可能丢最后一次未提交事务折中:多一层屏障(barrier)开销
writeback否,写入顺序不受约束无约束可能出现新元数据 + 旧块内容最高:少一层顺序约束与屏障

writeback 的坏数据形态必须能描述清楚,这是分水岭:/etc/passwd 刚被缩短,尾部块被释放并分给新建的 secret.txt;若元数据事务先提交而数据块后写,此时掉电,重启后 secret.txt 的长度与块映射指向该块,内容却还是旧 /etc/passwd 的尾部——结构完整,fsck 也查不出,数据却是别的文件的残留。这就是 ordered 存在的唯一理由:它保证的不是数据进日志,而是顺序。

1
2
3
mount | grep ' / '                                  # 当前生效的模式在这里(如 data=ordered)
tune2fs -l /dev/sda1 | grep 'Default mount options' # 这是默认值,不是当前生效值
mount -o remount,data=writeback /                   # 在线切换,不需 umount

注意

面试官追问的下一层通常是"那 ordered 会不会丢数据"。会——它不保证已返回的 write 落盘(那是 fsync 的职责,见第 9 章 9.9.3 节),只保证"不会读到不该看到的内容"。把一致性与持久性分开说,是这道题的加分点。

13.1.2 xfs 的延迟分配、预分配与在线扩容

延迟分配(delayed allocation, delalloc)把"分配哪些块"推迟到回写时刻。收益来自信息量:write 时只知道这一页,回写时能看到整个脏区间,于是一次分配出巨大的连续 extent,元数据条目数、碎片率与后续顺序读效率同时改善。ext4 默认也开 delalloc(nodelalloc 可关),xfs 的差别在更激进的投机预分配(speculative preallocation):写入时预留 EOF 之外一段空间,避免多个写入者交错插进同一文件造成碎片。代价有两条:ENOSPC 会推迟到回写阶段才暴露,应用必须处理"之前写成功、之后报错";预分配会让 df 的已用空间暂时大于 du 数出来的数据量,配额统计尤其容易被它误导。

1
2
3
growpart /dev/sda 3          # 先扩底层设备(分区 / LVM LV / 云盘),再让文件系统吃满
xfs_growfs -d /data          # xfs 只增不减:缩容只能备份重建
resize2fs /dev/sda3          # ext4 扩容可在线;缩小必须先 umount 并 fsck

大文件、高并发场景常选 xfs 的结构原因不是延迟分配,而是分配组(allocation group, AG):空闲空间、inode、extent 都按 B+ 树组织并按 AG 分区,多个写入者落在不同 AG 上可并行修改各自的元数据;ext4 的块分配与组描述符在写者多、目录大时成为争用点。所以选型语境里的"高并发"指的是元数据操作的并行度,不是单个文件的大小。

13.1.3 inode 的两种分配哲学

维度ext4xfs
分配时机mkfs 时按 bytes-per-inode 一次性建好整张 inode 表,位置固定按需分配:以 64 个 inode 为一组(inode chunk)从空闲空间里取
inode 数量创建时确定,默认每 16 KiB 一个;mke2fs -i/-N 可改动态增长,上限受 imaxpct(默认 25%,即 inode 最多占文件系统空间的 25%)约束
inode 号32 位,是该文件系统 inode 表的索引64 位,跨 AG 分布,天然支持海量 inode
典型故障小文件多时 df -i 先满,而 df -h 还有大量空间极少因 inode 耗尽失败,更常见的是真没空间或触到 imaxpct
空间代价inode 表固定占空间:inode 数 × inode 大小(默认 256 B)无固定预留,但预分配与 AG 元数据也占空间

ext4 的数字必须会心算,mke2fs 默认 bytes-per-inode = 16384:

1
2
3
2 TiB 的 ext4(mkfs 默认参数)
  inode 总数 = 2 TiB / 16 KiB = 2⁴¹ B / 2¹⁴ B = 2²⁷ = 134,217,728 ≈ 1.34 亿
  inode 表占用 = 1.34 亿 × 256 B = 2³⁵ B = 32 GiB   ← 固定吃掉,与存了多少文件无关

警告

“ext4 的 inode 用完了加几个就行"是错的。ext4 不能在线增加 inode,唯一途径是给文件系统扩容后由 resize2fs 按比例补建;在既有文件系统上 mke2fs -i 4096 等于重新格式化。预判为小文件密集(缓存、邮件池、Git 仓库)时必须在建盘时调小 -i 或调大 -N,事后无法补救。

13.2 inode 耗尽:df -h 有空间而 df -i 满

2 TiB 的 ext4 上若文件平均只有 1 KB:inode 总量 1.34 亿,每个文件至少占一个 4 KiB 块,1.34 亿个文件需要约 512 GiB 数据——inode 先于空间耗尽,而此时 df -h 只显示约 27%。这就是"监控显示磁盘很空、写入却报 No space left on device“最常见的形态。

现象根因典型场景
df -i 的 IUse% 100%,df -h 的 Use% 不高ext4 inode 数量在建盘时固定邮件 spool、PHP session 文件、缩略图缓存、/tmp 临时文件、Git 松散对象
总数不多但增长极快且从不下降临时文件只创建不清理(inode 泄漏)中间结果文件、失败任务残留、无 TTL 的本地缓存
只有某个挂载点异常inode 表是 per 文件系统的只有 /var 满而 / 正常

定位序列固定为"先确认维度,再逐层聚合”:

1
2
3
4
5
6
7
df -h; df -i                          # 第 1 步:必须同时看,否则会朝完全错误的方向排查
df -i / /var /data                    # 第 2 步:把范围从整机缩到某个挂载点
# 第 3 步:按目录聚合文件数。关键在 -xdev,不加就会跨挂载点、把别的文件系统算进来
find /var -xdev -type f -printf '%h\n' | sort | uniq -c | sort -rn | head -20
# 按 %h(目录)而不是 %p(完整路径)聚合:完整路径会把一个目录下的百万文件散成百万行
du --inodes -d1 /var/spool 2>/dev/null | sort -n | tail   # 第 4 步:下钻,只看数量不看大小
find /var/spool/clientmqueue -xdev -type f -mtime +7 | head  # 第 5 步:删之前确认能删

提示

面试话术:把"inode 满了"讲成因果链——inode 数与字节数解耦 ⟹ 小文件用很少字节消耗一个 inode ⟹ ext4 的 inode 总量建盘时固定 ⟹ 空间还剩 70% 时写入失败。只说"inode 满了删小文件"拿不到分,面试官接着就会问"ext4 的 inode 数量能改吗”。

13.3 du 与 df 不一致:三类根因

两者的数据来源完全不同,这是所有不一致的总根源:df 读超级块里的全局计数器(statfs),统计已分配出去的块,不管这些块有没有名字;du 按名字遍历目录树,累加每个文件的 st_blocks。凡"有分配、无名字"或"有名字、看不见"的块,都只出现在 df 里。

根因机制现象特征定位手段
文件已删除但仍被持有目录项被 unlink,但 struct file 与 inode 仍被 fd 引用,i_nlink 为 0 而数据块不释放df 很大、du 很小,删了日志空间也不回落lsof -nP +L1;/proc/[pid]/fd 中带 (deleted) 的链接
挂载点覆盖另一个文件系统挂在一个有内容的目录上,原目录被遮住,du -x 不跨挂载点自然看不见某个目录"数据凭空消失",或整盘 du 与 df 差出一份findmnt -R <dir>;用 bind mount 读取被遮住的原目录
稀疏文件表观大小远大于实占块;被计错的是 ls -l 与 du 这一对,不是 du 与 dfls -l 显示 10 GiB、du 只有几十 MBdu -h f 与 du -h --apparent-size f 对比;stat f 看 Size 与 Blocks
ext4 保留块默认 5% 空间只留给 root,非 root 进程能用的比 root 看到的少 5%root 手动写没问题,业务进程写不进去tune2fs -l 看 Reserved block count;tune2fs -m 1 下调

小心

把稀疏文件说成"du 和 df 不一致的原因"是高频错误。稀疏文件造成的差异在 ls -l 表观大小与 du 实占块之间;df 统计的也是实占块,它与 du 在这件事上是一致的。答错会立刻被追问"那 du --apparent-size 报的是什么"。

完整命令链按"先量化差值、再定向、最后验证"推进:

1
2
3
4
5
6
7
8
df -h /data; du -xsh /data      # -x 不跨挂载点;du 明显小于 df 就先怀疑"有分配、无名字"
lsof -nP +L1 | sort -k7 -rn | head -20   # +L1 = 链接数小于 1;-n 避免 DNS 反解卡住
# lsof 不可用时的兜底:直接读内核暴露的 fd 符号链接
find /proc/[0-9]*/fd -lname '* (deleted)' -printf '%l\n' 2>/dev/null | sort | uniq -c | sort -rn | head
findmnt -o TARGET,SOURCE,FSTYPE -R /data  # 排除挂载点覆盖
# 想看被遮住的原目录里有什么,用 bind mount 换一个视角看同一个文件系统
mkdir -p /mnt/root && mount --bind / /mnt/root && du -sh /mnt/root/data; umount /mnt/root
tune2fs -l /dev/sda3 | grep -E 'Block count|Reserved block count'   # 最后才看保留块

处置已删除但仍被持有的文件有三种方式,代价不同:让持有进程重新打开(reload/kill -USR1,最安全);重启进程(最彻底,有中断);直接截断 : > /proc/<pid>/fd/<n>(立即生效,但只对普通文件有效,且若进程以固定偏移随机写而非追加写,截断会造成数据损坏——数据库数据文件绝不能这么干)。

13.4 挂载选项:每一次读都可能变成一次写

13.4.1 atime 的写放大链路

纯读变成写的链路是:read → file_accessed() 更新 inode->i_atime 并把 inode 标脏 → 回写时写入 inode,且 ordered 模式下这次元数据修改还要提交一次 jbd2 日志事务。放大倍率的准确口径是"每个 inode 每次回写一次"而非"每次读一次写",因为同一 inode 的多次读会在内存里合并;但对每次读的都是不同 inode 的负载——静态资源服务、备份扫描、编译、find 遍历——两者等价:读一个文件就付一次随机元数据写加一次日志提交。

选项语义收益代价/风险
atime(strictatime)每次读取都更新访问时间语义完整读密集型负载的持续无谓元数据写
relatime(2.6.30 起默认)仅当 atime 早于 mtime/ctime 或距上次更新超过 24 h 才更新消除约 99% 的更新,几乎无风险热文件每天仍有一次写
noatime完全不更新 atime彻底消除这条写放大依赖 atime 的清理工具与邮件客户端语义改变
nodiratime只对目录关闭 atime保留文件的 atime 语义与 noatime 同给时是冗余的:noatime 已涵盖目录
1
2
3
mount -o remount,noatime,nodiratime /data   # 在线改,不需 umount
# 第四列的最终形态:rw,noatime,nodiratime,errors=remount-ro,barrier=1
findmnt -o TARGET,OPTIONS /data             # 核对实际生效值,而不是 fstab 里写的值

13.4.2 sync 与 async、barrier 与 nobarrier

async(默认)让写只到 page cache 就返回,真实落盘由回写线程异步完成;-o sync 则让文件系统层面所有写同步完成,近似于给每个文件加 O_SYNC,吞吐直接退化为约 1 / 设备写延迟——对 L = 100 μs 的设备也只有约 10⁴ 次写/秒。需要单点同步语义时该用 per-file 的 O_SYNC/O_DSYNC 或 fsync,不要动整个挂载点,把整盘拖进同步写是典型的一处配置毁掉整个文件系统。

barrier(现代内核由块层 flush/FUA 实现)保证的是顺序:日志提交不能早于它描述的数据与元数据落介质。nobarrier 关掉这条保证后,设备易失性写缓存中的写可能乱序落盘,掉电后果不是"丢最后几笔",而是文件系统结构可能不一致——这正是它与"丢数据"的区别。只有设备自带掉电保护(BBU、超级电容 + NAND,或本身不声明易失写缓存)时关屏障才安全;设备不支持 flush 时内核会打印 Disabling barriers, not supported by the underlying device,那属于设备能力不足而非配置选择。

errors=remount-ro 是 ext4 默认行为:检测到不一致就把文件系统重新挂载为只读,此后应用写失败并返回 EROFS。生产中"服务突然全变只读、进程没死、CPU 也不高"的第一嫌疑就是它,dmesg 里能找到 ext4 报错与 remount 记录;集群共享存储上更激进的取值是 errors=panic,用立即崩溃换取不一致状态不再扩散。

重要

三个选项的取舍压成一句:noatime 换吞吐且几乎无风险;barrier 换一致性且只在设备有掉电保护时才该关;sync 几乎不该用,它的位置应该由 fsync/O_SYNC 按文件粒度替代。

13.5 存储层次与 RAID:写惩罚是怎么算出来的

13.5.1 五个级别的四个维度

设成员盘数为 N、单盘容量 C、单盘随机 IOPS 为 I:

级别容量利用率容错能力正常读性能小随机写惩罚典型定位
RAID 0N × C(100%)0 块;任一块坏全丢≈ N × I1临时数据、可重建的缓存
RAID 1N/2 × C(50%)最多 N-1 块(每镜像组留一块)≈ N × I2系统盘、小容量高可靠
RAID 5(N−1) × C1 块≈ N × I4读多写少、顺序为主的归档
RAID 6(N−2) × C2 块≈ N × I6大容量、重构时间长、要求双盘容错
RAID 10N/2 × C(50%)每组镜像各 1 块≈ N × I2数据库、虚拟化、随机写为主

13.5.2 写惩罚的逐步推导

小随机写只改一个数据块,而校验必须基于整条条带重算,于是控制器被迫先读后写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
RAID 5 修改一个 4 KiB 数据块 D0(同一条带内还有 D1、D2 与校验 P)

  P_new = P_old ⊕ D0_old ⊕ D0_new        ← 校验重算只需异或,CPU 不是瓶颈
  ① 读 D0_old  ② 读 P_old  ③ 写 D0_new  ④ 写 P_new
  为什么必须先读:校验按整条带算,不读旧值就无法只更新其中一个块
  ⟹ 4 次设备 I/O 换 1 次逻辑写,写惩罚 = 4
RAID 6:两个独立校验 P、Q(Reed-Solomon),读 D0_old/P_old/Q_old、写三者
  ⟹ 3 读 + 3 写,写惩罚 = 6
RAID 10:数据写两份镜像 ⟹ 2 次设备 I/O,写惩罚 = 2

整条带写(full-stripe write)是唯一例外:一次写覆盖条带内全部 N−1 个数据块时
无需读旧值,新校验可由新数据直接算出 ⟹ 写惩罚摊到每个数据块上接近 1。
这就是"RAID 5 顺序大块写不吃亏、只怕小随机写"的量化根因。

据此得到工程估算式 可用随机写 IOPS = N × I / 写惩罚。用 12 块 10K rpm SAS(I ≈ 150)算一遍:

1
2
3
4
5
RAID 5 : 12 × 150 / 4 = 450 IOPS   容量 11 × C
RAID 6 : 12 × 150 / 6 = 300 IOPS   容量 10 × C
RAID 10: 12 × 150 / 2 = 900 IOPS   容量  6 × C   ← 写 IOPS 最高,容量只有 RAID 5 的 55%
读不产生惩罚:数据块分布在所有成员盘上,正常读可让全部 N 块并行参与,
因此读吞吐近似随盘数线性增长(实际受条带大小与请求大小限制)。

13.5.3 JBOD 与 RAID 的取舍

JBOD(just a bunch of disks,直通模式)把每块盘独立交给上层,容量求和、无冗余。分布式存储(对象存储、HDFS、Ceph 一类)几乎一律选 JBOD:副本或纠删码已在软件层提供冗余,硬件层再做一次是双重浪费;RAID 组容量按最小盘计算,迫使盘规格统一;而且 RAID 控制器本身是单点,固件、缓存、电池任何一处出问题都是整组风险。RAID 的适用面反而是"单机内需要冗余、且对上层透明"的传统部署——数据库单机、老应用的裸机盘。

13.5.4 硬件 RAID 的写缓存与 BBU

带写回缓存(write-back cache)的阵列卡先写缓存就向主机确认,单次写延迟从"介质往返"降到"缓存写入"。前提是缓存内容掉电不丢,所以必须配电池备份单元(battery backup unit, BBU)或超级电容 + NAND 的闪存备份缓存。两个生产现象由此解释:电池失效时多数控制器自动切回写穿(write-through),性能骤降,这是"昨天还很快、今天突然慢"的一类原因;而在没有掉电保护的易失缓存上再配 nobarrier,是缓存丢失与乱序落盘的双重风险叠加。

13.5.5 为什么数据库几乎只用 RAID 10

四条理由按重要性排序。其一,写惩罚:随机写 IOPS 是 RAID 5 的 2 倍、RAID 6 的 3 倍,而 OLTP 的随机写占比极高,这笔差距直接决定能承载的 TPS。其二,重构代价与风险:RAID 5/6 重构要读整组所有盘的条带并重算校验,耗时与整组容量成正比,期间性能显著塌陷,且窗口内再坏一块就全丢;RAID 10 只需从镜像盘复制,耗时与单盘容量成正比,不影响其他盘组。其三,不可恢复读错误(unrecoverable read error, URE):大容量盘上重构整组要读取天文数字的扇区,期间遇到一个无法纠正的读错误就会让 RAID 5 重构失败、整组不可用——这是"TB 级盘时代 RAID 5 不再被推荐"的标准论证。其四,RAID 5 的写洞(write hole):掉电可能留下数据与校验不一致的条带,潜伏到下一次掉盘才暴露。唯一代价是容量利用率腰斩,这是可预算的成本,而前述四条都是不可控的风险。

13.6 IOPS 与吞吐量的口径

13.6.1 必须同时报的口径

口径作用缺失后的后果
顺序还是随机(--rw)机械盘上决定寻道次数,SSD 上决定 GC 与通道并行度“IOPS 十万"可能只是顺序读,随机读只有几千
块大小(--bs)与 IOPS 相乘才是带宽4 KiB 与 1 MiB 下同一设备是两个完全不同的上限
队列深度(--iodepth × --numjobs)在途请求数,直接决定可达 IOPS少了它,测出的是应用下限而不是设备上限
读写比例(--rwmixread)混合负载下读被写拖慢(同盘争用、写惩罚)纯读数字无法外推到 7:3 混合负载
是否绕过 page cache(--direct)不绕过就是测内存结果虚高一个数量级且与真实存储无关

量化关系来自 Little 定律的直接推论:Q = IOPS × L,即 IOPS = Q / L,而 吞吐量 = IOPS × 块大小。同一块盘,4 KiB 随机读 100 K IOPS 对应 400 MB/s;换成 1 MiB 顺序读,IOPS 可能只有 1 K 而带宽达到 1 GB/s。“能跑多少 IOPS"与"能跑多少 MB/s"是两个独立上限,不能互相换算。

13.6.2 fio 的关键参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 随机读 IOPS 口径。在途请求总数 = iodepth × numjobs = 256,报数字必须一起给出
fio --name=randread --ioengine=libaio --direct=1 --rw=randread \
    --bs=4k --iodepth=32 --numjobs=8 --size=4G --runtime=60 --time_based \
    --filename=/data/fio.test --group_reporting --output-format=json
# 顺序带宽口径:1 MiB、队列深度 8、单任务,与上面相差数个数量级
fio --name=seqread --ioengine=libaio --direct=1 --rw=read \
    --bs=1M --iodepth=8 --numjobs=1 --size=16G --runtime=60 --time_based \
    --filename=/data/fio.test --group_reporting
# 持续写要够长:SLC 缓存几十秒就写满,--runtime=600 起步才能看到"掉速后"的性能
fio --name=sustained --ioengine=libaio --direct=1 --rw=randwrite \
    --bs=4k --iodepth=32 --numjobs=4 --size=8G --runtime=600 --time_based \
    --filename=/data/fio.test --group_reporting

警告

把 --filename 指向裸设备(如 /dev/sdb)会覆盖该设备数据且不可恢复。测盘请用独立分区或专用测试机;也不要在承载业务的分区上做写入测试,它产生的 I/O 会污染被测对象的真实延迟。

13.6.3 iodepth 与 %util 的关系

%util 来自 /proc/diskstats 的 io_ticks,定义是"该设备至少有一个在途请求的时间占比”,它回答忙不忙,不回答还能不能更快。两者的关系由队列深度决定:

  • 队列深度 1 的机械盘:一次只有一个在途请求,%util 到 100% 基本等于打满,此时 await ≈ 单次 I/O 时间。
  • 队列深度 32 以上的 NVMe:控制器内部可并行处理上百个请求,%util 停在 100% 时设备可能仍有余量;真正的饱和信号是 aqu-sz(平均在途请求数)持续升到接近设备的最佳队列深度,并伴随 await 明显上升。

因此判断顺序是先看 await 与 aqu-sz,再用 %util 做旁证(%util 为什么不可靠见第 11 章 11.3 节)。await 高且 aqu-sz 高说明设备过载;await 高但 aqu-sz 低说明单次 I/O 本身就慢,与排队无关。

13.6.4 SSD 的写放大、GC 与 TRIM

NAND 只能按擦除块(erase block,通常数 MB)擦除、按页(通常 416 KiB)写入,覆盖写必须先擦后写,而擦除单位远大于写单位,于是控制器必须做垃圾回收(garbage collection, GC):把某擦除块里仍有效的页搬到新块再整块擦除,搬移量就是写放大(write amplification):WA = 实际写入 NAND 的数据量 / 主机写入量。TRIM 的作用正在这条链的第一环:文件系统删除文件时不通知设备,设备就不知道那些逻辑块已失效,GC 会把它们当有效数据继续搬移,WA 随盘内碎片化上升,持续写性能与寿命同时下降。落地方式有两种:挂载 -o discard(每次删除立刻发 TRIM,给删除操作增加一次设备往返)或周期性 fstrim(推荐)。第二种衰减机制是 SLC 缓存:消费级 SSD 把一部分 TLC/QLC 当 SLC 用吸收突发写,缓存写满后速度落到原生水平,可能差 510 倍——这就是持续写测试必须跑够时长的原因。

1
2
fstrim -av                                   # 定期执行并观察各挂载点回收量
smartctl -a /dev/nvme0 | grep -Ei 'Percentage Used|Data Units Written|Available Spare'

13.7 谁在读写:从聚合指标到单次 I/O

工具粒度回答什么问题边界与代价
/proc/diskstats每设备累计计数原始数据源,一切聚合指标的起点需要自己算差值
iostat -x 1每设备每秒设备整体忙不忙、延迟多少、队列多长完全不知道是哪个进程
iotop -oP每进程每秒(taskstats 差值)哪个进程在读写、读写量多少采样窗口粗,看不到单次 I/O 延迟
biosnoop / biotop(eBPF)每次 I/O / 每进程谁、读写多少扇区、这一次延迟多少需要 eBPF 支持;高频 I/O 下自身开销明显
blktrace + blkparse/btt每个 I/O 的完整事件序列队列等待(Q→D)与设备处理(D→C)各占多少,可出延迟分布数据量巨大,必须限定设备与过滤条件
1
2
3
biosnoop -d nvme0n1     # 一句话看"谁在读写、延迟多少":pid、进程名、扇区、字节、延迟一应俱全
biosnoop -Q             # 延迟口径改为"从进入块层排队算起",含排队等待
blktrace -d /dev/nvme0n1 -o trace -w 10 && btt -i trace -o trace.btt   # 需要延迟分布时

日常顺序是”iostat 定位设备 → iotop 定位进程 → biosnoop 确认单次 I/O 与延迟 → 需要 P99 分布才上 blktrace",判断流程可固化成下图:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
现象:应用变慢,iostat 显示某块盘 await 升高
  ├─① 先看 await 与 aqu-sz(不是 %util)
  │    await 高 + aqu-sz 高  → 设备真的过载 ─────────────────▶ ②
  │    await 高 + aqu-sz 低  → 单次 I/O 本身就慢(掉速、介质错误
  │                            重试、被限流、后端抖动)───────▶ ③
  │    await 低 + %util 100% → 浅队列设备正常打满,不构成瓶颈证据 → 停止
  ├─② 找"谁":iotop -oP 定位进程 → biosnoop -Q 看单次 I/O 与延迟;
  │    再分是"量"(并发请求数超设备能力 → 限流/降并发/加盘)还是
  │    "质"(SSD 磨损与持续写掉速、RAID 降级或重构、写缓存退回
  │           write-through、网络存储抖动)
  └─③ 需要 P99/P999 时上 blktrace + btt;不要在秒均值上做长尾结论

13.8 文件描述符与句柄:三层排查

too many open files 必须按三层定位:

层次观测点触顶后的错误判据
进程级 RLIMIT_NOFILEulimit -n(软)、ulimit -Hn(硬)、systemd LimitNOFILE、容器 --ulimitEMFILE该进程 fd 数贴着软限制、稳态波动
系统级上限/proc/sys/fs/file-max、fs.nr_open(单进程硬上限)、/proc/sys/fs/file-nrENFILE全系统已分配 fd 接近 file-max
泄漏周期性采集 `ls /proc/[pid]/fdwc -l`先是 EMFILE,之后连带 ENFILE

区分"配置不足"与"泄漏"的唯一可靠判据是时间维度:配置不足在稳态水平上下波动、随时可复现;泄漏只涨不跌,或在某个操作后阶梯式上升。前者调大上限(注意 systemd 与容器运行时的配置会覆盖 shell 设置),后者必须修代码——调大上限只会让它以更难看的方式失败,fd 表占用大量内核内存,最终 OOM 或全系统 ENFILE。

1
2
3
4
5
6
7
8
lsof -p 12345 -n | awk '{print $5}' | sort | uniq -c | sort -rn   # 按 fd 类型看构成
lsof -p 12345 -n +L1        # 该进程持有的已删除文件
lsof -i :8080; lsof -i @10.0.0.5 -nP    # 谁占着端口、谁连着某台对端
lsof +d /data/logs          # 只看这一层目录(快)
lsof +D /data/logs          # 递归整棵子树(大目录上极慢,慎用)
fuser -v /data/app.log      # 谁占用了这个文件
fuser -vm /data             # 该挂载点上所有使用者,umount 报 busy 时必用
fuser -n tcp 8080           # 不加 -v 时输出的是 pid,容易被看错

Java 服务的 fd 泄漏按指向物分类:指向 .jar 查类加载器与未关闭的 JarFile;指向 socket 查客户端连接池与未关闭的响应体(见第 18 章 18.2 节);指向管道查未回收的子进程;表现为 anon_inode 的则是 epoll 实例、eventfd、timerfd、inotify 这类对象泄漏(fd 三层结构见第 7 章 7.1 节)。

注意

一个看起来像 fd 问题、其实不是的情况:inotify_add_watch 失败返回 ENOSPC,原因是 fs.inotify.max_user_watches 或 max_user_instances 触顶,与文件描述符无关。被追问"还有哪些资源上限会报 No space left on device"时,这是最容易被忽略的一个。

13.9 磁盘满的紧急处置顺序

顺序本身就是考点。先不要猜是"大文件"还是"已删除文件",用两条命令在几秒内定向:df -h 与 df -i 判断是块满还是 inode 满;du -xsh 与 df 对比判断空间是"有名字的"还是"无名字的"。这一步决定后面所有工作的方向。

1
2
3
4
5
6
7
8
df -h /data; df -i /data     # ① 判断维度:块 vs inode,两者必须同时看
du -xsh /data                # ② 判断有无名字:接近 df 去找大文件,远小于 df 去找已删除文件
# ③ 情况 A(du ≈ df):逐层下钻,先删可再生的
du -x -d1 /data | sort -h | tail -10
du -x -d1 /data/logs | sort -h | tail -10    # 命中目录后继续下钻,直到定位到具体文件
# ④ 情况 B(du << df):找持有者,让进程重开或重启后再删
lsof -nP +L1 | sort -k7 -rn | head -20
df -h /data                  # ⑤ 释放后必须回验,否则等于没做

处置的取舍原则:先删可再生的(日志、缓存、临时文件、core 转储、包管理器缓存),再考虑压缩或迁移;数据库数据文件、/etc 下的配置、正在写的 WAL 绝不能擅自 truncate;不要在满盘上跑无过滤的 find / 全盘遍历,它会加剧元数据与 I/O 压力,让本来还能响应的服务雪崩。

警告

rm 一个被进程持有的日志文件不会释放空间:目录项消失了,inode 与数据块仍被 fd 引用,而且日志被删后连排查线索也没了——这正是"删了日志盘还是满的"的原因。正确顺序是先让进程重新打开文件(reload、kill -USR1、重启任一),再删除旧文件,最后用 df 回验。

13.9.1 日志轮转失败导致的写满

logrotate 默认用 create 模式:把 app.log 改名为 app.log.1,再创建新的空 app.log,然后靠 postrotate 里的信号让应用重新打开日志。如果应用没有实现 reopen(或信号发错、进程名匹配不到),它会继续往已经改名的 app.log.1 里写——空间永不释放,而新文件一直是 0 字节,这是最典型的"轮转配置看起来正确、盘却一直满"的形态。

1
2
3
ls -l /var/log/app/app.log*     # 症状:.1 持续增长,而当前日志一直是 0
lsof -nP /var/log/app/app.log.1 # 确认是谁还在写旧 inode
grep -A5 -E 'postrotate|copytruncate|create' /etc/logrotate.d/app  # 检查信号与进程匹配

两条修法各有代价:让应用重新打开日志(首选,无丢失窗口,需要应用支持);改用 copytruncate(应用无需配合,但复制与截断之间的写入会丢失,且对大流量文件会额外产生一次全量拷贝)。另外三类常见来源也要能一眼认出:systemd-journald(journalctl --disk-usage 查看,SystemMaxUse 限制,journalctl --vacuum-size=1G 应急);容器日志(Docker 默认 json-file 驱动不限制大小,需在 daemon 配置里设 max-size/max-file,存量容器要重建才生效);内核 core dump 与崩溃转储目录。

本章小结

关键结论一句话表述常见错误
ext4 日志模式ordered 保证的是"数据先于元数据提交"这个顺序,不是数据进日志把 ordered 说成"会丢数据",或说不出 writeback 的坏数据是"新元数据 + 旧块内容"
xfs 选型理由真正理由是分配组带来的元数据并行度,延迟分配只解释大文件连续性只答"xfs 适合大文件"
在线扩容xfs 只增不减;ext4 可在线扩、离线缩以为 xfs 能缩小
ext4 inode建盘时按每 16 KiB 一个固定分配,事后不能增加答"inode 用完了加几个就行"
inode 耗尽小文件场景 inode 先耗尽,此时空间还剩大半只查 df -h 就下"磁盘没满"的结论
du vs dfdf 统计已分配块(含无名),du 按名字累加把稀疏文件列为 du/df 不一致的原因;漏掉挂载点覆盖与保留块
noatime读一次文件就让 inode 变脏,回写时是元数据写加日志提交认为 atime 只影响"文件访问记录"这类无关紧要的语义
barrier保证日志提交晚于数据落盘;关掉后掉电可能损坏结构而非仅丢数据把 nobarrier 当成"只是快一点的开关"
errors=remount-ro检测到不一致即挂为只读,应用写失败报 EROFS服务变只读时先怀疑应用而不是 dmesg
RAID 写惩罚RAID 5 = 4、RAID 6 = 6、RAID 10 = 2;整条带写接近 1只背数字,说不出 2 读 2 写的来源
RAID 10 的取舍用一半容量换 2~3 倍写 IOPS 与极短重构窗口只答"RAID 10 快"
IOPS 口径必须同时报顺序/随机、块大小、队列深度、读写比例、是否 direct报 IOPS 不提 iodepth,或用 IOPS = 1/延迟 忘了乘队列深度
%util定义是"至少一个在途请求的时间占比",不是饱和度看到 100% 就宣布磁盘到瓶颈
SSD 写放大GC 搬移有效页产生写放大;TRIM 让设备知道哪些块已失效以为 TRIM 能加快删除操作
fd 三层进程级 RLIMIT_NOFILE、系统级 file-max、泄漏;用趋势区分一上来就调大 ulimit -n
磁盘满定序先 df -h+df -i 定维度,再 du 对比 df 定有无名字上来就 du -sh /* 或 find / 全盘扫

自测题

Q1(计算)一个 2 TiB 的 ext4 按 mkfs 默认参数(每 16 KiB 一个 inode,inode 大小 256 B)创建,inode 总数与 inode 表占用空间分别是多少?若其上存了 800 万个平均 1 KB 的小文件,df -h 与 df -i 各显示什么?要多少个这种文件才会把 inode 用尽,届时空间用了多少?

答案与解析

inode 总数 2 TiB / 16 KiB = 2⁴¹ B / 2¹⁴ B = 2²⁷ = 134,217,728,约 1.34 亿个;inode 表占用 2²⁷ × 256 B = 2³⁵ B = 32 GiB,这是与存了多少文件无关的固定成本。存 800 万个 1 KB 文件时,df -i 显示 IUse% = 8×10⁶ / 1.342×10⁸ ≈ 6%;df -h 必须按块算——每个文件至少占一个 4 KiB 块,800 万 × 4 KiB ≈ 32 GiB,加上 inode 表 32 GiB 与其他元数据约 64 GiB 以上,占 2 TiB 的约 3%。两个维度都很宽松。把 inode 用尽需要 1.34 亿个文件,按每文件至少一个 4 KiB 块计约 512 GiB 数据,加 inode 表 32 GiB,此时 df -h 只显示约 27%——这就是"df -h 有空间但 df -i 满"的定量形态。

错在哪:①把 df -h 的占用按文件字节数(1 KB × 800 万 = 8 GB)算,忘了文件在块设备上按块分配;②答"inode 用尽了扩容就行",而 ext4 必须靠文件系统扩容 + resize2fs 补建。面试官会追问:“那你怎么在建盘时预判?“答按文件数与平均大小估算 bytes-per-inode,用 mke2fs -i 或 -N 在建盘时定死。

Q2(辨析)df -h 显示还有空闲空间,应用写日志却报 No space left on device。请给出至少三个不同根因,并说明各自的排除命令。

答案与解析

三个根因:inode 耗尽(空间有余量而 inode 维度已满,df -i 的 IUse% 到 100%,排除命令 df -i);ext4 保留块(默认 5% 只留给 root,以 root 运行 df 时 Avail 包含这部分,所以 root 看到的可用空间多于业务进程能用的,排除命令 tune2fs -l <dev> | grep 'Reserved block count',需要时 tune2fs -m 1 下调);配额触顶(user/group/project quota 满了同样报 ENOSPC,部分路径报 EDQUOT,排除命令 repquota -a 或 quota -s)。加分项:xfs 的延迟分配与投机预分配会让 ENOSPC 推迟到回写阶段暴露,应用会看到"之前写成功、之后报错”;inotify 的 watch 上限触顶也报 ENOSPC,但那是内核对象限制、与磁盘无关;若文件系统因 errors=remount-ro 被挂成只读,报的是 EROFS——把这两个错误码分开,说明真的理解来源。

错在哪:只答"删点文件"或只答"inode 满了”。面试官会追问:“如果是保留块,为什么业务进程报错而 root 手动写没问题?"——因为 df 对不同用户报告的 Avail 不同,答不出这一点就暴露了只是背结论。

Q3(计算)12 块 10K rpm SAS 盘,单盘随机 IOPS 约 150,单盘容量 1.8 TB。分别计算 RAID 5、RAID 6、RAID 10 的可用随机写 IOPS 与可用容量,并说明一个 OLTP 数据库该选哪个、代价是什么。

答案与解析

按 可用随机写 IOPS = N × I / 写惩罚、容量 = 有效数据盘数 × 单盘容量:RAID 5 得 12 × 150 / 4 = 450 IOPS、容量 11 × 1.8 = 19.8 TB;RAID 6 得 12 × 150 / 6 = 300 IOPS、容量 10 × 1.8 = 18 TB;RAID 10 得 12 × 150 / 2 = 900 IOPS、容量 6 × 1.8 = 10.8 TB。OLTP 数据库选 RAID 10:随机写 IOPS 是 RAID 5 的 2 倍、RAID 6 的 3 倍;重构只需从镜像盘复制,耗时与单盘容量成正比且不影响其他盘组,而 RAID 5/6 重构要读整组条带重算校验,期间性能塌陷且存在第二次故障与 URE 导致整组不可用的风险。代价是容量利用率只有 50%——10.8 TB 相比 RAID 5 的 19.8 TB 少了 45%,这笔成本可预算,而重构失败的风险不可控。

错在哪:①写惩罚记成 3 或 5;②只答"RAID 10 快"而说不出重构窗口与 URE 风险,面试官下一步必定问"RAID 5 重构为什么危险”;③忽略"整条带写"这个例外,于是无法回答"RAID 5 是不是完全没有价值"——顺序大块写时它接近无惩罚,且容量利用率更高,适合归档与顺序日志。

Q4(设计)一台机器 df -h /data 显示已用 900 GB,而 du -xsh /data 只有 30 GB。给出完整排查步骤、每一步的预期输出形态,并指出哪些"常见原因"在这个场景里可以被排除。

答案与解析

第一步 lsof -nP +L1 | sort -k7 -rn | head,预期看到若干行 NLINK 为 0、SIZE/OFF 很大的记录,NAME 列形如 /var/log/app.log (deleted),/proc/[pid]/fd 中是带 (deleted) 的符号链接。命中即根因是文件已删除但仍被进程持有:870 GB 的块有分配、无名字,du 按名字遍历自然看不见;处置是让持有进程重开或重启,或对普通文件执行 : > /proc/<pid>/fd/<n>。第二步若 lsof 无发现,检查挂载点覆盖:findmnt -o TARGET,SOURCE,FSTYPE -R /data,预期看到 /data/xxx 上挂着另一个文件系统;再用 mount --bind / /mnt/root 从根文件系统视角看被遮住的原目录,du -sh /mnt/root/data 就能把差额数出来。第三步两者都排除后看保留块与稀疏文件:保留块最多只占文件系统的 5%,差的 870 GB 远超这个量级,可直接排除;稀疏文件造成的是 ls -l 表观大小与 du 实占块的差异,df 与 du 在稀疏文件上一致,因此也不是本题原因。

错在哪:①一上手 du -sh /* 逐层找大文件——方向就错了,空间根本不在任何有名字的文件里;②把稀疏文件当答案,被追问"那 df 统计的是什么"就崩;③不知道 lsof +L1 与 /proc/[pid]/fd,只能靠重启服务碰运气。面试官会追问:“为什么 rm 之后空间没释放?“答案是目录项与 inode/数据块的分离:unlink 只删名字,fd 仍持有引用时 i_nlink 为 0 但块不回收。

Q5(推导)为什么 noatime 值得开?请从"一次读如何变成一次写"的链路推导,说明 relatime 与 noatime 的差别,并解释为什么 nodiratime 与 noatime 同时给出时是冗余的。

答案与解析

链路是 read → file_accessed() 更新 inode->i_atime 并把 inode 标脏 → 回写时写入 inode,且 ext4 默认 ordered 模式下这次元数据修改还要提交一次 jbd2 日志事务,于是一次纯读引出一笔随机元数据写加一次日志提交。注意放大倍率的准确口径:同一 inode 的多次读会在内存里合并成一次回写,因此是"每个 inode 每次回写一次"而非"每次读一次写”,但对每次读的都是不同 inode 的负载(静态资源、备份扫描、编译、目录遍历)两者等价。relatime(2.6.30 起默认)只在 atime 早于 mtime/ctime 或距上次更新超过 24 小时时更新,消除了约 99% 的更新并保留"这文件最近被读过吗"的粗粒度语义;noatime 完全不更新,收益彻底但有语义代价。nodiratime 只对目录关闭 atime,而 noatime 已涵盖包括目录在内的所有 inode,所以同给时不产生额外效果,它只在"想保留文件 atime、又想省掉目录那部分写"的折中场景下单独使用才有意义。

错在哪:答"atime 只是记录访问时间,开不开都行"等于放弃了整条因果链;或答"每次读都要写盘”,被追问"同一文件读一万次也写一万次吗"时无法解释内核的合并行为。面试官会追问:“那 relatime 为什么是默认值而不是 noatime?“答案是兼容性:保留弱 atime 语义的代价只是热文件每天一次写,收益是避免破坏依赖 atime 的既有工具。

面试高频问题与陷阱

面试问法考查角度陷阱 / 加分点对应本章小节
ext4 的三种日志模式有什么区别?生产上一般用哪种,为什么?文件系统一致性模型陷阱:只背"journal 最安全、writeback 最快”,说不出 writeback 的坏数据形态是"新元数据 + 旧块内容”。加分:说清 ordered 保证的是顺序而非数据进日志,并区分一致性(ordered 保证)与持久性(fsync 保证)13.1.1
df 和 du 的结果对不上,可能是什么原因?空间计量的数据来源陷阱:把稀疏文件当成 du/df 不一致的原因(它造成的是 ls -l 与 du 的差异),也想不到挂载点覆盖。加分:从 statfs 全局计数与按名字遍历的差异统一解释三类根因,并补上保留块13.3
已经 rm 掉的日志文件为什么空间没释放?怎么找到它?目录项与 inode 的分离陷阱:答"没删干净"“要 sync"“文件系统缓存”。加分:说清 unlink 只删目录项、fd 持有引用使 i_nlink 为 0 而块不回收,并给出 lsof -nP +L1 与 /proc/[pid]/fd 两条路径13.3
RAID 5 的随机写惩罚为什么是 4?RAID 6 为什么是 6?校验型 RAID 的写路径陷阱:只背数字说不出"读旧数据 + 读旧校验 + 写新数据 + 写新校验”。加分:指出整条带写时无需读旧值、惩罚接近 1,因此 RAID 5 怕小随机写而不怕顺序大块写13.5.2
数据库为什么一般推荐 RAID 10 而不是 RAID 5?冗余方案的工程权衡陷阱:只答"RAID 10 快"。加分:给出写 IOPS 差 2 倍、重构窗口与性能塌陷、重构期 URE 导致整组不可用、RAID 5 写洞,并承认代价是容量腰斩13.5.5
为什么测硬盘 IOPS 必须同时说明队列深度和块大小?性能口径陷阱:答"因为影响性能"。加分:给出 IOPS = Q / L 与 吞吐量 = IOPS × 块大小,指出 4 KiB 与 1 MiB 下 IOPS 与 MB/s 是两套独立上限,并说明不 direct 就是测内存13.6.1
iostat 里 %util 到 100% 是不是说明磁盘到瓶颈了?指标语义陷阱:答"是"。加分:指出 %util 的定义是"至少有一个在途请求的时间占比",深队列设备上它饱和不代表余量耗尽,应看 aqu-sz 与 await 的组合13.6.3
服务报 too many open files,你怎么排查?资源限制的层次陷阱:一上来就答"调大 ulimit -n",把泄漏掩盖成延迟爆发的 OOM。加分:分 EMFILE(进程级)与 ENFILE(系统级),用 fd 数的单调性区分配置不足与泄漏13.8
ext4 的 inode 用完了能扩吗?平时怎么预防?文件系统的静态布局陷阱:答"删小文件就行"或"能动态加"。加分:说清 inode 表在 mkfs 时按 bytes-per-inode 固定、只能靠扩容后 resize2fs 补建,预防手段是建盘时调 -i/-N13.1.3
NVMe SSD 上为什么还要关心 TRIM?它会加快删除文件吗?SSD 内部机制陷阱:答"TRIM 能加快删除操作"(删除本来就是元数据操作,与 TRIM 无关)。加分:从"设备不知道块已失效 → GC 搬运有效页 → 写放大升高 → 持续写性能与寿命双降"这条链解释,并区分 -o discard 与周期性 fstrim13.6.4

延伸阅读

  • man 5 ext4:三种 data= 模式的权威描述,含 writeback 数据暴露风险的原话。
  • man 8 mkfs.xfs、man 8 xfs_growfs、man 8 xfs_info:分配组、imaxpct、投机预分配与在线扩容参数。
  • man 8 mke2fs、man 8 tune2fs、man 8 resize2fs:bytes-per-inode/-N、保留块比例、离线缩容的前提。
  • man 8 mount:noatime/relatime/nodiratime、sync、barrier、errors=、discard 的逐条语义。
  • man 8 lsof、man 1 fuser:+L1、+d 与 +D、-v 与 -m 的行为差异。
  • man 1 fio 与 fio 官方 HOWTO:--rw/--bs/--iodepth/--numjobs/--direct/--ramp_time 的含义。
  • Linux 内核文档 Documentation/admin-guide/blockdev/ 与 Documentation/filesystems/:io_ticks 与 %util 的来源、文件系统挂载选项说明。
  • 第 7 章 7.1~7.3 节(fd 三层结构与 dentry 缓存)、第 9 章 9.9 节(块层栈与 fsync)、第 11 章 11.3 节(%util 为什么不可靠)、第 12 章 12.2 节(脏页回写)构成本章的前置与后续。