资讯动态

fallocate深度测评:文件系统预分配的快慢、虚实与坑

发布时间:2026/10/21 0:12:09 来源:尧图企业网站定制
1. 为什么值得给预分配做一次细粒度测试fallocate 看起来实在太简单了一个命令、一个路径、一个大小参数文件的大小就“变”了。在本地测试机上跑 10G 的预分配几毫秒弹回来你很容易忽略它背后真正做的那些事——物理块是从哪里找出来的、读回来为什么是零、第一次写同一个位置时会不会更慢、快满盘时失败了会留下什么。这些恰恰是“文件系统预分配”里最值得较真的地方。我把 fallocate 放到不同文件系统、不同模式、不同写入条件下拆开测了一遍这篇就是那份细粒度测试的完整记录。先说清楚这个测试解决什么问题。生产环境里我们经常用预分配来“抢地盘”下载任务预留空间、虚拟磁盘一次性铺满大小、数据库表空间初始化时先占住容量。最常见的替代方案是 truncate 或者直接写全零文件。truncate 虽然快但空间并没有真实落到磁盘上首次写入会触发大量实时分配直接写全零文件呢速度慢到让人怀疑人生但胜在空间是真实落地且内容是确定的。fallocate 号称兼具两边的优点可它到底快在哪、占用是不是真的“预分配”、读零语义靠什么保证这些如果不实测心里始终没底。这篇文章适合存储运维、后端开发、虚拟化平台维护者也适合任何被“预分配后性能反而变差”这种问题折磨过的人。我会从测试坐标系的建立开始一步步把时间、空间、内容、边界行为四个维度拆开最后给出一份能直接复跑的脚本和常见坑排查表。2. 测试环境、工具链与基线脚本2.1 测试环境选择与挂载方式细粒度测试最关键的不是命令本身而是环境是否可控。我的测试机上准备了两块物理存储一块普通 SATA SSD一块 7200 转机械盘都分成独立分区后格式化为 ext4 和 xfs 各一份。为什么同时覆盖两种文件系统因为 fallocate 的具体实现是文件系统相关的。ext4 依赖 extent 机制配合 unwritten extent 标记xfs 同样有自己的延迟分配与预分配逻辑两者在失败回滚、碎片分布、写时转换上的表现并不一致。只在一个文件系统上得出结论很容易被环境局限带偏。挂载参数全部走默认不开 nobarrier、不调 allocsize因为这些参数本身会影响结果。如果你要复现建议至少准备一个 ext4 和一个 xfs 分区并记录内核版本和文件系统版本。命令如下mkfs.ext4 -F /dev/vdb1 mkfs.xfs -f /dev/vdc1 mount /dev/vdb1 /mnt/ext4 mount /dev/vdc1 /mnt/xfs文件系统版本差异可能导致同一模式的不同行为尤其是 KEEP_SIZE 和 PUNCH_HOLE 这类组合标志。测试前先确认支持情况fallocate --help | grep -i keep fallocate --help | grep -i punch2.2 观测工具从系统调用到块分布基准测试不能只看一个命令的耗时还要能回答“耗在哪”。我准备了四组观测工具。第一组是时间观测。/usr/bin/time -v可以拿到 wall clock 和最大驻留内存strace -T能精确打印每个系统调用的耗时这对判断 fallocate 内部到底是元数据操作还是数据写盘非常有帮助。第二组是空间观测。ls -l告诉我们逻辑大小du -B1告诉我们实际物理块占用stat -c %b %B %s从 inode 层面直接输出块数和块大小。三者的差异能立刻区分“真预分配”和“假预分配”。第三组是块分布观测。filefrag -v输出文件的物理 extent 列表和碎片率debugfs -R stat inode 设备能直接看到 ext4 的 extent 树结构xfs 则用xfs_db。这部分能看到 fallocate 分配出来的是一个连续大块还是散布多处的小块。第四组是IO层观测。fio 用来测预分配文件与未预分配文件在随机写下的延迟分布这个数据能暴露 unwritten extent 转换对业务 IO 的真实影响。一套命令打下来基本能把 fallocate 从“用户态调用”到“磁盘块分布”的完整路径摸清。2.3 可直接复跑的测试脚本我写了个半自动脚本按三种方式各建一个 10G 文件比较逻辑大小、物理占用和耗时#!/usr/bin/env bash set -euo pipefail TARGET/mnt/ext4 SIZE10G COUNT$((10 * 1024)) echo fallocate rm -f $TARGET/falloc.img /usr/bin/time -v fallocate -l $SIZE $TARGET/falloc.img 21 | grep wall clock stat -c logical%s blocks%b block_size%B $TARGET/falloc.img echo dd write rm -f $TARGET/dd.img /usr/bin/time -v dd if/dev/zero of$TARGET/dd.img bs1M count$COUNT oflagdirect statusnone 21 | grep wall clock stat -c logical%s blocks%b block_size%B $TARGET/dd.img echo truncate rm -f $TARGET/trunc.img /usr/bin/time -v truncate -s $SIZE $TARGET/trunc.img 21 | grep wall clock stat -c logical%s blocks%b block_size%B $TARGET/trunc.img脚本里的oflagdirect是刻意的为了绕开页缓存干扰拿到真实写盘耗时。truncate 那行也保留下来作为“逻辑上也算预分配”的对照组。对着结果看后面每一条都能讲出故事来。3. 时间基准fallocate 为什么是“瞬移”3.1 10G 文件的三种分配方式耗时对比我在同一块 SATA SSD 的 ext4 分区上连续跑了五次取中间值量级大致如下方式逻辑大小物理块占用耗时量级是否真实占盘truncate -s 10G10G0微秒级否fallocate -l 10G10G约 10G毫秒级是dd 写入 10G 全零10G约 10G数十秒级是第一次看到这个结果的人通常会惊讶truncate 快可以理解因为只改了 inode 里的文件大小字段磁盘一个数据块都没分配dd 慢也可以理解因为要把 10G 的零实打实写一遍fallocate 居然也是毫秒级和 truncate 几乎同量级可它却能把物理块全部占住怎么做到的核心答案是fallocate 走的是“元数据分配 未初始化标记”的路径而不是“数据写盘”的路径。用户态一个系统调用进去文件系统在空闲块里找到足够的物理块范围更新 inode 的 extent 树把这段范围标记为 unwritten extent然后就返回了。整个过程不碰数据扇区省掉了最耗时的写盘环节自然快。3.2 unwritten extent快背后的机制与代价unwritten extent 这个概念值得好好展开。它本质上是文件系统在 extent 树上做的一个状态标记物理块已经分配出去了也有明确的逻辑范围对应但这些块在逻辑上还没有被真正写入数据。用户读取这块范围时文件系统不跑盘直接填零返回用户写入这块范围时文件系统先把状态转换为普通 written extent再把数据落盘。生活化一点类比fallocate 相当于你提前在停车场租下了一整片区域工作人员在系统里把这片区域标记为“已预订”但停车位地面的划线、指示牌都还没做。租用记录已经生效账面容量扣掉了。第一次有车开进来时才真正完成划线、铺设地面等物理动作这就是 unwritten extent 的“转换”。明白这个机制后预分配文件“首次写入可能比预想中慢”就说得通了。写入一个未初始化的 extent 时内核要把对应范围的页缓存先清零、再填上你的业务数据、最后执行 extent 状态切换并提交日志。一次 4KB 随机写入的耗时通常还在微秒到毫秒级影响不明显但如果业务上来就是大范围的随机写每次写入都触发成片的 extent 转换IO 延迟的尾巴会被明显拉长。这一点在 3.3 的实验里能直接看到。3.3 一个小实验预分配后的首次写入开销我用 fio 做了一组对照。同一个 8G 文件一份是用 fallocate 预分配好的一份是普通按需分配的文件两个都跑纯随机 4K 写direct1队列深度 1覆盖整个文件范围fio --namerandwrite-falloc --filenamefalloc.img \ --ioenginelibaio --rwrandwrite --bs4k --size8G \ --direct1 --iodepth1 --runtime60 --time_based \ --output-formatnormal fio --namerandwrite-normal --filenamenormal.img \ --ioenginelibaio --rwrandwrite --bs4k --size8G \ --direct1 --iodepth1 --runtime60 --time_based普通文件在前一段的随机写过程中每写到一个新块都得先实时找空闲块、更新位图、再写数据IO 延迟的分布比较散。预分配文件省掉了“找块”这一大步常规写延迟更平稳。但仔细观察 clat 分布的长尾部分预分配文件的 P99 在某些文件系统上反而有尖刺。尖刺来源就是 unwritten extent 转换原本一整段连续未初始化块被随机写打得七零八落extent 树需要不断进行分裂和合并操作每一次分裂加日志提交都是延迟尖峰。这个实验给我最大的提醒是预分配解决的是“分配路径上的延迟”它不会帮你消灭“状态转换路径上的延迟”。如果你的业务是一次性顺序写满文件预分配收益最大如果是长时间随机小写初始化完一波之后反而要留意 extent 树碎片化带来的深度增长。4. 空间与内容层面的细粒度观测4.1 du、ls、stat 三个视角的差异同一个 test.img用三个命令看会有三种观感。ls -l显示逻辑大小经过 fallocate 后是 10Gdu -B1统计文件占用的物理块字节数fallocate 后一般也接近 10Gstat -c %b %B %s则把物理块数和块大小直接拆开给你看。这三者一旦出现明显差异说明“预分配”并不彻底。truncate 出来的文件就是典型反例ls -l显示 10Gdu -B1却是 0因为文件系统把所有范围都当成“未分配空洞”物理块一个都没占用。fallocate 和它最大的区别就在这里逻辑大小和物理占用同时发生变化这才能叫占盘。仔细想想这个“占盘”对上层业务意味着什么意味着文件系统在后续分配时会把这些块排除在空闲空间之外意味着不管写不写数据这块盘的空间确实扣掉了意味着 fstrim 之类精简动作也不能随随便便把这些块回收。预分配的本质就是“预先承担存储成本换取后续写入路径的稳定性”。4.2 在 ext4 的 extent 树里到底发生了什么想看文件系统内部怎么记录这些块debugfs 是最好用的工具。先拿到 inode 号再 dump 元数据INODE$(stat -c %i /mnt/ext4/falloc.img) debugfs -R stat $INODE /dev/vdb1输出里能看到 extent 树的起始信息但直接输出对新手不太友好。实际观察中更直观的还是 filefragfilefrag -v /mnt/ext4/falloc.img对于一个布局良好的 10G 预分配文件ext4 通常会分配几个连续的物理区间显示成几个大 extent。如果文件系统碎片化严重区间会被拆成几十段甚至上百段逻辑上还是 10G物理上分散得很。碎片化会直接影响后续顺序读性能尤其是机械盘场景。这里我要强调一个容易被忽略的点fallocate 本身不承诺分配“物理连续”的大块它只承诺分配“足够大的总量”。文件系统当前空闲空间的连续性决定了一切。所以生产环境里预分配之前最好先做一次空间整理或者至少检查一下文件系统碎片率否则你预分配出来的可能是一盘散沙。4.3 读取预分配区域为什么“看”到的是零对 fallocate 生成的文件做读取测试很多人会疑惑明明磁盘上没有写过数据为什么读出来全是零这就要回到 unwritten extent 的语义。逻辑块与物理块已经有映射但映射状态是“未初始化”。读路径遇到未初始化的页不会发起真实的物理块读取而是直接在页缓存里生成一个全零页面返回给用户空间。所以预分配文件的“读零”不是磁盘扇区本来就是零而是文件系统在逻辑层给你保证一个确定的零视图。这个保证也带来一个副作用预分配之后你立即对整个文件做一次顺序读实际上不会产生对应的物理 IO速度反而比读真实数据文件快得多。有人把这种现象当成“预分配了但又没完全预分配”其实是误解。空间是真实占住的只是读路径被文件系统优化掉了。如果就是要物理盘上也确定全零可以配合 ZERO_RANGE 这类标志或者干脆用写零的方式来初始化。读零语义和物理零是两回事这一点在做合规审计或安全擦除时要格外注意。5. 边界场景与模式组合的实际表现5.1 磁盘空间不足时的部分失败行为细粒度测试不能只看顺利路径还得看失败路径。我用一个小 loop 文件模拟接近满盘的分区创建一个 2G 的镜像文件格式化后挂载写入大量小文件把剩余空间压到低于 5%然后尝试预分配一个 300M 的文件。dd if/dev/zero of/tmp/small.img bs1M count2048 mkfs.ext4 -F /tmp/small.img mount -o loop /tmp/small.img /mnt/small # 填充到剩余空间不足 dd if/dev/urandom of/mnt/small/fill bs1M count80 fallocate -l 300M /mnt/small/big.img echo $?失败路径下fallocate 会返回 ENOSPC但文件的最终状态取决于文件系统实现和具体版本。有些情况是在确认没有足够连续空闲块后直接拒绝不留下任何可见分配有些情况可能已经改写了文件大小或者留下了部分已分配的范围。我的建议很明确不要把 fallocate 当成无副作用的原子操作。空间紧张时先检查 df再在业务代码里捕获失败并做文件清理最稳妥的方式是先 fallocate 到临时路径再 rename 到目标路径避免业务入口处看到半个预分配文件。5.2 KEEP_SIZE容易被误用的“塔防式预分配”KEEP_SIZE 是 fallocate 的隐藏玩法。默认模式下预分配会把文件逻辑大小扩展到 offsetlen加上-k之后文件大小保持不变纯粹“占住”物理空间。我用一个小文件演示过fallocate -l 1G -k /mnt/ext4/hidden.img ls -l /mnt/ext4/hidden.img # 仍然显示 0 du -B1 /mnt/ext4/hidden.img # 显示约 1G这个组合很适合一个场景你想让某个文件“以后能快速释放出预分配空间”但逻辑大小必须保持为 0方便其他逻辑判断“文件还没有数据”。但也很容易踩坑团队里有人只看了ls -l判断文件大小没有意识到物理空间已经被扣掉于是在一个满盘系统里拼命创建这类文件等 df 报警时已经晚了。排查这类问题最直接的命令就是du加df对照。遇到“ls 显示很小、df 空间却少了”的怪现象优先怀疑 KEEP_SIZE 或 PUNCH_HOLE 打洞操作。5.3 PUNCH_HOLE从预分配到空间回收如果 KEEP_SIZE 是“只占空间不改大小”PUNCH_HOLE 就是反向操作释放空间同样不改逻辑大小。在已预分配的文件中间打洞fallocate -p -o 0 -l 5G /mnt/ext4/falloc.img du -B1 /mnt/ext4/falloc.img打完洞之后文件前 5G 变成“未分配空洞”读取返回零但ls -l的大小还是 10G。这个能力对数据库分区归档、日志文件裁剪非常有用不用重命名文件、不用重新 seek直接在文件内部按范围释放空间。但打洞也有代价它会破坏原有 extent 的连续性进一步增加文件碎片。在某些存储场景下PUNCH_HOLE 还会对精简分配的快照产生影响因为打洞本质上是在告诉存储“这块空间我不需要了”如果底层是精简供给的卷这个动作可能反过来放大碎片。业务上需要打洞时建议先评估文件碎片率变化再决定是否值得。5.4 碎片与 extent 退化的测量碎片化对预分配文件的影响是可量化的。我做了一个压力测试在一个文件上反复增加预分配区域然后打洞掉中间一小部分接着再追加预分配循环几十轮。每轮之后用 filefrag 观察 extent 数量和碎片率。for i in $(seq 1 50); do fallocate -l 200M /mnt/ext4/frag.img fallocate -p -o $((i * 100))M -l 10M /mnt/ext4/frag.img filefrag -v /mnt/ext4/frag.img | tail -1 done结果非常典型extent 数量从个位数慢慢涨到几十甚至上百。ext4 的 extent 树会为了维护这么多零散区间变深文件系统读写路径在做范围查找时的 CPU 开销也随之上升机械盘上的表现更明显。这个测试让我意识到预分配虽然能避免实时分配但它并不能代替存储层面的空间整理对大文件做周期性碎片检查依然是必要的维护动作。6. 常见坑与排查速查实录6.1 几个让人直挠头的现场现场一某脚本调用 fallocate 创建了一个 100G 的文件返回成功但 df 剩余空间几乎没变化。排查时看了一眼命令发现用了-k。去掉 KEEP_SIZE 后空间立即被真实占用。教训是读任何预分配相关脚本先确认有没有 KEEP_SIZE。现场二预分配文件读起来极快业务方怀疑是不是没有真正写盘、会不会丢数据。解释清楚 unwritten extent 的读零语义后对方仍不放心最后用 fio 在文件上跑一轮写覆盖加校验确认数据完整。这种情况不是故障是语义理解问题。现场三某存储服务用 fallocate 给新块设备预分配了 10T 空间刚创建完监控发现底层物理存储使用率飙升。原因是这套存储本身就是精简分配上层 fallocate 直接把空间“实占”了精简效果完全被破坏。后来方案改成按需增长不再在薄卷上做全量预分配。这个案例对云原生环境下使用预分配有很强的警示作用。6.2 快速排查命令速查表现象可能原因排查命令处理建议fallocate 成功但 df 空间未减少使用了 KEEP_SIZE 模式du -B1 file确认是否真的需要 keep 语义文件很大但占用很小truncate 或空洞文件filefrag -v file需要真实占盘时换 fallocate预分配后首次写入延迟高unwritten extent 转换fio 测 clat 分布评估是否分批预分配或预热预分配后读全文件极快未初始化 extent 返回零页strace -e read观察 IO物理全零需额外做写零filefrag 显示大量碎片空间不连续或打洞过多filefrag -v file整理空间或考虑文件重写fallocate 返回 EOPNOTSUPP文件系统不支持该模式fallocate -l 1M /tmp/t改用 dd 或考虑换文件系统薄卷使用率瞬间暴涨预分配破坏精简语义检查存储层分配状态按需增长避免全量预分配6.3 什么时候该用、什么时候不该用给一段基于实测的个人判断。需要保证首次写入延迟稳定的大文件场景比如虚拟机磁盘镜像、数据库数据文件、下载任务占位文件fallocate 是利器。它会提前把“找块”这步做完业务写入路径上少一层不确定性。反过来场景是频繁覆盖写的临时文件、追求精简空间的缓存目录、底层已经是精简供给的存储卷我建议慎用甚至不用 fallocate。预分配把空间成本提前支付了如果文件生命周期很短收益完全覆盖不了副作用。文件很小时也不值得用。为几十 KB 的文件预分配分配路径上的耗时差距可以忽略但空间碎片和管理复杂度是实打实多出来的。小文件走普通写路径就好。7. 实操心得与扩展建议在我自己环境上反复跑过这些测试之后一个很深的体会是fallocate 是个好工具但它不是“无脑提速器”它是存储空间管理策略里的一环。理解 unwritten extent 是理解整个工具的关键理解了它你就能解释为什么预分配能几毫秒建出 10G 文件、为什么读预分配区域比读真实数据快、为什么首次写入可能遇到延迟尖刺。这些现象背后是同一条机制摆在一起看就完全自洽。最后分享一个小技巧尤其适合做磁盘镜像或者数据库表空间初始化时使用。一次性把整个文件 fallocate 到底不如分批次做先 fallocate 一个 1G 的文件写完业务数据后再在原文件上追加 1G循环执行。这样既享受预分配的快路径又避免把海量 unwritten extent 一次性暴露给后续随机写减少转换时段的延迟抖动。配合 fio 验证一下效果会很明显。如果你也想在自己的环境里复现这套测试建议把脚本里的 SIZE 改小一点先跑通再把 10G 换成你的业务真实大小。不同文件系统、不同存储介质、不同内核版本得出的数据都会有差异但现象背后的机制是一致的。跑完一轮你再看到 fallocate就不会只把它当一个“占位命令”了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