Linux 磁盘与 LVM 管理:扩容文件系统的安全流程

介绍 lsblk、LVM 和文件系统扩容流程,强调分区变更前的备份与核对。

技术文章约 29 分钟177 次阅读

#文章解决什么问题:一条扩容因果链

云服务器扩磁盘是高频操作,但很多人只会"点控制台扩容,然后进系统随便敲两下"。如果不懂磁盘、分区、物理卷(Physical Volume,PV)、卷组(Volume Group,VG)、逻辑卷(Logical Volume,LV)、文件系统这几层各管什么,扩容就是一次碰运气的操作:运气好命令敲完容量就变了,运气不好数据盘瞬间写满、LV 与文件系统大小不一致、甚至分区表被改坏。这篇文章要解决的问题是:存储层把磁盘容量扩大之后,如何安全地把新增空间逐层传递到文件系统,让应用真正用得上,并能在每一步确认自己没有做错。

要回答这个问题,必须先看一个最基础的事实:从物理磁盘到文件系统之间隔着多层结构,每一层都维护自己独立的容量声明和地址映射。磁盘以扇区为单位暴露给操作系统;分区表把磁盘切成若干连续区间;LVM 在分区之上再抽象出 PV、VG、LV 三个层次;文件系统最后建立在一块"看起来连续"的块设备上——这块设备往往就是 LV。云控制台把磁盘从 40G 扩到 80G,操作系统看到的只是"整块盘变大了";分区、PV、VG、LV、文件系统的元数据里记录的容量,一个都还没变。扩容的本质,就是沿着这条因果链逐层扩大容量声明,顺序不能乱、层级不能跳。

这篇文章按"原理层 → 操作层 → 验证层 → 边界与坑"展开。先讲清楚每一层为什么存在、扩容为什么必须逐层传递,再给出两条可以直接照做的完整链路(XFS 根分区在线扩容、ext4 数据盘并入现有卷组),最后给出预期输出判定标准与常见失败模式的排查路径。

#原理层:从扇区到文件系统,每一层都在做什么

#块设备:扇区、LBA 与"磁盘变大了"的真正含义

磁盘(无论机械盘、SSD 还是云上的虚拟磁盘)对操作系统暴露的不是"文件",而是块设备:/dev/vda、/dev/sda、/dev/nvme0n1 都是块设备文件。块设备的寻址单位是扇区(sector),传统为 512 字节,新的大容量盘多用 4K 扇区;操作系统通过逻辑块地址(Logical Block Address,LBA)从 0 开始给每个扇区编号,读写请求本质上就是"从第 N 个扇区开始读/写 M 个扇区"。

云控制台"扩容磁盘"做了什么?它在虚拟化层把虚拟磁盘的容量字段改大(例如从 40G 改为 80G),对客户机来说,等价于一块变长了的块设备。内核重新扫描后,/dev/vda 从 40G 变成 80G,但其余一切不变:分区表里记录的还是旧的分区边界,文件系统超级块里记录的还是旧的块数。这就是"容量变了但什么都用不上"的根源——新增的容量是磁盘尾部一段无人认领的空白扇区,必须通过各层逐步"认领"。

#分区:把连续扇区区间登记进分区表

分区(partition)是磁盘上一个连续的 LBA 区间,由分区表登记。分区表有两种主流格式:

  • MBR(Master Boot Record):最多 4 个主分区(或 3 主 + 若干扩展/逻辑分区),分区起始/结束用 CHS 和 LBA 两种方式记录,只支持 2TB 以下磁盘,LVM 分区类型编号为 8e。
  • GPT(GUID Partition Table):现代 UEFI 引导的标准格式,支持 128+ 个分区、超大磁盘,每个分区有类型 GUID,Linux LVM 的 GPT 类型代码为 8e00。

无论是 MBR 还是 GPT,关键点一致:分区表只是"登记",它不负责数据布局。删除一个分区再原样重建,只要起始 LBA 不变,分区里的数据原封不动——这正是"删除分区重建来扩大小区"这一技巧可行的底层原因,也是它危险的原因:起始扇区一旦变了,数据区就整体错位。

扩容前先回答一个问题:磁盘尾部到底有没有空闲扇区? 判断工具是 parted 的 free 输出(或 fdisk -l 的设备容量对比)。例如盘从 40G 扩到 80G 之后:

$ sudo parted /dev/vda unit s print free
Model: Virtio Block Device (virtblk)
Disk /dev/vda: 167772160s
Sector size (logical/physical): 512B/512B
Partition Table: gpt
Disk Flags:
Number  Start       End        Size       File system  Name     Flags
        34s         4095s      4062s      Free Space
1       4096s       2101247s   2097152s   xfs          /boot    boot
2       2101248s    83886079s  81777632s               lvm
        83886080s   167772159s 83886080s  Free Space

