Linux 基础体系 · 第 55/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

ext4 深入:Extent、Journal、分配器、fsck 和性能参数

ext4 是 Linux 中常见的通用文件系统。它继承了 ext2/ext3 的块组和 inode 体系,又加入了 extent、延迟分配、多块分配器、日志校验和等机制。理解 ext4,不能只记住“它支持日志、性能不错”,而要把一次文件写入拆成几个相互关联的过程:

  1. 路径名如何找到 inode;
  2. inode 如何描述文件逻辑块到磁盘物理块的映射;
  3. 分配器如何选择物理空间;
  4. Page Cache 和 writeback 如何把数据写入块设备;
  5. Journal 如何记录元数据一致性;
  6. 崩溃后 journal replay 和 fsck 如何恢复文件系统结构;
  7. 挂载参数、文件系统参数和应用调用如何改变上述过程。

一、先建立 ext4 的磁盘模型

ext4 的基本分配单位是 block,通常为 4 KiB,但实际大小由创建文件系统时的 -b 参数决定。一个文件由 inode、目录项、数据块以及必要的元数据共同表示。

可以把路径解析简化为:

目录项 name
    ↓
inode number
    ↓
inode
    ↓
extent tree
    ↓
物理数据块

目录项保存“文件名到 inode 号”的映射;inode 保存权限、所有者、时间、大小、链接数和数据块映射;extent tree 再把文件的逻辑块映射到磁盘上的物理块。

1. 块组、inode 表和位图

ext4 将设备划分为多个 block group。每个块组通常包含:

  • 数据块位图:哪些 block 已分配;
  • inode 位图:哪些 inode 已使用;
  • inode 表:保存 inode;
  • 数据块;
  • 块组描述符等元数据。

现代 ext4 常启用 flex_bg,把多个传统 block group 组合成一个 flex group,使 inode 表、位图等元数据可以集中放置,减少大文件和目录操作时的寻址距离。

还需要区分两个概念:

  • 文件系统块大小:例如 4096 字节,影响逻辑块和分配粒度;
  • 设备扇区大小:例如 512 字节或 4096 字节,影响块设备 I/O 对齐和原子性边界。

可以使用以下命令查看实际参数:

findmnt -no SOURCE,FSTYPE,OPTIONS /data
sudo tune2fs -l /dev/DEVICE | less
sudo dumpe2fs -h /dev/DEVICE

/dev/DEVICE 必须替换为真实设备,例如 /dev/nvme0n1p2。不要把挂载点误传给 tune2fs,也不要对正在承载重要写入的设备执行修改型命令。


二、Extent:从块指针到区间映射

2.1 为什么需要 extent

早期 ext2/ext3 主要使用间接块描述文件布局。一个文件可能通过:

inode
 ├─ 直接块
 ├─ 一级间接块
 ├─ 二级间接块
 └─ 三级间接块

来找到数据。随着文件变大,间接块树会产生大量指针,元数据开销和随机寻址次数都会增加。

Extent 是一个连续物理块区间的描述:

文件逻辑块 L 起始的 N 个块
    映射到
磁盘物理块 P 起始的 N 个块

如果文件连续写入 1 GiB,4 KiB 块大小下有 262144 个逻辑块。间接块方案需要大量指针,而 extent 只需要若干个区间记录。

Extent 优化的是“连续区间的描述”,不是保证所有文件都连续。文件频繁随机追加、反复覆盖、空间高度碎片化时,一个文件仍可能包含很多 extent。

2.2 Extent tree 的结构

ext4 inode 中的 i_block 区域可以保存 extent tree 的根。extent 节点包含一个头部和若干条目:

  • eh_magic:extent 魔数,通常为 0xf30a
  • eh_entries:当前节点中的条目数;
  • eh_max:节点最多可容纳的条目数;
  • eh_depth:树深度;
  • 叶子条目:逻辑块区间到物理块区间的映射;
  • 索引条目:指向下一级 extent 节点。

叶子 extent 的核心字段可抽象为:

