我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

Ext4文件系统演进:从Ext2/Ext3到extent与日志,解决容量/一致性/碎片问题

Ext4文件系统演进:从Ext2/Ext3到extent与日志,解决容量/一致性/碎片问题 深夜三点半手机在床头柜上震得嗡嗡响。某台数据库服务器的SSD告警同事在群里说了一句开机就卡在fsck估计要等天亮。等我在电脑前坐定屏幕上的ext4卷检查进度条已经跑了三个小时还没到百分之二十。那台机器是ext3时代建的分区里挤了三百多万个文件。我一边看着进度条一点点挪一边忍不住想一个问题从Ext2到Ext3再从Ext3到Ext4我们每次升级文件系统到底是在解决什么这个问题大概就是这篇深入Ext4文件系统系列开篇最好的引子。Ext文件系统在Linux生态里活了快三十年代码不算最花哨也没那么多炫酷的高级特性但它每一次换代背后都是真实世界里被逼出来的事故与需求硬盘从MB级长到TB级、开机检查从几分钟变成几小时、一个虚拟机镜像文件塞进机械盘之后越用越慢。这篇文章我把Ext2/Ext3到Ext4这条演进路的来龙去脉完整拆一遍重点讲几代之间到底换了哪些核心设计、为什么换、换完的代价是什么。适合正在维护旧服务器的人也适合想真正理解Linux存储工作原理的开发者——读完你应该能回答那几个最经典的运维问题为什么ext4比ext3能装更大的文件为什么ext4的fsck没有那么吓人为什么有人说ext4断电会丢数据1. 先回答标题里的问题两次换代其实只围绕三件事文件系统是一个很特殊的系统组件。它不像内核函数那样可以频繁修修补补也不像驱动坏了随时可以换新。一个文件系统一旦上线上面就压着几亿个文件和数据所以它必须极度保守。也正因如此回顾Ext系列的历史会发现从Ext2到Ext3再到Ext4真正值得大动的点并不多归纳起来就是三件事。第一个是地址空间的问题一个文件最大能有多大一个分区的容量上限是多少这取决于文件系统用什么数据结构来记录数据块在哪。很多刚接触存储的工程师有一个根深蒂固的误解——CPU是64位了文件系统自然也能处理大文件吧完全不是。文件系统的地址表达能力和CPU位数没有半点关系它取决于块号的位宽。Ext2/Ext3的块号是32位的无论CPU多先进分区容量天花板就死死卡在那里。第二个是写入时序的问题当系统在写入过程中突然断电、崩溃文件系统里那些互相关联的结构inode、位图、目录项能不能保持一致会不会出现树已经倒了果子却还挂在半空的荒唐状态Ext2时代没有日志崩溃后只能靠fsck暴力扫描整盘来修复Ext3引入了日志机制用额外的写盘代价换取了崩溃后可回放的正确性Ext4则在日志之外做了大量改进把性能损失尽量压回去。第三个是分配策略的问题数据写入时块是怎么挑选和安排的。是一次来一个块就现场找空位还是囤一批请求之后统一规划连续空间这个选择直接决定了文件碎片的生长速度、元数据刷盘的次数、以及机械盘上寻道带来的随机IO开销。这三件事从Ext2一直贯穿到Ext4每次换代都是在某一两条线上做了实质性突破。所以建议看你读这篇文章时带着这条主线走——遇到任何一个特性先问问它属于哪一类问题是为容量服务、为一致性服务、还是为性能与碎片服务。这样再看后面那些具体机制就不会被细节淹没了。1.1 三场事故现场对应三个矛盾地址空间的问题在2010年前后变得尖锐。那时候单块机械盘已经做到2TB一个阵列轻松超过8TB而Ext3在4K块大小下分区上限只有16TB——看上去还够但单文件上限只有2TB左右就麻烦了。数据库表文件、视频素材、虚拟机镜像随随便便就能触到天花板。写入时序的问题更古老。Ext2没有日志文件系统内部多个结构之间的更新不是原子的你创建一个文件可能需要先更新inode表再更新目录项再更新块位图这三个动作如果只做到一半就断电重启后你就会看到一个有名字却无inode或有inode却无块的僵尸状态。老运维都有过ext2崩溃之后fsck把文件倒腾进lostfound的经历那种修是修回来了但文件名全没了的体验经历过一次就再也不想经历。分配策略的问题则属于慢性病。Ext2/Ext3分配块是按块来的每个块独立寻址、独立记账磁盘空闲稍微零散一点连续写入就被拆得七零八落。虚拟机镜像这种一次写很大、长期反复增删的文件在ext3上放几年碎片率能到几十个百分点IO延迟肉眼可见地恶化。1.2 本系列文章的阅读姿势这篇是系列的第一篇定位在演进逻辑与问题动机重点讲清楚Ext4解决了什么、为什么用这种方式解决。我不会把mount命令参数一个个罗列成说明书也不会直接丢给你源码级的结构体定义。但我希望每一部分讲到的机制都能让你在遇到实际故障时心里有数——至少知道该往哪个方向查。下一篇文章我会专门把Ext4的extent树和jbd2日志提交过程掰开揉碎讲这篇先把骨架搭好。2. 老底子Ext2的指针寻址与Ext3的日志补丁瓶颈是怎么一步步冒出来的2.1 间接块指针一个4K块换算出来的天花板Ext2把磁盘划分成一组一组的block group每个group内部维护自己的块位图、inode表和一大片数据块。这个分区框架后来一直沿用到Ext4属于经典设计。真正让Ext2/Ext3头疼的是单个文件的块地址记录方式。每个inode里有一块60字节的区域i_block用来存放数据块的地址。Ext2的做法是间接指针链最前面12个直接指针每个直接指向一个4K数据块然后是一个单间接块这个块本身不存数据里面存了1024个指向数据块的指针再往后是双间接块里面存的指针指向的还是指针块最后还有一个三间接块。以4K块大小、每指针4字节计算直接区能管12 × 4K 48KB单间接区能管1024 × 4K 4MB双间接区能管1024 × 1024 × 4K 4GB三间接区能管1024 × 1024 × 1024 × 4K 4TB理论上加起来能支撑一个约4TB的文件但由于32位块号的限制加上内核自身的文件大小字段约束实际能用的文件上限也就是2TB左右很多老内核甚至更低。分区的总容量天花板更尴尬块号总共32位乘以4K块最后算出来就是16TB。这个设计的另一个隐性成本是按块记账。每写一个数据块都要先定位到对应的间接块、读出来、改指针、写回去。当你在写一个几十GB的大文件时这种为每个数据块而进行的元数据读写要发生几百万次。ext3时代写大文件吞吐明显低于ext4很大一部分原因就是块分配和间接块更新在拖后腿。2.2 Ext3的日志三模式用性能换正确性Ext3在1999年前后算是救了Linux服务器一命因为它带来了日志机制。日志的核心思想很朴素在真正修改磁盘上那些互相关联的结构之前先把我接下来要做哪些改动以事务的形式记到一个专门的日志区域崩溃后重启系统读取日志把没做完的事情重放完文件系统就回到一个内部一致的状态——不需要全盘扫描就能恢复。Ext3日志有三种运行模式journal模式数据和元数据都先写日志。安全性最高但等于一次写入要写两遍盘性能损失巨大实际部署很少用。ordered模式只把元数据写入日志但要求元数据提交之前数据块必须先落盘。这是ext3默认的模式在数据完整和性能之间取了折中。writeback模式日志只记录元数据数据块何时落盘完全看内核调度。性能最好但崩溃后可能出现新元数据搭配旧数据的错位某些文件内容会是陈旧的。ordered模式是绝大多数ext3系统实际在用的。它保证了目录项和inode的一致性也保证了至少在文件内部不会出现新旧混搭但也带来了一个麻烦每次事务提交都要严格执行先刷数据再落元数据的顺序这等于给块设备层加了一道全局同步约束。当年很多老前辈抱怨ext3写着写着突然卡一下排名第一的原因就是这个顺序刷盘。2.3 被长期运行放大的三个老毛病第一fsck时间越来越长。ext3并非不做一致性检查而是因为文件系统里没有一张全局的、可供快速验证的文件数据块映射总表fsck必须把整个分区的位图、每个inode、每个目录项都读出来核对一遍。这个耗时和文件数量强相关。几百万、上千万个文件的分区开机跑一两个小时是家常便饭。第二没有延迟分配碎片只会单向增长。ext3来一块数据就立即分配一个空闲块并写进去下一个块继续现找现用。磁盘空闲空间充足时问题不大用了一年半载、空闲区域变零散之后新写入的文件会被拆得越来越碎。数据库表、虚拟机镜像这类写起来看似连续、实际会反复增删的文件碎片化几乎无法避免。第三32000个子目录的隐性陷阱。ext3早期没有目录索引机制一个目录下最多放32000个子目录再多就直接报错。这个问题在后来追上了很多做按用户/按套餐分目录的业务系统——单目录里塞了几万个业务子目录的全都炸过。这也说明ext3的很多设计基于一台服务器上目录不会太拥挤的老式假设是实际规模把它逼出了底线。3. Ext4的第一个胜负手用extent树替换间接块寻址3.1 extent是什么一张连续区间的入场券Ext4对inode里那60字节的i_block区域做了彻底改造不再放一个又一个的单个块指针而是改造成一棵extent树的根部。结构上前12字节是extent树头记录树深度、当前有效项数量等元信息后面最多可以容纳4个extent项。一个extent项是12字节它表达的意思从我指向第xxxx个块变成了从逻辑块A开始的一批连续块对应物理盘上从B开始的一段连续区域。一个典型extent项里逻辑起始块号占32位长度占16位最高位用于标记尚未写入物理起始块号用48位表达。长度字段最多表示32768个4K块——也就是一段128MB的连续空间。这就意味着在Ext4里描述一段128MB的连续文件数据只需要一条12字节的记录换成Ext2/Ext3同样大小的连续空间需要262144个指针分布在252个间接块里。这是一次数量级上的压缩。写一个1GB的文件理论上如果分配足够连续整个文件可能只需要8条extent就能描述完整而ext3需要26万个指针项还要把这些指针块全部读出来写进去。3.2 四条直接路径为什么大多数人访问小文件等于零成本inode里只给了4条extent的位置这不是抠门而是对现实的精确顺应。绝大多数服务器上的文件都很小配置文件几KB日志文件几百KB网页静态资源几MB。这些文件往往只需要1到2条extent就能完整映射4条配额绰绰有余。而且这意味着地址解析可以直接从inode内存数据中拿到不需要额外的磁盘I/O去读间接索引块。只有当文件大到超过4条extent能描述的范围时树才会向下生长出索引层和叶子层。extent树最多可以支撑到好几层深度配合48位物理块号和32位逻辑块号最终的容量和规模都远超Ext3。我的实际体会是普通业务服务器上90%以上的文件在ext4里都适配直接extent命中所以打开文件、读取数据时很少因为找块地址而额外增加磁盘读。3.3 一石三鸟大文件、低碎片、快fsckextent机制带来的好处是连锁的。第一单文件上限直接从2TB左右抬到16TB4K块下分区的逻辑容量借助64位块号特性可以做到1EB以上——至少容量不够不再是常规业务的第一瓶颈了。第二因为一条extent必须对应一段连续空间块分配器天然会往凑连续块的方向努力文件碎片的增长速度被大幅压低。第三fsck在检查文件时不再需要顺着三间接指针链一级一级去核对所有指针的有效性只需遍历extent树的叶子节点检查范围小得多。我经历过的真实案例很有说服力同样几百万文件的分区ext3崩溃后fsck跑了四个多小时机器整整一个上午没法用换成ext4之后类似规模的卷在崩溃后十几分钟就能完成检查回来。extent树建立的文件数据布局总览能力让一致性检查从逐块审问变成了提纲挈领这就是运维体验上最直观的换代差异。4. Ext4的第二个胜负手延迟分配与多块分配改变写入节奏4.1 延迟分配的账先把债记下来攒一批再还Ext4默认开启delalloc延迟分配。普通用户调用write()把数据交给内核之后内核并不会马上去磁盘上找空闲块而是先把数据放进page cache、把页面标记为脏真正去分配磁盘块的动作被推迟到后台写回阶段统一执行。这就是延迟分配四个字的含义——块分配从write调用时点被推迟到了刷盘时点。为什么这么干更好因为刷盘时内核能够一次看到这个文件这次一共修改了哪些页可以做全局规划。如果文件是连续写入的后台线程会把它识别为一个大区间然后在磁盘上找一整片连续空间一次性分配。而Ext3那时候的做法是来一块分一块每一块都要立刻去找空位、更新位图和inode指针完全看不到全局自然也就谈不上规划。多块分配器mballoc在这里负责把规划落地。它在内存里预先维护各个block group的空闲概况分配时倾向于选择更大的连续单位同时把分配决策的复杂度从每块一次摊薄到每批一次。两个机制配合起来的效果我在实际测过的一个例子里感受很深在ext4上写入一个5GB的raw虚拟机镜像磁盘空闲充足时这一个文件最终可能只有几十个extent几乎整片连续。同样的操作在ext3上映射表膨胀到几万个条目磁盘上到处都是碎片。4.2 元数据IO从一次一块变成一次一批延迟分配和多块分配合起来的另一个巨大收益是元数据刷盘次数的大幅下降。ext3时代写入一千个块可能要做一千次位图更新和一千次inode指针更新ext4把这上千个块作为一次分配完成位图只改一次inode只改一次元数据相关的journal记录也相应减少。这个差异直接体现在随机写和混合读写场景里。机械盘上减少元数据刷盘等于减少寻道次数实测吞吐能有30%以上的提升在SSD上则是显著降低了写放大和每秒钟的I/O请求数量。同时jbd2事务机制的批量提交能力也比ext3时代强很多每次事务尽量容纳更多更新内容整体吞吐自然就上来了。4.3 代价断电掉进中间态与奇怪的截断文件延迟分配不是免费的午餐。它有一个非常出名的问题当write()返回成功、但数据还停在page cache里没有真正分配到块时如果这时候断电数据当然会丢但比丢数据更微妙的是ext4会在文件长度看起来已经很大、块却还没分配的中间状态里挂起。如果恰好在这个中间时刻断电重启后某些文件可能出现长度和实际占用的块数量对不上严重时表现成文件变成了0字节或者读到一半就是空洞。这就是为什么ext4的mount参数里dataordered和datawriteback依然重要。ordered模式下数据块要先落盘元数据才提交能从根上避免新元数据旧数据的错位writeback模式放开这个约束换取更高的性能但崩溃恢复语义要弱不少。对于数据库、文件存储这类承载重要数据的服务我个人的建议始终是把应用层该做的fsync/fdatasync做好不要指望文件系统替你兜底。见过太多把ext4安全等同于不用fsync的项目最后在断电事故里吃了大亏。5. 放大镜看完剩目录索引、时间戳、flex_bg、日志校验这些小改进为何也要紧5.1 htree目录索引把线性翻目录改成B树查询ext3后期其实已经有了目录索引能力只是需要手动开启ext4把它变成了默认特性并继续完善。原理上目录项被组织成类似B树的索引结构查找一个文件名不用再从头到尾线性扫一遍目录而是从树根沿着索引往下走。复杂度从O(n)降到了O(log n)量级。这个改进对超级大目录场景是救命级的。早年ext3没有目录索引时32000个子目录就是硬上限有了htree之后一个目录内的条目数上限被大幅放宽创建、删除、查找的性能都不再随目录规模线性恶化。现在很多业务系统动辄按时间或按用户在每个目录下生成几万、几十万个对象没有htree这些业务在传统的线性目录模型下早就跑不动了。5.2 时间戳的2038年危机推迟到2446年Ext2/Ext3的时间戳是32位有符号整数从1970年1月1日算起上限到2038年1月19日。传统Unix时间戳的2038年问题在文件系统层面同样存在。ext4在inode的额外字段区域里给每个时间戳补上了更多高位的秒数同时把纳秒精度也纳入了标准字段。所以ext4在时间表达上直接把可用范围推到了2446年并且支持纳秒级别的粒度。这件事平常完全没感觉但2038年一到它就是决定老系统能不能继续无痛运行的分水岭。现在新建的任何文件系统默认都已经避开了这个雷。5.3 flex_bg把元数据收紧为顺序IO让路文件系统的性能损耗很大一部分来自元数据的物理分散。ext4提出了flex_bg弹性块组特性把连续的多个block group合并成一个逻辑组组内所有block group的位图和inode表被统一集中放置在这个逻辑组最靠前的位置让出来的连续空间全部用于数据块。这样做的好处很快就能感觉到。顺序写大文件时数据块可以连续铺开不需要频繁跨组跳跃创建新文件和它的数据时inode与对应数据块在物理位置上更接近目录项—inode—数据三者的局部性比ext3时代好得多。对数据库这种大量小文件按目录组织的工作负载flex_bg的空间局部性改善是实打实的。5.4 日志校验与未初始化inode表恢复体验的稳定化ext4在日志事务上默认引入了CRC32C校验配合metadata_csum等特性。崩溃后进行日志回放时系统会先校验每个事务是否完整有效遇到损坏的事务直接跳过而不是傻乎乎地把坏数据也重放到磁盘上。这比ext3时代日志一坏就全盘遭殃的体验要好得多。另一个容易被忽略的特性是未初始化inode表。mkfs的时候inode表可以先不做全量清零而是标成未初始化等系统mount之后后台慢慢补齐。这让大容量盘的格式化时间从可能几十分钟缩短到几分钟。对一次性要格式化几十TB阵列的运维来说这个差异是肉眼可见的。如果你要为生产环境新建一个分区我的建议很直接优先用当前发行版里较新版本的e2fsprogs执行mkfs.ext4默认参数让它自动带上64bit、flex_bg、metadata_csum、dir_index这些特性。老版本默认参数可能缺metadata_csum后续再想补就很麻烦。6. 从Ext3升到Ext4的实操路线与踩坑记录6.1 两条路线原地改造还是推倒重建第一条是平滑升级路线。在分区能够安全卸载或只读挂载的前提下执行tune2fs -O extent,uninit_bg,dir_index /dev/sdb1 e2fsck -pf /dev/sdb1 mount -t ext4 /dev/sdb1 /data这样老文件系统就变成ext4格式了。注意这一步只启用了extent和少数几个特性像flex_bg、metadata_csum、64bit这类特性必须在mkfs时生成是无法事后通过tune2fs补齐的。所以原地升级得到的ext4只是带extent特性的旧文件系统性能上比全新ext4要打折扣但比留在ext3还是强不少。第二条是重建路线把数据备份出来mkfs.ext4重新格式化再把数据迁回去。如果磁盘本身健康、数据量也在可接受的迁移时间范围内我更推荐这条路线——它能拿到ext4完整特性集从头就是干净清爽的布局长期运行后的底子好得多。很多用了一两年才开始抱怨ext4也不怎么快的服务器往往就是当年原地升级上来的。6.2 我踩过的三个坑第一个坑升级后没先跑e2fsck就直接挂载。tune2fs动过文件系统结构之后不先做一遍完整性检查等于让一个刚做完手术的病人直接去跑马拉松。日志刷着刷着就可能撞上结构不一致的角落然后整个分区报错、强制只读。顺序必须是tune2fs在前、e2fsck紧跟、再mount。第二个坑把ext3盘用错误的方式挂为ext4。有人图省事直接在fstab里把ext3分区的type改成ext4然后重挂结果有的机器根本挂不上有的挂上了却出现块号错乱。所有涉及文件系统格式变更的操作都必须遵循先tune2fs修特点、再fsck验证、最后挂载的路径不能绕过去。第三个坑默认以为delalloc对数据库天然友好。数据库有自己的页缓冲池和双写机制落盘粒度很碎。在某些内核版本和特定存储负载下关掉delallocmount -o nodelalloc反而更稳定。到底该不该关不能照搬网上的结论要用你自己的真实IO负载做一遍断电演练看结果再决定。6.3 针对不同工作负载的配置建议下面这张表是我在多种业务场景里用来做初始配置的参照实际部署时按你的工作负载微调工作负载建议mount参数理由普通Web目录、日志盘defaults,noatime去掉atime更新减少写入放大数据库数据盘defaults,noatime,dataordered一致性优先按实测决定是否加nodelallocKVM/QEMU磁盘镜像defaults,noatime创建时做预分配避免镜像运行期反复增块引发碎片海量小文件目录defaults定期e4defrag充分利用htree索引与flex_bg局部性创建虚拟机镜像时我最常用的命令是qemu-img create -f raw -o preallocationfull vm.img 20G这一条能让镜像文件在创建之初就占下一整段连续extent后续虚拟机运行时的随机写入不会频繁触发文件长度增长新块分配的连锁开销。对KVM存储来说这条命令带来的性能收益比任何mount参数都明显。6.4 什么时候不该无脑上ext4ext4当然不是万能的。如果你面对的是超大规模的分布式存储、持久化内存设备、或者明确需要COW快照、压缩、去重这类高级功能的场景XFS、Btrfs以及更现代的文件系统各有各的主场。但就常规Linux服务器、虚拟化镜像、单机数据库而言默认ext4几乎不会错。它不是功能最全的却是可预期性最好的稳定、文档多、出问题时随便搜一下就能找到历史案例。前几年我把公司所有虚拟机的存储后端统一从ext3迁到ext4过程不算顺利也踩过上面说的坑但长期运行下来的核心改善是看得见的存储监控里的平均IO延迟下降了百分之三四十SSD写放大明显降低再也没出现过开机fsck一整夜的救火事件。文件系统不会给你炫目的跑分报表它最好的表现就是不打扰你睡觉。这次的演进梳理到这里就收尾了。下一篇我打算把ext4的extent树和jbd2事务提交机制分别拆开用结构体级别的方式一路讲到底顺带聊聊ext4数据丢失那类故障消息到底是怎么产生的。如果你正在用ext4做生产存储或者正纠结要不要从ext3迁过来欢迎带着实际问题来一起对答案。
返回列表