Disk 行显示设备总扇区数(167772160s 正是 80GiB),Free Space 行显示分区 2 结束之后还有 83886080 个空闲扇区(约 40GiB)——这就是待认领的空间。如果最后一个分区之后没有 Free Space(例如中间还夹着别的分区),"扩最后一个分区"的思路就不成立,得改用"新建分区 → 新 PV → vgextend"路径(对应操作层的场景 B)。养成先查分区表、再动手的习惯,能避免"以为能扩却扩不了"的尴尬,也能看清自己到底要动哪一层。

#LVM:在分区之上加一层"可重组"的映射

没有 LVM 时,一个分区 = 一个文件系统,大小固定;想变大只能删了重建(数据全没)或依赖分区工具冒险调整。LVM 的价值是把"物理空间"与"逻辑空间"解耦:用户不再直接面对分区,而是面向逻辑卷,逻辑卷可以跨物理盘、可以随时变大、可以打快照。

LVM 由三层组成:

  • 物理卷 PV:把一块分区(或整块盘)标记为 LVM 成员。pvcreate 会在设备开头写入 PV header,并预留卷组元数据区;设备按固定大小切成物理扩展块(Physical Extent,PE),默认每个 PE 为 4 MiB(vgcreate -s 可改)。PE 是空间分配的最小单位。
  • 卷组 VG:一个或多个 PV 的 PE 池。VG 元数据记录了"这个卷组包含哪些 PV、每个 PV 上有哪些空闲 PE、哪些 PE 被哪些 LV 占用"。VG 元数据默认在 PV 上保留多份拷贝(通常 4 份),互备冗余。
  • 逻辑卷 LV:从 VG 中按 PE 粒度分配空间组成,对上层表现为一块连续块设备(/dev/mapper/vg0-root)。LV 的"大小"= 分配给它的 PE 数量。

因此整条链是:磁盘 → 分区 → PV → VG → LV → 文件系统。前文说的"扩容五步"就是逐层放大:分区变大(磁盘上新增空间被登记进分区表)→ PV 变大(重新扫描分区大小,PE 数增加)→ VG 吸收(空闲 PE 池变大)→ LV 变大(从 VG 分配新 PE)→ 文件系统变大(在 LV 上扩展元数据)。

PE 默认 4 MiB 是历史与性能的折中:PE 越大,VG 元数据越紧凑、空间分配越快,但粒度越粗(最后不足一个 PE 的零头会被浪费);PE 太小(如早期 64 KiB),大容量盘上的元数据会膨胀。除非有特殊粒度需求,生产环境一律用默认值。查看当前 PE 大小与占用:

sudo pvdisplay /dev/vda2 | grep -E 'PE Size|Total PE|Alloc PE|Free PE'

卷组有一个隐藏的全组硬约束:同一个 VG 内的所有 PV 必须使用相同的 PE 大小,否则 vgextend 会拒绝并入(报错 uses a different Physical extent size)。所以创建 VG 时若自定义了 -s,后续所有并入的 PV 都要与之对齐;这也是"先规划容量与粒度、再建组"的原因之一。

#逻辑卷的映射表:连续的逻辑空间是如何散落到多个物理盘的

LV 之所以能"跨盘",是因为它的地址不是直接指向物理扇区,而是通过一张映射表中转。内核里负责这块的是 device-mapper(DM)框架:/dev/mapper/vg0-root 实际是一个 DM 设备,它由若干 target 组成,每个 target 描述"逻辑扇区区间 → 底层真实设备 + 偏移"的对应关系。用 dmsetup table 能看到这张表,用 lvdisplay -m 能看到更友好的分段视图。

默认的映射方式是 linear(线性):LV 的第 N 个 PE 依序映射到某个 PV 的第 M 个 PE;当第一个 PV 的空间用尽,就从下一个 PV 继续,于是逻辑上连续的地址空间在物理上被切成了多段,散落在不同盘上。另一种常见方式是 stripe(条带):LV 按条带宽度把数据轮流写到多个 PV 上,读写并发提升,但任意一块盘损坏都会影响整卷。lvextend 扩出来的新 PE 默认追加在 LV 映射表末尾,由内核在 VG 空闲 PE 池中选择具体落盘位置——这也是为什么扩容后你能看到"逻辑空间增加了,但新 PE 到底在哪块物理盘上"不由你直接指定(除非用 -l +100%PVS /dev/xxx 指定特定 PV)。

dmsetup table /dev/mapper/vg0-root 的输出每行五段:逻辑起始扇区、逻辑长度(扇区数)、target 类型(linear)、底层设备主次号、底层偏移扇区。它只反映"这段逻辑区间对应哪块物理空间",数值由内核在创建与扩容时计算,不需要人工记忆。更直观的是 lvdisplay -m 的分段视图:

--- Segments ---
Logical extent 0 to 20223:
  Type                linear
  Physical volume     /dev/vda2
  Physical extents    0 to 20223