ee_block       文件逻辑起始块
ee_len         区间长度
ee_start_hi
ee_start_lo    物理起始块

物理块号由高低部分组合。ee_len 的高位还用于标记 unwritten extent,即“空间已经分配,但其中的数据尚未初始化为有效文件数据”。

2.3 完整映射算例

假设:

  • 文件系统块大小为 4096 字节;
  • 文件包含三个 extent;
  • 文件逻辑块号从 0 开始。
Extent A:
  logical = 0
  length  = 1024
  physical = 800000

Extent B:
  logical = 1024
  length  = 512
  physical = 950000

Extent C:
  logical = 1536
  length  = 256
  physical = 951000

文件逻辑块到物理块的计算如下:

逻辑块 100
  位于 Extent A
  物理块 = 800000 + (100 - 0)
         = 800100

逻辑块 1200
  位于 Extent B
  物理块 = 950000 + (1200 - 1024)
         = 950176

逻辑块 1600
  位于 Extent C
  物理块 = 951000 + (1600 - 1536)
         = 951064

注意,Extent B 和 Extent C 在逻辑上相邻,物理上也相邻,但它们仍然可能是两个 extent。文件系统是否合并它们,取决于长度、状态、索引更新和实现路径;不能仅根据物理连续性推断“必然只有一个 extent”。

2.4 Initialized 与 unwritten extent

预分配是理解 unwritten extent 的关键。执行:

fallocate -l 1G /data/test.img

文件大小和物理空间都可能立即增加,但这不表示 1 GiB 的每个数据块都已经由应用写入。

ext4 可以先建立 unwritten extent:

空间已分配
    ↓
extent 已记录
    ↓
实际写入某个范围
    ↓
该范围转换为 initialized extent

这样做的好处是:

  • 避免写入时逐块分配;
  • 适合数据库、虚拟机镜像等需要预留连续空间的场景;
  • 减少并发写入时的分配竞争。

边界是:预分配只保证空间已经保留,不等于数据已经持久化。若应用需要数据在断电后存在,仍然需要正确使用 fsync()fdatasync() 或等价机制。

可以观察文件的 extent:

sudo filefrag -v /data/test.img

示例输出中的 ext: 表示 extent 数量,logical:physical:length: 分别表示逻辑起始块、物理起始块和长度。filefrag 的结果用于观察,不应被当作稳定的应用接口;部分文件可能因为延迟分配尚未有最终物理布局。

2.5 Extent 的边界和反例

Extent 不能消除所有碎片:

  • 随机写入会生成多个不连续区间;
  • 文件增长时,如果尾部没有连续空闲空间,新的区间必须放在别处;
  • 删除文件后留下的洞会导致后续分配不连续;
  • 稀疏文件的“洞”根本没有物理块,不属于普通 extent。

例如:

truncate -s 10G /data/sparse
du -h /data/sparse
du -h --apparent-size /data/sparse

truncate 修改的是文件逻辑大小,通常不会分配对应的 10 GiB 数据块。--apparent-size 显示逻辑大小,而普通 du 更接近实际占用空间。读取洞时,文件系统返回零;写入洞的某个范围时,ext4 才需要分配对应物理块。


三、ext4 分配器:空间如何被选择

Extent 只描述结果,分配器决定这些物理块从哪里来。

ext4 的核心分配路径通常包括:

  1. 应用发出写入;
  2. 文件系统确定需要哪些逻辑块;
  3. 延迟分配暂时不立即决定物理位置;
  4. writeback 或显式同步触发实际分配;
  5. 多块分配器选择一段或多段物理空间;
  6. 更新块位图、extent tree 和相关元数据;
  7. 通过 journal 保护元数据变更。

3.1 延迟分配

delalloc 是常见默认行为。应用写入 Page Cache 时,ext4 可能只记录:

逻辑块 0~1023 已经有脏数据

而暂时不立即记录:

逻辑块 0~1023 必须使用物理块 800000~801023

