Skip to content

08|磁盘管理

服务跑着跑着突然报错"no space left on device"——磁盘满了。加了块新盘想用,却找不到在哪。扩容后空间还是没变。这些是磁盘运维的高频问题。

磁盘相关的概念有层次——从底层的物理设备到上层的挂载点,中间可能还有 LVM、RAID。理不清这些层次,排查磁盘问题时容易思路混乱。本篇按从底层到上层的顺序梳理:设备 → 分区 → 文件系统 → 挂载点,再讲空间占用排查和扩容。

一、概念层级

排查磁盘问题时,以下四个概念容易混淆,先明确区分:

概念含义示例
设备(device)系统识别到的物理或虚拟磁盘/dev/sdb
分区(partition)在磁盘上划分出的独立区域/dev/sdb1
文件系统(filesystem)组织数据的方式,决定文件如何存储与查找xfs、ext4、btrfs
挂载点(mountpoint)文件系统接入目录树的位置/data/mnt/backup

排查磁盘问题时的标准顺序是从下往上:系统有没有识别到这块盘 → 分区表是否正确 → 文件系统是否创建 → 是否已挂载。任何一层断开,上层都失去意义。

查看块设备与挂载关系

bash
lsblk            # 块设备树:设备→分区→挂载点,最直观的总览
blkid            # 查看分区的 UUID 和文件系统类型
df -h            # 各挂载点的空间使用情况
df -i            # 各挂载点的 inode 使用情况

lsblk 是磁盘总览最好的工具,一条命令展示设备、分区、挂载点三层关系:

$ lsblk
NAME            MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
sda               8:0    0   100G  0 disk
├─sda1            8:1    0     1G  0 part /boot
└─sda2            8:2    0    99G  0 part
  ├─vg-root     253:0    0    50G  0 lvm  /
  └─vg-data     253:1    0    49G  0 lvm  /data
sdb               8:16   0   200G  0 disk
└─sdb1            8:17   0   200G  0 part /mnt/backup

inode 耗尽:空间未满却写不进文件

df -h 看的是数据块空间,df -i 看的是 inode(文件元数据)的使用情况。两者是独立的资源——空间和 inode 都可能单独耗尽。

一个经典故障:df -h 显示某分区还有大量空闲空间,但应用却报"No space left on device"无法写入文件。这种情况几乎都是 inode 用尽:

bash
$ df -h /var
Filesystem      Size  Used Avail Use% Mounted on
/dev/vg-data    50G   20G   30G  40% /var

$ df -i /var
Filesystem      Inodes  IUsed  IFree IUse% Mounted on
/dev/vg-data    3.2M    3.2M   0   100% /var inode 100% 是问题所在

通常的原因是某个目录积累了几百万个小文件——每个文件占用一个 inode,空间没用多少但 inode 耗尽。常见场景:

  • session 文件、缓存文件未清理
  • mail 队列、错误日志按文件粒度堆积
  • 大量小图片、小日志文件

定位 inode 占用大户:

bash
# 找出 /var 下文件数最多的几个子目录
find /var -xdev -type f | awk -F/ '{print $1"/"$2"/"$3}' | sort | uniq -c | sort -nr | head

二、分区

MBR 与 GPT

分区表记录磁盘上每个分区的起止位置,有两种格式:

格式容量上限分区数上限备份分区表
MBR(DOS)2 TB4 个主分区(或 3 主 + 多个逻辑)
GPTEB 级默认 128 个有(主分区表损坏时可从备份恢复)

新部署的服务器与大容量磁盘都使用 GPT。MBR 仅在维护历史系统时还会遇到。

查看分区

bash
fdisk -l            # 查看所有磁盘的分区(同时支持 MBR 和 GPT)
parted -l           # 信息更详细,操作 GPT 更方便

给新磁盘分区的标准流程

parted 为例,把一块新磁盘 /dev/sdb 划分为一个占满整盘的 GPT 分区:

bash
# 第 1 步:确认设备名(防止操作错磁盘)
lsblk

# 第 2 步:进入 parted 交互模式
parted /dev/sdb

在 parted 提示符下:

(parted) mklabel gpt                    # 创建 GPT 分区表(注意:会清空原有分区表)
(parted) mkpart primary 0% 100%         # 创建占满整盘的主分区
(parted) print                          # 查看当前分区表,确认结果
(parted) quit

通知内核重新扫描分区表

分区操作完成后,内核可能尚未感知新的分区。手动通知内核重新读取:

bash
partprobe /dev/sdb
lsblk                  # 确认 /dev/sdb1 已出现

云环境的设备名陷阱

某些云主机的设备名在重启后可能发生变化——本次启动是 /dev/sdb,重启后可能变成 /dev/sdc。因此挂载配置不应当依赖设备名,而应当使用 UUID。UUID 是文件系统格式化时生成的唯一标识,与设备名解耦,不会因为设备顺序变化而失效。这也是 /etc/fstab 推荐使用 UUID 的根本原因。

三、格式化:创建文件系统

在分区上创建文件系统:

bash
mkfs.xfs /dev/sdb1
mkfs.ext4 /dev/sdb1

文件系统的选择

xfs 与 ext4 是两个最主流的选择:

文件系统特点适用场景
xfsRHEL 7+ 默认,大文件性能优秀,仅支持在线扩容,不支持缩容大文件、日志、数据库存储
ext4Debian / Ubuntu 默认,通用性强,支持在线扩容、卸载后可缩容通用场景
btrfs支持快照、子卷,功能丰富对快照、压缩有需求的场景
zfs强大的卷管理与数据完整性,但 Linux 上需额外模块存储服务器

格式化操作的不可逆性

mkfs 会清空分区上的全部数据,该操作不可逆。在生产环境执行 mkfs 之前,必须再次确认目标设备:

bash
# 1. 查看当前文件系统类型与挂载状态
lsblk -f

# 2. 确认目标分区未被挂载
mount | grep sdb1

# 3. 确认无误后执行
mkfs.xfs /dev/sdb1

"把正在跑业务的盘 mkfs 了导致数据全部丢失"是真实发生过的事故。多花几秒钟确认设备路径,远比事后数据恢复成本低。

四、挂载与 /etc/fstab

手动挂载

bash
mkdir -p /data
mount /dev/sdb1 /data
df -h /data

手动 mount 在重启后失效——分区需要在每次启动时自动挂载,必须写入 /etc/fstab

持久化挂载:/etc/fstab

获取分区的 UUID:

bash
$ blkid /dev/sdb1
/dev/sdb1: UUID="a1b2c3d4-5678-90ab-cdef-1234567890ab" TYPE="xfs"

/etc/fstab 中添加一行:

UUID=a1b2c3d4-5678-90ab-cdef-1234567890ab  /data  xfs  defaults  0  0

六列字段含义:

字段说明
1设备标识推荐使用 UUID,而非 /dev/sdb1 设备路径
2挂载点目标目录
3文件系统类型xfs、ext4 等
4挂载选项defaults 是一组默认参数;也可用 noatime,nodiratime
5dump 备份标志通常为 0(已废弃)
6fsck 检查顺序根分区为 1,其他分区为 2,0 表示跳过

写完 fstab 必须先用 mount -a 测试

修改 /etc/fstab 后,务必先执行 mount -a 验证语法:

bash
mount -a

mount -a 会按 fstab 挂载所有未挂载的条目。如果 fstab 中存在语法错误,这条命令会立即报错,可以当场修改。

跳过测试直接重启的风险:fstab 写错可能导致系统进入 emergency mode(紧急模式),正常登录都无法进行,必须通过控制台或救援盘修复。这一步测试只需几秒钟,但能避免一次严重故障。

验证挂载与持久化

bash
mount -a                         # 验证 fstab 语法
df -h /data                      # 确认挂载成功
cat /proc/mounts | grep /data    # 通过 /proc 验证内核视角的挂载

五、SWAP:磁盘充当应急内存

SWAP 是磁盘上划出的一块区域,当物理内存不足时,内核会把不活跃的内存页换出到 SWAP,腾出物理内存给更需要的进程。本质上是用磁盘充当"溢出的内存",由于磁盘速度比物理内存慢几个数量级,SWAP 只能用于应急,不能作为常态资源依赖。

查看 SWAP 状态

bash
free -h               # 查看内存与 SWAP 使用情况
swapon --show         # 查看当前所有 SWAP 设备

输出示例:

$ free -h
              total        used        free      shared  buff/cache   available
Mem:           7.7G        3.1G        2.4G        100M        2.2G        4.2G
Swap:          2.0G        100M        1.9G

创建 SWAP 文件

bash
# 第 1 步:创建 2GB 的文件
fallocate -l 2G /swapfile

# 第 2 步:权限收紧到 600(SWAP 文件可能包含敏感的内存内容)
chmod 600 /swapfile