如果卷跨了两块盘,这里会出现两个 Segment:前一段落在 /dev/vda2、后一段落在 /dev/sdb1——这就是"连续逻辑空间散落到多个物理盘"的现场证据。创建条带卷的命令是 lvcreate -i 2 -I 64 -n data -L 100G data_vg(-i 条带数、-I 条带单元大小),创建后立刻能看到多段映射。查映射表本身也是一种验证手段,尤其是想确认"扩容后的新空间到底落在哪块盘"的时候。

理解了映射表,就理解了两个重要推论:

  1. LV 扩容是"追加映射项",不移动已有数据,因此可以在线进行、风险相对可控;
  2. LV 的可靠性取决于它占用的每个 PV:linear 卷跨越两块盘,坏其中一块就丢那一段的数据;VG 内没有冗余机制,冗余要靠 LV 层面(mirror/raid)来实现。这正是后文"PV 跨盘风险"一节的伏笔。

#文件系统与块设备:df 和 lsblk 为什么都对、却不一样

文件系统(XFS、ext4 等)本身不知道自己运行在 LVM 之上,它只看到一块"块设备",并在这块设备上建立自己的元数据:超级块记录文件系统总块数、块位图(ext4)或 B+ 树(XFS)记录空闲块、inode 表记录文件。文件系统格式化时,会把当时块设备的大小固化进超级块。

由此出现一个关键分工:

  • lsblk 读的是设备层的容量(分区大小、LV 大小),它不关心文件系统;
  • df -h 读的是文件系统超级块里的容量,它不关心底层设备。

于是"LV 显示 80G,df -h 还是 40G"是完全正常的中间状态——文件系统还没 grow,它按旧超级块工作。扩容完成的标准,是这两者的数字最终一致。

#在线扩容的因果链与 xfs/ext4 的约束差异

把因果链串起来看:

  1. 控制台扩磁盘 → 块设备尾部多出空闲扇区;
  2. 扩分区(growpart/fdisk/gdisk/parted)→ 分区表把空闲扇区并入最后一个分区;
  3. pvresize → PV 重新扫描分区大小,PE 总数增加;
  4. vgextend(如需要)→ 新 PV 的 PE 进入 VG 空闲池;
  5. lvextend → 从 VG 空闲池取 PE 追加到 LV 映射表;
  6. xfs_growfs/resize2fs → 文件系统把扩大后的块设备容量写入超级块。

前 5 步锁步进行,第 6 步才是文件系统层。之所以必须按这个顺序:每一层元数据描述的对象依赖下层的结果——LV 不能分配 VG 里还不存在的 PE,VG 不能吸收 PV 没扫描到的空间,文件系统不能超过 LV 大小。跳层或乱序都会得到"上层看到的大小与下层实际不符"的状态。

为什么整套流程可以"在线"(文件系统挂载状态下)执行而不需要停机?因为每一步都是元数据更新 + 映射追加,不搬运已有数据:分区扩展只是改分区表登记;pvresize 只是让 LVM 重新统计 PE 数;lvextend 是在映射表末尾追加新段;xfs_growfs/resize2fs 只是把文件系统元数据里的容量上限改大,并把新增区域标记为空闲。数据块原地不动,读写中的业务不受影响。反过来,缩容必然涉及"把数据从尾部搬走",所以要么没有工具(XFS)、要么要求离线(ext4)——"可以在线扩"与"不能在线缩"这对看似矛盾的约束,在这一层解释下是统一的。

XFS 与 ext4 在第 6 步的差异不是命令不同,而是元数据结构不同:

  • ext4 把空闲块记录在每块的块位图(bitmap)中,文件系统扩容 = 增加更多的组描述符和位图,元数据结构支持双向调整,所以 resize2fs 既能在线变大,也能(离线)缩小。
  • XFS 按**分配组(Allocation Group,AG)**组织空间,每个 AG 内部用 B+ 树管理空闲空间,这种结构为并发分配和单向增长而设计,从根基上就不支持收缩。官方从未提供 xfs 缩容工具,xfs_growfs 只做一件事:把文件系统扩展到块设备的最大可用大小。所以业界有一句公认的话:XFS 只能长,不能缩。

这也是原稿强调"扩容前先确认需求,别想着先扩再说、不行缩回来——缩不回来"的根本原因。

#操作层:两条完整可复现的扩容链路

#前置条件与环境说明

本文命令以 RHEL/CentOS Stream 9 与 Ubuntu Server 22.04 LTS 为例,涉及以下软件(均为各发行版仓库内标准包):

  • util-linux(提供 fdisk、lsblk,2.31+)
  • gdisk(GPT 分区工具,1.0.x)
  • parted(3.2+,提供 partprobe 与 resizepart)
  • cloud-utils-growpart(RHEL 系)/ cloud-guest-utils(Debian/Ubuntu),提供 growpart
  • lvm2(2.03.x)
  • xfsprogs(提供 xfs_growfs、xfs_repair、xfs_info,5.10+)
  • e2fsprogs(提供 resize2fs、dumpe2fs,1.46+)

