ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux Ext文件系统详解:从inode到df/du排查,一次讲透

Linux Ext文件系统详解:从inode到df/du排查,一次讲透 上周我又被一台服务器折腾到凌晨。现象特别诡异df -h显示根分区用了87%可我拿du把整个根目录逐层加起来撑死只算到40%多剩下的空间像凭空蒸发了一样。排查到最后问题落在一个已经被rm掉、却还被某个进程攥着文件句柄的日志文件上3.7G进程一直在往里写。这种场景在老运维那里不算新闻但如果你没学过 Linux 的 Ext 文件系统根本不知道该往哪个方向查。这篇文章就是 Linux Ext 文件系统的第一篇。我不会让你去背内核源码而是把三件事讲透磁盘上的结构到底长什么样、数据是怎么一步步写进去的、异常情况下会出什么问题。搞清楚这三件事你至少能解释“删了文件空间没释放”“df 和 du 对不上”“为什么断电后要 fsck”“Ext4 比 Ext3 快在哪”面试被问到 inode 也不会心虚。1. 为什么搞懂 Ext 文件系统比背十条命令都管用先说实话文件系统不是“学不学都行”的知识它决定了你排查问题的效率。磁盘满了绝大部分初级运维的第一反应是du -sh /*逐层找大文件但如果是 inode 耗尽、文件被删但句柄未释放、文件系统损坏、UUID 冲突这招完全不灵。我一个朋友在嵌入式公司调试根文件系统挂载失败折腾了两天最后发现是内核编译时没把CONFIG_EXT4_FS选上——如果他对文件系统在内核里的角色有基本概念十分钟就能定位。Ext 文件系统是 Linux 最老牌的“亲儿子”文件系统。从当年 Red Hat、Debian 到今天的 Ubuntu、CentOS再到大量嵌入式 buildroot 环境生产环境里 ext3/ext4 的存量份额依然非常庞大。它不是一个只能“当作黑盒用”的东西mkfs、tune2fs、dumpe2fs、debugfs 这些工具能让你直接看到磁盘内部的记账结构。这篇作为系列第一篇我重点讲“静态结构 一条 IO 路径”。所谓静态结构就是你格式化以后磁盘上被划分成了哪些区域每个区域记录什么信息所谓 IO 路径就是一个write()系统调用从用户态出发最终怎么把数据落到磁盘上。日志机制、resize、故障恢复、性能调优这些我会在后面的文章展开不然一篇塞太多反而什么都记不住。在读后面的内容之前你先建立一个最朴素的认知文件系统做的事本质上是两件——记账和索引。记哪些块block是空的、哪些被占了索引一个文件由哪些块组成、文件名对应哪个 inode。所有复杂的概念都是围绕这两件事展开的。2. Ext 家族演进从 Ext2 到 Ext4 的“四兄弟”看 Ext 系列最好用演进的眼光看因为你只有知道 Ext2 当年缺什么才能理解 Ext3 为什么要引入日志也才能理解 Ext4 的 extent 和延迟分配为什么是革命性的。2.1 Ext2朴素但脆弱的老大哥1993 年Ext2 进入 Linux 内核替代当时逐渐不够用的 minix 文件系统。Ext2 已经具备了现代文件系统的基本骨架块组、位图、inode 表、目录项这些概念今天依然在用。但它有一个致命伤——没有日志journal。没有日志意味着什么想象你正在往一个文件里追加数据突然断电。系统可能只完成了半套动作块位图已经标记“这几个块已占用”但 inode 里的数据块指针还没来得及更新或者反过来inode 已经写了新指针但位图还是“空闲”。无论哪种情况文件系统都处于不一致状态。下次开机内核会跑fsck全盘扫描把这种不一致找出来。小分区还好到了 1TB、2TB 的年代一次 fsck 跑几个小时甚至更久是家常便饭而且扫描过程中很可能要交互式确认“是否修复”人在现场还好人不在就只能干等。2.2 Ext3日志机制救了命2001 年Ext3 加入了日志机制这是它最重要的改进除此之外几乎完全沿用了 Ext2 的磁盘结构所以 Ext2 可以直接原地升级成 Ext3tune2fs -j。日志原理听起来并不玄学。在真正修改文件系统的关键数据结构位图、inode、目录项之前先把“接下来我要做这些修改”按顺序写进一个独立的日志区域。系统崩溃后开机时只需要重放replay日志把那些“做了半截”的事务补完或丢弃文件系统就能快速恢复到一致状态不需要全盘扫描恢复时间从小时级降到秒级。代价是写性能略有牺牲毕竟多了一步“先写日志”的 IO。Ext3 提供了三种日志模式模式记录内容安全性典型场景journal数据本身也先写入日志最高对数据安全极度敏感的少量写入ordered默认只记元数据日志但保证数据先于元数据落盘高绝大多数生产环境writeback只记元数据数据落盘顺序随意低追求性能、能容忍异常丢数据的场景默认 ordered 是安全性和性能的平衡点也是绝大多数发行版的选择。2.3 Ext4extent、延迟分配和 64 位2008 年Ext4 发布这代改动比较大也确立了我下面要讲的现代磁盘布局。最核心的变化是引入extent块范围机制。老一代 Ext2/Ext3 用“直接块指针 间接块指针 双重间接指针 三重间接指针”的方式记录文件占用了哪些块文件一大间接块链就会变成一场灾难——每写 1GB 可能要维护海量指针块。extent 则改成了区间表示“从第 X 块开始连续占用 N 块”。一个大文件可能只需要几个 extent 条目就描述完了对读性能、写性能和 fsck 速度都是质变。然后是延迟分配delayed allocation。Ext4 不会在每次write()时立刻去磁盘上找块而是先把数据积压在内存的页缓存里等写回时一次性批量分配连续块。这个机制让文件在磁盘上更连续、碎片更少写性能大幅提升。当然副作用也很经典——断电时丢失数据的窗口变大了因为数据在内存里攒着没落盘。第三是64 位特性。传统 Ext3 受 32 位块号限制在 4K 块大小下文件系统和单文件上限分别在 16TB 和 2TB 量级。Ext4 支持 48 位块号配合 4K 块文件系统上限可达 1EB单文件上限常见说法是 16TB。做大数据、视频存储的服务器光这一点就足够成为升级理由。还有纳秒时间戳、预分配、多块分配等零碎改进先不用记。这里有一个反直觉的点值得说很多教程说“Ext3 挂载成 Ext4 就是原地升级”这说法不完全对。你把一个 Ext3 文件系统挂载为 ext4内核确实能以兼容模式读写但老文件的数据块记录方式是原来的 block mapping 格式不会自动转化为 extent。也就是说升级后新写的文件能享受 extent老文件该绕间接指针还绕。所以生产环境做“原地升级”后性能收益是逐渐显现的通常建议借迁移机会做完整数据搬运。3. 磁盘上的真实蓝图超级块、块组、inode 与目录项如果你已经理解了“记账 索引”现在就来解剖硬盘。这一节是全文的骨架后面所有排错都从这里推演。3.1 一块磁盘如何被“切”成文件系统把一整块 4TB 的磁盘想象成一面没有任何格子的墙你得在上面贴满便利贴才知道哪里写了什么。文件系统做的事情就是在墙上画出统一规格的格子再编上号。格式化时mkfs.ext4 会把整个分区划分为一个个块组block group。一个典型的 4K 块大小、每块组 32768 块时每个块组约 128MB。每个块组内部又分成这么几部分区域作用类比超级块副本记录整个文件系统的元信息整栋楼的物业台账块组描述符表记录每个块组的基本信息每层楼的告示牌块位图标记本块组哪些数据块空闲停车场空位指示灯inode 位图标记本块组哪些 inode 空闲房间入住登记表inode 表存放本块组所有 inode 实体每间房的房产证数据块区真正存放文件内容的区域房间本身所以你会看到一个规律磁盘块号和 inode 号都是“组内编号 组号”的组合定位。dumpe2fs -h能看到类似这样的输出$ dumpe2fs -h /dev/sdb1 | egrep Block size|Inode count|Block count|First block Block count: 209715200 Block size: 4096 Inode count: 13107200 First block: 0这些数字是理解一切的起点。3.2 超级块和块组描述符文件系统的身份证超级块是文件系统最核心的数据结构存放在整个分区偏移 1024 字节处。里面记着什么块大小、总块数、总 inode 数、文件系统状态是否 clean、挂载次数、上次挂载时间、UUID、还有最关键的一组东西——feature 标志位。feature 标志位决定了内核怎么对待这个文件系统。比如一个 Ext4 文件系统启用了64bit、flex_bg、metadata_csum等特性旧内核不认识这些标志就会拒绝挂载。嵌入式开发里最常见的“挂载不上”就是这个原因——用户把 PC 上格式化的 ext4 盘放到内核配置精简的板子上板子内核没开对应特性或没编译 ext4 驱动直接报错。解法也简单要么板子内核配置跟上要么格式化时用-O ^metadata_csum之类的方式降级特性满足双方交集。块组描述符表GDT则按块组记录关键位置这个组里 inode 位图在哪一块、块位图在哪一块、inode 表在哪一块、空闲 inode 数和空闲块数等。注意GDT 本身也要占空间它也像数据块一样被编号管理。这里有个小细节超级块和 GDT 在每个块组里都有副本吗不是的。mkfs 时默认只在第 0、1、3、5、7 等若干间隔的块组放置备份这就是为什么fsck修复时能“找回”被破坏的核心结构——它可以从备份超级块重建。你在排错时如果发现主超级块坏了可以用e2fsck -b 32768之类的参数指定备份块。3.3 inode文件在磁盘上的“个人档案”inode 是文件系统中最重要的概念也是面试题的重灾区。它大概长这样默认大小 256 字节本身不存文件名存的是mode文件类型普通文件/目录/软链接/设备文件等和权限位uid、gid属主和属组atime / mtime / ctime访问时间、修改时间、状态变更时间links_count硬链接计数i_size、i_blocks文件大小和占用的块数数据块索引区在 Ext4 下是一棵 extent 树一个文件对应一个 inode一个 inode 在格式化时就被预分配在 inode 表里。这就是经典的“删除文件慢不是删 inode 慢是删目录里的条目 检查链接计数”的说法的背景。Ext4 的 extent 树怎么工作每个 inode 里预留了一块空间放 extent 根节点根节点能直接内联几个 extent 条目每个条目是一对“起始块号 连续块数”。文件很大、一个根节点装不下时它会指向下级索引节点形成一颗多叉树。查找文件数据时按逻辑块号沿着树往下走很快就能算出它在磁盘上的物理位置。对比老办法每 4K 就要记一个指针大文件的 metadata 体积差了不止一个数量级。想看一个 inode 的真实内容用 debugfs$ debugfs -R stat /path/to/file /dev/sdb1 Inode: 786434 Type: regular Mode: 0644 Flags: 0x80000 Generation: 1709012345 Version: 0x00000000 User: 1000 Group: 1000 Project: 0 Size: 52428800 File ACL: 0 Directory ACL: 0 Links: 1 Blockcount: 102400 Fragment: Address: 0 Number: 0 Size: 0 ctime: 0x64abc123:... mtime: 0x64abc124:... atime: 0x64abc125:... Size of extra inode fields: 32 EXTENTS: (0-1023): 32768-33791 (1024-2047): 34816-35839 (2048-4095): 30000-32047看那个EXTENTS段每行就是一个 extent。这意味着文件在磁盘上并不是一块连续区域而是几段连续区间的拼接——这非常正常加文件系统碎片化程度就看这些区间多不多。3.4 目录项与硬链接名字和档案的桥梁inode 不存文件名那文件名在哪在目录里。目录本身也是一个文件它也是一个 inode数据块里存的是“目录项dirent”序列。每个目录项大概包含文件名 | inode 号 | 文件类型 | 记录长度你执行ls -li第一列显示的 inode 号就是通过目录项查出来的。所以最通俗的理解是目录负责从名字到 inode 的映射inode 负责从 inode 到数据块的映射。两层映射缺一不可。这就引出了硬链接的本质你在两个不同目录下创建指向同一个 inode 的硬链接就是在两个目录里各加一个目录项。两个目录项不同名但 inode 号一样inode 的链接计数是 2。你删掉其中一个名字链接计数减到 1数据还在两个都删链接计数归零数据块才真正被释放。删文件不是“立刻释放”是“链接计数减一”。这个认知对排错太重要了。软链接symlink又是另一回事它是一个专门的 inode里面存的是目标路径字符串。目标被删软链接就变成“悬空”的报“No such file or directory”。3.5 用 dumpe2fs 和 tune2fs 上手看真实磁盘纸上谈兵没用找一台测试机动手看看。格式化一块盘后# 查看超级块关键参数 dumpe2fs -h /dev/sdb1 # 查看块组编号、inode 位图位置、块位图位置 dumpe2fs /dev/sdb1 | egrep Block group|Primary inode|Block bitmap|Inode bitmap # 查看可调参数和挂载计数策略 tune2fs -l /dev/sdb1 # 设置挂载 20 次或 180 天后强制 fsck tune2fs -c 20 -i 180d /dev/sdb1我建议你重点看三个数据Block size、Blocks per group、Inode count。你可以拿这三个数心算一下“这盘格式化时预留了多少 inode”——如果磁盘上全是百万级小文件inode 数量不够就是定时炸弹这在第 5 节会展开。4. 一次写文件请求的完整旅程VFS、页缓存与 sync结构讲完现在看动态过程。很多人对“文件写入”的理解就是“程序调 write数据到磁盘”真实路径远比这个曲折而这段路正是各种数据丢失、空间未释放问题的大本营。4.1 从 write() 到 VFS 层你程序里调用write(fd, buf, len)这个 fd 本身是一个 VFS虚拟文件系统层面的抽象。VFS 是 Linux 的“翻译官”它定义了一套统一接口把 ext4、xfs、btrfs、NFS、tmpfs 都包装成同样的操作。内核根据 fd 找到对应的 inode再根据挂载信息找到具体文件系统类型最终调度到ext4_file_write_iter()这个函数。这一层的好处是应用程序和文件类型完全解耦你不用关心底层是磁盘还是网络统一按“文件”操作。但代价是你的一次写调用期间要经过无数层锁、状态检查和缓存机制所以 io_uring 这类新技术才会追求“绕过传统路径”。4.2 页缓存Page Cache与延迟分配大多数“写”不会直接碰磁盘。write()进来后内核先检查对应的页是否在页缓存里不在就从磁盘读入可能根本没内容是新建的空页然后把你的数据复制进内存页把页标记为“脏dirty”。到这里write()就可以返回了。这就是为什么dd一个大文件经常看起来“瞬间完成”——数据还在内存里。也是为什么很多人第一次用 SSD 上的临时目录测速会测出 10GB/s 的“假速度”。Ext4 的延迟分配正是在这个层面生效脏页在内存里攒着等回写前一次性为这一批数据计算出磁盘上最合适的连续块区间。好处是分配大块连续空间、减少碎片、合并 IO坏处是断电时如果内存里的脏页没来得及回写你刚写的文件可能找不回完整内容。这也是“all data 丢失”最经典的窗口。负责把脏页刷回磁盘的是内核的writeback 线程老内核里叫 pdflush/flusher。它受几个参数控制比如/proc/sys/vm/dirty_ratio内存脏页上限比例、dirty_writeback_centisecs回写周期。如果系统突然卡到 IO 上经常是脏页积压太多、回写线程开始大把刷盘导致的——这个现象排查思路以后单写一篇。想直观看到脏页量$ cat /proc/meminfo | egrep Dirty|Writeback Dirty: 102400 kB Writeback: 1024 kB4.3 sync、fsync、fdatasync到底什么时候真正落盘这是和热词“sync、vfs”最直接相关的一环也是日常开发最容易被坑的一环。sync刷整个系统的脏页内核发出指令让所有文件系统把数据写回磁盘。一般关机、重启前内核会自动调用。fsync(fd)把某个文件的所有脏页数据 元数据刷回磁盘并等待磁盘确认。fdatasync(fd)只刷文件的数据部分不保证 mtime/ctime 等元数据一定落盘性能通常比 fsync 快。这里要纠正一个流传很广的误解“应用程序调用了 write数据就安全了”是错的。write 只是把数据交给了内核的内存缓存。数据库类应用、消息中间件、记账系统一定要在关键事务提交后调用fsync否则一旦掉电应用说“写成功”的数据可能是假的。很多分布式系统的“双写 fsync 策略”就是为了在应用层和内核层都确认数据“确实死在磁盘上”。我自己排一个线上问题时的标准操作是# 看脏页是不是一直居高不下 watch -n 1 cat /proc/meminfo | grep -E Dirty|Writeback # 如果脏页长期超过几 GB检查是否有大量随机小文件写入或 io_uring 任务4.4 挂载选项与数据有序性上一节提到 Ext3/Ext4 的日志模式挂载时是可以显式指定的mount -o dataordered /dev/sdb1 /mnt/data mount -o datawriteback /dev/sdb1 /mnt/data默认 ordered 的含义要再强调一遍它保证文件的数据块先落盘然后才提交元数据日志。这样即使中途断电日志里看到的元数据所指向的数据块内容一定已经是新数据不会出现“目录条目指向一个内容和大小完全对不上”的惨状。datajournal 最安全但性能最差writeback 只保证文件系统结构一致不保证文件内容一致——如果你跑的是虚拟机镜像、数据库文件用 writeback 要非常谨慎。挂载时还有一个常见的优化项noatime。Linux 默认每次访问文件都会更新 atime访问时间这会产生大量写 IO。如果对 atime 没有强需求建议加noatime能明显减少磁盘写压力。注意 relatime 是目前很多发行版的默认策略兼顾读性能和 atime 更新如果你的场景不需要 atime直接 noatime 更省心。5. 三分实操七分避坑格式化、挂载与常见故障定位最后落到动手层。这一节更像是我个人的实操心得踩过坑才写得出来。5.1 格式化与参数选择格式化命令本身很简单mkfs.ext4 /dev/sdb1但生产环境我会额外指定几个参数mkfs.ext4 -m 2 -b 4096 -i 16384 /dev/sdb1-m 2代表预留块比例设为 2%默认是 5%。预留块是给 root 用户和 fsck 用的“安全垫”防止磁盘满到无法写入时连修复都没法做。大容量数据盘5% 意味着几百 GB 白放着不少公司会调低到 1%-2%但别调 0——磁盘写满后文件系统性能会断崖式下跌碎片率和后续维护成本都受不了。-i 16384是 inode 密度每 16384 字节数据就创建一个 inode。如果你知道这块盘将来会存海量小文件比如消息队列消息、Git 仓库、邮件存储inode 密度就要调高-i 8192甚至更低如果放的全是大视频、镜像文件inode 密度可以调低省下 inode 表空间。要特别提醒的是mkfs 前务必确认是否真的格式化对了盘。生产环境中把数据盘格式化成新的文件系统等于数据全灭。老手也会栽在这里我的习惯是格式化之前先lsblk、blkid、df -hT三连对照确认分区 UUID 和预期的完全一致再敲回车。5.2 三类经典故障的定位链路第一类df -h 显示满了但 du 找不到大文件。95% 是 deleted 文件还在被进程占用。排查链路# 找到已删除但仍打开的文件句柄 lsof L1 # 列出句柄对应的 PID、文件路径和大小 lsof L1 | sort -k7 -n | tail -20 # 确认后重启进程或 kill空间才会真正释放这类问题的本质就是我在 3.4 节讲的目录项删了但 inode 链接计数还没归零。其实准确说是被进程打开的 inode 还在内存里引用着内核不会释放数据块。这也是为什么“du 和 df 不一致”是磁盘排查的经典提示信号。第二类磁盘空间明明很空却报 No space left on device。优先看 inodedf -i /data如果IUsed%到 100%即使还剩几百 GB 数据块也没用——你建目录、建文件都需要 inodeinode 用完了就像酒店房间全住满大厅再大也没法开新房间。高发场景是消息队列的持久化目录、容器日志目录、npm 缓存这类“海量小文件”堆积地。快速定位哪个目录文件最多find /data -xdev -type d | while read d; do echo $(find $d -maxdepth 1 -type f | wc -l) $d; done | sort -rn | head -20或者用find /data -xdev -type f | awk -F/ {print $(NF-1)} | sort | uniq -c | sort -rn | head一层层聚拢。清理小文件、合并批量迁移、重新规划 inode 密度对症下药。第三类开机挂载报错UUID 冲突或文件系统损坏。blkid看所有分区的 UUID如果两个分区因为 dd 整盘克隆导致 UUID 相同系统会挂错或拒绝挂载。这时用 tune2fs 改掉其中一个 UUIDtune2fs -U random /dev/sdb1文件系统损坏的表现更直接挂载时 kernel 日志报EXT4-fs error或者目录读取出现I/O error。这时候才轮到fsck。5.3 fsck用不对就是灾难我一直强调一句话fsck 是外科手术不是保健品。很多新手盘一有点异常就 fsck还常常在挂载状态下强行操作这是非常危险的。正确姿势无论什么情况先把文件系统以只读方式挂载或干脆 umount。只在文件系统非活动状态运行否则会二次破坏结构。跑之前先备份关键元数据至少dumpe2fs -h看一眼当前状态。先做只读预检fsck.ext4 -fn /dev/sdb1-n表示“只是看看不修”。这一步能看到哪些地方异常心里有底。确认要修了用fsck.ext4 -y /dev/sdb1让它自动对交互式问题回答 yes。但注意丢失的文件会被放到lostfound目录恢复后需要人工找回。实际经验是如果文件系统在“可挂载但读写有异常”之间摇摆最稳妥的做法是先想办法把数据 copy 出来再重新格式化而不是执迷于 fsck 修复。元数据校验类特性打开后fsck 能定位到具体的 inode 和块但修复结果不一定是你想要的“原样”更常见的是“文件恢复成#12345这样的孤儿”。我把这称为倒数据优先原则修复只是止损备份才是本钱。另外说一个容易被忽略的小技巧如果你在嵌入式环境里用 NFS 或 SD 卡跑 ext4 根文件系统突然断电后启动时自动 fsck 可能因为找不到控制台而卡住。调tune2fs -c 20 -i 180d拉长检查间隔、或改/etc/fstab里的 pass 字段都能减少这种“起不来的尴尬”。到这里Ext 文件系统第一篇文章该收尾了。我最后分享一个自己的习惯生产环境每块数据盘格式化时我都会顺手看一眼块大小、inode 密度和预留比例再把这个盘的用途写进资产管理表里。还有一点很关键——不要迷信网上那些“优化挂载参数”脚本noatime可以加但 commit、journal_async_commit 这类参数默认值通常是最稳的真出故障时默认配置才好排查。后续文章我会继续写 Ext4 的日志内部机制、resize 扩容实践以及 ext4 损坏恢复的真实案例那些才是更硬核也更刺激的部分。
返回列表