# 第 3 步:格式化为 SWAP
mkswap /swapfile

# 第 4 步:启用
swapon /swapfile

# 第 5 步:验证
swapon --show
free -h

持久化:写入 /etc/fstab

/swapfile  none  swap  sw  0  0

数据库服务器的 SWAP 策略

MySQL、PostgreSQL、Redis 这类数据库服务通常不希望依赖 SWAP。数据库一旦开始大量换出页面到 SWAP,性能会断崖式下跌——本来毫秒级的查询会变成秒级,业务无法承受。

偶尔有少量页面被换出是正常的,但如果观察到 SWAP 在持续频繁活动(thrashing),说明物理内存严重不足,正确的应对是增加物理内存或优化应用配置(如调小 InnoDB buffer pool),而不是放任 SWAP 缓解。

可以通过 vmstat 观察 SWAP 活动:

bash
$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 0  0   100M   2.4G   200M   2.2G    0    0     0     5  150  300  5  2 92  0  1

si(swap in)与 so(swap out)持续大于 0 表示 SWAP 正在被频繁读写。

调整 swappiness

vm.swappiness 控制内核倾向于使用 SWAP 的程度,值越大越倾向于换出。数据库服务器通常调低这一参数:

bash
# 查看当前值(默认 60)
cat /proc/sys/vm/swappiness

# 临时设为 10
sysctl vm.swappiness=10

# 持久化
echo "vm.swappiness=10" >> /etc/sysctl.d/99-swappiness.conf

六、LVM:逻辑卷管理

LVM(Logical Volume Manager)在物理磁盘和文件系统之间引入了一层抽象。最大的价值是支持动态扩容——可以在不停止服务、不卸载文件系统的前提下扩展存储空间。在物理服务器环境(云环境的云盘本身就支持在线扩容)中,LVM 几乎是标配。

三层结构

LVM 由三层抽象构成:

物理磁盘/分区 →  PV(Physical Volume,物理卷)

                VG(Volume Group,卷组)   ← 多个 PV 组成的存储池

                LV(Logical Volume,逻辑卷)  ← 从 VG 中划出的逻辑空间

                文件系统  ← 建立在 LV 上

创建 LVM 的完整流程

bash
# 第 1 步:把分区初始化为物理卷
pvcreate /dev/sdb1

# 第 2 步:创建卷组 vg_data,包含 /dev/sdb1
vgcreate vg_data /dev/sdb1

# 第 3 步:从 vg_data 中创建逻辑卷 lv_data,占用全部剩余空间
lvcreate -n lv_data -l 100%FREE vg_data

# 第 4 步:在逻辑卷上创建文件系统
mkfs.xfs /dev/vg_data/lv_data

# 第 5 步:挂载
mkdir /data
mount /dev/vg_data/lv_data /data

LVM 在线扩容流程

假设业务运行一段时间后存储不足,加了一块新磁盘 /dev/sdc,要给 /data 扩容:

bash
# 第 1 步:分区(可选,也可以直接对整盘做 pvcreate)
parted /dev/sdc mklabel gpt mkpart primary 0% 100%

# 第 2 步:把新分区初始化为 PV
pvcreate /dev/sdc1

# 第 3 步:把新 PV 加入卷组(VG 容量增加)
vgextend vg_data /dev/sdc1

# 第 4 步:扩展 LV,并同步扩展文件系统
lvextend -r -l +100%FREE /dev/vg_data/lv_data

-r 参数极其关键——它会在扩展逻辑卷的同时,自动调用对应文件系统的扩容工具(xfs 用 xfs_growfs,ext4 用 resize2fs)。漏掉 -r 的后果是:逻辑卷确实扩大了,但文件系统仍然停留在原大小,df -h 看到的空间没有任何变化。

如果漏掉了 -r,事后单独扩文件系统:

bash
# XFS
xfs_growfs /data

# ext4
resize2fs /dev/vg_data/lv_data

LVM 状态查看

bash
pvs            # 查看所有物理卷
vgs            # 查看所有卷组
lvs            # 查看所有逻辑卷
pvdisplay      # 物理卷的详细信息
vgdisplay      # 卷组的详细信息
lvdisplay      # 逻辑卷的详细信息

LVM 缩容的困难

LVM 扩容方便,缩容麻烦。原因在于:

  • xfs 完全不支持缩容
  • ext4 支持缩容,但必须先卸载文件系统(umount),业务必须停服