内核版本 RHEL 9 为 5.14、Ubuntu 22.04 为 5.15;LVM 与文件系统的机制在内核 3.x 以后就基本稳定,命令用法在主流发行版上没有出入。若某工具未安装,按发行版补齐:

# RHEL / CentOS Stream
sudo dnf install -y cloud-utils-growpart lvm2 xfsprogs e2fsprogs gdisk parted

# Debian / Ubuntu
sudo apt-get update && sudo apt-get install -y cloud-guest-utils lvm2 xfsprogs e2fsprogs gdisk parted

统一前置条件:

  • 拥有 root 或 sudo 权限;
  • 云控制台/存储层已完成磁盘扩容,且已确认有快照或备份(动分区表之前必须留一手);
  • 磁盘设备名可能不同:virtio 盘为 vd*,SCSI/SAS 盘为 sd*,NVMe 盘为 nvme0n1。下文以 /dev/vda(系统盘)、/dev/sdb(新数据盘)为例,实际以 lsblk 输出为准;
  • 磁盘容量计数差异提醒:云控制台按 10 进制(1G=1,000,000,000 字节)计,Linux 的 lsblk/df 按 1024 进制(1G=1,073,741,824 字节)计,所以"扩到 80G"在系统里会看到约 80GiB(文中为叙述方便统一写作 80G/79G,属正常进制差异,不是故障)。

#场景 A:云系统盘在线扩容(XFS 根分区全链路)

这是最典型的场景:系统盘由 LVM 管理,根文件系统是 XFS,云控制台已把磁盘从 40G 扩到 80G。初始结构如下(这是任何操作之前必须先确认的现状):

lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT

预期看到:

NAME            SIZE TYPE  FSTYPE         MOUNTPOINT
vda             40G  disk
├─vda1           1G  part  xfs            /boot
└─vda2          39G  part  LVM2_member
  └─vg0-root    39G  lvm   xfs            /

看到 LVM2_member(分区是 PV)和 lvm(LV)这两行,说明系统盘走 LVM;vg0-root 是挂载在 / 的逻辑卷。这个是扩容的事实起点。

第 0 步:控制台扩容后,先在系统内确认块设备真的变大了。

lsblk

预期:vda 显示 80G,而 vda1/vda2 仍是 1G/39G——说明磁盘尾部已经多出约 40G 空闲扇区,但分区表还没认领(如果这里 vda 仍是 40G,请回到控制台确认扩容已生效,或稍等片刻让云厂商的热插拔通知到达)。

第 1 步:让内核重新读取分区表。

分区表在磁盘上,但内核内存里保留着旧的分区布局。扩容磁盘本身不改变分区表内容,一般不需要重读;但如果插入了新的分区、或刚把分区改过,需要让内核刷新:

sudo partprobe /dev/vda || sudo partx -u /dev/vda

partprobe 是推荐入口;若因设备繁忙(分区被挂载)报 Device or resource busy,用 partx -u /dev/vda 更新,或安排在维护窗口重启。对云盘来说,此时 vda 已 80G、vda2 仍 39G,是预期状态。

第 2 步:扩展分区,把尾部空闲扇区并入最后一个分区(注意:只有最后一个分区之后才有空闲空间时此法才成立;云镜像通常如此)。动手前先用上文的 parted /dev/vda unit s print free 确认:空闲空间确实在最后一个分区之后,就按下面路径走;如果空闲空间不在末尾(例如磁盘中间有未分配区域),"扩最后一个分区"不成立,应改为新建分区并入卷组(对应场景 B 的思路),不要试图跨过中间分区硬扩。

首选 growpart,它只做一件安全的事:把指定分区从当前位置延伸到磁盘末尾,起始扇区保持不变,因此在"扩最后分区"场景最不易出错:

sudo growpart /dev/vda 2

成功时输出形如:

CHANGED: partition=2 start=4096 old: size=... end=... new: size=... end=...

此时再 lsblk,vda2 应显示约 79G。

没有 growpart 时的替代方案——fdisk 删除后重建分区(MBR 为例)。要点只有一个:新分区的起始扇区必须与旧分区完全一致,否则 LVM 元数据就找不到了:

$ sudo fdisk /dev/vda

Welcome to fdisk (util-linux ...).
Command (m for help): p        # 先查看,记录 vda2 的 Start 扇区(例如 4096)
...
Command (m for help): d        # 删除分区
Partition number (1,2, default 2): 2
Command (m for help): n        # 新建分区
Partition number (2-4, default 2): 2
First sector (4096-..., default 4096): 4096    # 必须与原 Start 一致!默认值通常恰好一致,务必核对
Last sector, +/-sectors (..., default ...):    # 回车,默认延伸到磁盘末尾
Created a new partition 2 of type 'Linux LVM'.
Command (m for help): t        # 改分区类型
Partition number (1,2, default 2): 2
Hex code (type L to list all codes): 8e        # MBR 的 Linux LVM
Changed type of partition 'Linux' to 'Linux LVM'.
Command (m for help): w        # 写入分区表
The partition table has been altered.
...

