上周迁移一批静态资源的时候我又撞见了那个特别熟悉的场面源服务器上某个目录du -sh显示 12Grsync 同步到新机器之后du -sh变成了 16G。文件内容一模一样用ls -l逐个求和两边也对得上唯独du对不上。这不是玄学也不是机器坏了而是ls和du在“大小”这两个字上的定义压根就不是一回事。这篇就把这个老问题彻底讲透ls显示的到底是什么du统计的又是什么哪些原因会让两个“同内容”的目录表现出不同的尺寸以及真遇到的时候该怎么一步步排查。1. 先复现场景数据一样为什么大小不一样1.1 一个典型到不能再典型的操作先说我最早是怎么碰上的。当时在做目录迁移流程很简单rsync把目录 A 推到目录 B然后例行用du -sh核对两边体积。结果就出现了开头那张截图一样的现象A 是 12GB 是 16G。我当时第一反应是 rsync 漏了什么于是跑了rsync -av --dry-run复查文件数一致、增量列表为空又跑了一遍md5sum对比抽样文件哈希完全一致。数据没问题文件内容确实一样可du的结果差出 4 个 G。后来仔细一查问题出在源目录里有好几个用truncate预分配出来的稀疏文件比如日志占位文件、虚拟磁盘镜像。源目录迁移前用du统计时这些文件按“真实占用块数”算每个才几 KBrsync 同步时没有加-S/--sparse选项目标端就把稀疏文件的空洞全部填成真实数据块于是几十 GB 的“逻辑大小”在目标端变成了实打实的磁盘占用。1.2 用最小实验让差异现形不用等生产环境出问题本地就能复现。我在临时目录建 2000 个 100 字节的小文件然后对比两种统计方式mkdir -p test/a cd test/a for i in $(seq 1 2000); do head -c 100 /dev/urandom f$i; done cd .. du -sh test/a cd test/a ls -l | awk NR1 {s$5} END {print s}我这边实测结果大概是du -sh test/a - 8.1M ls -l 求和 - 200000约 200K2000 个 100 字节的文件ls求和是 200K 左右du却显示 8.1M。差哪了每个文件在文件系统里至少要占一个 4K 块100 字节的文件也要占 4096 字节2000 个文件就是 8MB 左右再加上目录项正好对上。这就是第一层认知ls按文件内容的字节数来算du按磁盘上实际被占用的块来算。1.3 别忘了比较方式本身的坑还有一种更常见的“假差异”拿ls -l看一个目录和用du -sh看同一个目录。很多人以为ls -l显示的目录大小就是里面所有文件的总和实际上不是。ls -ld 目录显示的是“目录文件本身”的大小也就是目录这个数据结构占了多少空间通常从 4096 字节起步文件特别多时会涨到几 MB。而du -sh 目录是递归统计目录下所有文件、子目录、甚至目录项的块数总和。所以如果某个人问“为什么我ls -ld看目录只有 4096du -sh却有 1G”这根本不是什么隐藏问题纯粹是两种统计口径。真正能拿来对比的是ls -l对单个文件的大小和du对同一批文件占用的块数。2. ls 和 du 的“大小”根本不是同一个概念2.1 inode 里的两个字段st_size 和 st_blocks要彻底搞懂这个问题得看文件在文件系统里的最基本记录——inode。inode 里保存了文件的权限、所有者、时间戳、数据块位置等信息其中有两个字段直接关系到本文的主题st_size文件逻辑大小单位是字节。这是“文件读出来有多少数据”应用层 read、网络传输、HTTP 的 Content-Length都是用这个值。st_blocks文件实际占用的磁盘块数量单位固定是 512 字节。内核用这个值乘以设备扇区大小就能算出文件占了多大磁盘空间。ls -l读的是st_sizedu默认读的是st_blocks。这就是两者最根本的差异。用stat命令可以直接看到这两个字段$ stat testfile File: testfile Size: 100 Blocks: 8 IO Block: 4096 regular file这里Size: 100是st_sizeBlocks: 8是st_blocks8 × 512 4096 字节也就是文件实际占用的磁盘空间。100 字节的内容因为要对齐 4K 块只能占一个整块。2.2 它们各自服务什么目的ls的大小是给人看的“内容大小”文件内容多少字节压缩包解压后多大视频文件多大网页返回多少数据。这在传输、计费、内容长度判断的场合非常有用。du的大小的“占用大小”实际在磁盘上划走了多少空间。这在磁盘容量规划、备份体积估算、配额统计、df看剩余空间这类的场景里才有意义。可以打个比方ls相当于房产广告里的“使用面积”du相当于房本上的“建筑面积”中间差着公摊。小文件多相当于公摊高的房子两个数字差距一眼就能看出来。2.3 为什么大多数时候两个数字看起来差不多如果文件都比较大比如单个文件 100MB块大小是 4K那么文件会占用 25600 个块多出来的尾块最多也就 4K误差占比只有 0.004%肉眼根本看不出来。一旦文件变小误差就放大了。一个 1 字节的文件ls看到 1du看到 4096差了 4096 倍。所以“目录里小文件越多ls和du的差距越明显”是一条非常实用的经验。3. 会让两个同内容目录大小不一致的真实原因3.1 稀疏文件逻辑大小和物理占用的典型分家先看这类最容易让人懵的文件。用truncate或者dd的seek写法可以创建一个“带空洞”的文件truncate -s 10G sparse.bin ls -l sparse.bin du -sh sparse.bin我实测的输出是-rw-r--r-- 1 root root 10737418240 sparse.bin du -sh sparse.bin - 0ls显示 10Gdu显示 0。因为这个文件从头到尾都是空洞内核并没有给它分配任何数据块只是在 inode 里记录了一个 10G 的逻辑大小。稀疏文件在虚拟磁盘镜像、数据库预分配文件、日志占位文件里很常见。它是合法的、高效的但如果用普通方式拷贝或打包就很容易变成“膨胀怪”。cp默认带--sparseauto会自动探测空洞tar不带--sparse就会拍到实数据rsync不带-S也会把空洞填平。这直接导致“同样内容的两个目录一个 du 是 10G另一个是 20G”。3.2 硬链接同一个 inode 被 ls 数了两遍du 只数一遍另一个高频原因是硬链接。硬链接的本质是让多个目录项指向同一个 inode并不复制数据。看起来是两个文件底层只有一份数据块。echo hello a.txt ln a.txt b.txt ls -l a.txt b.txt du -sh .ls -l会把a.txt和b.txt各显示一遍完整大小比如各 4K合计 8K而du默认只统计唯一 inode 一次所以显示 4K。换句话说ls按文件名数du按真实数据块数。这就出现了一个很有意思的错觉同样一批文件如果某个目录里硬链接多du会明显小于“ls 求和”。反过来如果你用不支持硬链接的方式拷贝比如直接走网络协议逐个传目标端的硬链接全部变成了普通复制du就一下子大了。文件内容没变占用的空间却变了。3.3 文件系统块大小的舍入效应前文的小文件实验已经展示了这一点。文件系统分配空间的单位是块块大小常见的有 1024、2048、4096 字节甚至 64K、128K某些大块文件系统。块越大小文件浪费越严重文件数量单文件大小ls 合计4K 块文件系统 du 表现1000100 B100 KB约 4 MB10003.5 KB3.5 MB约 4 MB1000100 KB100 MB约 100 MB同一个目录如果放在块大小不同的两个文件系统上du结果可能完全不同。比较“同内容的两个目录”时一定要先确认两边的文件系统块大小。3.4 目录项、间接块和元数据也被 du 算进去了du统计目录时不只是数文件数据块还包括目录自身的块目录也是文件它保存子项列表也要占块文件多时目录文件可能占好几 MB。大文件的间接块/扩展树文件数据太大管理这些数据的索引块也要占空间ls不会体现du会计入。子目录的目录项每个子目录自身也是一个或多个块。所以即使文件内容完全一样只要两个目录里的文件分布粒度不同比如一个目录塞满 10 万个小文件另一个把同样内容拆到 1000 个子目录里两者的du也会差出不少。ls对这个是无感的它只看每个文件自己的st_size。3.5 压缩、去重、reflink 和跨盘拷贝现代文件系统还有几重变数。有些文件系统支持透明压缩比如常见的 CoW 文件系统开启压缩后ls显示的是未压缩的逻辑大小du显示的是压缩后实际占用几倍差距很正常。cp的 reflink 功能cp --reflinkauto会让复制出来的文件共享底层数据块du可能只各占一半甚至更少。还有去重、overlay 文件系统只看最上层等场景都会让“内容相同但 du 不同”这件事变得完全合理。于是两个目录同内容但大小对不上通常可以从这几个大类里找原因有没有稀疏文件、有没有硬链接、两边文件系统块大小和特性是否一致、拷贝工具是否保持了稀疏/硬链接/压缩属性。4. 排查手段把差异来源一步步拆出来4.1 stat 是第一个要用的工具遇到对不上的情况第一步不是猜而是拿stat看具体文件的关键字段stat -c 文件名%n 逻辑大小%s 占用块%b 每块字节%B somefile # 输出示例 文件名sparse.bin 逻辑大小10737418240 占用块0 每块字节512 文件名small.txt 逻辑大小100 占用块8 每块字节512占用块为 0 或远小于逻辑大小换算出的块数基本就是稀疏文件。如果两个文件逻辑大小相同、占用块数不同那多半是文件系统块大小或压缩特性不同。4.2 用 du --apparent-size 一键对照逻辑大小GNU 的du自带一个很实用的选项直接按st_size统计du -sh --apparent-size 目录 du -sh 目录两条命令对比一下差距极小说明只是块舍入误差正常现象。差距很大说明目录里有稀疏文件、压缩文件或底层有去重/reflink。这个对照是排查的第一步几乎能立刻把问题分成两类。4.3 找出罪魁祸首稀疏与硬链接确认存在明显差距后用find直接定位具体文件。找稀疏文件可以按稀疏率排序find . -type f -printf %S\t%s\t%b\t%p\n | sort -n | head -20%S是稀疏率等于st_blocks*512/st_size越接近 0 表示越稀疏。正常文件这个值接近 1稀疏文件可能只有 0.001 甚至 0。找硬链接按链接数过滤find . -type f -links 1 -printf inode%i 链接数%n 大小%s 路径%p\n链接数大于 1 的文件就是多个目录项共享同一份数据。4.4 一份速查表目的命令查看文件的逻辑大小与占用块数stat -c %s %b 文件du 按逻辑大小统计du -sh --apparent-size 目录du 按实际占用统计du -sh 目录查稀疏文件find . -type f -printf %S %p\n | sort -n | head查硬链接find . -type f -links 1 -printf %i %n %p\n查看文件系统块大小stat -f -c %S 目录或blockdev --getbsz 设备拷贝时保留稀疏cp --sparsealways、rsync -S、tar -S --sparse这套组合拳打下来大部分“同内容目录 du 不一致”的问题都能定位到具体文件或文件系统属性上。5. 实操中我踩过的坑和现在的排查习惯5.1 备份脚本把稀疏文件撑爆的教训我经历过最痛的一次是某次打包归档目录里有好几个稀疏的日志占位文件单个逻辑大小都有几十 GB。打包脚本直接tar czf没有加--sparse。结果就是源目录du只有 3G打出来的压缩包 20 多 G差点把备份盘塞满。后来养成的习惯是凡是涉及备份、迁移、镜像制作的命令默认带上稀疏保留参数。tar用-Srsync用-Scp至少保证--sparseauto。不要赌数据里没有稀疏文件。5.2 磁盘容量监控要认准 du还有一次监控告警显示某台机器磁盘使用率异常升高排查半天结果是某个应用一直在给日志文件truncate又重新写入文件逻辑大小看着很大实际占用的块并不多。监控脚本用的是ls -l求和来估算自然就虚高了。从那以后凡是要判断“磁盘还够不够”、”备份要占多少空间”、“目录能不能放下”一律只看du不看ls。反之凡是要判断“这个文件传输要多少流量”、“web 服务返回多大内容”就看ls的逻辑大小。5.3 我现在遇到对不上的第一反应这几年再遇到两个同内容目录大小不一致我不会再怀疑文件丢了而是按固定顺序走先du -sh --apparent-size和du -sh各跑一遍判断是否稀疏/压缩类问题。再stat -f看两边文件系统的块大小是否一致。然后用find扫稀疏状态和高链接数定位具体文件。最后确认拷贝工具是否带了-S等保留参数的选项。这样基本能在十分钟内给出结论。说白了一句话ls和du没有谁对谁错它们只是用不同的尺子量同一批文件。你只要知道手里拿的是哪把尺子就不会再被这两个数字吓到了。
阅读完成 · 觉得有帮助?