因此规划存储时,"以后不够再扩"是合理假设,"以后过大再缩"不是。空间宁可一开始规划宽松,也不要预留过紧导致后期被迫做缩容操作。

七、RAID 简介

RAID(Redundant Array of Independent Disks)用多块磁盘组合成逻辑存储,提供性能提升或数据冗余。常见级别:

级别特点容量利用率适用场景
RAID 0条带化(数据分散到多块盘)100%性能优先,无冗余,坏一块盘数据全失
RAID 1镜像(两块盘内容完全一致)50%系统盘、重要数据
RAID 5分布式奇偶校验,可坏 1 块盘(N-1)/N一般业务存储,需 ≥3 块盘
RAID 10先镜像再条带化50%高性能 + 高可靠,需 ≥4 块盘

Linux 软件 RAID

Linux 通过 mdadm 管理软件 RAID:

bash
cat /proc/mdstat              # 查看当前 RAID 状态
mdadm --detail /dev/md0       # 查看指定 RAID 设备的详细信息

cat /proc/mdstat 输出示例:

md0 : active raid1 sdb1[0] sdc1[1]
      209715200 blocks super 1.2 [2/2] [UU]

[UU] 表示两块盘都健康,如果出现 [U_] 则表示某块盘故障(降级状态)。

云环境通常无需自建 RAID

在公有云环境中,通常不需要在虚拟机层面再做 RAID。云平台底层的块存储已经做了冗余:

  • AWS EBS、阿里云 ESSD 等使用三副本或纠删码
  • 块设备故障由云平台底层处理,对用户透明

在云盘上再叠加一层软件 RAID,既不能解决底层物理故障(因为已经被云平台处理),又会增加复杂度和管理成本。RAID 主要应用于物理服务器与本地存储场景。

八、空间占用排查

磁盘告警的标准排查流程分两步:定位高占用目录 → 找到具体大文件

bash
# 第 1 步:确认是哪个挂载点空间不足
df -h

# 第 2 步:从根目录开始,定位占用最多的子目录
du -sh /* 2>/dev/null | sort -hr | head -10

# 第 3 步:逐层深入(假设定位到 /var)
du -sh /var/* 2>/dev/null | sort -hr | head -10
du -sh /var/log/* 2>/dev/null | sort -hr | head -10

# 第 4 步:直接列出大于 1G 的文件
find /var -type f -size +1G -exec ls -lh {} \; 2>/dev/null

du 与 df 数值不一致

有时候 du -sh /vardf 显示的 /var 占用相差很大,常见原因:

  1. 被删除但仍被进程持有的文件(下面详述)
  2. 跨文件系统的子目录——du -s /var 不会跨越文件系统,但 df 显示的是整个文件系统的占用
  3. 挂载点被其他文件系统遮蔽——某个挂载点下原本有文件,挂载新文件系统后被遮蔽,du 看不到但仍占用空间

第 3 种情况的验证:

bash
# 临时挂载到其他目录看一眼原内容
mkdir /mnt/check
mount --bind / /mnt/check
du -sh /mnt/check/var
umount /mnt/check

已删除文件但空间未释放

典型现象:已经执行了 rm 删除一个大文件,df -h 显示空间没有任何减少。

原因:该文件仍被某个进程持有文件描述符。文件名从目录树中消失了,但 inode 引用计数未归零(因为进程还在引用),文件系统不会真正释放空间。

定位:lsof 中带 (deleted) 标记的文件即为这种情况:

bash
$ lsof | grep deleted
nginx  12345 www-data 3w  REG  8,1  10737418240  789012  /var/log/nginx/access.log (deleted)

输出显示 nginx(PID 12345)仍以写模式持有 /var/log/nginx/access.log 这个已删除的文件,占用 10GB。

解决方法:

bash
# 方法 1:让进程重新打开日志文件(nginx 支持 reload 触发日志重打开)
systemctl reload nginx

# 方法 2:重启占用文件的进程
systemctl restart some-service

# 方法 3:如果无法重启服务,可以通过 /proc 截断文件
# 注意:这只是把文件大小置零,进程仍可继续写,但已删除的旧数据空间会释放
: > /proc/12345/fd/3

正确的预防方式是先清空再删除——> /var/log/nginx/access.logrm 更安全,因为不会让 inode 失去名称。或者通过 logrotate 的 copytruncate 配置避免这一问题(详见 第 11 讲 日志管理)。