如果 fdisk 提示检测到 LVM2_member 签名、询问是否移除,必须回答 No 保留,否则等于把 PV 头删了。

GPT 磁盘用 gdisk,流程同构,注意分区类型代码是 8e00(Linux LVM):

$ sudo gdisk /dev/vda
Command (? for help): p        # 记录分区 2 的 First sector
Command (? for help): d        # 删除分区 2
Partition number (1-2): 2
Command (? for help): n        # 新建
Partition number (2-128, default 2): 2
First sector (...): 4096       # 与原 First sector 一致
Last sector (...):             # 回车,默认到末尾
Current type is 'Linux filesystem'
Hex code or GUID (L to show codes, Enter = 8300): 8e00   # GPT 的 Linux LVM
Changed type of partition to 'Linux LVM'
Command (? for help): w        # 写入
Do you want to proceed? (Y/N): y

使用 parted 的一行式替代(其设计就是"安全原位扩展分区",不要求删除重建):

sudo parted -s /dev/vda resizepart 2 100%

第 3 步:扩展 PV,让 LVM 重新感知分区的新大小。

sudo pvresize /dev/vda2

成功输出形如 Physical volume "/dev/vda2" changed 1 physical volume(s) ...。随后确认卷组吸收了新空间:

sudo vgs

注意 VFree 列:由约 0 变为约 40G,说明 VG 空闲 PE 池已经就绪。

第 4 步:扩展 LV,从 VG 空闲池取 PE 追加到逻辑卷。

sudo lvextend -l +100%FREE /dev/vg0/root

-l +100%FREE 表示"把 VG 当前所有空闲 PE 全部追加给 LV",是云主机根卷扩容最常用的写法。成功输出形如:

Size of logical volume vg0/root changed from 39.00 GiB (9984 extents) to 79.00 GiB (20224 extents).
Logical volume vg0/root successfully resized.

如果想"只加一部分",用 -L +10G(增加 10GiB)或 -l +25%FREE(增加当前空闲的 25%);想精确指定落到某块 PV,用 -l +100%PVS /dev/vda2。

第 5 步:扩展文件系统——XFS 与 ext4 命令不同,这是最容易翻车的一步。

XFS:在挂载状态下对挂载点执行 xfs_growfs(也可以传设备路径,但挂载点写法在只读/恢复场景更明确):

sudo xfs_growfs /

输出核心是 data blocks changed from ... to ... 一行,表示超级块里的块数已更新。XFS 的 grow 是纯增长操作,收尾自动完成,无需担心中途"写一半"。

ext4(若根分区是 ext4)则用 resize2fs,直接给 LV 设备路径,同样支持在线(挂载状态)扩展:

sudo resize2fs /dev/vg0/root

输出 The filesystem on /dev/vg0/root is now ... blocks long. 即为成功。

第 6 步:验证收尾。

df -h /

预期 / 的 Size 从 39G 变为约 79G,且 Used 不变。此时 lsblk 中 LV 大小与 df 中文件系统大小应一致——这才是"扩容真正完成"的判定标准。

#特例:根文件系统不在 LVM 上时

如果 lsblk 显示根分区直接是 part(FSTYPE 为 xfs 或 ext4),没有 lvm 层,说明系统盘没走 LVM。此时扩容路径更短:扩分区 → 直接扩文件系统,省掉 PV/VG/LV 三层:

sudo growpart /dev/vda 2
sudo xfs_growfs /                  # xfs 对挂载点执行
# 或
sudo resize2fs /dev/vda2           # ext4 对设备路径执行

少了 LVM 的缓冲,普通分区扩容的容错更低:文件系统 grow 之前同样不能随意缩;没有 LV 映射表这个"中间人",分区一旦写错就是文件系统错位,没有第二层保护。这也从反面说明 LVM 在云主机上的价值——多一层间接,多一重可回退的余地。

#场景 B:新增数据盘并入现有 VG(从分区到 ext4 文件系统全链路)

适用情形:已有卷组 vg0,想给某个 ext4 数据卷(如 /dev/vg0/data,挂载 /data)加一块新盘的空间。假设新盘在系统里是 /dev/sdb、尚未使用。

B1. 分区(推荐 GPT + gdisk)。 虽然 LVM 允许整盘直接做 PV(pvcreate /dev/sdb),但生产上建议分区:分区有格式可见性、方便以后做税务核对与工具识别,也避免 blkid 对"无分区表整盘"产生歧义。