等到回写时,文件系统可以一次看到较大的连续写入范围,于是更容易:

  • 合并相邻逻辑块;
  • 选择更大连续物理区间;
  • 减少碎片;
  • 降低分配锁竞争。

这也是为什么“应用的 write() 已成功”不代表数据已经写入设备。write() 可能只修改了 Page Cache,并产生脏页。

如果挂载 nodelalloc,分配会更早发生。它可能降低某些异常场景中的延迟分配复杂性,但通常会牺牲连续分配机会,不应仅凭直觉启用。

3.2 多块分配器与 locality

ext4 使用多块分配器,通常称为 mballoc。它不是简单地逐个扫描空闲位图,而是尽量以区间为单位寻找空间,并利用块组、flex group、空闲空间缓存和请求大小等信息。

分配器通常会考虑:

  • 文件所在目录和 inode 所属位置;
  • 当前文件已有 extent 的尾部;
  • 请求的块数;
  • 块组中的空闲空间;
  • 并发写入线程的 locality;
  • 是否为元数据、日志或数据分配。

同一目录下的文件不保证连续,也不保证不同文件互不交错。分配器的目标是综合吞吐、碎片、并发和空间利用率,而不是单一地追求“物理连续”。

3.3 空间不足不只是 df 的问题

下面两个指标含义不同:

df -h /data
df -ih /data
  • df -h 查看数据块空间;
  • df -ih 查看 inode 使用情况。

小文件数量过多时,可能还有很多数据块,却耗尽 inode。反过来,大文件系统也可能有足够 inode,但数据块已满。

还存在保留块:

sudo tune2fs -l /dev/DEVICE | grep -E \
  'Block count|Free blocks|Reserved block count|Reserved blocks percentage'

ext4 可以为特权进程保留一部分空间,使普通用户无法把文件系统完全写满。修改保留比例属于生产变更:

sudo tune2fs -m 1 /dev/DEVICE

这会影响空间紧张时 root 进程的生存余量。不能在不了解服务容量和恢复方式时随意改为 0


四、Journal:它保证什么,不保证什么

ext4 的日志由 JBD2 提供。Journal 主要保护文件系统元数据一致性,而不是自动保证所有用户数据都已经持久化。

一次典型的元数据变更可能需要修改:

  • inode;
  • 目录项;
  • 块位图;
  • inode 位图;
  • inode 中的 extent tree;
  • 超级块或块组描述信息。

如果这些修改只写了一半,崩溃后就可能出现:

目录项说文件存在,但 inode 没有被标记为已分配
inode 说块已使用,但块位图说它是空闲
目录链接数与实际目录项数量不一致

Journal 将相关修改组织到事务中,设备崩溃后可以重放已提交事务,避免大量结构处于任意中间状态。

4.1 Journal 的基本状态流

可以把一次日志事务抽象为:

开始事务
  ↓
记录需要修改的元数据
  ↓
把事务描述和修改内容写入 journal
  ↓
提交事务 commit record
  ↓
将元数据 checkpoint 到正式位置
  ↓
释放 journal 空间

崩溃时:

  • 有完整提交记录的事务:通常可以 replay;
  • 没有完整提交记录的事务:通常不会作为已提交事务应用;
  • 已经 checkpoint 的内容:可能早已在正式位置;
  • journal 本身损坏:可能需要 e2fsck 做更广泛的检查。

Journal 有有限大小,因此它不是无限历史记录,也不是备份。提交并 checkpoint 后,旧事务可以被覆盖。

4.2 三种数据模式

ext4 常见的数据模式由 data= 挂载选项控制:

data=ordered

常见默认模式。元数据进入 journal;与该事务相关的数据块会先于元数据提交到正式数据位置。

它主要避免一种危险状态:目录项已经显示文件存在,但新 inode 指向的旧数据块中仍残留旧内容。

但它不等于:

  • 每次 write() 都同步;
  • 所有文件数据都立即落盘;
  • 数据库事务语义;
  • 断电后最近写入一定存在。

data=writeback

