0.10 磁盘、文件系统、挂载与 LVM/RAID
df 和 du 为什么对不上?磁盘还有空间却报 No space left on device 是怎么回事?LVM 扩容要按什么顺序动 PV、VG、LV 和文件系统?RAID 0/1/5/10 各自坏一块盘之后会发生什么?
从 lsblk/findmnt 确认一块盘的归属;用 df -i、lsof +L1、du -x 定位「空间去哪了」;在 loop 设备上完成一次 LVM 扩容和 RAID5 故障替换,并安全回滚。
0.9 里 D 状态的进程,多半卡在块设备 I/O 上。再往上走一层,就是磁盘、分区、文件系统、挂载点这套栈。日常故障里,「磁盘满了」这四个字的含义至少有五种:数据真的写满了、inode 用完了、预留块吃掉了余量、删除的文件仍被进程打开、或者写的是另一个挂载点。
这一章先把「看」和「算」讲清楚,再动「改」。所有分区、格式化、删除类命令都会给安全警告和回滚;LVM 和 RAID 的实验全部在 /var/tmp/block-lab 下的镜像文件上通过 losetup 完成,不碰 /dev/nvme* 或 /dev/sd*。
只提醒一次:mkfs、wipefs、lvremove、mdadm --create 都是不可逆操作。真盘误操作唯一可靠的回滚是备份;testdisk 之类的工具是补救,不是回滚。
动手前固定做三件事:
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINTS确认目标不是根盘、不是已挂载的数据盘;findmnt <目标>与df -h <目标>确认它没有被挂载;- 生产环境用
/dev/disk/by-id/下的稳定名字,不用/dev/sdX——盘序会变,名字会漂。
本章的 LVM/RAID 脚本只操作 losetup 出来的 /dev/loopN;如果你把它们改成真盘路径,后果由你承担。
0.10.1 先看清块设备:lsblk、findmnt、df
lsblk 读的是 /sys 里的块设备树,findmnt 读的是挂载表,df 读的是已挂载文件系统的统计。三个视角合起来,才能回答「这块盘是什么、挂在哪、用了多少」。
$ lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINTS,MODEL NAME SIZE TYPE FSTYPE MOUNTPOINTS MODEL nvme0n1 476.9G disk Samsung SSD 980 500GB ├─nvme0n1p1 1G part vfat /boot/efi ├─nvme0n1p2 475G part ext4 / └─nvme0n1p3 1G part swap [SWAP] $ findmnt -no SOURCE,FSTYPE,OPTIONS / /dev/nvme0n1p2 ext4 rw,relatime $ df -hT / /boot/efi Filesystem Type Size Used Avail Use% Mounted on /dev/nvme0n1p2 ext4 468G 391G 54G 88% / /dev/nvme0n1p1 vfat 1.1G 6.2M 1.1G 1% /boot/efi
lsblk 的 TYPE 告诉你层级:disk 是整盘,part 是分区,lvm 是逻辑卷,raid5 是软 RAID 阵列。MOUNTPOINTS 为空表示没挂载;[SWAP] 表示被用作交换区。MODEL 能帮你确认「这块是不是那块 SSD」,比 /dev/sda 可靠。
findmnt 的 OPTIONS 是实际生效的挂载选项,不是 /etc/fstab 里写的。rw,relatime 说明可读写、访问时间按相对时间更新。想看全部挂载用 findmnt --real,它会过滤掉 proc、sysfs、cgroup2 这些伪文件系统。
df 的 Size/Used/Avail 来自文件系统的超级块,单位是 1K 块再换算;Use% 的分母不含 ext4 的预留块,所以 Use% 到 100% 时 root 仍可能写进去一点。df -i 看 inode,df -T 看文件系统类型,df -h /path 看某个路径落在哪个挂载点。
分区表用 GPT 还是 MBR,只影响引导和老系统兼容;UEFI 时代新盘一律 GPT。parted 能看分区表,fdisk 也能,但两者都不是日常必需:先 lsblk,需要细节再 sudo parted -l。旧式的 fdisk 交互命令、MBR 主分区上限这些内容在 A.1。
0.10.2 文件系统选型与创建
ext4、XFS、Btrfs 都能在 Ubuntu 24.04 上用,差别在扩展性、快照和修复工具:
| 文件系统 | 在线扩容 | 缩容 | 快照 | 校验和 | 适合 |
|---|---|---|---|---|---|
| ext4 | 支持 | 支持(卸载后) | 无 | 元数据 | 通用、根文件系统、老工具链 |
| XFS | 支持 | 不支持 | 无(需 LVM) | 元数据 | 大文件、高并发写、RHEL 系默认 |
| Btrfs | 支持 | 支持 | 内置 | 数据 + 元数据 | 快照、回滚、单盘或 RAID1 |
选型的硬约束只有两条:XFS 不能缩容,要缩只能备份、重建、恢复;Btrfs 的 RAID5/6 不可靠,不要用它做 RAID5。其余差别在性能和运维习惯,不在「哪个更高级」。
创建文件系统会抹掉目标设备上的所有数据。命令本身只有一行,风险全在设备名:
# 以下三条任选其一;执行前用 lsblk 确认 /dev/vgdata/lvdata 是空盘
sudo mkfs.ext4 -L data -m 1 /dev/vgdata/lvdata # 预留块从 5% 降到 1%
sudo mkfs.xfs -L data /dev/vgdata/lvdata
sudo mkfs.btrfs -L data /dev/vgdata/lvdata
ext4 的 -m 1 把预留块比例从默认 5% 降到 1%,对大容量数据盘能多出几十 GB;根分区保留默认值,给 root 留应急空间。-L 设置卷标,之后可以用 LABEL=data 挂载,比 UUID 好记,但卷标可能重复,生产环境仍推荐 UUID。
创建后核对三件事:
sudo tune2fs -l /dev/vgdata/lvdata | grep -E 'Block count|Block size|Inode count|Reserved block count'
sudo xfs_info /dev/vgdata/lvdata | head -5
sudo btrfs filesystem usage /mnt/data
tune2fs -l 里的 Inode count 是格式化时定下的,之后不能增加(只能靠 resize2fs 扩容时按比例增加)。小文件特别多的场景(邮件队列、容器镜像层、Git 对象)要在 mkfs.ext4 时用 -N 指定更多 inode,或者选 XFS——XFS 动态分配 inode,但仍受 imaxpct/maxpct 限制,极端小文件场景仍可能 inode 用尽。
0.10.3 df 与 du 为什么对不上
df 统计文件系统超级块,du 遍历目录累加文件块数。两者统计口径不同,对不上是常态;对得上的情况反而是巧合。
$ df -hT / Filesystem Type Size Used Avail Use% Mounted on /dev/nvme0n1p2 ext4 468G 391G 54G 88% / $ sudo du -xsh /var/* 2>/dev/null | sort -h | tail -4 1.2G /var/lib 2.4G /var/cache 3.1G /var/log 11G /var/lib/docker $ sudo lsof +L1 | head -3 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME nginx 1234 www-data 5w REG 259,2 3221225472 0 1234567 /var/log/nginx/access.log (deleted) $ df -i / Filesystem Inodes IUsed IFree IUse% Mounted on /dev/nvme0n1p2 30539776 812345 29727431 3% /
五个常见原因,按排查顺序:
| 现象 | 原因 | 证据 | 处理 |
|---|---|---|---|
df 满,du 小很多 | 删除的文件仍被进程打开 | lsof +L1 显示 (deleted) | 重启/重载进程,或 truncate -s 0 该 fd |
df 满,du 小 | ext4 预留块 | tune2fs -l 的 Reserved block count | 数据盘可 -m 1,根盘保留 |
df 满,du 小 | 被挂载点遮住的目录 | mount --bind、findmnt | 从别处挂载后原目录内容不可见,umount 后复查 |
du 大,df 不满 | 稀疏文件、硬链接、快照 | du --apparent-size、stat | 按实际块还是表观大小统计,选对工具 |
df -h 有空间但写入失败 | inode 用尽 | df -i 的 IUse% 100% | 删小文件或重建文件系统 |
第一行是生产环境最常见的一种:日志文件被 rm 掉,nginx 还开着 fd,df 不会释放空间,du 也看不到它。lsof +L1 列出「link count 为 0 但仍打开」的文件,truncate -s 0 /proc/1234/fd/5 能立刻释放(对正在写的日志要谨慎,可能丢当前缓冲)。
du -x 的 -x 很重要:不加它会跨挂载点,把 /proc、/sys、其他数据盘都算进来。du -sh /var/* 只统计一层,找大目录要逐层下钻,或者用 ncdu(apt install ncdu)交互式浏览。
/var/log/nginx/access.log 被 rm,但 nginx 的 worker 还持有 fd。文件的目录项没了,inode 和 3 GB 数据块仍被引用,df 统计的是块,所以不释放。lsof +L1 能看到 (deleted) 和持有它的 PID;kill -HUP 让 nginx 重开日志,或 truncate -s 0 那个 fd,空间才回来。正确做法是 logrotate + copytruncate 或 postrotate 重开,而不是 rm。
0.10.4 inode 用尽:磁盘还有空间却写不进去
df -h 显示 11% 使用,touch 却报 No space left on device,第一反应是看 df -i:
df -h /data
df -i /data
sudo find /data -xdev -type f | wc -l
sudo find /data -xdev -type d -name '.cache' -prune -o -type f -printf '%s %p\n' |
sort -n | head
如果 df -i 的 IUse% 是 100%,就是 inode 用完了。每个文件至少占一个 inode,ext4 在格式化时按 bytes-per-inode 比例预分配;小文件多到超过这个比例,空间再多也写不进去。清理办法是删掉无用的小文件(缓存、临时文件、旧会话),或者把目录迁到 inode 更多的文件系统。
重建文件系统能增加 inode,但会清空数据,属于最后手段:
# 只在空盘或已备份的盘上执行;mkfs 会清空
sudo mkfs.ext4 -N 8000000 -L data /dev/vgdata/lvdata
-N 直接指定 inode 总数;不指定时 ext4 按 -i(bytes-per-inode,默认 16384)计算。-N 越大,元数据占用越多,可用数据空间越少。XFS 没有这个问题,它的 inode 是动态分配的;如果目录天然会有几百万小文件,选 XFS 比调 ext4 参数更省事。
0.10.5 挂载、/etc/fstab 与救援
挂载把文件系统接到目录树上。临时挂载用 mount,开机自动挂载写 /etc/fstab:
sudo mount /dev/vgdata/lvdata /mnt/data
findmnt /mnt/data
sudo umount /mnt/data
umount 报 target is busy 时,先找谁在用,不要直接 umount -l:
sudo fuser -vm /mnt/data
sudo lsof +D /mnt/data
fuser -vm 列出占用该挂载点的进程和 PID,lsof +D 列出打开的文件。杀掉或让进程 cd 走之后再 umount。umount -l(lazy)只是把挂载点从命名空间里摘掉,设备仍然忙,文件系统仍然占着,不是修复手段。
/etc/fstab 每行六个字段:
UUID=6f1c2a9e-3b7d-4c8e-9a10-5d2f8b1c7e44 /data ext4 defaults,nofail,x-systemd.device-timeout=10 0 2
字段依次是:设备(推荐 UUID 或 LABEL=)、挂载点、类型、选项、dump(基本为 0)、fsck 顺序(根为 1,其他为 2,不检查为 0)。nofail 表示设备不存在时继续启动;没有它,一块掉线的数据盘会让系统卡在 emergency mode。x-systemd.device-timeout=10 把等待时间从默认 90 秒降到 10 秒。x-systemd.automount 让 systemd 在第一次访问时才挂载,适合不常用的网络盘或备份盘。
改完 fstab 必须验证,不要直接重启:
sudo systemctl daemon-reload # systemd 从 fstab 生成 .mount unit
sudo findmnt --verify
sudo mount -a # 按 fstab 挂载所有未挂载项
findmnt /data
findmnt --verify 会解析 fstab 并报告语法、重复挂载点、设备不存在等问题;mount -a 真正执行一遍。两条都过了再重启,风险小得多。
fstab 写错导致开不了机,救援顺序是:
- 进 emergency/rescue shell(GRUB 菜单里选 recovery,或内核参数加
systemd.unit=rescue.target); mount -o remount,rw /把根分区改成可写;- 编辑
/etc/fstab,注释掉出错的行,或加nofail; systemctl daemon-reload,reboot。
如果是云主机,控制台串口/救援模式是唯一入口;这也是为什么改 fstab 前要先确认云厂商的救援方式。
0.10.6 LVM:分层扩容
LVM 把「物理盘」和「文件系统」之间加了两层:物理卷(PV)、卷组(VG)、逻辑卷(LV)。好处是可以在线扩容、跨盘拼容量、给 LV 起稳定名字;代价是多一层映射,故障排查要多看几个命令。
物理盘 /dev/nvme0n1
└─ 分区 /dev/nvme0n1p2(GPT)
└─ PV /dev/nvme0n1p2 ──┐
├─ VG vgdata ─┬─ LV lvdata → ext4 → /data
PV /dev/nvme1n1p1 ─────┘ └─ LV lvlog → XFS → /var/log
读图:扩容只能从左往右加,缩容只能从右往左减。加一块盘,先 pvcreate 成 PV,再 vgextend 进 VG,然后 lvextend 扩 LV,最后扩文件系统;lvextend -r 会把最后一步一起做掉。缩容反过来:先缩文件系统(XFS 不支持),再 lvreduce,最后才 pvremove。顺序错了就是数据丢失。
下面的实验只碰 loop 设备。先建两个 1 GB 的镜像文件,挂成 loop,做成 VG,再建一个 1 GB 的 LV:
lab=/var/tmp/block-lab
mkdir -p "$lab" && cd "$lab"
for i in 1 2; do truncate -s 1G "disk$i.img"; done
loop1=$(sudo losetup -f --show "$lab/disk1.img")
loop2=$(sudo losetup -f --show "$lab/disk2.img")
printf 'loop1=%s loop2=%s\n' "$loop1" "$loop2"
sudo pvcreate "$loop1" "$loop2"
sudo vgcreate vglab "$loop1" "$loop2"
sudo lvcreate -L 1G -n lvdata vglab
sudo mkfs.ext4 -L lvlab /dev/vglab/lvdata
sudo mkdir -p /mnt/lvlab
sudo mount /dev/vglab/lvdata /mnt/lvlab
df -hT /mnt/lvlab
再加第三个 1 GB 镜像,把它扩进 VG,然后在线扩 LV 和文件系统:
lab=/var/tmp/block-lab
truncate -s 1G "$lab/disk3.img"
loop3=$(sudo losetup -f --show "$lab/disk3.img")
sudo pvcreate "$loop3"
sudo vgextend vglab "$loop3"
sudo lvextend -L +1G -r /dev/vglab/lvdata
df -hT /mnt/lvlab
$ sudo pvs PV VG Fmt Attr PSize PFree /dev/loop0 vglab lvm2 a-- 1.00g 0 /dev/loop1 vglab lvm2 a-- 1.00g 1.00g $ sudo lvs LV VG Attr LSize lvdata vglab -wi-ao---- 1.00g $ sudo lvextend -L +1G -r /dev/vglab/lvdata Size of logical volume vglab/lvdata changed from 1.00 GiB (256 extents) to 2.00 GiB (512 extents). Logical volume vglab/lvdata successfully resized. resize2fs 1.47.0 (5-Feb-2023) Filesystem at /dev/mapper/vglab-lvdata is mounted on /mnt/lvlab; on-line resizing required The filesystem on /dev/mapper/vglab-lvdata is now 524288 (4k) blocks long. $ df -hT /mnt/lvlab Filesystem Type Size Used Avail Use% Mounted on /dev/mapper/vglab-lvdata ext4 2.0G 24K 1.9G 1% /mnt/lvlab
pvs/vgs/lvs 分别看 PV、VG、LV;pvs -o+pv_used 能看每个 PV 用了多少。lvextend -r 的 -r 调用 fsadm,ext4 走 resize2fs,XFS 走 xfs_growfs;没有 -r 时 LV 变大了但文件系统没变,df 仍是旧大小。这是 LVM 扩容最常见的「我扩了但没生效」。
回滚这套实验:
lab=/var/tmp/block-lab
sudo umount /mnt/lvlab 2>/dev/null || true
sudo lvremove -y vglab/lvdata 2>/dev/null || true
sudo vgremove -y vglab 2>/dev/null || true
for img in "$lab"/disk*.img; do
for dev in $(sudo losetup -j "$img" -O NAME --noheadings); do
sudo pvremove -y "$dev" 2>/dev/null || true
sudo losetup -d "$dev"
done
done
rm -rf "$lab"
回滚顺序和创建相反:先卸文件系统,再删 LV、VG,最后清 PV 标签、解 loop、删镜像文件。lvremove 会二次确认,-y 跳过;在生产上不要习惯性加 -y,先 lvs 看清 LV 名字。
pvcreate 会覆盖设备上的文件系统签名,vgcreate 会接管 PV,lvremove 会删除 LV 上的全部数据。对真盘执行前,用 lsblk、pvs、vgs 确认设备没有被其他 VG 使用;多路径(MPIO)和 iSCSI 设备还要先确认 /dev/mapper 的映射关系,避免对同一块盘两条路径各建一次 PV。
0.10.7 RAID:级别取舍与 mdadm
RAID 用多块盘换容量、冗余或性能。级别不是越高越好,是先确定「能接受几块盘同时坏」和「写性能能不能降」:
| 级别 | 最少盘 | 可用容量 | 容错 | 读 | 写 | 典型场景 |
|---|---|---|---|---|---|---|
| RAID 0 | 2 | N × 单盘 | 0 | 快 | 快 | 临时数据、缓存,坏了就重建 |
| RAID 1 | 2 | 单盘 | 1 | 快 | 中 | 系统盘、小容量高可靠 |
| RAID 5 | 3 | (N−1) × 单盘 | 1 | 快 | 中(写惩罚) | 通用文件存储 |
| RAID 6 | 4 | (N−2) × 单盘 | 2 | 快 | 较慢 | 大容量、重建时间长 |
| RAID 10 | 4 | N/2 × 单盘 | 每组 1 | 快 | 中 | 数据库、高 IOPS |
RAID 5 的「写惩罚」来自每次写要读旧数据、旧校验、算新校验、写数据、写校验;小随机写会放大成多次 I/O。RAID 10 牺牲一半容量换更简单的重建和更好的随机写。RAID 6 多一块校验盘,能扛两块盘同时坏,但写更慢、重建更久。
用三块 512 MB 的 loop 盘做一个 RAID5:
lab=/var/tmp/block-lab
mkdir -p "$lab" && cd "$lab"
for i in 1 2 3; do truncate -s 512M "raid$i.img"; done
loops=()
for img in raid1.img raid2.img raid3.img; do
loops+=("$(sudo losetup -f --show "$img")")
done
printf '%s\n' "${loops[@]}"
sudo mdadm --create /dev/md/raidset --level=5 --raid-devices=3 \
--chunk=64 "${loops[@]}"
sudo mkfs.ext4 -L raidlab /dev/md/raidset
sudo mkdir -p /mnt/raidlab
sudo mount /dev/md/raidset /mnt/raidlab
cat /proc/mdstat
$ cat /proc/mdstat Personalities : [raid6] [raid5] [raid4] md127 : active raid5 loop2[2] loop1[1] loop0[0] 1046528 blocks super 1.2 level 5, 64k chunk, algorithm 2 [3/3] [UUU] $ sudo mdadm /dev/md/raidset --fail /dev/loop0 mdadm: set /dev/loop0 faulty in /dev/md/raidset $ sudo mdadm /dev/md/raidset --remove /dev/loop0 mdadm: hot removed /dev/loop0 from /dev/md/raidset $ truncate -s 512M spare.img $ spare=$(sudo losetup -f --show spare.img) $ sudo mdadm /dev/md/raidset --add "$spare" mdadm: added /dev/loop3 $ cat /proc/mdstat md127 : active raid5 loop3[3] loop2[2] loop1[1] 1046528 blocks super 1.2 level 5, 64k chunk, algorithm 2 [3/2] [UU_] [=>...................] recovery = 8.2% (43008/523264) finish=0.1min speed=60000K/sec
/proc/mdstat 的 [3/3] [UUU] 表示 3 块成员都在、3 块正常;[3/2] [UU_] 表示缺一块,正在用热备盘重建。mdadm --detail 会给出更完整的 State、Active Devices、Failed Devices 和每块盘的角色。重建期间性能会下降,RAID5 的大容量盘重建可能持续数小时到数天。
回滚:
lab=/var/tmp/block-lab
sudo umount /mnt/raidlab 2>/dev/null || true
sudo mdadm --stop /dev/md/raidset
for img in "$lab"/raid*.img "$lab"/spare.img; do
[ -e "$img" ] || continue
for dev in $(sudo losetup -j "$img" -O NAME --noheadings); do
sudo mdadm --zero-superblock "$dev" 2>/dev/null || true
sudo losetup -d "$dev"
done
done
rm -rf "$lab"
mdadm --stop 停阵列但不会清掉成员盘上的超级块;不 --zero-superblock 就复用这些盘,mdadm --assemble 可能把旧阵列重新组装出来。清超级块是 RAID 实验里最容易被忘的一步。
- 「
df100% 就是数据写满了」——可能是 inode 用尽、预留块、已删除但仍打开的文件,先看df -i和lsof +L1。 - 「
df和du应该相等」——超级块统计与目录遍历口径不同;差几 GB 正常,差几十 GB 要查 deleted/open 文件或被遮住的挂载点。 - 「
rm大文件后df立刻下降」——只要还有进程持有 fd,数据块就不释放;要重开或截断该 fd。 - 「LVM 扩了 LV,文件系统自动变大」——
lvextend只改块设备;要-r或手动resize2fs/xfs_growfs。 - 「RAID 5 坏一块还能随便跑」——降级模式没有冗余,再坏一块就全丢;重建期间是风险最高的窗口,RAID 不是备份。
- 「
mount -o discard总比fstrim好」——每次删除都同步发 TRIM 会增加延迟;Ubuntu 默认用每周的fstrim.timer批量回收。
0.10.8 swap、TRIM、配额与磁盘健康
Ubuntu 24.04 默认用 /swap.img 作为交换文件,swapon --show 能看到它:
$ swapon --show NAME TYPE SIZE USED PRIO /swap.img file 2G 0B -2 $ free -h total used free shared buff/cache available Mem: 15Gi 3.2Gi 8.1Gi 123Mi 4.4Gi 11Gi Swap: 2.0Gi 0B 2.0Gi $ systemctl list-timers fstrim.timer --no-pager NEXT LEFT LAST PASSED UNIT Mon 2025-03-03 00:00:00 UTC 13h left Mon 2025-02-24 00:00:01 UTC 6 days ago fstrim.timer
加一块 swap 文件的完整步骤与回滚:
sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
swapon --show
# 写入 fstab 才会开机自动启用
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
# 回滚
sudo swapoff /swapfile
sudo sed -i '\#^/swapfile #d' /etc/fstab
sudo rm /swapfile
fallocate 比 dd 快,但在 Btrfs 上不能直接给 swap 文件用 fallocate,要用 btrfs filesystem mkswapfile。chmod 600 是必须的:swap 里可能有明文密钥和密码,其他用户不可读。swapon 前用 free -h 确认内存和 swap 余量,swapoff 时系统需要把 swap 里的页换回内存,内存不够会触发 OOM。
SSD 的 TRIM 告诉盘哪些块不再使用。两种方式:挂载选项 discard(每次删除同步发)或定期 fstrim(批量发)。Ubuntu 默认启用 fstrim.timer,每周跑一次,比 discard 对延迟更友好。手动跑一次:
sudo fstrim -av
磁盘健康用 SMART。SATA 盘 smartctl -a /dev/sda,NVMe 盘 smartctl -a /dev/nvme0 或 nvme smart-log /dev/nvme0(apt install smartmontools nvme-cli)。重点看 Reallocated_Sector_Ct、Pending_Sector、Media_Wearout、Percentage Used、Available Spare;数值异常时先备份,再做 smartctl -t short 自检。内核日志里的 I/O 错误也要看:
sudo dmesg -T | grep -iE 'I/O error|nvme|ata[0-9]|medium error'
dmesg -T 把内核时间戳转成可读时间;medium error、Unrecovered read error 通常意味着盘要换了。RAID 能扛住一块盘坏,但扛不住「坏了不换」;SMART 告警和 mdadm --monitor 应该接到监控里,而不是等人发现。
0.10.8.1 磁盘配额:quota、xfs_quota 与 project quota
多用户/多租户机器上,「磁盘满了」往往不是整块盘满,而是某个用户或项目写爆了配额。配额按 用户/组/项目 限制 inode 数与块数,分软限、硬限与宽限期。
启用(ext4):
# /etc/fstab 加 usrquota,grpquota(XFS 用 uquota,gquota,prjquota)
sudo mount -o remount /home
sudo quotacheck -ugm /home
sudo quotaon -ug /home
查看/设置:
quota -u alice # 用户配额
repquota -a # 所有用户
edquota -u alice # 交互编辑软/硬限与宽限期
setquota -u alice 500M 600M 1000 1200 /home # 块软/硬、inode 软/硬
XFS 用 xfs_quota(配额在挂载时启用,不能后开):
xfs_quota -x -c 'report -h' /home
xfs_quota -x -c 'limit -u bsoft=500m bhard=600m alice' /home
project quota 是容器/多租户最常用的一种:给一个目录树打 project ID,限制整棵树的用量,而不是限制某个 UID。overlayfs 的上层目录也可以挂 project quota,防止单个容器写爆宿主盘。
要点:
- 配额统计的是块数,不是「文件大小」;稀疏文件按实际分配的块算。
- ext4 的配额文件是
aquota.user/aquota.group;XFS 的配额元数据在文件系统内部,不要手工删。 df看到的是文件系统总量,不显示配额;用quota/repquota/xfs_quota report。- 软限超了只警告并启动宽限期(默认 7 天),硬限超了直接
EDQUOT;应用要处理EDQUOT,不要只处理ENOSPC。
0.10.8.2 配额的三种口径与 EDQUOT
配额不是「一块盘的总量」,而是「某个主体在某个文件系统上的用量上限」。三种主体的统计口径不同,选错就会限不到真正想限的人:
| 主体 | ext4 挂载选项 | XFS 挂载选项 | 统计对象 | 典型场景 |
|---|---|---|---|---|
| 用户 | usrquota | uquota | 该 UID 拥有的全部文件 | 多用户登录机、共享家目录 |
| 组 | grpquota | gquota | 该 GID 拥有的全部文件 | 项目组共享目录 |
| 项目 | prjquota(4.5+) | prjquota | 打了 project ID 的目录树 | 容器、租户、单目录限额 |
XFS 的 prjquota 与 gquota 不能同时启用;要按目录限,就放弃组配额。ext4 从 Linux 4.5 起也支持 project quota(挂载选项 prjquota,旧盘要先启用 project/quota 特性),但打 ID 与报表的工具链和 XFS 不同,见本节末尾。Btrfs 的 qgroup 是另一套模型,不在本章范围。
ext4 的启用流程比 XFS 多两步,因为配额文件是普通文件,需要先创建:
# 1. fstab 加选项,重新挂载
# /dev/vgdata/lvhome /home ext4 defaults,usrquota,grpquota 0 2
sudo mount -o remount /home
# 2. 扫描并生成 aquota.user / aquota.group;-m 表示不尝试 remount 只读
sudo quotacheck -ugm /home
# 3. 打开配额
sudo quotaon -ug /home
quotaon -p /home # 查看状态
# 4. 设置:块单位是 1K block,inode 单位是个数
sudo setquota -u alice 512000 614400 1000 1200 /home
sudo edquota -t # 全局宽限期,默认 7 天
sudo repquota -a # 报表
块软限 512000 个 1K 块 = 500 MiB,硬限 600 MiB;inode 软限 1000、硬限 1200。用户超过软限只收到警告并开始倒计时,超过硬限立刻 EDQUOT。edquota -u alice 是交互式编辑,适合单个用户;批量用 setquota。
XFS 的配额在挂载时决定,不能 remount 后开:
# /dev/vgdata/lvhome /home xfs defaults,uquota,gquota 0 0
sudo umount /home && sudo mount /home
sudo xfs_quota -x -c 'state' /home
sudo xfs_quota -x -c 'report -ubih' /home
sudo xfs_quota -x -c 'limit -u bsoft=500m bhard=600m alice' /home
sudo xfs_quota -x -c 'timer -u -b 14days' /home
report 的 -u/-g/-p 选主体,-b/-i/-h 选块/inode/人类可读。state 会显示 User quota state、Group quota state、Project quota state 与宽限期;df 只显示文件系统总量,不显示配额余量。
project quota 的关键是「给目录树打 ID」,不是给用户:
# 假设 /srv 是启用了 prjquota 的 XFS 挂载点
sudo mkdir -p /srv/app
# 42 是任意未使用的 project ID;-s 让该目录下新建文件继承这个 ID
sudo xfs_quota -x -c 'project -s -p /srv/app 42' /srv
sudo xfs_quota -x -c 'limit -p bhard=20g 42' /srv
sudo xfs_quota -x -c 'report -p -h' /srv
project -s 会递归给现有文件打 ID;之后新建的文件靠目录的继承标志自动带上。容器运行时给 overlay 上层目录加 project quota,就是这套机制。
ext4 的 project quota 走另一套命令:用 chattr -p 42 /srv/app 打 ID,用 chattr +P /srv/app 设继承,再用 repquota -P /srv 看报表。旧盘要先 tune2fs -O project,quota 并离线跑一次 e2fsck -f;挂载选项写 prjquota。它和 XFS 的 xfs_quota project 不通用,照抄命令会报 Operation not supported。
应用要同时处理 ENOSPC 和 EDQUOT(errno 122)。很多程序只判断前者,配额超限时给出「磁盘空间不足」的误导性错误。写文件的循环还应该区分「写到一半失败」:write 返回短写或 EDQUOT 时,已写部分仍占配额,回滚要显式 ftruncate。
0.10.9 分区表、对齐与 fdisk/sgdisk/parted
分区表决定「这块盘怎么切」,文件系统只看见自己所在的分区。分区表坏了,文件系统再健康也挂不上;分区起止没对齐,性能会莫名其妙掉一截。
| 特性 | MBR(DOS) | GPT |
|---|---|---|
| 分区表位置 | 0 号扇区(512 B),含引导代码 | LBA 1 起,含 128 个表项;末尾有备份 |
| 最大磁盘 | 2 TiB(512 B 扇区) | 8 ZiB |
| 主分区数 | 4 个,或 3 主 + 1 扩展 | 128 个(可更多) |
| 分区 ID | 1 字节类型码 | 16 字节 GUID 类型 |
| 完整性 | 无校验 | 头部 CRC32,主备互校 |
| 引导 | BIOS + boot flag | UEFI 需要 ESP(EF00) |
MBR 的 2 TiB 限制来自「32 位 LBA × 512 B 扇区」。4Kn 盘(逻辑扇区 4096 B)上 MBR 能到 16 TiB,但兼容性更差。新盘一律 GPT;只有在给老 BIOS 机器装系统、或对接只认 MBR 的硬件阵列时才用 MBR。
分区工具的选择:
| 工具 | 风格 | 适合 | 常用命令 |
|---|---|---|---|
fdisk | 交互式,MBR/GPT 都支持 | 手工探索、救急 | g、n、p、t、w |
gdisk/sgdisk | GPT 专用,脚本友好 | GPT 修复、批量建分区 | sgdisk -n 1:1MiB:+512M |
parted | 交互/脚本,支持 resizepart | 一次性建表、查看对齐 | parted -s /dev/X mklabel gpt |
sfdisk | 脚本化,可导出/导入 | 批量复制分区表 | sfdisk -d /dev/sda | sfdisk /dev/sdb |
新建 GPT 并建一个分区,用 sgdisk 最不容易误操作:
sudo sgdisk --zap-all /dev/loop0
sudo sgdisk --new=1:1MiB:+512M --typecode=1:8300 --change-name=1:data /dev/loop0
sudo sgdisk --print /dev/loop0
sudo partx -u /dev/loop0 # 让内核重读分区表
lsblk -o NAME,SIZE,TYPE,PARTTYPENAME,PARTUUID /dev/loop0
--new=1:1MiB:+512M 的起点写 1MiB 而不是 2048,让工具按实际扇区大小换算;8300 是通用 Linux 文件系统,8309 是 Linux LUKS,8e00 是 Linux LVM,别把加密或逻辑卷的类型码当数据分区用。partx -u 只更新指定设备的分区,partprobe 会扫描全部设备;两者都不如「卸载后重读」可靠,根盘分区表变更需要重启。
parted 的对齐检查是少数能直接告诉你「有没有对齐」的命令:
sudo parted -s /dev/loop0 mklabel gpt
sudo parted -s /dev/loop0 mkpart data ext4 1MiB 100%
sudo parted /dev/loop0 align-check optimal 1
sudo parted -l
align-check optimal 1 输出 1 aligned 才算通过。现代工具默认从 1 MiB 起,是为了避开 MBR 与 GPT 头部、并匹配绝大多数 SSD/RAID 的条带;老教程里从 63 扇区起的写法会让每次读写跨两个物理页。
4Kn 盘要额外确认逻辑/物理扇区:
blockdev --getss /dev/nvme0n1 # 逻辑扇区,通常 512 或 4096
blockdev --getpbsz /dev/nvme0n1 # 物理扇区,通常 4096
lsblk -o NAME,LOG-SEC,PHY-SEC,OPT-IO /dev/nvme0n1
OPT-IO 是 optimal_io_size,RAID 卡或 SSD 会给出建议的 I/O 大小。分区起点与文件系统条带参数(mkfs.ext4 -E stride=,stripe_width=)应尽量与它对齐;不对齐不会报错,只会让写放大。
分区表也有备份。GPT 的备份头在磁盘最后一个 LBA,sgdisk --backup=table.bin /dev/sda 会把主备两份都存下来,--load-backup=table.bin /dev/sdb 可以原样恢复到同容量的盘。恢复前先 sgdisk --verify;容量不同的盘恢复后要 --move-second-header 把备份头挪到新盘的末尾。
0.10.10 mkfs 参数与挂载选项
mkfs.ext4 与 mkfs.xfs 的默认值适合通用场景,但有几项一旦定下就不能改,必须在创建时想清楚。
| 参数 | ext4 | XFS | 定型后能否改 |
|---|---|---|---|
| 块大小 | -b 1024/2048/4096 | -b size=4096 | 不能 |
| inode 大小 | -I 256(默认) | 固定,随 CRC 变化 | 不能 |
| inode 数量 | -N 个数 或 -i 字节/ino | 动态分配,-i maxpct= | ext4 只能扩容时增加 |
| 日志 | -J size= | -l size= | ext4 可 tune2fs -J,XFS 不能 |
| 卷标/UUID | -L / -U | -L / -U | 可以(tune2fs/xfs_admin) |
| 预留块 | -m 百分比 | 无此概念 | 可以 |
| 条带 | -E stride=,stripe_width= | -d su=,sw= | 不能 |
ext4 的 -m 预留块是给 root 的应急空间,默认 5%。数据盘可以降到 1%,根盘保留 5%;降到 0 会让「磁盘满后 root 也登录不了、日志也写不进」的故障直接发生。
-E stride/stripe_width 要按 RAID 的 chunk 与数据盘数算:stride = chunk / block_size,stripe_width = stride × 数据盘数。算错不会损坏数据,但会让文件系统按错误的条带写,RAID5 的小写惩罚更明显。
# 4 块盘 RAID5,chunk 64K,块大小 4K
# stride = 64K/4K = 16;stripe_width = 16 * 3 = 48
sudo mkfs.ext4 -b 4096 -E stride=16,stripe_width=48 /dev/md0
# XFS 用 su/sw(条带单元/条带宽度,单位字节)
sudo mkfs.xfs -d su=64k,sw=3 /dev/md0
XFS 的 -i maxpct= 控制 inode 最多占文件系统空间的百分比,默认 25%。小文件极多的场景可以调大,但 inode 本身也占空间;用 xfs_info 看 imaxpct 与 agcount。
创建后立即核对:
sudo dumpe2fs -h /dev/vgdata/lvdata | grep -E 'Block count|Block size|Inode count|Inode size|Reserved block count|Filesystem UUID'
sudo xfs_info /mnt/data
sudo tune2fs -l /dev/vgdata/lvdata | grep -E 'Filesystem state|Last checked|Mount count'
dumpe2fs -h 只读超级块,不扫全盘,秒回;不带 -h 会打印每个 block group,慢且长。xfs_info 的参数是挂载点,不是设备名;对未挂载的 XFS 用 xfs_db -r -c 'info' /dev/...。
挂载选项分四类,记不住就查 man 5 mount 与 man 5 ext4:
| 类别 | 选项 | 作用 | 风险 |
|---|---|---|---|
| 权限 | ro、rw、nosuid、nodev、noexec | 限制挂载点能力 | noexec 会让脚本/解释器无法执行 |
| 时间 | atime、noatime、relatime、lazytime | 控制访问时间更新 | noatime 影响依赖 atime 的备份/邮件 |
| 可靠性 | sync、async、errors=remount-ro、data=ordered | 写回与出错策略 | sync 极慢;data=writeback 崩溃后可能读到旧数据 |
| 性能 | discard、commit=N、nobarrier | TRIM、提交间隔、屏障 | 关屏障可能丢数据 |
defaults 不是「默认最优」,它展开成 rw,suid,dev,exec,auto,nouser,async。/tmp 应该用 nosuid,nodev,noexec;/boot 可以 ro;网络盘加 _netdev 和 nofail,否则网络没起来时开机卡住甚至进 emergency。
/etc/fstab 的六个字段:
| 字段 | 内容 | 例子 | 注意 |
|---|---|---|---|
| 1 | 设备 | UUID=...、LABEL=...、/dev/vg/lv | 优先 UUID |
| 2 | 挂载点 | /data | 路径不能有空格(可用 \040) |
| 3 | 类型 | ext4、xfs、swap | 写错会挂载失败 |
| 4 | 选项 | defaults,nofail | 逗号分隔,不能有空格 |
| 5 | dump | 0 | 现代系统不用 |
| 6 | fsck 顺序 | 0、1、2 | 根为 1,其他为 2,不检查为 0 |
改完 fstab 必须验证,不要等重启:
sudo findmnt --verify --verbose
sudo mount -a -v
sudo systemctl daemon-reload # 让 systemd 重新生成 .mount 单元
findmnt --verify 会检查设备是否存在、挂载点是否重复、选项是否被识别;mount -a 会真正挂载所有 auto 条目。nofail 让挂载失败不阻塞开机,x-systemd.device-timeout=10s 限制等待设备的时间;这两个一起用,才能避免「一块数据盘掉了,整台机器起不来」。
0.10.11 fsck、xfs_repair 与超级块救援
文件系统不一致的来源只有三类:非正常断电/崩溃、硬件写缓存丢数据、内核或驱动 bug。fsck 系列工具把元数据读出来交叉验证,能修就修,不能修就隔离。
ext4 的 e2fsck 分五个阶段:inode、目录结构、目录连通性、引用计数、组摘要。退出码是位掩码,不是简单的 0/1:
| 退出码 | 含义 | 处理 |
|---|---|---|
| 0 | 无错误 | 正常 |
| 1 | 已修正错误 | 可继续启动,检查日志 |
| 2 | 已修正,需重启 | 重启后再查 |
| 4 | 有未修正错误 | 必须人工介入,别挂载写入 |
| 8 | 操作错误 | 参数/设备问题 |
| 16 | 用法错误 | 看 man |
| 32 | 被取消 | 用户中断 |
| 128 | 共享库错误 | 环境问题 |
常用选项:
| 选项 | 作用 | 场景 |
|---|---|---|
-p | preen,自动修「安全」的问题 | 开机 systemd-fsck 默认 |
-n | 只检查,回答全部 no | 只读诊断,绝不改 |
-y | 回答全部 yes | 已确认要修,慎用 |
-f | 强制检查,即使标记 clean | 怀疑元数据损坏 |
-b 32768 | 用备份超级块 | 主超级块损坏 |
-B 4096 | 指定块大小 | 配合 -b |
-D | 优化目录索引 | 大目录性能 |
-c | 用 badblocks 扫坏块 | 怀疑盘坏 |
主超级块在块组 0,备份超级块在 32768、98304 等位置(4K 块)。主超级块损坏时,e2fsck 会报 Bad magic number in super-block,此时先不要 mkfs:
sudo mke2fs -n /dev/loop0 # -n 只打印布局,不写盘
# 输出会列出 Backup superblock at 32768, 98304, ...
sudo e2fsck -f -b 32768 -B 4096 /dev/loop0
sudo mount -o ro /dev/loop0 /mnt/rescue
mke2fs -n 的关键是 -n:没有它就会真的格式化。-b 32768 告诉 e2fsck 用哪个备份块,-B 4096 告诉它块大小;两者不匹配会报 Attempt to read block from filesystem resulted in short read。
XFS 没有「备份超级块」的概念,元数据分布在各 AG(allocation group),修复工具是 xfs_repair:
sudo umount /data
sudo xfs_repair -n /dev/vgdata/lvdata # 只检查,不写
sudo xfs_repair -v /dev/vgdata/lvdata # 实际修复
xfs_repair -n 可能很慢,但它是唯一安全的预检。xfs_repair -L 会清空日志,只在日志本身损坏、且你接受「丢失最近未落盘事务」时使用;它不能和 -n 一起用。
如果 XFS 因为日志脏而挂不上,可以先只读挂载、不重放日志:
sudo mount -o ro,norecovery /dev/vgdata/lvdata /mnt/rescue
norecovery 让 XFS 跳过日志重放,可能看到不一致的元数据,只用来拷数据。ext4 对应的是 noload,语义类似,同样只用于救援。
fsck 绝不能对已读写挂载的文件系统执行。e2fsck 会拒绝,xfs_repair 会直接报 device or resource busy;如果强行 -f,坏的是正在使用的数据。正确顺序永远是:umount → fsck → mount。根文件系统只能在启动时由 initramfs 检查,或进 rescue target 后卸载检查。
开机自动检查由 systemd-fsck@.service 按 fstab 第 6 字段驱动。想强制下次开机检查,不要再 touch /forcefsck(systemd 已不识别),用内核参数:
# GRUB 启动项临时加:fsck.mode=force fsck.repair=yes
sudo tune2fs -c 1 /dev/vgdata/lvdata # ext4:挂载计数到 1 就检查
tune2fs -c 1 对 XFS 无效;XFS 用 xfs_admin 没有等价开关,靠 fsck.mode=force 或定期离线 xfs_repair -n。
0.10.12 三条巡检与操作脚本
0.10.12.1 块设备全景图
排障第一步不是 df,是把「盘—分区—RAID/LVM—文件系统—挂载点」这条链一次打出来:
#!/usr/bin/env bash
# blk-map.sh —— 只读:块设备、LVM、RAID、挂载、用量
set -euo pipefail
export LC_ALL=C
echo "== block devices"
lsblk -o NAME,SIZE,TYPE,FSTYPE,FSVER,LABEL,UUID,MOUNTPOINTS,ROTA,TRAN
echo "== mounts (real only)"
findmnt --real -o TARGET,SOURCE,FSTYPE,OPTIONS
echo "== usage"
df -hT -x tmpfs -x devtmpfs
df -i -x tmpfs -x devtmpfs
if command -v pvs >/dev/null; then
echo "== LVM"; pvs; vgs; lvs -o +devices
fi
if [[ -r /proc/mdstat ]]; then
echo "== md"; cat /proc/mdstat
command -v mdadm >/dev/null && mdadm --detail --scan
fi
lsblk 的 MOUNTPOINTS 可能有多行(同一设备 bind mount 多次),脚本里不要假设一行一个设备。findmnt --real 过滤伪文件系统,--list 或 -J 更适合程序解析。df -i 必须和 df -h 一起看:空间够但 inode 尽,是两种完全不同的故障。
0.10.12.2 容量与错误哨兵
这个脚本按阈值检查空间、inode、只读挂载和内核 I/O 错误,返回非 0 表示「有人该看一眼」:
#!/usr/bin/env bash
# fs-guard.sh [空间阈值%] [inode阈值%]
set -euo pipefail
sp=${1:-85}; ip=${2:-90}; bad=0
while read -r fs size used avail pct mnt; do
[[ $fs == Filesystem ]] && continue
p=${pct%\%}
if (( p >= sp )); then
echo "SPACE $pct $mnt ($used/$size)"; bad=1
fi
done < <(df -PT -x tmpfs -x devtmpfs)
while read -r fs inodes iused ifree ipct mnt; do
[[ $fs == Filesystem ]] && continue
p=${ipct%\%}
if (( p >= ip )); then
echo "INODE $ipct $mnt ($iused/$inodes)"; bad=1
fi
done < <(df -Pi -x tmpfs -x devtmpfs)
# 只读挂载:可能是 fsck 失败后 systemd 自动 remount-ro
findmnt --real -no TARGET,OPTIONS | awk '$2 ~ /(^|,)ro(,|$)/ {print "READONLY " $1}'
# 内核 I/O 与文件系统错误(需要 root 才能看全部)
dmesg -T --level=err,warn 2>/dev/null |
grep -iE 'I/O error|medium error|EXT4-fs error|XFS.*corrupt|remounting filesystem read-only' |
tail -20 || true
exit "$bad"
df -P 保证 POSIX 单行输出,避免长设备名折行;-T 加类型列。阈值用参数传入,方便在监控系统里按机器角色调整。只读挂载那一行只打印不置 bad,因为 / 之外的只读挂载可能是故意的;要让只读也告警,把它改成 bad=1。
0.10.12.3 带护栏的 mkfs
mkfs 的危险不在命令本身,而在「设备名敲错一个字母」。让脚本在写之前把证据摆出来,并要求输入设备名确认:
#!/usr/bin/env bash
# safe-mkfs.sh /dev/loopX ext4 LABEL
set -euo pipefail
dev=${1:?device}; fs=${2:?ext4|xfs}; label=${3:-lab}
[[ -b $dev ]] || { echo "not a block device: $dev" >&2; exit 2; }
if findmnt -S "$dev" >/dev/null; then
echo "REFUSE: $dev is mounted" >&2; exit 3
fi
if [[ $(lsblk -no TYPE "$dev") == disk ]] && lsblk -no NAME "$dev" | tail -n +2 | grep -q .; then
echo "REFUSE: $dev has partitions; pass a partition, not the whole disk" >&2; exit 4
fi
echo "== target"; lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINTS "$dev"
echo "== signatures"; sudo wipefs -n "$dev" || true
read -r -p "type the device path to continue: " answer
[[ $answer == "$dev" ]] || { echo "aborted" >&2; exit 5; }
case $fs in
ext4) sudo mkfs.ext4 -L "$label" -m 1 "$dev" ;;
xfs) sudo mkfs.xfs -L "$label" -f "$dev" ;;
*) echo "unsupported fs: $fs" >&2; exit 2 ;;
esac
wipefs -n 只列出签名不删除;看到 LVM2_member、linux_raid_member、ext4 时应该停下来查归属,而不是继续。lsblk -no TYPE 为 disk 且有子分区时拒绝,是为了防止把整盘当分区格式化。
0.10.13 两个实验:在线扩容与 RAID 校验
实验一:loop 上的 ext4 扩容与超级块救援
lab=/var/tmp/fslab; mkdir -p "$lab"; cd "$lab"
truncate -s 256M fs.img
loop=$(sudo losetup -f --show fs.img); echo "loop=$loop"
sudo mkfs.ext4 -L lab -m 1 "$loop"
sudo mkdir -p /mnt/fslab && sudo mount "$loop" /mnt/fslab
sudo dd if=/dev/zero of=/mnt/fslab/fill bs=1M count=100 status=none
df -hT /mnt/fslab
# 文件变大后,让 loop 重新读取容量,再在线扩文件系统
truncate -s 512M fs.img
sudo losetup -c "$loop"
sudo resize2fs "$loop"
df -hT /mnt/fslab
sudo dumpe2fs -h "$loop" | grep -E 'Block count|Block size|Reserved block count'
sudo umount /mnt/fslab
sudo e2fsck -fn "$loop"; echo "e2fsck rc=$?"
sudo mke2fs -n "$loop" | grep -i 'backup superblock'
预期:df 从约 240M 变成约 490M;resize2fs 输出新的块数;e2fsck -fn 返回 0。losetup -c 是必需的一步,只 truncate 文件,loop 设备仍记着旧容量,resize2fs 会报「nothing to do」。
救援演练(在副本上做,不要对刚扩好的文件系统做):
cp fs.img broken.img
loop2=$(sudo losetup -f --show broken.img)
# 覆盖主超级块所在的 1024 字节(仅实验!)
sudo dd if=/dev/zero of="$loop2" bs=1024 count=1 seek=1 conv=notrunc status=none
sudo e2fsck -fn "$loop2" || echo "expected failure"
sudo e2fsck -f -b 32768 -B 4096 "$loop2"
sudo mount -o ro "$loop2" /mnt/fslab && ls /mnt/fslab | head
预期:第一次 e2fsck 报 Bad magic number;用备份超级块后能挂载并看到 fill。备份块编号以 mke2fs -n 的输出为准,32768 只适用于 4K 块、默认 inode 布局的常见配置。
实验二:RAID5 的一致性校验与降级重建
lab=/var/tmp/mdlab; mkdir -p "$lab"; cd "$lab"
loops=()
for i in 1 2 3; do
truncate -s 256M "r$i.img"
loops+=("$(sudo losetup -f --show "r$i.img")")
done
sudo mdadm --create /dev/md/mdcheck --level=5 --raid-devices=3 \
--chunk=64 "${loops[@]}"
sudo mkfs.ext4 -L mdcheck /dev/md/mdcheck
sudo mkdir -p /mnt/mdcheck && sudo mount /dev/md/mdcheck /mnt/mdcheck
sudo dd if=/dev/urandom of=/mnt/mdcheck/data bs=1M count=40 status=none
md=$(basename "$(readlink -f /dev/md/mdcheck)")
# 触发一次只读一致性检查,统计不匹配的条带数
echo check | sudo tee "/sys/block/$md/md/sync_action"
cat "/sys/block/$md/md/mismatch_cnt"
sudo mdadm --detail /dev/md/mdcheck | grep -E 'State|Active|Failed|Resync|Consistency'
# 模拟一块盘故障,然后换新盘重建
sudo mdadm /dev/md/mdcheck --fail "${loops[0]}"
sudo mdadm /dev/md/mdcheck --remove "${loops[0]}"
truncate -s 256M spare.img
spare=$(sudo losetup -f --show spare.img)
sudo mdadm /dev/md/mdcheck --add "$spare"
watch -n 1 cat /proc/mdstat
预期:mismatch_cnt 正常为 0(刚写完、没有掉电);--fail 后 [UU_],--add 后出现 recovery 进度。重建完成后 State : clean,Failed Devices : 0。如果 mismatch_cnt 不为 0,先 echo repair > sync_action 让 RAID 重写校验块,再查内存/磁盘错误——校验不一致往往意味着某处静默损坏。
0.10.14 块层证据与两次回滚实验
0.10.13 验证了「扩容」和「重建」,这一节补两块日常证据:块设备到底忙不忙,以及一次误操作能不能用 LVM 快照回滚。前者决定你要不要换盘/拆阵列,后者决定你敢不敢在数据盘上做变更。
0.10.14.1 读 iostat 与 lsblk -D 的字段
iostat -x 是块层最常用的采样工具,但它的第一帧是自开机以来的累计值,直接看会得出错误结论。正确用法是 iostat -x 1 2,只看第二帧:
$ iostat -x 1 2 | awk '/^Device/{p=1} p' | tail -4 Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s wrqm/s %wrqm w_await wareq-sz aqu-sz %util nvme0n1 12.4 812.0 0.20 1.6 0.18 65.5 48.2 2048.0 2.10 4.2 0.42 42.5 0.31 8.4 loop0 0.0 0.0 0.00 0.0 0.00 0.0 0.0 0.0 0.00 0.0 0.00 0.0 0.00 0.0 $ lsblk -D -o NAME,DISC-GRAN,DISC-MAX,DISC-ZERO /dev/nvme0n1 NAME DISC-GRAN DISC-MAX DISC-ZERO nvme0n1 512B 2G 0
| 命令/字段 | 单位 | 读法 | 陷阱 |
|---|---|---|---|
r/s、w/s | 次/秒 | 完成的读/写请求数,不是字节数 | 小请求多不代表吞吐高 |
rkB/s、wkB/s | KiB/秒 | 实际吞吐 | 与 r/s 一起算平均请求大小 |
r_await、w_await | ms | 从提交到完成的平均延迟(含排队) | 比 svctm 可信;svctm 已废弃 |
aqu-sz | 个 | 平均队列深度 | 持续 > 1 说明设备在排队 |
%util | % | 设备忙时间比例 | NVMe/RAID 可并行,100% 不等于饱和 |
d/s、d_await | 次/秒、ms | discard 请求与延迟 | 只有支持 discard 的设备才有意义 |
DISC-GRAN | 字节 | 最小 discard 粒度 | 为 0 表示设备不支持 discard |
DISC-MAX | 字节 | 单次 discard 最大长度 | 超过会被内核拆分 |
DISC-ZERO | 0/1 | discard 是否保证读回全零 | 为 0 时不能拿 discard 当清零 |
%util 是最容易被误读的一列:它统计的是「设备有请求在处理的采样时间比例」,不是「带宽利用率」。一块 NVMe 在 %util=100% 时仍可能只跑了标称 IOPS 的一半;反过来,RAID 卡后面的多块盘可以并行,%util 到 100% 也不代表应用在等。判断饱和看 aqu-sz 与 r_await/w_await 是否随负载同步上升,再看应用侧 P99。
lsblk -D 的 DISC-GRAN=0 表示设备不支持 discard,此时 fstrim 会返回 the discard operation is not supported,不是文件系统坏了。loop 设备的 discard 能力继承自后端文件所在文件系统;在 ext4 的镜像文件上通常能看到非零粒度。
实验三:LVM 快照回滚一次误删
目的:在 loop 上建一个 LV,写基线数据,打快照,故意改坏,再用 lvconvert --merge 回到快照点。快照是块级时间点,不是备份,但它是本地误操作最快的一条回滚路径。
lab=/var/tmp/snaplab; mkdir -p "$lab"; cd "$lab"
truncate -s 1G base.img
loop=$(sudo losetup -f --show base.img)
sudo pvcreate "$loop"
sudo vgcreate vgtest "$loop"
sudo lvcreate -L 512M -n origin vgtest
sudo mkfs.ext4 -L origin /dev/vgtest/origin
sudo mkdir -p /mnt/snaplab
sudo mount /dev/vgtest/origin /mnt/snaplab
echo "version=1" | sudo tee /mnt/snaplab/data.txt
sudo mkdir -p /mnt/snaplab/important
echo "keep me" | sudo tee /mnt/snaplab/important/note.txt
sudo sync
# 打快照;COW 区要能装下快照生命周期内的所有改动
sudo lvcreate -s -L 128M -n snap /dev/vgtest/origin
sudo lvs -o lv_name,lv_attr,data_percent,size vgtest
# 模拟误操作
echo "version=2" | sudo tee /mnt/snaplab/data.txt
sudo rm -rf /mnt/snaplab/important
sudo umount /mnt/snaplab
# 回滚:先卸文件系统,再 merge;若提示 Delayed merge,按下面的说明处理
sudo umount /mnt/snaplab
sudo lvconvert --merge /dev/vgtest/snap
sudo lvchange -ay /dev/vgtest/origin 2>/dev/null || true
sudo mount /dev/vgtest/origin /mnt/snaplab
cat /mnt/snaplab/data.txt
cat /mnt/snaplab/important/note.txt
sudo lvs vgtest
预期:merge 后 data.txt 是 version=1,important/note.txt 回来,lvs 里不再有 snap。如果 lvconvert 输出 Delayed merge,说明 origin 还有打开者:lvchange -an /dev/vgtest/origin 再 lvchange -ay,或者重启后自动完成。data_percent 是 COW 区使用率,接近 100% 时快照会失效,merge 会失败——这也是快照必须监控的原因。
快照是崩溃一致,不是应用一致:数据库、虚拟机磁盘在打快照前要先停写或用 fsfreeze:
sudo fsfreeze --freeze /mnt/snaplab
sudo lvcreate -s -L 128M -n snap2 /dev/vgtest/origin
sudo fsfreeze --unfreeze /mnt/snaplab
fsfreeze 只保证文件系统缓存落盘并暂停新写,不保证应用自己的事务日志;数据库要用 FLUSH TABLES WITH READ LOCK 或备份工具自己的快照钩子。
实验四:ext4 配额与 EDQUOT
目的:给 nobody 设 1 MiB 硬限,写 2 MiB,观察 dd 在硬限处失败,并用 repquota 验证记账。
# 需要 quota 包提供 quotacheck/setquota/repquota
sudo apt install -y quota
lab=/var/tmp/quotalab; mkdir -p "$lab"; cd "$lab"
truncate -s 256M quota.img
loop=$(sudo losetup -f --show quota.img)
sudo mkfs.ext4 -L quotalab "$loop"
sudo mkdir -p /mnt/quotalab
sudo mount -o usrquota,grpquota "$loop" /mnt/quotalab
sudo chmod 1777 /mnt/quotalab
sudo quotacheck -cugm /mnt/quotalab
sudo quotaon -ug /mnt/quotalab
# 块软限 512K、硬限 1M;inode 软限 20、硬限 40(单位都是 1K block / 个)
sudo setquota -u nobody 512 1024 20 40 /mnt/quotalab
sudo runuser -u nobody -- dd if=/dev/zero of=/mnt/quotalab/fill bs=1K count=2048
echo "dd rc=$?"
sudo repquota -u /mnt/quotalab
sudo quota -u nobody
预期:dd 在约 1024 KiB 处报 Disk quota exceeded,退出码非 0;repquota 的 nobody 一行显示 1024 块左右、IUse% 不高。dd 的报错来自 write(2) 返回 EDQUOT(errno 122),不是 ENOSPC(28);应用只判断 ENOSPC 时会把配额超限误报成「磁盘满」。
回滚:
sudo quotaoff -ug /mnt/quotalab
sudo umount /mnt/quotalab
sudo losetup -d "$loop"
rm -rf "$lab"
setquota 的数字没有单位后缀时按 1K block 解释:512 1024 是 512 KiB 软限 / 1 MiB 硬限,不是 512 MiB。软限只启动宽限期(edquota -t,默认 7 天),硬限才立刻返回 EDQUOT。quotacheck 要在没有并发写的文件系统上跑;在忙的分区上先停应用,或离线检查。
块层与回滚的九个坑
| # | 现象 | 原因 | 修复/判断 |
|---|---|---|---|
| 1 | iostat 第一帧数值巨大 | 统计自开机以来的累计值 | iostat -x 1 2,只看第二帧 |
| 2 | %util 100% 但应用不慢 | NVMe/RAID 可并行,%util 不是带宽 | 看 aqu-sz、await 与应用 P99 |
| 3 | lsblk -D 全是 0 | 设备或后端文件系统不支持 discard | 换支持 discard 的存储;fstrim 报错属正常 |
| 4 | blkdiscard 后分区表没了 | 对整盘执行会清掉分区表 | 只对未挂载的整盘/LV 用,先 lsblk |
| 5 | 快照突然不可用 | COW 区写满 | 监控 data_percent,扩容或缩短快照寿命 |
| 6 | lvconvert --merge 不生效 | origin 仍打开,merge 被延迟 | umount + lvchange -an,或重启 |
| 7 | 回滚后数据库起不来 | 快照是崩溃一致,不是应用一致 | 用 fsfreeze 或数据库备份钩子 |
| 8 | 配额报「磁盘满」 | 应用只处理 ENOSPC,没处理 EDQUOT | 两个 errno 都判断;repquota 看用量 |
| 9 | setquota 512 1024 只给了 512 KiB/1 MiB | 单位是 1K block,不是 MiB | 写 512M/1G 后缀或换算好再传 |
第 9 条是配额实验里最常见的单位错误:setquota 的块参数以 1K 为单位,512 就是 512 KiB;xfs_quota 的 bsoft=500m 才带单位。两套工具的单位不同,照抄命令会把限额设小一千倍。
0.10.15 十四个磁盘与文件系统陷阱
| # | 现象 | 原因 | 修复/判断 |
|---|---|---|---|
| 1 | mkfs 后整块盘的数据没了 | 对整盘而非分区执行,或设备名错 | 用 /dev/disk/by-id、脚本护栏、备份 |
| 2 | 改完 fstab 开不了机 | 设备不存在/选项错,systemd 等超时 | nofail、x-systemd.device-timeout=、findmnt --verify |
| 3 | mount -a 成功,重启失败 | 启动时设备还没出现 | _netdev、nofail、UUID |
| 4 | 克隆虚拟机后 XFS 挂不上 | 两台机 UUID 相同 | xfs_admin -U generate、tune2fs -U random |
| 5 | df 100% 但 root 还能写 | ext4 预留块 | tune2fs -l 看 Reserved block count |
| 6 | 有空间却 No space left | inode 用尽 | df -i;删小文件或重建 -N |
| 7 | 删了大文件空间不释放 | 进程仍持有 fd | lsof +L1,重启/截断 fd |
| 8 | LVM 扩了 LV,df 没变 | 没扩文件系统 | lvextend -r 或 resize2fs/xfs_growfs |
| 9 | XFS 缩容后数据损坏 | XFS 不支持缩容 | 备份→重建→恢复 |
| 10 | LVM 快照突然失效 | 快照 COW 区写满 | 监控 lvs 的 Data%,扩快照或删旧快照 |
| 11 | RAID 重组出两个阵列 | 成员盘旧超级块没清 | mdadm --zero-superblock |
| 12 | fsck 把文件系统修得更坏 | 对已挂载/读写中的 fs 执行 | 卸载后检查;根 fs 进 rescue |
| 13 | xfs_repair 报设备忙 | 文件系统仍挂载 | umount,或 -o ro,norecovery 先拷数据 |
| 14 | SSD 删除后延迟升高 | discard 每次删除同步 TRIM | 改用 fstrim.timer 周期回收 |
第 11 条值得展开:mdadm --stop 只停阵列,不擦成员盘上的超级块。把这些盘再用于新阵列时,内核 udev 可能按旧超级块自动组装,出现「明明删了阵列,/proc/mdstat 里又回来了」。正确收尾是 mdadm --zero-superblock 每块成员盘,再 wipefs -a。
0.10.16 本章出口检查
| 检查项 | 达标线 | 没达标去哪 |
|---|---|---|
用 lsblk/findmnt/df 确认设备归属 | 能说出哪块是根盘、哪块没挂载 | 0.10.1 |
| 解释 ext4/XFS/Btrfs 的扩容与缩容边界 | 知道 XFS 不能缩 | 0.10.2 |
| 定位「df 满、du 小」 | 能给出 lsof +L1 证据 | 0.10.3 |
| 判断 inode 用尽 | 会用 df -i 并解释 -N | 0.10.4 |
写一条带 nofail 的 fstab | 能用 findmnt --verify 验证 | 0.10.5 |
| 在 loop 上完成 LVM 扩容 | 顺序 PV→VG→LV→FS 正确 | 0.10.6 |
| 在 loop 上完成 RAID5 替换 | 会看 [UU_] 与重建进度 | 0.10.7 |
| 说出 RAID 不是备份的理由 | 能举重建期间再坏一块 | 0.10.7 |
用 iostat -x/lsblk -D 判断块层状态 | 能解释 %util、aqu-sz、DISC-GRAN | 0.10.14.1 |
| 用 LVM 快照回滚一次误删 | 能说出 merge 延迟与 COW 写满的后果 | 0.10.14 |
触发并解释 EDQUOT | 知道 setquota 的 1K block 单位 | 0.10.14 |
Q1 df -h /data 显示 100%,du -sh /data 只有 40 GB,文件系统是 100 GB。列出三种可能,并给出各自的验证命令。
Q2 df -h 还有 17 GB,touch /data/x 却报 No space left on device。最可能是什么?怎么确认?
Q3 一个 1 TB 的 XFS 文件系统在 LVM 上,需要缩到 800 GB。可行步骤是什么?如果必须缩,应该怎么做?
Q4 RAID5 三块盘坏了一块,你换上新盘开始重建。此时再坏一块会怎样?为什么说「RAID 不是备份」?
答案
A1 ① 删除的文件仍被打开:sudo lsof +L1 看 (deleted);② ext4 预留块:sudo tune2fs -l /dev/... | grep Reserved;③ 挂载点遮住了原目录:findmnt 看是否有叠加挂载,umount 后复查;④ 还有快照/稀疏文件:du --apparent-size、btrfs filesystem usage。
A2 inode 用尽。df -i /data 看 IUse% 是否 100%,sudo find /data -xdev -type f | wc -l 数文件数。确认后删无用小文件,或迁到 inode 更多的文件系统;重建加 -N 是最后手段。
A3 XFS 不支持缩容。可行路径是:备份数据 → umount → lvreduce 到 800 GB → mkfs.xfs 重建 → 恢复数据。不能先 lvreduce 再 xfs_growfs 反向缩,也不能对已缩小的块设备上的 XFS 直接挂载写入。若必须频繁缩容,选 ext4 或 Btrfs。
A4 RAID5 降级时没有冗余,再坏一块就整阵列失效,数据全丢。重建期间所有盘都在满负荷读,第二块盘在这段时间故障的概率显著上升;而且 RAID 只防盘坏,不防误删、勒索软件、控制器故障、机房事故。备份必须是独立的、离线的、可恢复验证过的副本。
母本对应
| 母本 | 章节 | PDF 页 | 本书落点 | 程度 |
|---|---|---|---|---|
| 鸟哥 | 第 2 章 主机规划与磁盘分区 | p.122–156 | 0.10.1–0.10.2 | 完整(分区、文件系统概念) |
| 鸟哥 | 第 7 章 磁盘与文件系统管理 | p.342–416 | 0.10.1–0.10.5 | 完整(挂载、df/du、fstab) |
| 鸟哥 | 第 14 章 磁盘配额与进阶文件系统管理 | p.704–750 | 0.10.6–0.10.8、0.10.14 | 完整(quota p.704–717、RAID p.718–730、LVM p.731–749) |
| shell-book | 第 4 章 监测磁盘空间 | p.85–106 | 0.10.1、0.10.3 | 精选(df/du/挂载) |
| shell-book | 第 8 章 管理文件系统 | p.166–182 | 0.10.5–0.10.7 | 精选(挂载、LVM) |
| Shotts | 第 15 章 存储介质 | p.166–180 | 0.10.1–0.10.5 | 完整(分区、挂载、文件系统) |
| Kerrisk | 第 14 章 系统编程概念 | — | 0.10.2、0.10.5 | 背景(设备文件、VFS、挂载) |
| LVM2 / mdadm 文档 | lvm(8)、mdadm(8)、lvconvert(8) | man | 0.10.6–0.10.7、0.10.14 | 精选(命令、快照与实验) |
| sysstat / util-linux | iostat(1)、lsblk(8)、fstrim(8) | man | 0.10.14.1 | 精选(块层字段与 discard) |
下一站:0.11 软件包、编译、库与镜像从「块设备上的文件系统」走到「文件系统里的程序」:
.deb怎么装、源码怎么编、动态库为什么找不到、容器镜像和包管理是什么关系。