$ sudo gdisk /dev/sdb
Command (? for help): n
Partition number (1-128, default 1): 1
First sector (34-..., default = 2048):      # 回车使用 2048 扇区对齐
Last sector (...):                          # 回车,用完整块盘
Hex code or GUID (...): 8e00                # Linux LVM
Command (? for help): w
Do you want to proceed? (Y/N): y

(也可以用 fdisk 配 MBR,分区类型 8e,流程同场景 A 的 fdisk 小节。)

若坚持整盘 PV,跳过 B1,直接在第 B2 对 /dev/sdb 执行 pvcreate,并接受 WARNING: ... is not a partition 的提示——该警告只是提醒"这不是分区"。

B2. 建 PV 并入 VG。

sudo pvcreate /dev/sdb1

预期输出 Physical volume "/dev/sdb1" successfully created.;接着把新 PV 的 PE 池加入 vg0:

sudo vgextend vg0 /dev/sdb1

预期输出 Volume group "vg0" successfully extended。vgs 中 #PV 应变为 2,VFree 相应增加。

B3. 扩展 LV 与文件系统。

sudo lvextend -l +100%FREE /dev/vg0/data
sudo resize2fs /dev/vg0/data

ext4 在线扩容在挂载状态下即可执行,无需卸载。resize2fs 输出 The filesystem on /dev/vg0/data is now ... blocks long. 后,/data 的容量即刻可用,不必重启。

B4. 验证。

lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT /dev/sdb
df -h /data
pvs; vgs; lvs

/data 的 Size 应等于原来大小加上新盘贡献的空间。

#场景 C:新数据盘建独立 VG(简要链路)

如果不想动现有卷组,新盘完全可以自成一个 VG:

sudo pvcreate /dev/sdc1
sudo vgcreate data_vg /dev/sdc1
sudo lvcreate -n data -L 500G data_vg
sudo mkfs.xfs /dev/data_vg/data     # 或 mkfs.ext4
sudo mkdir -p /data && sudo mount /dev/data_vg/data /data

独立 VG 的好处是故障隔离(后文"PV 跨盘风险"一节会展开):数据盘出问题不会牵连系统卷组。

#常用参数速查:lvextend 的取值

参数含义用途
-l +100%FREE增加 VG 全部空闲 PE根卷/数据卷"全给 LV"最常用
-l +25%FREE增加当前 VG 空闲 PE 的 25%预留一部分余量
-l +100%PVS增加指定 PV 上空闲 PE(需同时给 PV 路径)把新盘空间定向给某 LV
-L +10G绝对值增加 10GiB明确的增量扩容
-L 100G设置 LV 大小为 100GiB一次性指定目标大小
-r扩容同时自动 resize2fs(ext4 可用;XFS 建议手动 xfs_growfs)少敲一步,但别依赖它处理 XFS

-r 的说明务必注意:lvextend -r 依赖 fsadm 对文件系统的识别,对 XFS 的在线扩展支持不完整,XFS 场景老老实实 lvextend 后手动 xfs_growfs。

#验证层:怎么确认我做对了

扩容不能以"命令没报错"作为成功标准,要以各层的预期输出为准。

#1. 结构验证:lsblk

最终预期(场景 A 完成后):

NAME            SIZE TYPE  FSTYPE         MOUNTPOINT
vda             80G  disk
├─vda1           1G  part  xfs            /boot
└─vda2          79G  part  LVM2_member
  └─vg0-root    79G  lvm   xfs            /

判定:vda2 约等于磁盘减 /boot;vg0-root 的 SIZE 与 vda2 一致(LV 用满了该 PV)。

#2. LVM 三层验证:pvs / vgs / lvs

$ sudo pvs
PV         VG  Fmt  Attr PSize   PFree
/dev/vda2  vg0 lvm2 a--  79.00g  40.00g

$ sudo vgs
VG  #PV #LV #SN Attr   VSize  VFree
vg0   1   1   0 wz--n- 79.00g 40.00g

$ sudo lvs
LV   VG  Attr       LSize
root vg0 -wi-ao---- 79.00g

判定:pvs 的 PSize 与分区大小一致、PFree 表示尚未分配给 LV 的空间;vgs 的 VFree 应为 0(如果上一步用了 +100%FREE);lvs 的 LSize 已增大、Attr 第一字符为 -(正常)、第四字符 a(active)。若 pvs 看不到某 PV 或大小未变,说明分区扩展或 pvresize 没生效,先回头查第 2/3 步。

进一步验证映射与落盘位置:

sudo lvdisplay -m /dev/vg0/root              # LV 的段映射:每个逻辑区间落在哪个 PV 的哪些 PE
sudo dmsetup table /dev/mapper/vg0-root      # 内核侧线性映射表(只读查看,无害)

判定:扩容后 lvdisplay -m 里的 Logical extent 范围应覆盖新增 PE(例如从 0 to 9983 变为 0 to 20223);若原先只有一个 linear 段、扩容后出现第二段落在新 PV 上,说明空间确实散落到了跨盘区域——这是预期行为,但要回到"坑六"评估这份跨盘是否在你接受的可靠性之内。