元数据仍然记录,但数据块写入顺序限制较弱。崩溃恢复后,文件可能出现旧数据、未初始化数据或写入顺序不符合应用预期的情况。除非明确接受语义变化,否则不应把它当作普通性能开关。

data=journal

数据和元数据都写入 journal,再写入正式位置。它可能提高一致性语义,但会产生额外写放大,并不适合所有负载。

查看当前模式:

findmnt -no OPTIONS /data

4.3 Journal 与 fsync

fsync(fd) 的语义是请求把文件相关修改刷新到稳定存储语义可接受的层次,并在必要时同步元数据。fdatasync(fd) 更偏向文件数据和保证访问所需的元数据,具体行为仍受文件系统和内核实现影响。

几个常见误区:

write(fd, buf, len);   // 成功:通常只表示数据已被内核接受
fsync(fd);             // 请求该文件的修改持久化

如果要保证“创建或替换文件后,目录中新的文件名也持久存在”,通常还需要同步父目录:

int fd = open("new.tmp", O_WRONLY | O_CREAT | O_TRUNC, 0644);
/* write */
fsync(fd);
close(fd);

int dirfd = open(".", O_RDONLY | O_DIRECTORY);
fsync(dirfd);
close(dirfd);

这段模式的意义是:

  1. 同步文件内容;
  2. 关闭文件;
  3. 同步目录项变化。

实际程序还必须检查 open()write()fsync()close() 的返回值;close() 也可能报告延迟写回错误。临时文件替换通常还会使用 rename(),并根据持久性要求同步文件和目录。

设备写缓存、虚拟化存储和 RAID 控制器还会影响“稳定存储”的实际边界。文件系统发出的 flush/barrier 请求需要底层设备正确实现;禁用写屏障或强制启用不可靠的写缓存可能破坏断电一致性。

4.4 commit= 不是 fsync

例如:

mount -o commit=30 /dev/DEVICE /data

它控制 journal 事务周期的常规提交间隔,单位通常是秒。它不是“每 30 秒自动保证所有应用数据安全”的替代品:

  • 脏页可能有自己的回写时机;
  • fsync() 可以主动触发同步;
  • 设备缓存和电源故障仍是另一层问题;
  • 进程在提交间隔内崩溃时,未同步数据可能丢失。

减小 commit= 可能缩短普通崩溃后的潜在丢失窗口,但会增加提交频率和写放大。增大它可能减少日志提交开销,却扩大未主动同步数据的窗口。


五、Page Cache、writeback 与 ext4 的衔接

Linux 普通 buffered I/O 通常经过 Page Cache:

应用 write()
    ↓
Page Cache 中的页变脏
    ↓
内核 writeback
    ↓
ext4 分配物理块并构造 bio
    ↓
块层、驱动、设备缓存
    ↓
介质

在延迟分配模式下,ext4 在第一步可能还没有最终物理块映射。writeback 阶段才更适合进行大范围分配。

因此,下面三种“完成”不是一回事:

事件 含义
write() 返回 内核接受了数据,通常不表示设备已持久化
writeback 完成 数据已提交到块设备路径,但仍需考虑设备缓存语义
fsync() 返回成功 文件系统完成了应用要求的同步请求,通常是持久性边界

使用 O_DIRECT 可以绕过部分 Page Cache,但它有对齐、长度、偏移和并发语义要求,并不会自动解决元数据持久化或应用事务问题。数据库通常会结合 direct I/O、预分配和显式 flush,但具体策略取决于数据库实现。


六、崩溃恢复:Journal replay 和 fsck 的分工

6.1 为什么重启时有时很快

如果文件系统是干净卸载的,超级块和相关状态通常会表明它没有未完成事务。非正常断电后,内核挂载 ext4 时可能先执行 journal replay:

发现未完成日志事务
    ↓
验证日志记录
    ↓
重放已提交事务
    ↓
恢复元数据到一致状态
    ↓
继续挂载

这通常比扫描整个文件系统快,因为 journal 只记录最近的元数据事务。

