磁盘快满了这件事,在一台跑着业务的服务器上处理起来有个绕不开的矛盾:你把数据从 A 目录挪到 B 目录,如果 A 和 B 在同一个分区,那空间一点也没多出来;要扩容就得动分区表,而很多人对「动分区表」的印象停留在「必须停机、必须重装、数据可能没」。这个印象在十年前基本是对的,现在则不太成立——只要文件系统选对了、操作顺序对了,扩容可以在业务不停的情况下完成。
这篇按「先判断情况,再挑手段,最后执行」的顺序讲清楚:你的磁盘到底卡在哪一层、几种扩容方式各自的前提和代价、以及几个真正会丢数据的地方。
一、先分清三层:物理盘、分区表、文件系统
说「磁盘满了,扩一下」,实际上要动的东西有三层,弄清每一层当前的状态,是决定走哪条路的前提。
最底下是物理层:可以是云主机的云盘(可以在控制台在线扩容)、可以是 LVM 下面的物理卷(PV)、也可以是一块被直连使用的裸盘。中间是分区表:MBR 或 GPT,一个盘上分了几个区、每个区起止扇区在哪。最上面是文件系统:ext4、XFS、Btrfs,以及它当前认到的块设备大小。
三层的关系可以用一句话概括:文件系统能用的空间,等于「分区大小」和「底层块设备大小」取小值。所以扩容的完整动作永远是「先把底层变大,再把分区变大,最后通知文件系统重新认大小」,少一步都不生效。
lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT,TYPE
df -hT /
mount | grep ' / '
pvs; vgs; lvs # 有 LVM 时看这一层
cat /proc/partitions # 内核眼里每个块设备多大最后一条 /proc/partitions 很关键:云主机在控制台扩容之后,块设备本身变大了,但内核可能还没重新读分区表,所以 lsblk 显示的仍是旧大小。这时候要先刷新,再谈后面的事。
二、云盘在线扩容:最常见的路径
个人站大多数跑在云主机上,所以最常遇到的是「在控制台把 40G 改成 80G」。控制台操作本身不会影响运行中的系统,但接下来服务器上还有三步,顺序不能错。
第一步,让内核认到新容量。如果是 virtio 磁盘,通常会自己认到;如果没认到,用 rescan 触发:
echo 1 > /sys/class/block/vda/device/rescan
cat /sys/class/block/vda/size # 单位是 512B 扇区,乘以 512 再除 1073741824 得 GB注意 vda 这种设备名要按 lsblk 的实际输出改,SCSI 盘是 sda,NVMe 是 nvme0n1,路径相应变成 /sys/class/block/nvme0n1/device/rescan。
第二步,扩分区。如果没有分区表,整块盘直接做的文件系统(lsblk 里 TYPE 是 disk 且 FSTYPE 有值),这一步可以跳过。有分区的话,用 growpart 比手搓 parted 安全得多:
growpart /dev/vda 1 # 设备名 + 分区号,把第 1 分区扩到占满可用空间
partprobe /dev/vda # 让内核重新读分区表
lsblk # 确认 vda1 的 SIZE 变大了growpart 来自 cloud-guest-utils(Debian 系)或 cloud-utils-growpart(RHEL 系)。它的好处是只改分区的结束扇区、不动起始扇区,所以原有数据的位置完全不变,风险极低。手写 parted 的 resizepart 也能做同样的事,但起始扇区写错就是灾难,没必要。
第三步,扩文件系统。这一步才真正让新空间可用,而且必须在挂载状态下做——这是在线扩容的核心:
# ext4:在线扩容,可以在挂载状态下执行
resize2fs /dev/vda1
# XFS:不能缩小,只能在线增大,可以在挂载状态下执行
xfs_growfs /
# Btrfs
btrfs filesystem resize max /注意 XFS 的参数是挂载点而不是设备名,这一点经常有人写错,报错是「not a mounted XFS filesystem」。ext4 则是设备名。resize2fs 在挂载态下执行是官方支持的,但前提是文件系统本身没有错误——如果之前有过异常掉电,先 e2fsck -f(需要卸载或在救援模式),否则 resize 可能直接报错中止。
三、LVM:弹性最好的那一层
如果服务器上是 LVM(lvs 能看到逻辑卷),扩容的路径会灵活很多,因为它多了一层「逻辑卷」的抽象,可以随意跨物理盘分配空间,也可以在线做快照。代价是多一层映射,排查时要多看两条命令。
LVM 扩容分两种情形。第一种是卷组里还有空闲空间(vgs 的 VFree),这时候最省事:
lvextend -l +100%FREE /dev/vg0/lv_root # 逻辑卷扩到卷组剩余全用
resize2fs /dev/vg0/lv_root # ext4 跟着扩
# 或者一条搞定:lvextend -r -l +100%FREE /dev/vg0/lv_root (-r 自动调文件系统)-r 是 --resizefs,LVM2 会自己识别 ext4 或 XFS 并调对应的工具,比手动两步不容易漏。但要注意它对 XFS 会去调 xfs_growfs,而 XFS 只能增大不能缩小,lvreduce 配 -r 在 XFS 上会直接拒绝——这也是为什么 XFS 上的操作顺序必须是「先扩文件系统再谈逻辑卷」,逻辑卷先变小会立刻破坏文件系统。
第二种是卷组也满了,需要先加物理卷。这要求云主机上再挂一块盘,或者把现有的一块未分配盘纳入卷组:
pvcreate /dev/vdb
vgextend vg0 /dev/vdb
lvextend -r -l +100%FREE /dev/vg0/lv_root这条路的前提是有地方挂新盘。云主机可以随手加一块,物理机就要看有没有空盘位和背板了。所以「当初装系统时有没有把根分区做成 LVM 全占盘」这件事,在扩容这一刻的差别非常大:占满盘的做法省事,但以后只能靠加盘;留了余量的做法随时能扩,但会浪费一部分空间。
四、三个真的会让你丢数据的坑
坑一:在 ext4 上做了收缩。ext4 理论上支持缩小,但必须先卸载、必须先 e2fsck -f、并且不能小于当前数据量。任何一步漏了就是文件系统损坏。而 XFS 根本不支持缩小,这一点没有任何补救手段——如果你的 XFS 根分区把自己扩得太大、想缩回去,唯一的路是备份、重建文件系统、恢复数据。所以扩容前先确认文件系统类型,XFS 上的操作是单向的。
坑二:分区表改了但没同步到内核。fdisk 或 parted 改完分区表,内核里缓存的还是旧的分区信息,于是 resize2fs 会报「nothing to do」或者设备大小不对。这时候 partprobe 或 blockdev --rereadpt 能解决;如果提示设备忙、拒绝重读,那就只能重启。所以改分区表这件事,事先要看一眼 mount 输出里这个盘上有没有挂载的分区——有的话,先确认 partprobe 在没有重启的情况下能成功,别等到改完才发现必须重启。
坑三:扩容成功但应用没感知。这看起来像笑话,但很常见:文件系统扩完了,df 显示 80G,可某个服务的日志还是停止写入、或者 MySQL 报磁盘满。原因是这个服务在 systemd 里配了 ReadWritePaths 或 ProtectSystem=strict,或者跑在容器里、容器启动时记录的配额是旧的。容器场景尤其明显——docker run 时用了 --storage-opt size= 限定的容器,扩宿主机盘并不会让这个限额变大。判断方法很简单:宿主机上 df 有空间、容器内 df 没空间,就是这一层的问题。
五、扩容前后的检查单
扩容是「改底层块设备」的操作,属于少数几个一旦搞砸就救不回来的动作,所以检查单比技巧更重要。我自己的做法是固定六条,每次都走一遍:
- 扩容前:确认文件系统类型(
df -T)、确认是 LVM 还是直连分区(lsblk)、确认该盘上挂载了哪些路径(findmnt -S /dev/vda1)。 - 扩容前:留一份当前
lsblk、df -h、pvs/vgs/lvs的输出到/root/disk-state-$(date +%F).txt,出问题时这是对照的基线。 - 扩容前:确认备份可用。不一定整盘备份——至少关键目录(数据库、网站目录、
/etc)有一份在别的机器或对象存储上的、最近 24 小时内的备份。 - 扩容中:一步一步来,每步执行完都
lsblk+df确认后再走下一步,不要写成一整条脚本一次性跑完。 - 扩容后:
df -h和df -i都看一眼,前者确认空间、后者确认 inode 也够(很多文件系统扩容时 inode 数量是按比例加的,但旧分区的固定 inode 布局不会变)。 - 扩容后:验证业务,不要只看
df。写一个大文件试试(dd if=/dev/zero of=/tmp/t bs=1M count=200),确认真的能写进去,然后删掉。
还有一个容易被忽略的小事:扩容完成后 /etc/fstab 不需要改(设备名没变),但如果之前因为磁盘满而关掉了某些功能(比如禁用了 binlog、把日志轮转周期从 7 天改成 3 天、关掉了备份),记得把这些一一恢复。我见过不止一次「磁盘扩完了,但两个月后才想起来日志只保留三天」的情况。
六、如果扩容不是最优解
在动手扩之前,值得花十分钟问一句:这个分区的空间到底是被什么吃掉的?因为有些「磁盘满」并不是真的需要更多空间,而是有东西在无节制地写。
最常见的三个元凶:日志没有轮转或轮转策略失效(单文件几个 G)、MySQL 的 binlog 或慢查询日志累积、以及上传目录里堆了几年没清理的重复图片。这三种情况,扩 100G 也只是把问题推迟三个月。
du -xhd1 / | sort -rh | head -12
du -xhd1 /var | sort -rh | head -12
find / -xdev -type f -size +500M -ls 2>/dev/null | sort -k7 -rn | head-x(--one-file-system)不能省,否则 du / 会跨进挂载的其它盘,数字对不上。三条命令基本能定位到具体目录,再往下 du -h 递归就行。
另外,如果被吃掉空间的是「大量小文件」,扩分区未必是最优解——把这类目录单独挂到另一块盘上(或者单独分一个 LV),既解决了空间问题,又隔离了风险;将来这个目录需要清理、需要换存储,都不用动根分区。
七、小结
在线扩容在今天是常规操作,前提是三件事都做对:认清文件系统类型(XFS 不可缩)、把「物理层扩 → 分区扩 → 文件系统扩」三步走全、每一步都确认后再走下一步。云盘在线扩容最省事,LVM 最灵活,但两者都改变不了一个事实:扩容只是把边界往外推,真正决定你能不能长期安稳的,是知道空间被谁吃掉了。
所以每次扩容完,我都会顺手把那三条 du 的结果存一份。下一次磁盘满的时候,先拿它对比,就能判断是「业务真的长了」还是「有东西失控了」——这两种情况,处理方式完全不同。