#3. 文件系统验证:df 与深度检查

df -h /
Filesystem             Size  Used Avail Use% Mounted on
/dev/mapper/vg0-root   79G   8.1G   71G  12% /

判定:Size 已增至约 79G,Use% 相对扩容前应明显下降。df 看的是文件系统,lsblk 看的是块设备——两条命令都显示一致的大小时才算完成。

深度确认文件系统确实扩展到了新容量(而不是只改了显示):

  • 通用:stat -f / 读取文件系统统计,Blocks: Total 乘以块大小即当前文件系统容量,应与 df 的 Size 一致;
  • XFS:xfs_info / 查看 data blocks(块数)与超级块记录的设备容量是否匹配当前 LV;
  • ext4:dumpe2fs -h /dev/vg0/data | grep -E 'Block count|Block size',Block count × Block size 应约等于 LV 容量。
sudo stat -f /                       # 通用:文件系统类型与块统计
sudo xfs_info /                     # XFS,看 data blocks
sudo dumpe2fs -h /dev/vg0/data | grep -E 'Block count|Block size'   # ext4
sudo blkid /dev/vg0/data            # 确认文件系统类型没有被误改

#4. 一键汇总检查

lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT && sudo pvs && sudo vgs && sudo lvs && df -hT

把 lsblk 的 LV 大小与 df 的 Size 放一起对比,就是最直观的"逐层对齐"验收。

#边界与坑:常见失败模式、为什么失败、怎么排查

这一节的坑,本质上是同一件事的不同侧面:层与层之间的信息不同步,或对层结构约束的误判——要么上层没跟上(LV 大了 df 没大)、要么下层被误改(分区起始错位)、要么对结构的硬约束理解不足(XFS 不可缩、PE 大小必须一致)。逐个看:

#坑一:LV 大了、文件系统没大(df 没变)

lvextend 只动了块设备层,文件系统超级块还按旧容量工作。表现形式:lvs 显示 80G,df -h / 还是 40G,磁盘使用率一路写满报警。原因就是本节开头讲的"df 看文件系统、不看块设备"。解决:补做第 5 步(XFS 用 xfs_growfs、ext4 用 resize2fs),再 df -h 复核。这个坑最常见也最容易规避——把"文件系统 grow"当成扩容流程的第 6 步固定动作,而不是可选项。

#坑二:XFS 不能缩容,扩容前必须想清楚

XFS 的空间管理按分配组 + B+ 树组织,从结构上就不支持收缩;官方没有任何工具能做 xfs shrink。所以:

  • 扩容前确认峰值需求,别指望"先扩再说,不行缩回来";
  • 如果确实需要"弹性加减",方案是在创建卷时就用 thin LV(精简配置)或提前规划好最终大小;
  • 一旦对 XFS 执行了 xfs_growfs,LV 只能继续增、不能减(lvreduce 会直接破坏已超出文件系统描述的块区域)。

#坑三:ext4 缩容必须离线

ext4 的 resize2fs 支持在线放大,但缩小必须在卸载状态下进行,而且顺序是反的:先缩文件系统、再缩 LV。正确做法(生产环境务必先在测试盘演练):

sudo umount /data
sudo e2fsck -f /dev/vg0/data              # 缩容前强制检查
sudo resize2fs /dev/vg0/data 500G         # 先把文件系统缩到目标值
sudo lvreduce -L 500G /dev/vg0/data       # 再缩 LV;LV 不能小于文件系统
sudo mount /data

顺序错了(先 lvreduce 后 resize2fs)等于把文件系统尾部直接切掉,数据会损坏。这同样解释了为什么"回退窗口"只在文件系统 grow 之前存在。

#坑四:分区表重读失败(Device or resource busy)

partprobe 报错的重灾区是磁盘上有挂载中的分区。处置顺序:先 sudo partx -u /dev/vda 尝试局部更新 → 仍失败则安排维护窗口重启(重启后内核按新分区表加载)。云环境里磁盘扩容本身不触发重读需求,但"删除重建分区"这类操作必然要过重读这一关,别在业务高峰期做。

#坑五:fdisk/gdisk 重建分区时起始扇区不一致

这是"删除重建扩大分区"操作最致命的手误:起始扇区一旦改变,PV 头找不到了,pvs 看不到卷,数据看起来"全没了"。规避三件事:动手前 p 记录原 Start/First sector;重建时逐字核对;fdisk 询问是否移除 LVM2_member 签名时回答 No。真出了事也别慌:只要分区起始没变、数据没被覆盖,删除重建只是改登记,partprobe 后 pvscan 还能把 PV 找回来;万一回不来且无备份,立即停止写盘,走数据恢复流程。

#坑六:PV 跨盘——VG 不是保险箱