但 replay 不是完整一致性检查。它不会替代对所有 inode、目录和位图的全面验证。

6.2 e2fsck 的五个主要阶段

离线执行 e2fsck 时,典型过程包括:

  1. 检查 inode、块和大小
    验证 inode 类型、大小、extent、块引用和明显非法地址。
  2. 检查目录结构
    验证目录项、...、哈希目录等结构。
  3. 检查目录连接性
    找出未连接的 inode,并尝试连接到 lost+found
  4. 检查引用计数
    比较 inode 中的链接数与实际目录项引用数。
  5. 检查组摘要信息
    检查块组描述符、空闲块和 inode 计数等汇总信息。

不同版本可能增加或调整细节,但这些阶段表达了 fsck 的核心任务:从 inode 和目录出发重建“哪些块被谁引用”的关系,并与位图、计数器比较。

6.3 安全检查流程

先识别设备:

findmnt -no SOURCE,TARGET,FSTYPE /data
lsblk -f

只读观察可以使用:

sudo e2fsck -fn /dev/DEVICE

其中:

  • -f:即使文件系统看起来干净,也强制检查;
  • -n:对修复问题回答否,尽量不修改。

但只读检查的输出不能等同于一次成功修复。真正修复前必须卸载:

sudo umount /data
sudo e2fsck -f /dev/DEVICE

根文件系统通常不能在正常运行的系统中直接卸载,应从救援系统、安装介质或 initramfs 环境处理。

自动修复选项有不同风险:

sudo e2fsck -p /dev/DEVICE

-p 只自动处理被认为安全的问题;无法安全自动处理时会退出并要求人工介入。-y 会对所有问题回答 yes,可能丢弃无法证明归属的数据或重建目录结构,生产环境不应盲目使用。

6.4 lost+found 的含义

如果 fsck 找到一个 inode 仍有数据,但无法从任何目录路径到达,它可能把该 inode 连接到 lost+found,文件名通常是 inode 号。

这不表示 fsck 知道原始文件名,也不表示内容一定完整。恢复流程应包括:

  1. 保存 fsck 输出;
  2. lost+found 中的文件计算哈希;
  3. 根据文件头、大小、应用日志和备份判断用途;
  4. 从业务层验证内容;
  5. 不要直接把“能打开”当成“恢复正确”。

6.5 备份超级块与设备级风险

ext4 通常有备份超级块,但备份位置由创建时的布局决定,不能套用网上固定数字。可用 mke2fs -n 在不实际创建文件系统的情况下估算布局:

sudo mke2fs -n /dev/DEVICE

-n 是不写入设备的模拟选项,但仍必须确认设备名称。误把真实设备交给不带 -nmke2fs 会破坏文件系统。

如果底层设备存在坏扇区、掉盘、链路错误或 RAID 降级,先修复设备层和制作镜像,通常比反复对原盘执行 fsck 更安全。fsck 只能修复可读到的文件系统结构,不能恢复已经无法读取的物理数据。


七、常用性能参数及其真实含义

7.1 relatimenoatime

访问时间 atime 会造成额外元数据更新。现代 Linux 常见默认是 relatime:只在满足一定条件时更新 atime,减少写入但保留大部分语义。

mount -o relatime /dev/DEVICE /data

noatime 会进一步减少 atime 更新:

mount -o noatime /dev/DEVICE /data

这可能影响依赖访问时间的程序,例如某些邮件、备份或内容管理系统。不能简单地把 noatime 当成无副作用优化。

7.2 discard 与 TRIM

SSD、Thin Provisioning 或部分虚拟块设备可能支持 discard:

mount -o discard /dev/DEVICE /data

持续 discard 可能把回收操作暴露在普通 I/O 路径上。很多发行版更倾向定期执行:

sudo fstrim -v /data

是否有效取决于设备和设备栈:

lsblk --discard

显示支持并不代表整个 RAID、虚拟化或加密链路都能正确传递 discard。生产环境应先验证延迟和设备行为。

