先说个几天前刚遇到过的事。一块 RK3568 开发板,SD 卡里烧了 Ubuntu 根文件系统,启动倒是很顺利,结果一执行df -h,挂载根/的那个分区可用空间只剩 400 多 MB,而系统本身才刚装上不到三天。然后我想往/opt里放一个交叉编译工具链,直接提示“no space left on device”。这就是典型的“挂载根的磁盘空间太小”——不是说扩容按钮在哪里,而是你得先搞清楚:根文件系统被挂载到了哪个分区、这个分区到底有多大、空间是被什么吃掉的,以及在不重新烧写的情况下还能不能救得回来。
这篇文章我从实际排查顺序出发,把根分区空间不足的场景拆开揉碎,覆盖嵌入式开发板、虚拟机、服务器三类最常见情况。内容会包含具体命令、判断逻辑和基于常见实践的扩容方法。适合正在被/100% 占满吓到的新手,也适合想系统整理根分区管理思路的运维或开发。
1. 判断挂载根之前,先回答三个关键问题
很多人一看到根分区没空间,第一反应就是rm -rf一堆日志缓存。但在我经历过几次“删完还是满”或者“删错东西系统崩了”之后,现在我遇到这类问题,永远先花五分钟回答下面三个问题,再决定动不动刀。
1.1 挂载根“根”的是哪个设备
“根”字在 Linux 系统里不是抽象概念,它必然对应一个具体的块设备、一个分区,或者一个网络文件系统。最简单的判断方式是用findmnt /,它会直接告诉我们根挂载点对应的源、文件系统类型和挂载选项。
findmnt /如果是本地分区,你会看到类似/dev/mmcblk0p2或者/dev/sda3;如果是开发板通过 NFS 启动,源可能是192.168.1.100:/srv/nfs/rootfs。这一步的意义在于:如果你连根到底落在哪个存储介质上都没确认,后面所有清理和扩容都是盲打。
与此同时,我还习惯再看一眼lsblk,把整个块设备拓扑拉出来:
lsblk这个输出能直接看出分区表结构。比如 SD 卡的mmcblk0下面有几个分区,哪个分区挂载成了/,哪个是/boot,哪些分区还没有挂载。虚拟机上则是/dev/vda或/dev/sda下面划分了vda1、vda2,逻辑卷则会有/dev/mapper/ubuntu--vg-ubuntu--lv这种名字。
1.2 别把 tmpfs 和 overlay 误当成“真实的根”
这里有个很容易踩的坑:某些容器环境或者特殊开发板里,/可能是 overlay 文件系统或者 tmpfs。df -h /会显示一个看起来占用率很高的虚拟设备,比如overlay或者tmpfs,但它的后端可能是一个宿主机目录,也可能是内存。
df -h /如果Filesystem那一列显示overlay,说明你看到的“根”实际上是多个下层目录叠加出来的视图,真正的磁盘消耗是在宿主机端的 upperdir。如果显示tmpfs,那你的根文件系统就是内存盘,重启后所有改动都会消失,空间当然也取决于内存大小,而不是磁盘。搞清楚这一点,才不会出现“在容器里清了半天、宿主机空间没变化”的尴尬。
1.3 确认是空间不足,还是 inode 不足
df -h只显示数据块使用率,但还有一种情况:磁盘明明还有几十 GB,却依然报“No space left on device”。这个时候要看df -i /。
df -i /inode 用尽意味着文件系统里已经没有任何空闲的文件索引节点了,就算有空间也建不了新文件。这种事经常发生在需要创建海量小文件的应用场景,比如邮件队列、临时目录、容器镜像层。我见过一个案例,根分区空间用了不到 30%,但/var/tmp里堆了几十万个几 KB 大小的临时文件,把 inode 全部吃光,服务全部报磁盘满。所以在根分区不够用的排查链路里,df -h和df -i必须成对查看,只查一个一定会漏判。
2. 空间被谁吃掉了:根分区爆满的真实原因排查
当确认根确实落在某个磁盘分区上之后,下一步不是立刻删文件,而是弄清楚空间去哪了。根分区的数据通常是分层结构的:系统本身、软件包缓存、日志、临时文件、用户数据,以及容器和虚拟机可能产生的镜像层。每一层都可能成为真正的空间杀手。
2.1 日志膨胀:最常见也最容易被忽略的元凶
系统日志是根分区爆满的“首犯”,尤其是systemd-journald。默认情况下,journal 日志在/var/log/journal下累积,如果没配限制,它可以一路涨到占用根分区相当大的比例。我见过一块 8GB 根分区的板子,光 journal 日志就占了 5GB 多,原因是有个服务每天疯狂报错。
快速看一下日志占用:
journalctl --disk-usage如果占用异常,可以立即清理到指定大小以下:
journalctl --vacuum-size=200M这条命令会把归档日志压缩清理,让所有 journal 文件的总量控制在 200MB 附近。需要注意的是,--vacuum-size只作用于已归档日志,当前活跃的 journal 不会被删掉。所以更稳妥的做法是直接在配置里写死上限:
# /etc/systemd/journald.conf SystemMaxUse=200M改完执行systemctl restart systemd-journald才真正生效。除了 journal,传统的 syslog、应用日志(比如 Nginx、Tomcat、Python 服务)也往往堆在/var/log下。可以用du -sh /var/log/*快速扫一遍,看到哪个文件上了 GB 级别,再决定是 truncate 还是配 logrotate。这里我特别提醒一句:千万不要直接rm正在被进程写入的日志文件,因为文件描述符还握着,空间不会真正释放,后面单独讲。
2.2 包管理缓存与软件残留
平常用apt或yum安装软件,下载的.deb、.rpm包都会缓存到本地。嵌入式板子和云服务器上都一样,时间一长,这些缓存能攒到几个 GB。Ubuntu/Debian 系清理很简单:
apt clean apt autocleanapt clean会清掉/var/cache/apt/archives下所有已下载的安装包,autoclean则只清除已无法下载的旧包。对于 CentOS/RHEL 系,则是:
yum clean all另一个容易被忽略的是/var/cache下的其他临时文件,以及/tmp里的历史残留。嵌入式环境长期不重启的话,/tmp甚至可能藏着上次构建的临时产物。清理前先du -sh /tmp/*看清大小再动手。
2.3 文件已删除,但空间未释放
这个问题特别隐蔽。你明明删掉了一个 3GB 的日志文件,df -h却纹丝不动。原因是有进程仍然持有这个文件的文件描述符,Linux 只有在所有引用都关闭后才会真正释放空间。我自己的排查习惯是:
lsof +L1这个命令会列出所有已被删除但仍被进程打开的文件。输出里SIZE列对应的就是它们占用的空间大小。找到 PID 之后,要么重启对应服务,要么让进程重新加载配置关闭旧日志文件。如果这个文件属于某个关键服务不能随便重启,可以考虑先 truncate 它:
: > /proc/<PID>/fd/<FD编号>但更标准的做法还是解决进程引用的来源,比如应用本身写了日志但没断开句柄,那就把日志轮转配置修好,再重启一次服务,空间才会真正回来。
2.4 容器镜像层与 overlay 堆积
如果你的“根”出现在一台跑 Docker 的机器上,或者你的开发板用容器方式跑应用,那么/var/lib/docker往往是隐藏的空间大户。镜像、容器层、构建缓存、挂载卷都可能占用大量根分区块。最直接的是先看 Docker 自己报的占用:
docker system df我的习惯是清理掉所有不再使用的悬空镜像和构建缓存:
docker system prune -a --volumes但这条命令很激进,-a会清除所有没有被容器使用的镜像,--volumes会删除未挂载到任何容器的卷。如果是在生产环境,建议先docker system df看清楚各类对象占用,再手动清理更稳妥。对开发板这种存储紧张的环境,我通常还会检查/var/lib/docker/overlay2里是否有异常大的层目录,然后通过docker image ls+docker rmi精准清理。
2.5 根分区自身的保留空间(reserved blocks)
ext4 文件系统默认会预留 5% 的 blocks,给 root 用户和紧急恢复用。对于几十 GB 的服务器,这 5% 可能有好几个 GB;但对于只有 4GB 的 SD 卡根分区,5% 就是 200MB,非常可观。这不算“空间被吃掉”,但它确实导致可用空间比实际容量少。可以用tune2fs -l查看:
tune2fs -l /dev/mmcblk0p2 | grep -i reserved如果这是个人开发板或测试虚拟机,不需要那么高的安全余量,可以调低保留比例:
tune2fs -m 1 /dev/mmcblk0p2但生产环境不建议动这个值。它存在的意义是防止 root 用户在磁盘满时连登录系统、清理空间都做不到——一旦没预留空间,连mount、touch这种操作都可能失败。
3. 清理操作的正确顺序与验证手段
空间分析做完了,清理的顺序很重要。很多人上来就把/usr、/opt里的“不常用”文件删了,结果系统直接起不来。我下面的顺序是从“安全系数高”到“风险系数高”排列的,前两步基本不会出问题,后两步则需要你清楚在删什么。
3.1 先动日志与临时文件
第一步永远是清理日志、包缓存和/tmp。这一步风险最小,收益却常常最大。推荐按这套来:
journalctl --vacuum-size=200M apt clean # 或 yum clean all rm -rf /tmp/* # 注意:不要在生产环境直接删,先检查是否有服务依赖/tmp下如果有正在使用的 socket 文件,删除可能导致服务异常。正确做法是重启后再清,或者只清超过一定时间的文件:find /tmp -type f -atime +7 -delete。
3.2 用 du 找出真实占空间的大目录
为了不被零散文件迷惑,我喜欢用du从根目录一层层往下定位。一次性扫完整根分区可能慢,但最直观:
du -x -h --max-depth=1 / 2>/dev/null | sort -h-x表示不要跨越文件系统边界,否则会把/proc、/sys、其他挂载点全算进来,结果就会非常混乱。看到占用最高的几个目录后,再进入下一层重复执行,直到锁定具体文件。比如:
du -x -h --max-depth=1 /var 2>/dev/null | sort -h du -x -h --max-depth=1 /var/log 2>/dev/null | sort -h这一套组合拳打完,根分区的空间分布基本就清楚了。之后该删哪个文件,心里才有底。
3.3 处理已删除但未释放的文件
如果df -h显示使用率还是高,执行一次lsof +L1,找到仍然持有着已删除文件的进程。注意,lsof +L1的输出可能有大量行,最好配合sort -k7 -n按大小排序查看。根据我的经验,最常出问题的是数据库、Nginx、Python 日志,以及嵌入式设备上常驻的采集程序。
对这类问题的处理,如果你能接受短暂重启服务,直接systemctl restart对应服务是最快的。不能重启的话,就用 proc 文件系统将其 truncate:
: > /proc/<PID>/fd/<FD编号>这样相当于把那个已经删除但仍被占用的文件截断为空,空间立即释放。实际操作时要仔细核对 PID 和 FD 编号,别把标准输出或标准错误给截了。
3.4 清理完成后的验证
清理不是df -h看着下降了就完事。我会再做三件事:
sync df -h / df -i /sync是把缓存中的脏数据写回磁盘,防止显示数据和实际数据不一致。然后重新确认空间与 inode 都在健康范围。最后,如果是因为日志膨胀引起的,我还会检查一下 journal 配置是否已经限了大小,避免过几天又满。
4. 真正解决根分区空间太小:从 SD 卡到虚拟机的扩容实操
清理只是急救,扩容才是根治。根分区不够用,本质上是因为创建系统时没有给/预留出未来的增长空间。不同环境的扩容手段差异很大,下面按“嵌入式开发板 SD 卡/板载存储”“VMware/QEMU 虚拟机”“LVM 逻辑卷”三个场景展开。
4.1 嵌入式开发板:SD 卡根文件系统分区扩展
RK3568、树莓派这类开发板,根分区空间太小最典型。用写卡工具烧录镜像时,通常只会烧一个和镜像文件大小一致的分区,不会自动扩展到整张 SD 卡。比如镜像里的根分区只有 2GB,但你的卡是 32GB,开机后/就是 2GB,剩下的空间是未分配的。
处理方式分两步。第一步用fdisk或parted调整分区表,把根分区扩展到未分配区域;第二步用resize2fs扩展文件系统。注意,根分区本身正在被挂载,不能直接在系统运行中卸载。安全做法是:在开发板上下电,取出 SD 卡,插到另外一台 Linux 机器上操作;或者使用一个独立的急救系统启动,再操作 SD 卡。
以fdisk为例,目标是把/dev/mmcblk0p2扩展到整张卡剩余空间:
sudo fdisk /dev/mmcblk0 # 进入交互模式 p # 打印分区表,记下根分区的起始扇区,这个值不能变 d # 删除要扩展的分区(注意别删错) n # 新建分区,默认起始扇区必须和之前一致 p # 确认新分区 w # 写入分区表分区表改完后立即让内核重新读取分区表,然后扩展文件系统:
sudo partprobe /dev/mmcblk0 sudo e2fsck -f /dev/mmcblk0p2 sudo resize2fs /dev/mmcblk0p2一定要先跑e2fsck再resize2fs,不然文件系统元数据不干净,扩展过程可能会报错。这套流程也可以套用在板载 eMMC 上,只是设备名可能是/dev/mmcblk1之类的,清理操作前一定先lsblk确认。
4.2 虚拟机:VMware / QEMU 扩展虚拟磁盘与分区
虚拟机内部看到的是虚拟磁盘,扩容前需要先在宿主机上把虚拟磁盘文件加大,再进虚拟机系统里分区和文件系统跟着变。以前我在 VMware 上扩容,过程跟 Windows 下“扩展磁盘容量”类似;QEMU 环境则用 qemu-img 直接调整镜像容量。
qemu-img resize ubuntu.qcow2 +20G虚拟磁盘增大后,进入虚拟机执行lsblk会看到磁盘整体变大了,但分区还是原来的大小。此时再用growpart扩展分区。Ubuntu 系一般自带cloud-guest-utils或growpart工具:
sudo growpart /dev/vda 1 sudo resize2fs /dev/vda1growpart的参数分别是磁盘设备和分区号,它会把该分区扩展到磁盘末尾。紧接着扩展文件系统,一条resize2fs就能完成。如果是 xfs,则用xfs_growfs /。
4.3 LVM 根分区在线扩容
服务器上很多根分区本身是 LVM 逻辑卷,这种结构扩容最方便,不用重启。先看逻辑卷名:
lvdisplay假设卷组叫ubuntu-vg,逻辑卷叫ubuntu-lv,把根分区从 50GB 扩到 80GB,用lvextend扩展逻辑卷。前提是卷组有足够剩余空间。如果没有,先加一块新物理磁盘,pvcreate后vgextend加入卷组,再lvextend。扩展逻辑卷后,同样要扩展文件系统:
sudo lvextend -L 80G /dev/ubuntu-vg/ubuntu-lv sudo resize2fs /dev/ubuntu-vg/ubuntu-lv线上操作 LVM 时,我最谨慎的点在于:-L指定最终大小还是增量大小容易混淆。-L 80G是最终 80GB,-L +30G是增加 30GB。写错一个符号,结果天差地别。如果文件系统是 xfs,执行的是xfs_growfs,而且 xfs 只能扩大不能缩小,扩容前不要动缩小的念头。
4.4 云服务器与 NAS 挂载类扩容
云服务器(如 Ubuntu 虚拟机在公有云上)的情况通常是控制台里扩了云盘,但系统里根分区没感知到。这跟虚拟机原理一致,但部分云平台需要先执行一条命令让内核重新读盘:
sudo partprobe /dev/vda或者某些云环境需要sudo growpart /dev/vda 1后使用resize2fs。另外热词里还有“linux 挂载 nas 存储 csdn”这类情况,如果根分区不够是因为想要挂载 NAS 却写到本地根分区下,那就得先理解:NFS 挂载只是把一个远程目录挂到本地挂载点,它本身不占用本地根分区磁盘空间,但挂载点目录如果不存在或创建在了根分区里,那只是目录占一点点空间,不能解决“本地根分区分区空间小”的问题。真要让 NAS 缓解根分区空间,得把大文件迁移到 NAS 目录,再通过 bind mount 或者符号链接引到原来的路径上。这属于存储布局优化,不是扩容。
5. 挂载根时的“空间假象”:NFS、bind mount 与 overlay 的边界
排查过程中我发现很多人说的“挂载根的磁盘空间太小”,其实并不是根分区真的满了,而是对挂载机制产生了混淆。这一节把几个典型假象拆开讲。
5.1 NFS 根文件系统的空间取决于服务端
开发板通过 NFS 挂载根文件系统时,执行df -h /显示的“Filesystem”是192.168.x.x:/path,这个空间来自 NFS 服务端所在机器的磁盘。如果服务端挂载点是某个小分区,那开发板的“根空间”自然就小。热词里“rk3568 nfs 根文件”指的就是这种场景。
这种情况下,你清理开发板上的文件毫无意义,因为最终影响的是服务端那个目录。正确做法是:
- 在宿主机上确认
/srv/nfs/rootfs所在磁盘分区的容量; - 如果需要扩大,直接在宿主机上扩建它所在分区;
- 如果想限制 rootfs 目录占用,用
quota或单独的挂载点来控制。
我之前犯过的错就是:疯狂清理开发板根文件系统里的文件,结果宿主机根分区还是越来越满,最后发现 NFS 导出目录处在宿主机的/分区里,该扩容的是宿主机。
5.2 bind mount 把别处的空间“挪”了过来
mount --bind不会创建新的存储空间,它只是把 A 路径挂到 B 路径上。比如:
mount --bind /data /var/lib/docker执行后/var/lib/docker里的内容在逻辑上是/data里的内容,实际占用的是/data所在分区的空间。如果此时执行df -h /var/lib/docker,看到的是/data所在分区的大小,而不是根分区的大小。这本身是一个非常好的策略:当根分区塞满容器数据时,把/var/lib/docker挪到大分区去。但很多新手会误以为这是“把空间变大了”,于是再往/var/lib/docker里写东西,实际写进了/data。想彻底解决,可以把/data放到更大分区,或者干脆从根上迁移docker-root配置。
5.3 overlay 根分区与宿主机空间的关系
容器里的根文件系统是 overlay 挂载,df -h /显示的是 overlay 层的总容量,这个容量通常继承自宿主机根分区。也就是说,在容器里看到“根分区空间太小”,要扩容的是宿主机根分区。但如果宿主机根分区本身并不小,容器里却显示空间小,那就可能是 Docker 的 storage driver 配置,或运行容器时指定的--storage-opt size=...限制。检查 Docker daemon 配置里的存储驱动选项,以及容器创建时的 size 参数,就能定位。开发板场景中,如果用的是podman或containerd,逻辑也类似:容器的可写层落在宿主机的某个目录下,根空间背后就是那块磁盘。
5.4 挂载点目录被一个“看不见”的分区挡着
还有一种情况:你想把一个新分区挂载到/mnt/data,但/mnt本身在根分区上,而/mnt/data这个目录创建时只占了几个 KB,挂载成功后df -h /mnt/data显示的是新分区的容量。看起来没问题,但如果挂载失败,或者挂载到的是/mnt/data-other,此时/mnt/data仍然属于根分区,空间自然不够。排查时我会用mount | grep 挂载点确认挂载是否真的生效,而不是只看df的结果。曾经有个朋友说根分区被/mnt/data占满了,我一查发现他的/mnt/data根本是空的,只是挂载新分区的操作失败,后续数据全写进了根分区下的同名目录。这种情况只要调整/etc/fstab里的挂载项,再重新 mount 一次就能解决问题。
6. 防止根分区再次爆掉的实际经验
扩容完成不代表一劳永逸,如果没有配合有效的监控和日志限制,下一次爆满只是时间问题。下面这些习惯,是我踩过不少坑之后固定下来的。
6.1 日志轮转和 journal 限额一定要配置好
不要依赖系统默认。默认的 journald 虽然也有一定策略,但很多时候它会累积到较大容量。建议在/etc/systemd/journald.conf里显式设置:
SystemMaxUse=300M MaxRetentionSec=7day重启后检查journalctl --disk-usage是否回落到目标值以内。对于应用日志,写好/etc/logrotate.d/配置,按天轮转,最多保留 7 份,并且compress压缩旧日志。这一条在嵌入式设备、开发板、生产服务器上都适用。
6.2 根分区容量监控喂到耳朵边上
根分区空间是那种“平时不看没事,一旦满了立即出事”的指标。服务器上我用简单的脚本配合钉钉/邮件告警:当/使用率超过 80% 时触发通知,超过 90% 时执行整理操作。其实不用太复杂的工具,一个 cron 加一条命令就行:
df -h / | awk 'NR==2 {gsub("%","",$5); if ($5 > 80) print $5}' | mail -s "rootfs usage high" admin@example.com开发板上资源紧张,我用的是开机自启的一个 shell 检测,每 10 分钟检查一次,超过阈值就把最老的 journal 和/tmp文件清掉。虽然粗暴,但很救命。
6.3 给根分区以外的大目录单独分区
如果系统盘只有 16GB,但你预期/var或/home会长到几十 GB,那从一开始就别让它们长在根分区里。SD 卡上规划分区时,分一个p2给根文件系统,再分一个p3挂载/data或/var,让大量数据落到独立分区。这样即使数据塞满独立分区,系统的/还能正常启动与登录,方便救援。服务器上同理,/home、/var/lib/docker、/opt这类易膨胀目录,尽量独立分区,或者用 LVM 单独建逻辑卷。
6.4 定期检查 inode 和碎片化文件
除了空间,还要监控df -i。开发板上长时间跑数据采集,特别容易产生海量小文件,把 inode 吃光。如果确实遇到“空间很多但没法写”的怪事,多半就是 inode 满了。解决方案是找出小文件堆积的目录,清掉,或者考虑更换文件系统类型。比如嵌入式场景改用 ext4 或 f2fs,小文件场景下 inode 规划更合理。
最后,说一个我个人的实操体会:处理根分区空间不足,最忌讳的就是“头痛医头”。每次遇到这个问题,我都强迫自己从分区表、日志、镜像层、inode 四个维度完整走一遍,而不是随手删了某个大文件就收工。因为根分区不像数据分区,它承载的是整个系统的运行底座,一次的敷衍往往换来未来更贵的故障。把上面这套流程沉淀成一个检查清单,等你下次在开发板上挂载根、在虚拟机里扩容、或者被生产服务器上“根分区 100%”告警吓到的时候,把清单照着走一遍,大概率十分钟内就能定位问题,剩下的就是按需扩容。