把新盘 vgextend 进现有 VG 时,要清醒认知:VG 只是空间池,不做冗余。linear 卷跨两块 PV,坏一块盘就丢那块盘上对应的 PE 数据段;stripe 卷更脆弱,坏一块整卷不可用。因此:

  • 系统盘 VG 建议保持单盘或至少与数据盘隔离,别为省事把数据盘塞进系统 VG,扩大故障半径;
  • 数据卷需要冗余时,用 lvcreate --type raid1(或镜像)创建跨盘卷,或用存储层 RAID/云盘多副本兜底;
  • 单盘 VG 的"跨盘"收益只是空间池化,可靠性没有任何提升——规划前先问"我要的是池子还是保护"。

#坑七:新 PV 带着"上一个 VG 的 PE 大小"来入组

vgextend 偶发报 Volume group "vg0" uses a different Physical extent size,多发生在复用旧盘/旧 PV 的场景:该 PV 元数据里记录的 PE 大小与目标 VG 不一致。原因:PE 大小是 VG 的全局属性,PV 入组时必须与整组对齐。处置:确认该 PV 上没有需要保留的数据后,先清掉旧身份再重新登记:

sudo pvremove /dev/sdb1           # 清除旧 PV 元数据(数据盘无有效数据时才可执行)
sudo pvcreate /dev/sdb1
sudo vgextend vg0 /dev/sdb1

这个坑提醒一句关键认知:PV 是"登记在册"的设备,不是随便一块空闲盘。复用任何设备之前,先 pvs/pvscan 看它有没有历史身份、属于哪个 VG,不要默认新盘就是"干净"的。

#坑八:在 /boot 等普通分区上想当然地 grow

/boot 是独立小分区(示例中 vda1 1G xfs),不是 LVM 管辖区。growpart 也能把它扩大,但普通分区扩了之后文件系统不会自动跟着长,要手动算偏移、跑 xfs_growfs /boot,且起始扇区错位的风险全由自己承担。默认建议:普通分区不动它,空间不够优先检讨内核镜像/引导文件占用,扩展留给 LVM 卷。

#坑九:动分区表前没有备份/快照

growpart 本身不写数据,任何分区表操作理论上可逆,但"理论上"不等于"实际上"。云厂商快照是秒级、低成本的回退手段,生产数据盘动分区表之前必须确认有快照或备份;无快照的裸机环境,至少把 lsblk -b、pvs/vgs/lvs -v、fdisk -l 完整输出留档,供恢复时对照。

#坑十:回退窗口只在 grow 之前

完整回退逻辑:lvextend 之后、文件系统 grow 之前,LV 末尾多出来的 PE 尚未被文件系统使用,此时 lvreduce -L <原大小> 可安全缩回;一旦 xfs_growfs/resize2fs 执行完,XFS 永久绑定新容量(ext4 则需走到坑三的离线流程才能缩)。所以每一步验证、每步留余地,才是 LVM 扩容的正确打开方式。

#失败排查路径(从现象到原因)

扩容后一切不对时,按这个顺序定位:

dmesg | tail -n 30          # 内核是否报分区/设备错误
lsblk                       # 分区表、LV 结构是否如预期
sudo pvscan && sudo vgscan && sudo vgchange -ay   # LVM 能否重新发现并激活卷
sudo pvs && sudo vgs && sudo lvs                   # 各层容量与状态

典型的错误对应:pvs 找不到 PV → 分区起始或签名问题(坑五);vgscan 找不到 VG → PV 元数据区异常,先别写盘;lvs 正常但 df 没变 → 文件系统没 grow(坑一)。

#收尾:什么时候该用 LVM,什么时候不该

LVM 的价值一句话概括:把"物理盘"和"可用空间"解耦,让容量管理从"一次性规划"变成"随时可调"。它适合:多块盘需要池化、业务容量会持续增长、需要快照/精简配置的服务器与云主机。

它也有代价:多一层映射、多一层元数据、多一组命令,故障排查面变大。如果场景是"单块盘、容量写死、三年不扩容",直接用分区 + 文件系统更简单;此时强行引入 LVM 只是负担。

回到开头那条因果链:磁盘变大 → 分区认领 → PV 认领 → VG 吸收 → LV 追加 → 文件系统扩展。每一步都有一组对应命令、一组预期输出、一种典型失败方式。把这条链记在脑子里,再做扩容就不是"敲命令碰运气",而是逐层确认、可验可回退的工程操作。下一步建议:把 lsblk/pvs/vgs/lvs/df 五条只读检查做成一个脚本,每次扩容前后各跑一遍留档;XFS 卷的扩容请求,永远在动手前确认好"只增不减"这一约束。

相关阅读

Related

留言讨论

0 条

还没有留言,来说点什么吧。

Linux 磁盘与 LVM 管理:扩容文件系统的安全流程 · LXH·BLOG