7.3 barriernobarrier

现代 ext4 通常默认启用必要的写屏障/flush 语义。它们用于约束设备缓存中的写入顺序,保护 journal 提交的可靠性。

nobarrier 可能降低部分设备上的延迟,但如果设备断电保护不足,可能导致日志提交顺序失效。除非底层设备明确提供可靠的持久化缓存,否则不应启用。

7.4 commit=

如:

mount -o commit=5 /dev/DEVICE /data

它通常使 journal 更频繁提交。性能结果与负载相关:

  • 小文件创建密集:日志提交可能成为瓶颈;
  • 大量顺序写:主要瓶颈可能是设备带宽;
  • 大量 fsync():应用主动同步会压过 commit= 的影响;
  • 断电敏感业务:应设计正确的同步协议,而不是只调整间隔。

7.5 data=delallocnodelalloc

这些参数改变的是一致性、分配时机和写放大之间的平衡,不是单纯“快或慢”。

findmnt -no OPTIONS /data

可以确认当前是否带有 data=ordereddelalloc 等选项。改变 data= 需要按照应用语义评估;数据库、虚拟机镜像和普通文件服务对崩溃后的要求不同。

7.6 创建时的参数

部分重要布局参数主要在 mkfs.ext4 时确定:

sudo mkfs.ext4 -b 4096 -i 16384 /dev/DEVICE
  • -b 4096:使用 4 KiB 文件系统块;
  • -i 16384:平均每 16 KiB 数据空间预留一个 inode,影响可创建的小文件数量和 inode 表大小。

现代 mkfs.ext4 通常会默认启用 extent、flex_bg、metadata checksum 等特性,但默认值会随 e2fsprogs 版本和发行版变化。应以实际输出为准:

sudo tune2fs -l /dev/DEVICE

bigalloc 使用比 block 更大的 cluster 作为分配单位,适合大文件为主的工作负载,但会改变小文件和稀疏空间的占用特征,不能作为普通 ext4 的通用升级选项。


八、如何判断“性能问题”究竟发生在哪里

不要看到 ext4 就直接修改挂载参数。先区分几类现象。

8.1 extent 碎片

sudo filefrag -v /data/large-file

如果 extent 数量很多,可能存在碎片,但这不一定就是瓶颈。SSD 上寻址成本低,顺序性的重要程度与机械盘不同;同时,文件被频繁随机写入时,碎片可能是应用访问模式的结果,而不是单个参数造成的。

8.2 Page Cache 和回写压力

可以观察:

grep -E 'Dirty|Writeback' /proc/meminfo
vmstat 1

如果 DirtyWriteback 长时间升高,可能是:

  • 设备写入速度不足;
  • 应用持续产生脏页;
  • writeback 限制或 cgroup I/O 限制;
  • fsync 频繁造成同步写压力;
  • 文件系统分配和日志提交出现竞争。

这时只改 commit= 往往不能解决根因。

8.3 Journal 是否成为瓶颈

可以使用:

iostat -xz 1
pidstat -d 1

如果写入量明显高于业务有效数据量,可能存在日志写放大、重复覆盖、同步策略或数据库自身 WAL 的影响。data=journal 会进一步增加数据进入 journal 的路径,必须用实际 workload 验证。

8.4 空间和 inode

df -h /data
df -ih /data
sudo tune2fs -l /dev/DEVICE | grep -E \
  'Free blocks|Free inodes|Reserved block'

空间接近耗尽时,分配器更难找到大区间,extent 碎片和延迟可能恶化。inode 耗尽则会表现为无法创建新文件,即使 df -h 看起来仍有空间。


九、常见误解与失败路径

误解一:有 Journal 就不会丢数据

Journal 主要保护文件系统结构。应用未调用同步接口时,最近写入仍可能只在内存或设备易失缓存中。Journal replay 可以让文件系统重新可挂载,但不能凭空恢复尚未提交的数据内容。

误解二:文件只有一个 extent 就一定性能好

