目录

Linux-11 磁盘、分区与挂载:从块设备到 fstab 的完整链路

这一篇解决「一块盘从插上到能用」的完整链路,以及最高频的运维事故:磁盘满了。

先看五个问题:

  1. lsblkdfdufdisk -l 分别在回答什么问题?为什么它们的结果可能互相矛盾?
  2. MBR 和 GPT 有什么区别?为什么 2TB 是一个坎?
  3. /etc/fstab 里为什么要写 UUID 而不是 /dev/sdb1
  4. 磁盘满了,但 du 加起来对不上,可能是什么原因?
  5. 一块新盘从插上到能用,需要做哪几步?

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 分隔。同理 mmcblk0p1loop0p1

主次设备号才是内核的真身份

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 写 noneswap 必须已存在
类型 ext4 xfs swap tmpfs nfs none(bind) auto auto 让系统自己探测
选项 逗号分隔(见 5.2) 非根分区加 nofail
dump 是否被 dump 备份工具处理 写 0dump 早已过时)
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        挂载

它带来的三个能力

  1. 在线扩容:加一块盘 → 加入 VG → 扩 LV → 扩文件系统,全程不停机
  2. 跨盘聚合:一个 LV 可以横跨多块物理盘(容量不再受单盘限制)
  3. 快照:秒级创建一致性快照,用于备份

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 统计 %utilawait
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:lsblkdfdu 三个命令有什么区别?为什么它们的结果可能对不上?

它们看的是不同层次lsblk 从 sysfs 读块设备信息(有哪些盘、怎么分区、挂在哪);df 通过 statfs()文件系统超级块的统计(O(1),极快);du 遍历目录树累加每个 inode 的占用(O(n),慢)。

结果对不上有五个常见原因:

  1. 已删除但仍被打开的文件 —— df 算,du 遍历不到(头号原因lsof +L1 查)
  2. 被挂载遮盖的文件 —— 挂载点原有的文件仍占空间但 du 看不到(mount --bind / /mnt 查)
  3. 保留块 —— ext4 默认给 root 留 5%,所以 Size ≠ Used + Avail
  4. 硬链接 —— du 只算一次
  5. 权限不足 —— 非 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/bashsystemd.unit=emergency.target

Q:umounttarget 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?

三个原因:

  1. swap 让 cgroup 的内存限制失去意义 —— 容器可以超出 limit 继续运行(只是被换到磁盘),kubelet 无法准确判断 Pod 是否超限,QoS 分级和调度决策全部失真
  2. 延迟不可预测 —— 换入一页要经历主要缺页(磁盘 IO,SSD 也要几十微秒,机械盘几毫秒),一个原本 1ms 的请求可能变成几秒。对在线服务来说「响应 5 秒」比「进程被杀然后自动重启」更糟,因为它会引发上游超时、重试、雪崩,而且极难定位
  3. 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% 的情况:

  1. df -hdf -i 都看 —— 先确认是空间满还是 inode 满(症状完全一样,但 df -h 看不出 inode 问题)
  2. du -xh --max-depth=1 / | sort -h 逐层下钻找大目录(-x 不跨文件系统很关键),或者用交互式的 ncdu -x /
  3. find / -xdev -type f -size +1G 找大文件
  4. lsof +L1 查已删除但仍被打开的文件 —— 这是 du/df 不一致的头号原因,处理方式是 truncate -s 0 /proc/<pid>/fd/<n> 或让进程重开文件,不要 rm
  5. mount --bind / /mnt 查被挂载遮盖的文件

清理时按安全性排序:先清 cache 类(apt cleanjournalctl --vacuum-sizedocker 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 验证,不要直接重启
  • umount busyfuser -vm 找占用者,umount -l 是相对安全的强制手段
  • K8s 关 swap 是因为它让 cgroup 限制失效、延迟不可预测
  • LVM 的价值是在线扩容和快照,但云主机上常常不必要
  • 磁盘满了的五步df -h/df -idu -x 下钻 → 找大文件 → lsof +L1mount --bind / 查遮盖

下一篇讲 归档、压缩与 rsynctar 的参数为什么这么古怪、gzip/bzip2/xz/zstd 该选哪个(附压缩比与速度对比)、tar 的绝对路径陷阱、以及 rsync 的增量算法与那些能把目标目录清空的危险参数。