Linux-11 磁盘、分区与挂载:从块设备到 fstab 的完整链路
这一篇解决「一块盘从插上到能用」的完整链路,以及最高频的运维事故:磁盘满了。
先看五个问题:
lsblk、df、du、fdisk -l分别在回答什么问题?为什么它们的结果可能互相矛盾?- MBR 和 GPT 有什么区别?为什么 2TB 是一个坎?
/etc/fstab里为什么要写 UUID 而不是/dev/sdb1?- 磁盘满了,但
du加起来对不上,可能是什么原因? - 一块新盘从插上到能用,需要做哪几步?
1. 块设备的层次
1.1 从物理盘到挂载点
理解这条链路,前面那些命令的分工就清楚了:
物理设备 / 云盘
+----------------------+
| /dev/sdb (整块盘) | lsblk / fdisk -l 看这一层
+----------------------+
|
| 分区(可选!云上常常不分区)
v
+----------+-----------+
| sdb1 | sdb2 | fdisk / parted / sgdisk 操作这一层
+----------+-----------+
|
| LVM(可选,为了在线扩容与快照)
v
+----------------------+
| VG: data | pvs / vgs / lvs
| +----+ +--------+ |
| | LV | | LV | |
+----------------------+
|
| 创建文件系统(格式化)
v
+----------------------+
| ext4 / xfs | mkfs.* / blkid / tune2fs
+----------------------+
|
| 挂载到目录树
v
+----------------------+
| /data | mount / findmnt / df
+----------------------+
|
v
里面的文件与目录 du 统计这一层
四个命令的分工由此明确:
| 命令 | 回答的问题 | 数据来源 |
|---|---|---|
lsblk |
有哪些块设备、怎么分区、挂在哪 | sysfs(/sys/block) |
fdisk -l / parted -l |
分区表长什么样 | 直接读磁盘的分区表扇区 |
df |
每个文件系统用了多少 | statfs() 读超级块,O(1) |
du |
某个目录树里的文件加起来多少 | 遍历每个 inode |
它们结果不一致是正常的(第 10 章详解):df 能看到「已删除但仍被打开」的文件占用,du 看不到;lsblk 显示的是设备容量,df 显示的是文件系统可用量(差额是元数据和保留块)。
1.2 lsblk:最该先敲的命令
lsblk
# NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
# sda 8:0 0 100G 0 disk
# ├─sda1 8:1 0 1G 0 part /boot/efi
# ├─sda2 8:2 0 2G 0 part /boot
# └─sda3 8:3 0 97G 0 part
# ├─vg0-root 253:0 0 50G 0 lvm /
# └─vg0-var 253:1 0 47G 0 lvm /var
# sdb 8:16 0 500G 0 disk
# └─sdb1 8:17 0 500G 0 part /data
# sr0 11:0 1 1024M 0 rom
逐列解释:
| 列 | 含义 |
|---|---|
NAME |
设备名(树形缩进表示包含关系) |
MAJ:MIN |
主:次设备号 —— 内核靠这两个数字找驱动,不是靠文件名 |
RM |
removable,是否可移动介质(U 盘、光驱为 1) |
SIZE |
容量 |
RO |
read-only,是否只读 |
TYPE |
disk 整盘 / part 分区 / lvm 逻辑卷 / crypt 加密层 / rom 光驱 |
MOUNTPOINTS |
挂载点(空表示没挂载) |
常用变体:
lsblk -f # ✅ 加上文件系统类型、LABEL、UUID —— 最常用
# NAME FSTYPE FSVER LABEL UUID MOUNTPOINTS
# sdb1 ext4 1.0 data a1b2c3d4-... /data
lsblk -o NAME,SIZE,FSTYPE,FSAVAIL,FSUSE%,MOUNTPOINT # 自定义列
lsblk -d # 只看整盘,不展开分区
lsblk -p # 显示完整设备路径 /dev/sdb1
lsblk -a # 包括空设备
lsblk -J # JSON 输出(脚本用)
lsblk -t # 显示拓扑(扇区大小、队列深度、调度器)
lsblk /dev/sdb # 只看某个设备
lsblk -o +MODEL,SERIAL # 追加列(认盘用)
1.3 设备命名规则
/dev/sda, /dev/sdb # SCSI/SATA/USB 盘(sd = SCSI disk)
/dev/sda1 # sda 的第 1 个分区
/dev/vda, /dev/vdb # virtio 虚拟盘(KVM/云主机常见,v = virtio)
/dev/xvda # Xen 虚拟盘(AWS 老实例类型)
/dev/nvme0n1 # NVMe:控制器 0 的命名空间 1
/dev/nvme0n1p1 # 它的第 1 个分区(注意有 p!)
/dev/hda # 老 IDE 盘(已基本绝迹)
/dev/mmcblk0p1 # SD 卡 / eMMC
/dev/loop0 # 回环设备(把文件当块设备用)
/dev/mapper/vg0-root # device-mapper(LVM、加密、multipath)
/dev/md0 # 软 RAID
为什么 NVMe 分区要加 p? 因为 nvme0n1 本身以数字结尾,直接接数字会歧义(nvme0n11 到底是「控制器 0 命名空间 11」还是「命名空间 1 的分区 1」?),所以用 p 分隔。同理 mmcblk0p1、loop0p1。
主次设备号才是内核的真身份:
ls -l /dev/sdb1
# brw-rw---- 1 root disk 8, 17 Aug 12 18:30 /dev/sdb1
# ^^^^^ 主设备号 8(sd 驱动),次设备号 17
# b = block device(块设备),c = character device(字符设备)
cat /proc/devices | grep -A5 'Block devices'
# Block devices:
# 8 sd
# 9 md
# 11 sr
# 253 device-mapper
# 259 blkext
# 文件名只是给人看的,内核认的是这两个数字(04 篇讲过同样的道理)
sudo mknod /tmp/myfakedisk b 8 17 # 手动创建一个指向同一设备的节点
sudo blkid /tmp/myfakedisk # 能识别出来,因为主次号相同
1.4 设备名不可靠:为什么需要 UUID
这是开篇第三问的核心。 设备名由内核探测顺序决定,而探测顺序会变:
# 会导致设备名变化的情况
# ① 插拔顺序不同(USB 盘)
# ② BIOS/UEFI 里改了启动顺序
# ③ 云上加卸载云盘
# ④ 内核版本升级导致驱动加载顺序改变
# ⑤ 多路径(multipath)环境
# 后果:原来的 /dev/sdb 变成了 /dev/sdc,
# fstab 里写死 /dev/sdb1 的挂载就会挂错盘或挂载失败 -> 系统启动卡住
所以要用内容标识而不是位置标识:
sudo blkid
# /dev/sda2: UUID="a1b2c3d4-e5f6-7890-abcd-ef1234567890" TYPE="ext4" PARTUUID="..."
# /dev/sdb1: LABEL="data" UUID="f0e1d2c3-..." TYPE="xfs"
# 四种标识方式的可靠性
# ① UUID 文件系统的唯一 ID ← 【最推荐】格式化时生成,跟着文件系统走
# ② LABEL 文件系统标签 人类可读,但可能重复
# ③ PARTUUID 分区的 UUID(GPT) 跟着分区走,重新格式化不变
# ④ /dev/sdX 设备名 ❌ 不可靠
# 内核还提供了按标识访问的软链接
ls -l /dev/disk/by-uuid/
ls -l /dev/disk/by-label/
ls -l /dev/disk/by-id/ # 按厂商+序列号(认物理盘最可靠)
ls -l /dev/disk/by-path/ # 按物理连接路径(PCI 插槽)
ls -l /dev/disk/by-partuuid/
# 这些都是 udev 根据设备属性自动创建的软链接
2. 查看命令的分工
2.1 blkid:查 UUID 与文件系统类型
sudo blkid # 列出所有块设备的标识
sudo blkid /dev/sdb1 # 只看一个
sudo blkid -s UUID -o value /dev/sdb1 # 只输出 UUID 的值(脚本用)
# f0e1d2c3-4b5a-6978-8c9d-0e1f2a3b4c5d
sudo blkid -L data # 按 LABEL 反查设备名
sudo blkid -U f0e1d2c3-... # 按 UUID 反查
sudo blkid -p /dev/sdb1 # 探测模式(不用缓存,直接读设备)
sudo blkid -c /dev/null /dev/sdb1 # 绕过缓存 /etc/blkid.tab
# 写 fstab 时的标准姿势
echo "UUID=$(sudo blkid -s UUID -o value /dev/sdb1) /data xfs defaults,noatime 0 2" \
| sudo tee -a /etc/fstab
2.2 df:文件系统用量
df -hT
# Filesystem Type Size Used Avail Use% Mounted on
# /dev/mapper/vg0-root ext4 50G 28G 20G 59% /
# /dev/sdb1 xfs 500G 120G 380G 24% /data
# tmpfs tmpfs 7.8G 1.2M 7.8G 1% /run
| 参数 | 作用 |
|---|---|
-h |
人类可读(1024 进制) |
-H |
1000 进制(磁盘厂商口径) |
-T |
显示文件系统类型 |
-i |
显示 inode 用量(「空间还有却创建不了文件」必查) |
-a |
包含伪文件系统 |
-x TYPE |
排除类型(-x tmpfs -x overlay 去噪音) |
-t TYPE |
只看某类型 |
-l |
只看本地(跳过 NFS,避免命令卡死) |
-P |
POSIX 格式,强制单行输出(脚本必用) |
--total |
追加总计行 |
--output=FIELD,... |
自定义列 |
df -h /var/log/app.log # ✅ 给文件路径,显示它所在的文件系统
df -i # inode 用量
df -hT -x tmpfs -x devtmpfs -x overlay # 只看真实磁盘
df --output=target,fstype,size,pcent,ipcent -h # 空间与 inode 一起看
timeout 5 df -h # 防 NFS 卡死
# 监控脚本:超过 85% 就告警
df -PH -x tmpfs -x devtmpfs | awk 'NR>1 && int($5)>85 {print $6, $5}'
Size ≠ Used + Avail 的原因(04 篇 10.8 讲过):ext4 默认给 root 预留 5% 的块。
sudo tune2fs -l /dev/sdb1 | grep -i 'reserved block'
# Reserved block count: 6553600 <- 块数
sudo tune2fs -m 1 /dev/sdb1 # 把保留比例从 5% 降到 1%(大盘上能释放几十 GB)
# xfs 没有这个机制
2.3 findmnt:挂载树
比 mount 好读得多,是查「这个目录属于哪个文件系统」的首选:
findmnt
# TARGET SOURCE FSTYPE OPTIONS
# / /dev/mapper/... ext4 rw,relatime
# ├─/proc proc proc rw,nosuid,nodev,noexec
# ├─/sys sysfs sysfs rw,nosuid,nodev,noexec
# ├─/run tmpfs tmpfs rw,nosuid,nodev,size=1608468k
# ├─/data /dev/sdb1 xfs rw,relatime,attr2
# └─/var/lib/docker/overlay2 overlay overlay rw,relatime,lowerdir=...
findmnt /data # 查某个路径属于哪个挂载点
findmnt -t ext4,xfs # 只看指定类型
findmnt --df # 带用量(类似 df 但树形)
findmnt -o TARGET,SOURCE,FSTYPE,OPTIONS,USE%
findmnt -S /dev/sdb1 # 按源设备查
findmnt -T /var/log/app.log # 按目标路径查(文件也行)
findmnt --verify # ✅ 检查 fstab 语法与可挂载性
findmnt -R / # 递归显示子挂载
findmnt -J # JSON
# 老式命令(输出杂乱,但一定存在)
mount | column -t
cat /proc/mounts # 内核视角,最可靠,脚本用这个
cat /proc/self/mountinfo # 更详细(含挂载 ID、传播属性)
# 判断是不是挂载点(脚本防御用)
mountpoint -q /data && echo "已挂载" || echo "未挂载"
mountpoint -q /data || { echo "数据盘未挂载,中止"; exit 1; }
# ^^ 防止往未挂载的目录写数据,把根分区写爆
2.4 其他查看命令
# 分区表
sudo fdisk -l # 所有磁盘的分区表
sudo fdisk -l /dev/sdb
sudo parted -l # 同上,输出格式不同
sudo parted /dev/sdb print # 单个设备
sudo sfdisk -l /dev/sdb # 脚本友好
sudo sgdisk -p /dev/sdb # GPT 专用
# 磁盘硬件信息
lsscsi # SCSI 设备列表
sudo nvme list # NVMe 盘列表(型号、序列号、容量)
sudo smartctl -a /dev/sda # ✅ SMART 健康信息(坏道、通电时间、温度)
sudo smartctl -H /dev/sda # 只看健康状态结论
sudo hdparm -I /dev/sda # 磁盘参数
cat /sys/block/sda/queue/rotational # 1=机械盘 0=SSD
cat /sys/block/sda/queue/scheduler # IO 调度器
cat /sys/block/sda/size # 扇区数(× 512 = 字节)
# IO 性能
iostat -xz 1 # 每秒刷新的 IO 统计(%util、await)
iotop -o # 哪个进程在读写(-o 只显示有 IO 的)
sudo blktrace / biosnoop # 更底层的追踪(eBPF)
# 目录用量
du -xh --max-depth=1 /var | sort -h # 找大目录(-x 不跨文件系统)
ncdu /var # ✅ 交互式的 du(强烈推荐,可以边看边删)
3. 分区表:MBR 与 GPT
3.1 开篇第二问:2TB 为什么是个坎
MBR(Master Boot Record,主引导记录)诞生于 1983 年,它把分区表塞在磁盘的**第一个扇区(512 字节)**里:
/dev/sdb 的第 0 号扇区(512 字节)
+------------------------------------------+
| 引导代码 446 字节 |
+------------------------------------------+
| 分区表项 1 16 字节 |
| 分区表项 2 16 字节 | 只有 4 个槽位!
| 分区表项 3 16 字节 |
| 分区表项 4 16 字节 |
+------------------------------------------+
| 结束标志 0x55AA 2 字节 |
+------------------------------------------+
每个分区表项里,"起始 LBA" 和 "扇区数" 各占 4 字节 = 32 位
2TB 上限的算术:
最大可寻址扇区数 = 2^32 = 4,294,967,296
每扇区 512 字节
最大容量 = 4,294,967,296 × 512 = 2,199,023,255,552 字节 ≈ 2 TiB
所以 MBR 有三个硬限制:
| 限制 | 数值 | 原因 |
|---|---|---|
| 最大容量 | 2 TiB | 32 位 LBA × 512B 扇区 |
| 主分区数 | 4 个 | 只有 4 个 16 字节的槽位 |
| 逻辑分区 | 需要牺牲一个主分区做「扩展分区」 | 打补丁式的扩展方案 |
| 无冗余 | 分区表只有一份 | 第 0 扇区损坏 = 所有分区信息丢失 |
GPT(GUID Partition Table)是 UEFI 标准的一部分,解决了全部问题:
| MBR | GPT | |
|---|---|---|
| 最大容量 | 2 TiB | 9.4 ZB(64 位 LBA) |
| 分区数 | 4 主(或 3 主 + 扩展) | 128(可扩展) |
| 分区表备份 | ❌ 无 | ✅ 磁盘头尾各一份 |
| 校验 | ❌ 无 | ✅ CRC32 |
| 分区标识 | 1 字节类型码 | GUID + 可读名字 |
| 引导方式 | BIOS | UEFI(也可兼容 BIOS) |
| 保护 MBR | — | 头部保留一个假 MBR,防老工具误认为空盘 |
# 看当前用的是哪种
sudo parted /dev/sdb print | grep 'Partition Table'
# Partition Table: gpt
sudo fdisk -l /dev/sdb | grep -i 'disklabel type'
# Disklabel type: gpt
# 现在的选择很简单:新盘一律用 GPT
# 唯一例外:需要在老 BIOS 机器上启动,且不支持 UEFI 时
注意 4Kn 磁盘:现代大盘的物理扇区是 4096 字节(4Kn),此时 MBR 的理论上限变成 16TiB,但兼容性问题多,不值得依赖。
cat /sys/block/sdb/queue/hw_sector_size # 512 或 4096(物理)
cat /sys/block/sdb/queue/logical_block_size # 逻辑扇区大小
sudo fdisk -l /dev/sdb | grep 'Sector size'
# Sector size (logical/physical): 512 bytes / 4096 bytes
# ^^^ 逻辑 ^^^^ 物理(512e 盘)
3.2 分区操作
先说一个现实:云主机上经常不需要分区——直接在整块盘上建文件系统。
# 云盘的常见做法:不分区,直接格式化整盘
sudo mkfs.ext4 /dev/vdb # 注意是 vdb 不是 vdb1
sudo mount /dev/vdb /data
# 好处:扩容极简(云控制台扩容后直接 resize2fs,不用先动分区表)
# 代价:不能在一块盘上放多个文件系统(但云上加盘很便宜,一般不需要)
需要分区时,三个工具:
# ── fdisk:交互式,MBR/GPT 都支持(最常用)──
sudo fdisk /dev/sdb
# 常用子命令:
# m 帮助
# p 打印当前分区表
# n 新建分区(然后按提示:分区号、起始扇区、结束扇区或 +500G)
# d 删除分区
# t 改分区类型(8e00=Linux LVM, 8300=Linux filesystem, 8200=swap)
# g 创建新的空 GPT 分区表 ← 新盘用这个
# o 创建新的空 MBR 分区表
# w 【写入并退出】 ← 在此之前所有操作都在内存里,没落盘
# q 不保存退出 ← 改错了用这个
# ── parted:脚本友好(自动化用)──
sudo parted -s /dev/sdb mklabel gpt
sudo parted -s /dev/sdb mkpart primary ext4 1MiB 100%
# ^^^^ 从 1MiB 开始(对齐!)
sudo parted -s -a optimal /dev/sdb mkpart primary 0% 100%
# ^^^^^^^^^^ 自动最优对齐
sudo parted /dev/sdb print free # 看空闲空间
sudo parted -s /dev/sdb rm 1 # 删除分区 1
# ── sgdisk:GPT 专用,最适合脚本 ──
sudo sgdisk -og /dev/sdb # 清空并建 GPT
sudo sgdisk -n 1:0:+500G -t 1:8300 -c 1:"data" /dev/sdb
# ^^^^^^^^^^^^ 分区1,起始默认,大小 500G
# ^^^^^^^^^ 类型码
# ^^^^^^^^^^^^^ 分区名
sudo sgdisk -p /dev/sdb # 打印
sudo sgdisk --backup=/root/sdb.gpt /dev/sdb # ✅ 备份分区表
sudo sgdisk --load-backup=/root/sdb.gpt /dev/sdb # 恢复
sudo sgdisk -Z /dev/sdb # ⚠️ 抹掉所有分区表(zap)
改完分区表要通知内核:
sudo partprobe /dev/sdb # 让内核重读分区表
sudo partx -u /dev/sdb # 或者
sudo blockdev --rereadpt /dev/sdb # 或者
lsblk /dev/sdb # 确认新分区出现了
# 如果提示 "Device or resource busy"(有分区正在使用),只能重启
# 所以生产上改分区表前要先 umount 所有相关分区
分区对齐(现在的工具都会自动处理,但要知道为什么):
# 分区起始位置应该对齐到 1MiB(2048 个 512B 扇区)
# 原因:SSD 的擦除块和 RAID 的条带通常是 512KB~4MB,
# 不对齐会导致一次逻辑写落到两个物理块上 -> 写放大、性能下降
sudo parted /dev/sdb align-check optimal 1
# 1 aligned ✅
4. 文件系统
4.1 选哪个
| ext4 | xfs | btrfs | zfs | |
|---|---|---|---|---|
| 成熟度 | 极高(默认选择) | 极高 | 较高 | 高(许可证问题,需单独装) |
| 大文件性能 | 好 | 更好 | 好 | 好 |
| 海量小文件 | inode 预分配可能用尽 | inode 动态分配 | 好 | 好 |
| 能否缩小 | ✅ 可以(离线) | ❌ 不能缩小 | ✅ | ✅ |
| 在线扩容 | ✅ | ✅ | ✅ | ✅ |
| 快照 | ❌(靠 LVM) | ❌(靠 LVM/reflink) | ✅ 原生 | ✅ 原生 |
| reflink(秒级复制) | ❌ | ✅ | ✅ | ✅ |
| 校验和 | 仅元数据 | 仅元数据 | ✅ 数据+元数据 | ✅ |
| 默认于 | Debian/Ubuntu | RHEL/Rocky/Alma 7+ | openSUSE、Fedora(部分) | — |
实践建议:
# 通用场景、Debian 系 -> ext4(最稳,社区经验最多)
# RHEL 系、大文件、数据库 -> xfs(RHEL 默认,大文件与并发写更优)
# 海量小文件 -> xfs(inode 动态分配,不会用尽)
# 需要快照/校验和 -> btrfs 或 ZFS
# ⚠️ 关键区别:xfs 不能缩小!规划容量时要留意(但实际很少需要缩小)
4.2 创建文件系统
# 基本用法
sudo mkfs.ext4 /dev/sdb1
sudo mkfs.xfs /dev/sdb1
sudo mkfs -t ext4 /dev/sdb1 # 等价写法
# 常用参数(ext4)
sudo mkfs.ext4 -L data /dev/sdb1 # -L 设 LABEL
sudo mkfs.ext4 -m 1 /dev/sdb1 # -m 保留块百分比(默认 5%,大盘设 1%)
sudo mkfs.ext4 -N 10000000 /dev/sdb1 # -N 指定 inode 总数(海量小文件)
sudo mkfs.ext4 -i 4096 /dev/sdb1 # -i 每多少字节一个 inode(默认 16384)
sudo mkfs.ext4 -b 4096 /dev/sdb1 # -b 块大小
sudo mkfs.ext4 -T largefile /dev/sdb1 # -T 用途预设(largefile/news/small)
sudo mkfs.ext4 -E lazy_itable_init=0 /dev/sdb1 # 立即初始化 inode 表(不加会后台慢慢做)
sudo mkfs.ext4 -F /dev/sdb1 # -F 强制(设备正在使用时)
# 常用参数(xfs)
sudo mkfs.xfs -L data /dev/sdb1
sudo mkfs.xfs -f /dev/sdb1 # -f 强制覆盖已有文件系统
sudo mkfs.xfs -m reflink=1 -m crc=1 /dev/sdb1 # 开启 reflink(秒级复制,见 04 篇 4.2)
sudo mkfs.xfs -d su=64k,sw=4 /dev/sdb1 # RAID 条带对齐
# ⚠️ mkfs 会【立即销毁】设备上的所有数据,没有确认提示
# 执行前务必用 lsblk -f 确认设备名和当前内容
lsblk -f /dev/sdb # 先看清楚!
sudo wipefs -a /dev/sdb1 # 想彻底清掉旧签名再格式化
4.3 调整与检查
# ── 扩容(在线,不用卸载)──
# ext4
sudo resize2fs /dev/sdb1 # 扩到分区/设备的最大可用
sudo resize2fs /dev/sdb1 400G # 扩到指定大小
# xfs(只能扩不能缩,且必须【已挂载】)
sudo xfs_growfs /data # 注意参数是【挂载点】不是设备
sudo xfs_growfs -d /data # -d 扩到最大
# ── 缩小(ext4 才行,且必须先卸载)──
sudo umount /data
sudo e2fsck -f /dev/sdb1 # 必须先检查
sudo resize2fs /dev/sdb1 200G # 缩小
# xfs 不支持缩小 —— 只能备份、重建、恢复
# ── 检查与修复 ──
sudo fsck /dev/sdb1 # 通用入口(会调对应的 fsck.<type>)
sudo e2fsck -f /dev/sdb1 # ext4:-f 强制检查(即使标记为 clean)
sudo e2fsck -p /dev/sdb1 # -p 自动修复安全的问题
sudo e2fsck -y /dev/sdb1 # -y 对所有提问回答 yes
sudo xfs_repair /dev/sdb1 # xfs 用这个(不是 fsck.xfs)
sudo xfs_repair -n /dev/sdb1 # -n 只检查不修复
# ⚠️ 必须先卸载才能检查!在挂载状态下 fsck 会破坏文件系统
findmnt /dev/sdb1 && echo "还挂着,先 umount"
# ── tune2fs:调整 ext4 参数(无需重新格式化)──
sudo tune2fs -l /dev/sdb1 # ✅ 查看全部参数
sudo tune2fs -L newlabel /dev/sdb1 # 改 LABEL
sudo tune2fs -U random /dev/sdb1 # 重新生成 UUID(克隆盘冲突时用)
sudo tune2fs -m 1 /dev/sdb1 # 改保留块百分比
sudo tune2fs -c 100 /dev/sdb1 # 每挂载 100 次强制检查
sudo tune2fs -i 30d /dev/sdb1 # 每 30 天强制检查
sudo tune2fs -c -1 -i 0 /dev/sdb1 # 关闭定期检查(生产上常做,避免意外的长时间启动)
sudo tune2fs -o journal_data_writeback /dev/sdb1 # 改日志模式(性能↑安全性↓)
sudo tune2fs -O ^has_journal /dev/sdb1 # 关闭日志(只在特殊场景,如纯缓存盘)
# xfs 的对应命令
sudo xfs_admin -L newlabel /dev/sdb1
sudo xfs_admin -U generate /dev/sdb1
sudo xfs_info /data # 查看 xfs 参数(含 reflink 是否开启)
5. 挂载
5.1 基本用法
sudo mkdir -p /data # 挂载点必须先存在
sudo mount /dev/sdb1 /data # 挂载
sudo mount -t xfs /dev/sdb1 /data # 显式指定类型(通常能自动探测)
sudo mount -o noatime /dev/sdb1 /data # 带选项
sudo mount UUID=f0e1d2c3-... /data # 按 UUID 挂载
sudo mount LABEL=data /data # 按 LABEL
sudo mount /data # ✅ fstab 里有这条记录时,只写挂载点即可
sudo mount -a # 挂载 fstab 里所有未挂载的(改完 fstab 必做)
sudo umount /data # 卸载(注意命令是 umount 不是 unmount)
sudo umount /dev/sdb1 # 也可以指定设备
5.2 常用挂载选项
每个选项都在解决一个具体问题:
| 选项 | 解决什么问题 |
|---|---|
defaults |
= rw,suid,dev,exec,auto,nouser,async |
ro / rw |
只读 / 读写。只读挂载是防篡改的有力手段(03 篇讲的 /usr 静态可共享) |
noatime |
不更新访问时间 —— 消除「读文件也要写盘」的写放大(04 篇 6.1) |
relatime |
折中方案(现代默认):只在 atime 早于 mtime 或超过 24h 时更新 |
nodiratime |
只对目录不更新 atime |
nosuid |
该分区上的 SUID/SGID 位失效 —— 用户可写的分区必加(10 篇 5.1) |
nodev |
忽略设备文件 —— 防止用户造一个 /dev/sda 绕过权限直接读磁盘 |
noexec |
禁止执行该分区上的程序 —— /tmp、/var/tmp、上传目录常加 |
nofail |
挂载失败时不阻塞启动 —— 非关键分区必加(见 6.3) |
noauto |
开机不自动挂载(要手动 mount) |
user / users |
允许普通用户挂载/卸载 |
sync / async |
同步写 / 异步写(默认 async,性能高得多) |
discard |
在线 TRIM(SSD 删文件时立即通知硬盘)—— 现在更推荐用定时 fstrim |
errors=remount-ro |
ext4 出错时自动改为只读(防止错误扩散,ext4 常用) |
barrier=1 |
写屏障(保证日志顺序,默认开,别关) |
data=ordered|writeback|journal |
ext4 日志模式(安全性 vs 性能) |
_netdev |
声明这是网络文件系统,等网络就绪后再挂 |
x-systemd.automount |
首次访问时才挂载(懒挂载) |
x-systemd.device-timeout=10 |
等设备的超时时间 |
x-systemd.requires= |
声明依赖 |
remount |
重新挂载(改选项不用卸载) |
bind / rbind |
目录绑定挂载(见 5.4) |
uid= gid= umask= |
给不支持 Unix 权限的文件系统(FAT/NTFS/exFAT)指定属主与权限 |
# 不卸载就改选项
sudo mount -o remount,ro /data # 临时改为只读
sudo mount -o remount,rw /data # 改回读写
sudo mount -o remount,noatime /data
# 一个典型的加固组合
sudo mount -o rw,noexec,nosuid,nodev /dev/sdb1 /srv/upload
# ^^^^^^^^^^^^^^^^^^^^^ 上传目录:不能执行、SUID 失效、无设备文件
# 数据库/日志盘的性能组合
sudo mount -o rw,noatime,nodiratime /dev/sdb1 /var/lib/mysql
# 查看当前生效的挂载选项
findmnt -o TARGET,OPTIONS /data
cat /proc/mounts | grep /data
5.3 挂载会「遮盖」原有内容
这是一个必须知道的机制:
# 挂载前,/data 目录里已经有文件
sudo mkdir -p /data && echo "重要数据" | sudo tee /data/important.txt
ls /data # important.txt
# 现在把一块盘挂到 /data
sudo mount /dev/sdb1 /data
ls /data # (空的!)important.txt 不见了
# 原来的文件【没有丢】,只是被新挂载的文件系统"盖住"了,仍然占着根分区的空间
sudo umount /data
ls /data # important.txt 又回来了
这解释了一类诡异现象:df 显示根分区快满了,但 du -sh / 加起来对不上——被遮盖的文件 du 遍历不到。
# ✅ 找出被遮盖的文件:把根重新挂到另一个位置
sudo mkdir -p /mnt/rootfs
sudo mount --bind / /mnt/rootfs
# ^^^^^^ bind mount:把根文件系统"再挂一份"到 /mnt/rootfs,
# 此时 /mnt/rootfs/data 显示的是【被遮盖的原始内容】
sudo du -sh /mnt/rootfs/data
# 45G /mnt/rootfs/data <- 找到了!45GB 被遮盖的数据
sudo ls /mnt/rootfs/data
sudo rm -rf /mnt/rootfs/data/* # 确认无用后清理
sudo umount /mnt/rootfs
# 预防:挂载前确认目录是空的
[[ -z "$(ls -A /data)" ]] || echo "⚠️ /data 非空,挂载会遮盖这些文件"
5.4 bind mount:把目录挂到另一个位置
sudo mount --bind /data/www /var/www # 把一个目录"映射"到另一个位置
sudo mount --rbind /data /mnt/data # r = recursive,连子挂载一起
sudo mount -o bind,ro /data/config /app/config # 只读绑定
# ⚠️ 老内核上 bind 时的 ro 不生效,要分两步:
sudo mount --bind /data/config /app/config
sudo mount -o remount,bind,ro /app/config
# fstab 里的写法
# /data/www /var/www none bind 0 0
# 实际用途
# ① 把大目录搬到别的盘但保持原路径可访问
# (比如 /var/lib/docker 太大,搬到数据盘后 bind 回来)
sudo systemctl stop docker
sudo mv /var/lib/docker /data/docker
sudo mkdir /var/lib/docker
sudo mount --bind /data/docker /var/lib/docker
# fstab: /data/docker /var/lib/docker none bind 0 0
# ② chroot / 容器里提供宿主机的目录
# ③ 给同一份数据提供不同的挂载选项(一处可写、一处只读)
# ④ systemd 的 BindPaths= 就是用它实现的
5.5 umount:target is busy
sudo umount /data
# umount: /data: target is busy.
# ── 找出谁在占用 ──
sudo lsof +D /data # 递归列出该目录下被打开的文件
sudo lsof /data # 或者
sudo fuser -vm /data # ✅ 更直接:列出使用该挂载点的进程
# USER PID ACCESS COMMAND
# root 1234 F..c.. myapp
# ^^^ F=打开文件 c=cwd 在里面 r=root目录 m=mmap
# ── 常见原因 ──
# ① 有进程的 cwd 在里面(最常见!包括你自己的 shell)
cd / # 先离开这个目录
# ② 有进程打开了里面的文件
sudo fuser -km /data # ⚠️ k = kill 所有占用的进程(危险)
sudo fuser -TERM -km /data # 先发 TERM 而不是 KILL
# ③ 有子挂载
findmnt -R /data # 看有没有子挂载,先卸载子的
# ④ 有 NFS 导出
sudo exportfs -u '*:/data'
# ⑤ swap 文件在里面
sudo swapoff /data/swapfile
# ── 强制手段(谨慎)──
sudo umount -l /data # l = lazy:立即从目录树摘除,等引用归零后真正卸载
# ✅ 相对安全,常用
sudo umount -f /data # f = force:强制(主要用于失联的 NFS)
sudo umount -f -l /nfs/dead # NFS 服务器挂了时的组合拳
6. /etc/fstab
6.1 六个字段
cat /etc/fstab
# <设备> <挂载点> <类型> <选项> <dump> <fsck>
UUID=a1b2c3d4-e5f6-7890-abcd-ef1234567890 / ext4 errors=remount-ro 0 1
UUID=f0e1d2c3-4b5a-6978-8c9d-0e1f2a3b4c5d /data xfs defaults,noatime 0 2
LABEL=backup /backup ext4 defaults,nofail 0 2
/dev/mapper/vg0-swap none swap sw 0 0
tmpfs /tmp tmpfs defaults,size=4G,noexec,nosuid 0 0
/data/www /var/www none bind 0 0
nfs-server:/export /mnt/nfs nfs defaults,_netdev,nofail 0 0
| 字段 | 含义 | 要点 |
|---|---|---|
| ① 设备 | UUID= / LABEL= / PARTUUID= / 设备路径 / 网络地址 |
一律用 UUID(见 1.4) |
| ② 挂载点 | 目录路径(swap 写 none 或 swap) |
必须已存在 |
| ③ 类型 | ext4 xfs swap tmpfs nfs none(bind) auto |
auto 让系统自己探测 |
| ④ 选项 | 逗号分隔(见 5.2) | 非根分区加 nofail |
| ⑤ dump | 是否被 dump 备份工具处理 |
写 0(dump 早已过时) |
| ⑥ fsck 顺序 | 开机检查顺序 | 根分区 1,其他 2,不检查 0 |
6.2 修改 fstab 的安全流程
写错 fstab 会导致系统起不来,所以必须按流程做:
# ① 先备份
sudo cp /etc/fstab /etc/fstab.bak.$(date +%F)
# ② 用 UUID 而不是设备名
sudo blkid -s UUID -o value /dev/sdb1
# ③ 编辑
sudo vim /etc/fstab
# ④ 【关键】验证语法与可挂载性,不要直接重启!
sudo findmnt --verify # ✅ 检查 fstab 的语法、设备是否存在、目录是否存在
# Success, no errors or warnings detected
sudo mount -a # ✅ 实际挂载 fstab 里所有条目
# 没报错说明配置是好的
df -h # 确认挂上了
findmnt /data # 确认选项生效
# ⑤ 确认无误后才敢重启
sudo systemctl daemon-reload # systemd 会把 fstab 转成 .mount 单元
sudo reboot
6.3 nofail:为什么它至关重要
# ❌ 没有 nofail 的非关键分区
UUID=xxx /backup ext4 defaults 0 2
# 如果这块盘坏了/被拔了/云盘没挂上,systemd 会一直等它,
# 默认 90 秒超时后进入 emergency mode —— 【系统起不来,SSH 也连不上】
# 云主机上这意味着只能用 VNC 控制台救援
# ✅ 加上 nofail
UUID=xxx /backup ext4 defaults,nofail 0 2
# ^^^^^^ 挂不上就跳过,系统正常启动
# ✅ 更完整的写法(限制等待时间)
UUID=xxx /backup ext4 defaults,nofail,x-systemd.device-timeout=10s 0 2
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 最多等 10 秒
# ✅ 网络文件系统必加 _netdev
nfs:/export /mnt/nfs nfs defaults,_netdev,nofail,x-systemd.mount-timeout=30 0 0
# ^^^^^^^^ 等网络就绪后再挂
# ✅ 懒挂载:首次访问才挂(适合不常用的备份盘)
UUID=xxx /backup ext4 noauto,x-systemd.automount,x-systemd.idle-timeout=300 0 0
# ^^^^^^ 开机不挂 ^^^^^^^^^^^^^^^^^^^^^ 访问时才挂,闲置 5 分钟后自动卸载
规则:除了 /、/boot、/usr 这些系统必需的分区,其他一律加 nofail。
6.4 fstab 写错了怎么救
# 症状:启动时卡住,或者进入 emergency shell 提示
# "You are in emergency mode. Give root password for maintenance"
# ── 情况一:能进 emergency shell ──
# 输入 root 密码后
mount -o remount,rw / # 根分区默认是只读的,先改成可写
vim /etc/fstab # 注释掉有问题的行
systemctl daemon-reload
reboot
# ── 情况二:进不去(没设 root 密码)──
# 重启到 GRUB 菜单(开机时按住 Shift 或 Esc)
# 按 e 编辑启动项,在 linux 那一行末尾加:
# init=/bin/bash
# 或者加:
# systemd.unit=emergency.target
# 按 Ctrl-X 启动,然后
mount -o remount,rw /
vim /etc/fstab
sync && reboot -f
# ── 情况三:云主机 ──
# 用 VNC/串口控制台按上面操作,或者把系统盘挂到另一台实例上修复
# ── 预防(最重要)──
sudo findmnt --verify && sudo mount -a # 改完必须验证再重启
7. swap
7.1 分区还是文件
# 现代做法推荐用【swap 文件】:可以随时调整大小,不用改分区表
sudo fallocate -l 4G /swapfile # ✅ 必须用 fallocate 或 dd,【不能用 truncate】
# (内核要求连续真实块,稀疏文件不行,见 04 篇 10.9)
sudo chmod 600 /swapfile # 权限必须 600,否则 swapon 拒绝
sudo mkswap /swapfile
sudo swapon /swapfile
# fstab: /swapfile none swap sw 0 0
# swap 分区的做法
sudo mkswap /dev/sdb2
sudo swapon /dev/sdb2
# fstab: UUID=xxx none swap sw 0 0
# 查看与管理
free -h # 看 swap 用量
swapon --show # ✅ 列出所有 swap 设备
# NAME TYPE SIZE USED PRIO
# /swapfile file 4G 0B -2
cat /proc/swaps # 同上
sudo swapoff /swapfile # 关闭(会把数据换回内存,内存不足时会失败)
sudo swapoff -a # 关闭所有
sudo swapon -a # 启用 fstab 里所有的
sudo swapon -p 10 /swapfile # 设优先级(数字大的先用)
7.2 swappiness
cat /proc/sys/vm/swappiness # 60(默认)
# 含义:内核「倾向于换出匿名页 vs 回收 page cache」的权重(0~200)
# 值越低 = 越倾向于保留匿名内存、优先丢弃 page cache
# 值越高 = 越积极把不活跃的匿名页换出去
sudo sysctl vm.swappiness=10 # 临时改(数据库服务器常设 1~10)
echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swap.conf # 持久化
# ⚠️ swappiness=0 不等于「禁用 swap」,它只是最不情愿用
# 真要禁用得 swapoff -a
7.3 容器与 K8s 为什么关 swap
# kubelet 默认要求关闭 swap(1.22+ 可以有条件开启)
sudo swapoff -a
sudo sed -i '/\sswap\s/s/^/#/' /etc/fstab # 注释掉 fstab 里的 swap 行
# 三个原因
# ① swap 让 cgroup 的内存限制【失去意义】:容器可以超出 limit 继续跑(只是被换到磁盘),
# kubelet 无法准确判断 Pod 是否超限,QoS 保证和调度决策全部失真
# ② 【延迟不可预测】:换入一页要经历主要缺页(磁盘 IO),
# 一个原本 1ms 的请求可能变成几秒 —— 对在线服务来说
# 「响应 5 秒」比「进程被杀然后重启」更糟(会引发上游超时、重试、雪崩)
# ③ OOM Killer 的行为变得难以预测
8. LVM 入门
8.1 三层模型与它解决的问题
LVM 存在的核心理由:分区表是「静态」的,改分区大小要动分区表且往往要停机;LVM 在中间加了一层,让容量分配变成「动态」的。
物理卷 PV (Physical Volume) = 物理盘或分区,被"贡献"给 LVM
+----------+ +----------+
| /dev/sdb1| | /dev/sdc1|
+----------+ +----------+
\ /
\ / 加入同一个卷组
v v
卷组 VG (Volume Group) = 一个"容量池"
+---------------------------+
| vg_data |
| +--------+ +---------+ |
| | lv_app | | lv_logs | | 逻辑卷 LV (Logical Volume) = 从池里切出来的"虚拟分区"
| +--------+ +---------+ |
+---------------------------+
| |
v v
mkfs.ext4 mkfs.xfs 在 LV 上建文件系统
| |
v v
/app /var/log 挂载
它带来的三个能力:
- 在线扩容:加一块盘 → 加入 VG → 扩 LV → 扩文件系统,全程不停机
- 跨盘聚合:一个 LV 可以横跨多块物理盘(容量不再受单盘限制)
- 快照:秒级创建一致性快照,用于备份
8.2 基本操作
# ── 创建 ──
sudo pvcreate /dev/sdb1 /dev/sdc1 # 把分区变成 PV
sudo vgcreate vg_data /dev/sdb1 /dev/sdc1 # 创建 VG
sudo lvcreate -L 100G -n lv_app vg_data # 创建 100G 的 LV
sudo lvcreate -l 100%FREE -n lv_logs vg_data # 用掉 VG 里所有剩余空间
sudo mkfs.ext4 /dev/vg_data/lv_app # 格式化
sudo mount /dev/vg_data/lv_app /app
# fstab 里用 /dev/mapper/vg_data-lv_app 或 UUID
# ── 查看(三层各有一组命令)──
sudo pvs; sudo pvdisplay # 物理卷
sudo vgs; sudo vgdisplay # 卷组
sudo lvs; sudo lvdisplay # 逻辑卷
sudo lvs -o +devices # LV 在哪些物理盘上
sudo vgs -o +vg_free # VG 剩余空间
lsblk # 树形看整体结构
8.3 在线扩容的完整流程
这是 LVM 最常用的场景:
# 场景:/app 快满了,需要扩容 200G
# ① 加一块新盘(云上是控制台加盘,物理机是插盘)
lsblk # 确认新盘 /dev/sdd 出现了
# ② 把新盘变成 PV 并加入 VG
sudo pvcreate /dev/sdd
sudo vgextend vg_data /dev/sdd
sudo vgs # 确认 VFree 增加了
# ③ 扩展 LV
sudo lvextend -L +200G /dev/vg_data/lv_app # 增加 200G
sudo lvextend -l +100%FREE /dev/vg_data/lv_app # 或者用光所有剩余
# ④ 扩展文件系统(不同类型命令不同!)
sudo resize2fs /dev/vg_data/lv_app # ext4
sudo xfs_growfs /app # xfs(参数是【挂载点】)
# ⑤ 验证
df -h /app
# ── 一步完成(lvextend 的 -r 自动调用 resize 工具)──
sudo lvextend -r -L +200G /dev/vg_data/lv_app
# ^^ resizefs,自动识别文件系统类型并扩展
如果是「同一块盘扩容」(云盘扩容后):
# 云控制台把 100G 扩到 200G 之后
sudo lsblk # sdb 现在是 200G,但 sdb1 还是 100G
# 情况一:整盘做 PV(没分区)—— 最简单
sudo pvresize /dev/sdb # PV 自动感知新容量
sudo lvextend -r -l +100%FREE /dev/vg_data/lv_app
# 情况二:有分区 —— 要先扩分区
sudo growpart /dev/sdb 1 # ✅ cloud-utils 提供的工具,在线扩分区
sudo pvresize /dev/sdb1
sudo lvextend -r -l +100%FREE /dev/vg_data/lv_app
# 情况三:没用 LVM,直接是分区 + 文件系统
sudo growpart /dev/sdb 1
sudo resize2fs /dev/sdb1 # ext4
sudo xfs_growfs /data # xfs
8.4 快照
# 创建快照(CoW 机制,秒级完成)
sudo lvcreate -L 20G -s -n lv_app_snap /dev/vg_data/lv_app
# ^^ snapshot
# ^^^^^^ 快照的空间用来存"变化的部分",不需要和原卷一样大
# 用途:一致性备份(不用停服务)
sudo mount -o ro /dev/vg_data/lv_app_snap /mnt/snap
sudo tar -czf /backup/app-$(date +%F).tar.gz -C /mnt/snap .
sudo umount /mnt/snap
sudo lvremove -y /dev/vg_data/lv_app_snap # 备份完立即删掉快照
# ⚠️ 三个注意点
# ① 快照空间满了会【失效】(变成 invalid),要监控
sudo lvs -o +snap_percent
# ② 快照会显著降低原卷的写性能(每次写都要先拷贝原数据 = CoW 开销)
# ③ 快照不是备份!它和原卷在同一组物理盘上,盘坏了一起完
8.5 其他操作
# 缩小 LV(ext4,必须先缩文件系统!顺序反了会丢数据)
sudo umount /app
sudo e2fsck -f /dev/vg_data/lv_app
sudo resize2fs /dev/vg_data/lv_app 50G # ① 先缩文件系统
sudo lvreduce -L 50G /dev/vg_data/lv_app # ② 再缩 LV
sudo mount /app
# ⚠️ xfs 不能缩小,只能备份重建
# 从 VG 移除一块盘(先把数据迁走)
sudo pvmove /dev/sdb1 # 把 sdb1 上的数据迁到 VG 里其他 PV
sudo vgreduce vg_data /dev/sdb1
sudo pvremove /dev/sdb1
# 删除
sudo umount /app
sudo lvremove /dev/vg_data/lv_app
sudo vgremove vg_data
sudo pvremove /dev/sdb1
# 重命名
sudo lvrename vg_data lv_old lv_new
sudo vgrename vg_old vg_new
# 备份 LVM 元数据(自动备份在 /etc/lvm/backup/)
sudo vgcfgbackup
ls /etc/lvm/backup/
sudo vgcfgrestore vg_data # 恢复元数据
LVM 值不值得用?
✅ 值得:物理机、需要在线扩容、需要快照、单盘容量不够要聚合
❌ 不必:云主机上「加一块新盘挂到新目录」就能解决的场景
(云盘本身就能在线扩容,且 LVM 多一层复杂度与故障点)
9. 实战:一块新盘从插上到能用
把前面的内容串成完整流程(云主机场景):
# ── ① 确认新盘 ──
lsblk
# sdb 8:16 0 500G 0 disk <- 新盘,没有分区,没挂载
sudo lsblk -f /dev/sdb # 确认 FSTYPE 是空的(盘是干净的)
# ⚠️ 如果不为空,务必先确认这块盘真的可以清掉!
# ── ② 决定要不要分区 ──
# 云上推荐:不分区,直接整盘格式化(扩容最简单)
# 需要分区时:
sudo parted -s /dev/sdb mklabel gpt
sudo parted -s -a optimal /dev/sdb mkpart primary 0% 100%
sudo partprobe /dev/sdb
lsblk /dev/sdb # 确认 sdb1 出现
# ── ③ 创建文件系统 ──
sudo mkfs.xfs -L data /dev/sdb # 整盘(或 /dev/sdb1)
# 或 sudo mkfs.ext4 -L data -m 1 /dev/sdb
# ── ④ 创建挂载点并挂载 ──
sudo mkdir -p /data
[[ -z "$(ls -A /data)" ]] || echo "⚠️ 目录非空,挂载会遮盖内容"
sudo mount -o noatime /dev/sdb /data
# ── ⑤ 写入 fstab(用 UUID!)──
UUID=$(sudo blkid -s UUID -o value /dev/sdb)
echo "UUID=$UUID /data xfs defaults,noatime,nofail 0 2" | sudo tee -a /etc/fstab
# ── ⑥ 验证(必做,不要直接重启)──
sudo findmnt --verify
sudo umount /data && sudo mount -a # 用 fstab 重新挂一次,确认配置有效
df -hT /data
findmnt /data
# ── ⑦ 设置权限 ──
sudo chown myapp:myapp /data
sudo chmod 750 /data
# ── ⑧ 部署脚本里加防御 ──
mountpoint -q /data || { echo "数据盘未挂载,中止部署"; exit 1; }
10. 实战:磁盘满了的五步排查
开篇第四问的完整答案。按这个顺序走,能覆盖 95% 的情况:
# ══ 第 1 步:确认是空间还是 inode ══
df -h /
# /dev/vda1 50G 50G 0 100% / <- 空间满了
df -i /
# /dev/vda1 3276800 3276800 0 100% / <- 或者是 inode 满了(症状完全一样!)
# 如果是 inode 满:找出小文件最多的目录
sudo du --inodes -d1 /var | sort -n | tail # coreutils 8.22+
# 老版本:
for d in /var/*/; do echo "$(sudo find "$d" -xdev | wc -l) $d"; done | sort -rn | head
# ══ 第 2 步:找大目录(逐层下钻)══
sudo du -xh --max-depth=1 / 2>/dev/null | sort -h | tail -10
# ^^ -x 不跨文件系统(关键!否则把别的盘也算进来)
# 12G /var
# 8.2G /usr
# 45G /data
# 然后进入最大的那个继续下钻
sudo du -xh --max-depth=1 /var | sort -h | tail
# 或者用交互式工具(强烈推荐)
sudo ncdu -x /
# ══ 第 3 步:找大文件 ══
sudo find / -xdev -type f -size +1G -printf '%s %p\n' 2>/dev/null | sort -rn | head -20
# ══ 第 4 步:查「已删除但仍被打开」的文件 ══
# 这是 du 和 df 不一致的【头号原因】(04 篇第 3 节详解)
sudo lsof +L1
# COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
# java 1234 app 3w REG 253,1 12884901888 0 2621998 /var/log/app.log (deleted)
# ^^^^^^^^^^^ ^ NLINK=0 ^^^^^^^^^
# 还占 12G 已删除
# ✅ 处理:截断而不是重启
sudo truncate -s 0 /proc/1234/fd/3
# 或让进程重开文件
sudo systemctl reload app
# ══ 第 5 步:查被挂载遮盖的文件 ══
sudo mkdir -p /mnt/rootfs
sudo mount --bind / /mnt/rootfs
sudo du -xh --max-depth=1 /mnt/rootfs | sort -h | tail
# 对比第 2 步的结果,多出来的就是被遮盖的
sudo umount /mnt/rootfs
# ══ 其他可能原因 ══
# ⑥ 保留块(ext4 默认 5%):Use% 100% 但 root 还能写
sudo tune2fs -m 1 /dev/vda1 # 临时腾出空间的应急手段
# ⑦ 稀疏文件:ls 显示很大但实际占用少(或反之)
du -sh --apparent-size f; du -sh f
# ⑧ 硬链接:du 只算一次,所以"各目录之和"可能远大于"总和"
du -sh --count-links snap1 snap2
# ⑨ 权限不足导致 du 漏算
sudo du -xsh / # 一定要用 sudo
常见的清理动作(按安全性排序):
# ① 最安全:清缓存类
sudo apt clean # /var/cache/apt(Debian)
sudo dnf clean all # RHEL
sudo journalctl --vacuum-size=500M # 压缩 systemd 日志到 500MB
sudo journalctl --vacuum-time=7d # 只保留 7 天
sudo docker system prune -a --volumes # ⚠️ 会删未使用的镜像和卷,先确认
sudo rm -rf ~/.cache/* # 用户缓存
# ② 日志:截断而不是删除(正在写入的日志)
sudo truncate -s 0 /var/log/app.log # ✅
sudo rm /var/log/app.log # ❌ 空间不会释放(进程还开着 fd)
# ③ 检查 logrotate 是否正常工作
sudo logrotate -d /etc/logrotate.conf # -d 演练模式
ls -lh /var/log/*.gz | head # 有轮转产物说明在工作
# ④ 找出增长最快的目录(对比两次快照)
sudo du -x --max-depth=2 / > /tmp/du1; sleep 300
sudo du -x --max-depth=2 / > /tmp/du2
diff <(sort /tmp/du1) <(sort /tmp/du2) | head -30
# ⑤ 应急:如果连 ssh 都登不上(磁盘 100% 导致无法创建临时文件)
# 云主机用 VNC 控制台;或者先删一个大文件腾出空间
11. 知识点扩展
11.1 命令速查
| 命令 | 用途 | 关键参数 |
|---|---|---|
lsblk |
块设备树 | -f 文件系统信息 -o 自定义列 -d 只看整盘 -J JSON |
blkid |
UUID/类型 | -s UUID -o value 取值 -L 按标签查 -p 探测 |
df |
文件系统用量 | -h -T 类型 -i inode -x 排除类型 -l 只本地 -P 脚本格式 |
du |
目录用量 | -sh 汇总 -x 不跨 fs -d N 深度 --inodes 数 inode --apparent-size |
ncdu |
交互式 du | -x 不跨 fs(推荐) |
findmnt |
挂载树 | --verify 检查 fstab -T 按路径查 --df 带用量 |
mountpoint |
判断是否挂载点 | -q 静默(脚本防御) |
mount |
挂载 | -a 全挂 -o remount,X 改选项 --bind 绑定 -t 类型 |
umount |
卸载 | -l lazy -f 强制 |
fuser |
谁在占用 | -vm 列出 -km 杀掉(危险) |
fdisk |
分区(交互) | -l 列出;子命令 g/n/d/t/p/w/q |
parted |
分区(脚本) | -s 静默 -a optimal 对齐 mklabel mkpart print free |
sgdisk |
GPT 专用 | -p 打印 -n 新建 --backup 备份分区表 -Z 抹除 |
partprobe |
通知内核重读分区表 | — |
growpart |
在线扩分区 | growpart /dev/sdb 1 |
mkfs.ext4 |
建 ext4 | -L 标签 -m 保留% -N/-i inode 数 -T 用途 |
mkfs.xfs |
建 xfs | -L -f 强制 -m reflink=1 |
wipefs -a |
清除文件系统签名 | ⚠️ 破坏性 |
resize2fs |
调整 ext4 大小 | 可在线扩,缩要先卸载+fsck |
xfs_growfs |
扩展 xfs | 参数是挂载点,只能扩 |
e2fsck / xfs_repair |
检查修复 | 必须先卸载;-n 只检查 |
tune2fs |
调 ext4 参数 | -l 看全部 -m 保留% -L 标签 -U 改 UUID -c -1 -i 0 关定期检查 |
xfs_info / xfs_admin |
xfs 参数 | xfs_info /mnt 看配置 |
mkswap/swapon/swapoff |
swap | swapon --show 列出 |
smartctl |
磁盘健康 | -H 健康结论 -a 全部信息 |
iostat -xz 1 |
IO 统计 | 看 %util、await |
iotop -o |
哪个进程在 IO | -o 只显示活跃的 |
fstrim |
手动 TRIM | -av 所有挂载点 |
pvs/vgs/lvs |
LVM 查看 | -o +字段 追加列 |
pvcreate/vgcreate/lvcreate |
LVM 创建 | lvcreate -L 大小 -n 名字 VG |
lvextend -r |
LVM 扩容 | -r 自动扩文件系统 |
pvresize |
PV 感知新容量 | 云盘扩容后用 |
11.2 SSD 的 TRIM
# 为什么需要:SSD 不知道哪些块被文件系统标记为「已删除」,
# 不知道就无法提前擦除,导致写入时要「读-改-写」,性能下降
# 两种方式:
# ① 在线 discard(挂载选项)—— 每次删文件立即通知,但有性能抖动
sudo mount -o discard /dev/sdb1 /data
# ② 【推荐】定时批量 TRIM
sudo systemctl enable --now fstrim.timer # 大多数发行版默认已开(每周一次)
systemctl status fstrim.timer
sudo fstrim -av # 手动执行所有挂载点
# /data: 120.5 GiB (129371852800 bytes) trimmed
# 检查设备是否支持
lsblk -D # DISC-GRAN 和 DISC-MAX 非 0 表示支持
11.3 IO 调度器
cat /sys/block/sda/queue/scheduler
# [none] mq-deadline kyber bfq
# ^^^^ 当前使用的
# 选择原则
# none NVMe SSD(设备自己的队列足够快,软件排队反而是负担) ← NVMe 默认
# mq-deadline SATA SSD、通用(保证延迟上限) ← 常见默认
# bfq 桌面(交互优先,保证公平)
# kyber 高性能场景
# 临时改
echo mq-deadline | sudo tee /sys/block/sda/queue/scheduler
# 持久化:udev 规则或内核参数 elevator=
11.4 其他有用的操作
# 回环设备:把文件当块设备用(做测试、挂载镜像)
sudo losetup -fP disk.img # -f 找空闲 loop 设备,-P 扫描分区
losetup -a # 列出所有
sudo mount /dev/loop0p1 /mnt
sudo losetup -d /dev/loop0 # 卸载
# 直接挂载 ISO
sudo mount -o loop,ro ubuntu.iso /mnt
# tmpfs:内存文件系统
sudo mount -t tmpfs -o size=2G,noexec,nosuid tmpfs /mnt/ramdisk
# fstab: tmpfs /mnt/ramdisk tmpfs size=2G,noexec,nosuid 0 0
# 测试磁盘性能(fio 最准确)
sudo fio --name=randwrite --ioengine=libaio --direct=1 --rw=randwrite \
--bs=4k --size=1G --numjobs=4 --runtime=30 --group_reporting
# 简单测顺序写(注意 dd 不是好的性能测试工具)
dd if=/dev/zero of=/data/test bs=1M count=1024 oflag=direct
# 测顺序读
dd if=/data/test of=/dev/null bs=1M iflag=direct
# 克隆整盘
sudo dd if=/dev/sda of=/dev/sdb bs=4M status=progress conv=noerror,sync
# 更好的工具
sudo ddrescue /dev/sda /dev/sdb rescue.log # 能跳过坏块并重试
12. 面试题
Q:lsblk、df、du 三个命令有什么区别?为什么它们的结果可能对不上?
它们看的是不同层次:lsblk 从 sysfs 读块设备信息(有哪些盘、怎么分区、挂在哪);df 通过 statfs() 读文件系统超级块的统计(O(1),极快);du 遍历目录树累加每个 inode 的占用(O(n),慢)。
结果对不上有五个常见原因:
- 已删除但仍被打开的文件 ——
df算,du遍历不到(头号原因,lsof +L1查) - 被挂载遮盖的文件 —— 挂载点原有的文件仍占空间但
du看不到(mount --bind / /mnt查) - 保留块 —— ext4 默认给 root 留 5%,所以
Size ≠ Used + Avail - 硬链接 ——
du只算一次 - 权限不足 —— 非 root 跑
du会静默跳过读不了的目录
另外 du 不加 -x 会跨文件系统,把别的盘也算进来。
Q:为什么 MBR 最大只支持 2TB?GPT 怎么解决的?
MBR 把分区表放在磁盘第一个扇区(512 字节)里,其中每个分区表项的「起始 LBA」和「扇区数」各占 4 字节 = 32 位。所以最大可寻址 2^32 = 4294967296 个扇区,每扇区 512 字节,2^32 × 512 = 2 TiB。
MBR 还有另外两个限制:只有 4 个 16 字节的槽位(所以只能 4 个主分区,更多要牺牲一个做扩展分区),以及分区表只有一份、无校验(第 0 扇区损坏就全丢)。
GPT 用 64 位 LBA(理论上限 9.4 ZB)、128 个分区、磁盘头尾各存一份分区表、CRC32 校验、分区用 GUID + 可读名字标识。它还在头部保留一个「保护 MBR」,防止老工具把 GPT 盘误认为空盘。
新盘一律用 GPT,唯一例外是必须在不支持 UEFI 的老 BIOS 上启动。
Q:/etc/fstab 为什么要用 UUID 而不是 /dev/sdb1?
因为设备名由内核探测顺序决定,而探测顺序会变:插拔顺序不同、BIOS 启动顺序改了、云上加卸载盘、内核升级导致驱动加载顺序变化、多路径环境……原来的 /dev/sdb 可能变成 /dev/sdc。
后果很严重:fstab 里写死设备名会导致挂错盘(数据写到不该写的地方)或挂载失败。而挂载失败如果没写 nofail,systemd 会等 90 秒然后进入 emergency mode —— 系统起不来、SSH 连不上,云主机上只能靠 VNC 救援。
正确做法是用内容标识:UUID=(最推荐,格式化时生成、跟着文件系统走)、LABEL=(人类可读但可能重复)、PARTUUID=(跟着分区走)。/dev/disk/by-uuid/ 等目录是 udev 自动维护的软链接。
Q:改完 /etc/fstab 直接重启有什么风险?正确流程是什么?
风险是写错会导致系统起不来,而你可能只有 VNC 控制台能救。
正确流程:
sudo cp /etc/fstab /etc/fstab.bak # ① 备份
sudo vim /etc/fstab # ② 用 UUID 编辑
sudo findmnt --verify # ③ 检查语法、设备与目录是否存在
sudo mount -a # ④ 实际挂载所有条目,没报错说明配置有效
df -h && findmnt /data # ⑤ 确认挂上了、选项生效
sudo reboot # ⑥ 才敢重启
另外非关键分区一律加 nofail(挂不上就跳过),网络文件系统还要加 _netdev,并用 x-systemd.device-timeout=10s 限制等待时间。
万一起不来:进 emergency shell → mount -o remount,rw / → 改 fstab → reboot;进不去就在 GRUB 里给内核加 init=/bin/bash 或 systemd.unit=emergency.target。
Q:umount 报 target is busy,怎么处理?
先找出谁在占用:fuser -vm /data(最直接,会显示进程和占用方式)或 lsof +D /data。
fuser 输出的 ACCESS 列含义:f=打开了文件、c=cwd 在里面、r=根目录、m=有 mmap。
五个常见原因:① 有进程的 cwd 在里面(最常见,包括你自己的 shell —— 先 cd /);② 有进程打开了里面的文件;③ 有子挂载(findmnt -R /data 查,先卸子的);④ 有 NFS 导出;⑤ swap 文件在里面。
强制手段:umount -l(lazy)——立即从目录树摘除,等引用归零后真正卸载,相对安全且最常用;umount -f 主要用于失联的 NFS;fuser -km /data 会杀掉所有占用进程,危险,生产上先用 -TERM。
Q:挂载会「遮盖」原有内容是什么意思?会造成什么问题?
如果挂载点目录里原本有文件,挂载后这些文件会被新文件系统「盖住」——看不见,但没有丢,仍然占着原文件系统的空间。卸载后它们又会出现。
造成的典型问题是「df 说根分区满了,du -sh / 加起来却对不上」——被遮盖的文件 du 遍历不到。
排查方法是 bind mount 根文件系统到别处:
sudo mount --bind / /mnt/rootfs
sudo du -sh /mnt/rootfs/data # 此时看到的是被遮盖的原始内容
sudo umount /mnt/rootfs
预防:挂载前检查 [[ -z "$(ls -A /data)" ]]。相关的还有部署防御 mountpoint -q /data || exit 1 —— 防止数据盘没挂上就往里写,把根分区写爆。
Q:K8s 为什么要求关闭 swap?
三个原因:
- swap 让 cgroup 的内存限制失去意义 —— 容器可以超出 limit 继续运行(只是被换到磁盘),kubelet 无法准确判断 Pod 是否超限,QoS 分级和调度决策全部失真
- 延迟不可预测 —— 换入一页要经历主要缺页(磁盘 IO,SSD 也要几十微秒,机械盘几毫秒),一个原本 1ms 的请求可能变成几秒。对在线服务来说「响应 5 秒」比「进程被杀然后自动重启」更糟,因为它会引发上游超时、重试、雪崩,而且极难定位
- OOM Killer 的行为变得难以预测
关闭方式:swapoff -a + 注释掉 fstab 里的 swap 行。补充:vm.swappiness=0 不等于禁用 swap,它只是让内核最不情愿使用。
Q:LVM 解决了什么问题?在线扩容的完整流程是什么?
核心问题:分区表是静态的,改分区大小要动分区表且往往要停机。LVM 在「分区」和「文件系统」之间加了一层抽象(PV → VG → LV),让容量分配变成动态的。
三个能力:在线扩容、跨盘聚合(一个 LV 可横跨多块盘)、快照(秒级一致性备份)。
在线扩容流程:
sudo pvcreate /dev/sdd # ① 新盘变 PV
sudo vgextend vg_data /dev/sdd # ② 加入 VG
sudo lvextend -r -L +200G /dev/vg_data/lv_app # ③ 扩 LV,-r 自动扩文件系统
不加 -r 就要手动第四步:ext4 用 resize2fs 设备,xfs 用 xfs_growfs 挂载点(注意参数不同,且 xfs 不能缩小)。
云盘扩容后是另一条路:growpart 扩分区 → pvresize 让 PV 感知 → lvextend -r。
云主机上 LVM 常常不必要(云盘本身能在线扩容,加一块新盘挂到新目录更简单),LVM 多一层复杂度和故障点。
Q:磁盘满了,怎么系统地排查?
五步,按顺序走能覆盖 95% 的情况:
df -h和df -i都看 —— 先确认是空间满还是 inode 满(症状完全一样,但df -h看不出 inode 问题)du -xh --max-depth=1 / | sort -h逐层下钻找大目录(-x不跨文件系统很关键),或者用交互式的ncdu -x /find / -xdev -type f -size +1G找大文件lsof +L1查已删除但仍被打开的文件 —— 这是du/df不一致的头号原因,处理方式是truncate -s 0 /proc/<pid>/fd/<n>或让进程重开文件,不要rmmount --bind / /mnt查被挂载遮盖的文件
清理时按安全性排序:先清 cache 类(apt clean、journalctl --vacuum-size、docker system prune),日志用截断而不是删除,然后检查 logrotate 是否正常工作。应急手段是 tune2fs -m 1 临时释放保留块。
小结
- 块设备的层次:物理盘 → 分区(可选)→ LVM(可选)→ 文件系统 → 挂载点。
lsblk/fdisk/df/du分别看不同层 - 设备名不可靠(探测顺序会变),fstab 一律用 UUID
- MBR 卡在 2TB 是因为 32 位 LBA × 512B 扇区;GPT 用 64 位 LBA + 双份分区表 + CRC。新盘一律 GPT
- ext4 vs xfs:ext4 通用且能缩小,xfs 大文件性能更好、inode 动态分配但不能缩小
- 挂载选项都在解决具体问题:
noatime消除读写放大、nosuid/nodev/noexec加固、nofail防止启动卡死 - 挂载会遮盖原有内容 —— 是「
df满了但du找不到」的原因之一,用mount --bind /排查 - 改完 fstab 必须
findmnt --verify+mount -a验证,不要直接重启 umountbusy 用fuser -vm找占用者,umount -l是相对安全的强制手段- K8s 关 swap 是因为它让 cgroup 限制失效、延迟不可预测
- LVM 的价值是在线扩容和快照,但云主机上常常不必要
- 磁盘满了的五步:
df -h/df -i→du -x下钻 → 找大文件 →lsof +L1→mount --bind /查遮盖
下一篇讲 归档、压缩与 rsync:tar 的参数为什么这么古怪、gzip/bzip2/xz/zstd 该选哪个(附压缩比与速度对比)、tar 的绝对路径陷阱、以及 rsync 的增量算法与那些能把目标目录清空的危险参数。
xingliuhua