单个 extent 说明布局连续,但实际性能还受:

  • I/O 请求大小;
  • Page Cache 命中率;
  • 并发度;
  • 设备队列;
  • fsync 频率;
  • CPU 和加密层;
  • RAID 或虚拟化层

影响。extent 数量只是布局的一个指标。

误解三:e2fsck 可以在线运行

对已挂载且正在读写的文件系统运行修复型 e2fsck,可能观察到不断变化的状态,甚至造成进一步损坏。检查和修复应在卸载、只读挂载或救援环境中完成;即使某些只读检查在技术上可以进行,也不能将其当作在线修复。

误解四:sync 等于应用事务提交

sync 会请求系统范围的脏数据和元数据同步,但不表达“这一批业务记录已经完成”的事务边界。多进程、多文件更新仍需要应用层顺序、临时文件、rename、文件和目录同步等设计。

误解五:调小 commit= 就能避免断电丢失

它只能改变常规 journal 提交节奏,不能代替应用的 fsync(),也不能修复没有断电保护的设备缓存。


十、一个可控的实验流程

下面的实验只操作普通文件,不需要直接修改块设备:

sudo mkdir -p /mnt/ext4-lab
sudo truncate -s 2G /tmp/ext4-lab.img
sudo mkfs.ext4 -F /tmp/ext4-lab.img
sudo mount -o loop /tmp/ext4-lab.img /mnt/ext4-lab

truncate 创建的是稀疏镜像文件;mkfs.ext4 -F 会在该镜像上创建文件系统。-F 只应对明确确认的目标使用,不能对真实生产设备随意使用。

创建不同布局的文件:

sudo fallocate -l 256M /mnt/ext4-lab/preallocated
sudo dd if=/dev/zero of=/mnt/ext4-lab/stream \
        bs=1M count=256 status=progress
sudo truncate -s 256M /mnt/ext4-lab/sparse

观察空间和 extent:

df -h /mnt/ext4-lab
sudo filefrag -v /mnt/ext4-lab/preallocated
sudo filefrag -v /mnt/ext4-lab/stream
du -h /mnt/ext4-lab/sparse
du -h --apparent-size /mnt/ext4-lab/sparse

最后卸载:

sudo umount /mnt/ext4-lab
sudo e2fsck -f /tmp/ext4-lab.img

这个实验能展示三个事实:

  1. 预分配和实际写入是不同操作;
  2. 稀疏文件的逻辑大小不等于实际块占用;
  3. 文件系统结构检查应在卸载后进行。

如果要观察延迟分配,实验结果会受内存、内核版本、文件大小和回写时机影响;因此不应把一次 filefrag 输出当作所有发行版和所有负载下的固定规律。


十一、生产环境中的选择边界

ext4 适合许多通用场景,尤其是:

  • 系统根文件系统;
  • 普通服务器数据盘;
  • 中小规模文件服务;
  • 需要成熟工具链和广泛兼容性的环境。

但它的参数选择必须围绕负载和故障模型:

  • 需要应用级持久性时,优先设计正确的 fsync/rename/目录同步流程;
  • 需要大文件连续空间时,考虑预分配并监控 extent;
  • 小文件极多时,评估 inode 比例和目录规模;
  • SSD 或虚拟盘上,验证 discard 和 flush 的真实支持;
  • 文件系统损坏时,先保护原始数据和设备状态,再进行 fsck;
  • 修改 data=writebacknobarrier、保留块比例或文件系统特性前,必须明确一致性和恢复代价;
  • 重要数据不能依赖 Journal 充当备份,仍需要独立备份和恢复演练。

ext4 的核心不是某一个“最佳参数”,而是几个状态机的组合:extent 描述空间,分配器选择空间,Page Cache 延迟并合并写入,Journal 保护元数据事务,fsck 在日志无法覆盖的范围内重建结构。只有把这些层次分开,才能正确解释“写入成功”“数据落盘”“崩溃可恢复”“文件系统没有损坏”之间并不相同的含义。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。