磁盘明明还剩几十个GB,创建个大文件却提示“No space left on device”;删了一个好几GB的日志,df -h 一看,可用空间纹丝不动。如果你在 Linux 上遇到过这种让人抓狂的场面,那说明你对“存储堆栈”的理解还停留在表面。这篇内容,就是想带你把这个堆栈从头到尾捋一遍。
这里不罗列命令大全,而是站在一个常年跟 Linux 服务器打交道的运维视角,把块设备、分区、文件系统、挂载点、IO 调度这些概念串起来,讲清楚一个文件从写入到底层磁盘之间发生了什么,以及当空间消失、IO 飙升时应该怎么排查。无论你是刚接触 Linux 的新手,还是被存储问题折磨过的初级运维,这篇文章都值得花十分钟读完——读完你至少能搞懂系统提示“磁盘满”时,到底是谁满了。
1. 一张图看懂存储堆栈:从硬件到应用经历了什么
很多人把 Linux 的存储管理理解成“分个区、格个盘、挂载一下”,这没错,但这只是最外层操作。实际上,一个写操作从应用程序发起,到真正落到磁盘的磁道上,中间至少经过五六个层次。理解这个层次结构,后面所有的排查和调优才有依据。
1.1 存储堆栈的五个核心层次
从下往上看,整个存储堆栈可以这样拆:
- 物理设备层:这是最底层的硬件,包括 SATA/SAS 硬盘、NVMe SSD、U 盘、SD 卡等。操作系统通过驱动识别它们,并在
/dev目录下生成对应的设备文件,比如/dev/sda、/dev/nvme0n1。 - 块设备层:Linux 把物理设备抽象成块设备,这是内核与硬件交互的统一接口。块设备以固定大小的块为单位读写,通常 512 字节或 4K。这一层还负责 IO 调度——也就是决定先处理哪个读写请求。
- 分区与设备映射层:在块设备之上,你可以把一整块磁盘划分成多个分区(
/dev/sda1、/dev/sda2),也可以通过 LVM、RAID、dm-crypt 等机制做逻辑卷管理、磁盘冗余或加密。这一层是灵活的,普通场景可以跳过,但生产环境基本绕不开。 - 文件系统层:这一层负责把块设备组织成目录树,管理文件名、权限、元数据。Linux 上常见的有 ext4、XFS、Btrfs、ZFS 等,各有各的适用场景。
- 虚拟文件系统层(VFS):这是 Linux 内核提供给用户空间程序的统一接口。不管你底层用的是 ext4 还是 XFS,
open()、read()、write()这些系统调用长得一模一样。VFS 还会维护页缓存(Page Cache),这也是后面要讲的“删了文件空间不释放”的根源之一。
1.2 一个写请求的完整旅行
举个实际的例子。你在终端执行:
bash复制echo "hello" > /mnt/data/test.txt
这条命令背后发生的事情是:
- shell 调用
write()系统调用,把“hello\n”这个字符串交给 VFS。 - VFS 检查文件权限和页缓存。如果数据不大,大概率不会立刻写磁盘,而是先写进 Page Cache,标记为脏页。
- 脏页在后台由
pdflush(老内核)或writeback机制刷到文件系统层。 - 文件系统把逻辑偏移量翻译成块设备上的物理块地址,通过块设备层提交 IO 请求。
- 块设备层的 IO 调度器对请求排序、合并,发给驱动。
- 驱动把命令发给磁盘固件,磁盘完成读写。
你看,用户态程序跟硬件之间隔了这么多层。每一层都可能出问题——文件系统满了、inode 耗尽了、块设备只读挂载了、磁盘本身坏了、页缓存太多导致写不进去。理解了这条链路,你排查问题时就能按层定位,而不是瞎猜。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先从底层说起:块设备、分区和系统盘命名
这一节把第一层和第二层铺开讲。很多新手最容易困惑的就是盘符命名:为什么有的叫 /dev/sda,有的叫 /dev/vda,还有的叫 /dev/nvme0n1p1?搞懂这个,后续操作才不会对着错设备执行命令。
2.1 设备命名规则背后的历史包袱
Linux 的设备命名看起来乱,其实有迹可循:
| 设备类型 | 命名示例 | 说明 |
|---|---|---|
| 老式 IDE 硬盘 | /dev/hda, /dev/hdb | 现在已经很少见 |
| SATA/SCSI 硬盘 | /dev/sda, /dev/sdb | 字母 a 表示第一块,b 表示第二块 |
| 虚拟化环境磁盘 | /dev/vda, /dev/vdb | virtio 驱动,常见于 KVM 虚拟机 |
| NVMe SSD | /dev/nvme0n1, /dev/nvme0n2 | 控制器编号+命名空间编号 |
| SD/MMC 卡 | /dev/mmcblk0 | 树莓派、嵌入式设备常见 |
| LVM 逻辑卷 | /dev/mapper/vg-lv | 或 /dev/vg/lv 软链接 |
这里有个重要的坑:/dev/sda 这种命名并不固定。如果你给机器加了块盘,或者启动时设备探测顺序变了,盘符可能错位。比如原本的 /dev/sdb 在重启后变成 /dev/sdc,如果 fstab 里写死了盘符,系统可能挂载失败。所以生产环境挂载推荐用 UUID(blkid 命令查看)而不是设备名。
2.2 分区、LVM 与裸设备的选择逻辑
拿到一块新磁盘,摆在面前的有三条路:
第一条路:直接格式化整盘。把 /dev/sdb 整个做成一个文件系统(比如 ext4),不分区。好处是简单,坏处是不灵活,以后想拆出别的用途得先备份数据。
第二条路:传统分区。用 fdisk 或 parted 工具划分分区表(MBR 或 GPT),得到 /dev/sdb1、/dev/sdb2。每个分区挂不同的目录。这种方式适合场景固定的个人机器。注意:超过 2TB 的盘必须用 GPT,MBR 最多支持 2TB。
第三条路:LVM 逻辑卷。先把物理磁盘做成物理卷(PV),再合并成卷组(VG),再从卷组里划逻辑卷(LV)。这是生产中我最推荐的方式。它的核心价值在于“弹性”——卷组空间不够时,可以加一块新盘扩展;逻辑卷快满了,可以在线扩容,不用卸载文件系统。这省下的可都是业务停机时间。
还有一个容易忽略的细节:分区对齐。现在 SSD 和 4K 扇区硬盘要求分区起始地址对齐到 1MB 边界,否则读写性能可能掉一半。用 fdisk 时保持默认起始扇区(2048 或更大)通常就没问题,但如果从旧工具或网上复制的老命令手写起始扇区,踩坑概率很高。
2.3 实践中查看磁盘信息的三板斧
刚接手一台陌生服务器,我一般先用三条命令摸清底细:
bash复制lsblk # 查看块设备树状结构、挂载点、大小
blkid # 查看设备的 UUID 和文件系统类型
fdisk -l # 查看分区表详细信息
lsblk 输出里的 sda 8:0 0 100G 0 disk 意思是块设备主设备号 8、次设备号 0,整盘 100G。下面的 sda1、sda2 是分区。如果看到 └─vg-root 这种缩进,说明底层用了 LVM,真实设备要通过 /dev/mapper/vg-root 访问。
3. 文件系统选型:格式化不只是执行一条 mkfs
到了文件系统这一层,选择就变得重要了。我见过太多人不管三七二十一,一律 mkfs.ext4,这其实损失了性能和功能上的很多可能性。Linux 主流的文件系统各有性格。
3.1 ext4、XFS、Btrfs 到底该选谁
ext4:稳定、保守、兼容性最好。无论是嵌入式设备、启动分区还是普通数据盘,它都不会给你惹麻烦。但它的扩展能力相对有限——单文件最大 16TB,文件系统最大 1EB(理论值),实际使用中主要看元数据性能。小文件多、并发量高的场景,ext4 表现一般。
XFS:主打大文件、高吞吐。MySQL、PostgreSQL、大数据组件(HDFS、Kafka)的数据目录基本都是 XFS。它分配空间时采用“延迟分配”策略,文件删除后空间释放比较快;对超大目录和超大文件的索引能力也比 ext4 强。但 XFS 有个短板:不能在线缩小,只能扩大。所以一旦你的 XFS 分区建小了,想缩回来只能备份重来。
Btrfs:自带快照、压缩、校验和、子卷管理,功能非常现代。它适合容器场景(docker 默认的 overlay2 底层就常用它)和需要定期快照的个人 NAS。但 Btrfs 在重负载下偶尔出现性能抖动,早期版本有过元数据损坏的坏名声。用之前先确认你的内核版本足够新,且对数据可靠性要求不是“绝对不能出任何问题”那种级别。
ZFS:如果你用的是支持它的系统(比如 TrueNAS、Ubuntu 也可以装),ZFS 的数据校验、池化存储、快照和复制能力无可匹敌。但它吃内存,需要 ECC 内存才能发挥最大优势,建议只在专门的存储服务器上用。
3.2 inode 与保留块:两个最容易忽略的“小容量坑”
文件系统格式完后,有两个参数直接决定你能存多少数据,很多人看都不看。
第一个是inode 数量。inode 是文件系统的索引节点,每个文件(包括目录)都要消耗一个。格式化时如果没显式指定 -i 参数,系统会根据分区大小自动推算。但如果你要存海量小文件(比如几十万个缓存文件、日志碎片),inode 可能先用完——这时 df -i 会显示 100%,而 df -h 的空间还有一大堆。实战里我见过缓存目录 inode 耗尽的典型案例,解决办法是重新格式化时加大 inode 密度:
bash复制mkfs.ext4 -T small /dev/sdb1 # 使用 small 类型的 inode 密度
# 或显式指定:mkfs.ext4 -i 4096 /dev/sdb1(每 4096 字节一个 inode)
第二个是保留块(reserved blocks)。ext4 默认保留 5% 的空间给 root 用户,目的是防止磁盘写满后系统连日志都写不了。但如果是 2TB 的大数据盘,5% 就是 100GB 被白白“扣掉”。对专门存储数据的分区,建议调成 0:
bash复制tune2fs -m 0 /dev/sdb1
不要觉得这个无所谓——我亲眼看到过数据节点报警说磁盘 95% 满,实际上 5% 是保留块。
3.3 挂载参数里藏着性能和安全
mount 命令不是只有 mount /dev/sdb1 /data 这么简单。往 /etc/fstab 里写挂载条目时,以下参数值得根据场景组合使用:
- noatime/nodiratime:禁止每次读文件时更新访问时间戳,能显著减少写放大,对数据库和高频读取场景提升明显。对绝大多数场景都推荐加。
- discard/ssd:SSD 上启用 TRIM 命令,让固态盘回收空闲块。但频繁 discard 可能带来性能抖动,也可以依赖定时
fstrim。 - barrier=1(ext4 默认开启):保证日志提交时数据顺序的可靠性。断电保护要求高的场景不建议关。
- nofail:挂载点设备不存在时跳过报错,而不是卡住启动流程。U 盘、移动硬盘、某些次要分区建议加。
fstab 示例:
code复制UUID=3f2a1e89-... /data xfs noatime,nodiratime 0 0
关于这一行最后的两个数字,解释一下:第一个 0 表示不参与 dump 备份,第二个 0 表示启动时不做 fsck 检查。但如果挂载的是 ext4 根分区,建议第二个数字写 1,确保启动阶段能自动检查文件系统一致性。XFS 一般写 0,因为 XFS 启动时不做常规 fsck。
4. 空间管理实操:df、du、lsof 三大命令的完整配合
从这一节开始进入真正的“排障实战”。标题里也提到了热搜词里很多人搜“linux删除文件后空间没释放”,这正是本节的重点。我们把工具和排查思路绑在一起讲。
4.1 df 与 du:为什么统计结果总对不上
df -h 报告的是文件系统的已用空间,du -sh /path 统计的是目录里实际占用的空间。两者不一致是常态,原因有两个层面:
第一,文件系统元数据也占空间,ext4 的日志、位图、inode 表都有开销,这部分 du 统计不到,只能靠 df 体现。
第二,已删除但被进程打开的文件。进程持有文件句柄时,即使文件名从目录树里抹掉了,占用的块也不会释放,直到进程关闭句柄。这是“删除不释放空间”最常见的原因。
第三,稀疏文件和延迟分配也会造成偏差。比如数据库的预分配文件,显示在 du 里可能很大,但实际占用的物理块少得多。
4.2 文件删了空间不释放:一条完整的排查链路
当你发现 df -h 显示某个分区空间持续很高,即使清理了大文件也没降下来,按这个顺序排查:
第一步,先确认是哪个分区满了:
bash复制df -h
df -i # 顺便看一下 inode 是否耗尽
第二步,找出当前被删除但仍被占用的文件:
bash复制lsof +L1 /data
命令的 +L1 参数会列出 link count 为 0 的文件,也就是删除了但还打开着。看输出的 COMMAND 和 PID 列,定位到具体进程。通常凶手是日志进程、数据库进程,或者一个正在写临时文件的后台任务。
第三步,针对性地处理:
bash复制# 查看进程打开了哪些文件(确认杀掉后果)
ls -l /proc/PID/fd/
# 安全的做法:让进程重新加载配置,轮转日志
kill -USR1 PID # 对于部分服务有效,如 nginx
# 或者直接重启服务
systemctl restart xxx
# 如果进程确实没用,可以直接结束
kill PID
注意:千万不要在 /proc/PID/fd/ 里手动删除符号链接文件,那是伪文件系统,不会释放空间,还可能引发未知错误。正确思路是“重启持有句柄的进程”。
4.3 空间没少,却无法写入:可能是 inode 满了
前面说过 inode,这里给一套完整的判断方法。当 df -h 显示空间还剩 50%,但应用报“No space left on device”时,第一反应看:
bash复制df -i /var/lib/docker
如果 IUsed% 是 100%,那就是 inode 耗尽。排查方向是找出哪个目录堆积了海量小文件,比如 Docker 的 overlay2 目录、邮件队列、上传临时目录、session 文件目录。用这条命令找出子目录文件数:
bash复制find / -xdev -type f | awk -F'/' '{print $2}' | sort | uniq -c | sort -rn | head -20
或者按目录统计文件数:
bash复制for dir in /home /var /tmp /usr /opt; do echo -n "$dir: "; find $dir -maxdepth 1 -type f | wc -l; done
定位到源头后,清理无用的旧文件、加大 inode 密度重新格式化,或者调整应用配置(减少文件数量)、改用对象存储等,都是后续选项。
4.4 用 find 定位大文件与大目录的正确姿势
空闲空间明明不少,但某个目录怎么都清理不动,这时候需要精准打击:
bash复制# 找出 /data 下最大的 20 个文件
find /data -xdev -type f -size +100M -exec ls -lh {} \; | sort -k5 -rh | head -20
# 找出 /data 下占用最多的一级目录
du -h --max-depth=1 /data | sort -rh | head
# 按时间维度清理 30 天前的日志文件
find /data/logs -type f -mtime +30 -name "*.log" -exec rm -f {} \;
经验提醒:如果目录里有大量文件(几十万以上),du 本身会跑很久。可以用 du -x --exclude=/proc --exclude=/sys / 2>/dev/null 慢慢等,或者改用 ncdu 这种交互式 TUI 工具,浏览起来高效得多:
bash复制yum install -y ncdu
ncdu /data
5. IO 性能的感知与调优:从寻道算法到监控命令
存储堆栈不只是“装得下”,还要“跑得快”。这一节涉及热搜词里的“linux设置磁盘寻道算法”——其实这个概念现在有点过时了,但理解它背后的原理,对理解现代内核 IO 行为很有帮助。
5.1 磁盘寻道算法(IO 调度器)的今与昔
机械硬盘时代,磁头在盘面上移动是最大的耗时来源。内核的 IO 调度器负责把一堆杂乱的读写请求重新排序,尽量让磁头按顺序移动,减少寻道时间。经典的调度器有三种:
- CFQ(完全公平队列):给每个进程分配时间片来访问 IO,保证公平性。适合桌面和一般服务器,但不适合数据库这种高并发随机读写。
- Deadline(限期调度):给每个请求设置期限,读请求优先,防止请求饿死。数据库场景下比 CFQ 强很多。
- NOOP(无操作调度器):请求基本按先来先服务处理,主要依赖下层硬件或设备自己排序。适用于 SSD 以及硬件 RAID 卡这种自带智能排队的场景。
查看当前调度器:
bash复制cat /sys/block/sda/queue/scheduler
运行中修改:
bash复制echo deadline > /sys/block/sda/queue/scheduler
让它开机生效,可以写到 udev 规则里,或者在 systemd 服务里执行。
但到了 NVMe SSD 时代,传统调度器的排序意义大幅降低,因为 SSD 没有磁头移动动作,随机读写和顺序读写的延迟差异很小。现在内核默认用 none(即 NOOP 的现代版本),直接把请求下发给设备,尽最大可能降低延迟。所以如果你搜“设置磁盘寻道算法”,在 2024 年后的主流发行版里,答案往往是“默认就行,别折腾”。
5.2 队列深度与 merge 参数:比调度器更值得关注
对 NVMe SSD,真正影响性能的吞吐指标是队列深度(queue depth),它决定了一瞬间能有多少个请求在途。可以这样看:
bash复制cat /sys/block/nvme0n1/queue/nr_requests
cat /sys/block/nvme0n1/queue/max_sectors_kb
块设备层还有个概念叫IO 合并(merge)/ 批处理(batch),相邻的请求会被合并成一个大请求。这也是顺序写吞吐高的原因之一。不需要手动改这些参数,但要明白:为什么测试工具(如 fio)要用不同的 --iodepth 来测?就是为了把设备的并行能力测出来。
5.3 用 iostat、iotop 和 pidstat 定位 IO 瓶颈
系统整体变卡,怎么确认是 IO 而不是 CPU 或内存?我的习惯是三连:
bash复制iostat -x 1 5 # 看每个设备的 util、await、svctm、avgqu-sz
iotop -o # 看哪些进程在读写,-o 只显示有 IO 的进程
pidstat -d 1 # 按进程统计 IO 速率
iostat -x 里的关键指标:
- %util:设备繁忙程度,接近 100% 不代表“坏”,对 SSD 可能只是说明打满了一部分并行能力。但机械盘 util 长期 90% 以上就是明显瓶颈。
- await:IO 请求从提交到完成平均等待毫秒数。机械盘正常在 5~15ms,SSD 应该小于 1ms。如果 await 很高但
svctm(真正服务时间)不高,说明请求在队列里排队,瓶颈在并发量而非设备本身。 - avgqu-sz:队列长度。持续超过设备能消化的值,随之 await 会涨。
定位到进程后,再去排查这个进程的日志目录、数据目录是否在同一块盘,是否跟其他重 IO 业务共用了一块盘,必要时把数据迁移到独立盘或上 RAID。
5.4 不要忽视 Page Cache 对 IO 假象的影响
最后再说一个特殊的坑:free -h 显示内存几乎被 cache 占满,很多人看着慌,其实这是正常的。Linux 总会尽量把空闲内存用作页缓存,加速文件读写。只有当应用程序需要内存时,内核会回收缓存。所以判断内存是否紧张要看 free -h 的 available 列,而不是 used 列。
但页缓存也带来了一个副作用:如果你做了大文件拷贝、解压、压缩,脏页写回期间 iostat 会看到大量 write 流量,即使你的程序已经结束。这也不是故障,是 writeback 机制在后台把缓存里的脏数据刷到磁盘。
手工强迫刷盘(不建议频繁做)可以用:
bash复制sync; echo 3 > /proc/sys/vm/drop_caches
drop_caches 写 1 清理页缓存,写 2 清理 dentries 和 inode 缓存,写 3 全清。生产环境尽量别动,除非你在做性能基准测试,需要清掉缓存消除干扰。
6. 从入门到排障:我总结的几个核心习惯
讲了这么多层,最后落回到工作习惯上。存储问题属于“平时不出事,出事就是大事”的类型——数据丢了、库起不来了、日志写不进去了,哪个都够喝一壶的。以下几条是我踩坑踩出来的经验,分享给你,能少走很多弯路。
6.1 操作前先备份元数据
无论对磁盘做什么操作,先留底:
bash复制# 备份分区表
sgdisk --backup=/root/sda_gpt.backup /dev/sda
# 记录当前挂载与 UUID
blkid > /root/blkid_$(date +%F).txt
df -h > /root/df_$(date +%F).txt
这些操作一分钟能做完,但在关键时刻能救命——我曾经碰到过一条误 mkfs 命令把整个数据盘格式化的场景,幸好有 blkid 记录和分区表备份,才能恢复出可用的结构。
6.2 别在磁盘只剩几个 G 的时候才做清理
内存不足有 OOM Killer 兜底,磁盘满了可没有类似的“优雅保护”。应用写不进数据,轻则报错,重则数据损坏。建议设置一个日常巡检监控,比如每个月跑一次:
bash复制df -h | awk 'NR>1 && $5+0 > 85 {print $0}' | mail -s "Disk Usage Warning" admin@example.com
或者用现成的监控工具(Zabbix、Prometheus node_exporter)盯住 node_filesystem_avail_bytes 指标,报警阈值设置在 20% 剩余空间以下。
6.3 不要碰你不知道作用的参数
网上随手抄来的 sysctl 参数、/sys 下的内核开关、fstab 的奇奇怪怪的选项,不理解就乱加,很容易把一台正常机器搞到重启后起不来。你想调什么,先想清楚三个问题:这个参数解决的问题是什么?默认值为什么不够用?调错了最坏影响是什么?想不清楚就不要动。
6.4 把排查链路练成肌肉记忆
再遇到“磁盘满了”的报警,你的反应应该是这样的:
df -h、df -i确认是空间还是 inode。lsof +L1查已删除未释放文件。du --max-depth=1逐层定位大目录。- 清理或扩展后,用
sync确认数据落盘。 - 复盘:这一步是什么操作导致问题的,下一次怎么提前预防。
这套链路你练熟了,处理一个磁盘告警的时间能压缩到五分钟以内,而不是手忙脚乱地到处翻找命令。
存储堆栈这东西,说到底是“层”的艺术——每一层解决上一层的问题,也为下一层提供基础。理解了层,你就能在任何一层出问题时保持冷静,知道该去哪里找原因,而不是把整台服务器重启一遍碰运气。希望这篇内容能在你排障时,成为手边那个靠谱的参考。
