LVM —— pvcreate / vgcreate / lvcreate 三件套
一句话定义
LVM (Logical Volume Manager) 把多个物理盘虚拟化成"逻辑卷",提供动态分区 / 在线扩容 / snapshot / 跨盘聚合 等能力。企业 Linux 95% 系统都用 LVM——比传统分区灵活十倍。
典型场景
- 装机时让系统盘可以动态扩容(不预先划死)
- K8s Local PV 用 LVM 动态切给不同 PV
- 数据盘加大:扩物理盘 → LVM 扩 LV → 文件系统扩
- 生产 snapshot(备份 / 测试)
- 多盘聚合成一个大 LV
1. LVM 心智模型
graph TB
subgraph 物理层
D1["/dev/sda"]
D2["/dev/sdb"]
D3["/dev/sdc"]
end
subgraph LVM层[LVM 抽象层]
PV1[PV /dev/sda]
PV2[PV /dev/sdb]
PV3[PV /dev/sdc]
VG["VG: data-vg<br>(三个 PV 合一)"]
LV1["LV: mysql<br>(50G)"]
LV2["LV: logs<br>(30G)"]
LV3["LV: kafka<br>(100G)"]
end
subgraph 文件系统
FS1["ext4 on /dev/data-vg/mysql"]
FS2["xfs on /dev/data-vg/logs"]
FS3["ext4 on /dev/data-vg/kafka"]
end
D1 --> PV1
D2 --> PV2
D3 --> PV3
PV1 --> VG
PV2 --> VG
PV3 --> VG
VG --> LV1
VG --> LV2
VG --> LV3
LV1 --> FS1
LV2 --> FS2
LV3 --> FS3
style 物理层 fill:#e1f5ff
style LVM层 fill:#ffe1f5
style 文件系统 fill:#fff4e1
三层抽象
| 层 | 命令前缀 | 干什么 |
|---|---|---|
| PV (Physical Volume) | pv* | 把物理盘 / 分区"标记"成 LVM 可用 |
| VG (Volume Group) | vg* | 把多个 PV 聚合成"存储池" |
| LV (Logical Volume) | lv* | 从 VG 里切出"逻辑卷"给应用用 |
对应关系:
PV (物理盘) VG (存储池) LV (逻辑卷) 应用
/dev/sda ┐ ┌─ mysql (50G) ─→ MySQL 数据
/dev/sdb ├─→ data-vg ──────►├─ logs (30G) ─→ 日志
/dev/sdc ┘ └─ kafka (100G)─→ Kafka
与传统分区的区别
| 维度 | 传统分区 (fdisk / parted) | LVM |
|---|---|---|
| 大小 | 创建时固定 | 动态扩缩 |
| 跨盘 | ❌ | ✅(多盘合一) |
| Snapshot | ❌ | ✅ |
| 在线操作 | 受限 | ✅ |
| 复杂度 | 简单 | 中等 |
2. 一次完整 LVM 操作
# 0. 看现有盘
lsblk
# NAME SIZE TYPE
# sda 100G disk ← 我们要用这块新盘
# 1. 让 LVM "接管" 物理盘(创建 PV)
pvcreate /dev/sda
# Physical volume "/dev/sda" successfully created
# 2. 把这块盘放进一个 VG(存储池)
vgcreate data-vg /dev/sda
# Volume group "data-vg" successfully created
# 3. 从 VG 里切个 LV 出来
lvcreate -n mysql -L 50G data-vg
# Logical volume "mysql" created
# 4. 在 LV 上建文件系统
mkfs.ext4 /dev/data-vg/mysql
# 5. 挂载
mkdir -p /var/lib/mysql
mount /dev/data-vg/mysql /var/lib/mysql
# 6. 持久化(fstab)
echo "UUID=$(blkid -s UUID -o value /dev/data-vg/mysql) /var/lib/mysql ext4 defaults,nofail 0 2" >> /etc/fstab
# 看所有 PV
$ pvs
PV VG Fmt Attr PSize PFree
/dev/sda data-vg lvm2 a-- 100.00g 50.00g ← 50G 剩余
# 看所有 VG
$ vgs
VG #PV #LV #SN Attr VSize VFree
data-vg 1 1 0 wz--n- 100.00g 50.00g
# 看所有 LV
$ lvs
LV VG Attr LSize Pool Origin Data%
mysql data-vg -wi-ao---- 50.00g
# 详细信息
pvdisplay
vgdisplay
lvdisplay
/dev/<VG>/<LV> # 友好路径 (符号链接)
/dev/mapper/<VG>-<LV> # 真实路径
# 比如:
/dev/data-vg/mysql
/dev/mapper/data--vg-mysql ← 注意 -- (因为 VG 名含 -)
3. 扩容(LVM 最杀手锏的能力)
在线扩 LV + 文件系统
# 看当前
$ lvs
LV VG LSize
mysql data-vg 50.00g
# 扩到 80G(绝对值)
$ lvextend -L 80G /dev/data-vg/mysql
Size of logical volume data-vg/mysql changed from 50.00 GiB to 80.00 GiB
# 或者增加 30G(相对值)
$ lvextend -L +30G /dev/data-vg/mysql
# 或者用所有剩余空间
$ lvextend -l +100%FREE /dev/data-vg/mysql
# ⚠️ 文件系统还是 50G!要扩文件系统
$ df -h /var/lib/mysql
/dev/mapper/data--vg-mysql 50G 10G 40G 20%
# ext4 扩容
$ resize2fs /dev/data-vg/mysql
# xfs 扩容(参数是挂载点不是设备)
$ xfs_growfs /var/lib/mysql
# 再看
$ df -h /var/lib/mysql
/dev/mapper/data--vg-mysql 80G 10G 70G 13%
一步搞定:-r 自动 resize 文件系统
$ lvextend -L +30G -r /dev/data-vg/mysql
^^
--resizefs 自动检测 fs 类型 + 扩文件系统
生产推荐 -r——避免忘扩 fs。
扩 VG(加新盘到现有 VG)
# 1. 新盘 /dev/sdb 接入
$ lsblk
sdb 200G disk ← 新盘
# 2. 创建 PV
$ pvcreate /dev/sdb
# 3. 加进现有 VG
$ vgextend data-vg /dev/sdb
# 4. 现在 VG 多了 200G 可用
$ vgs
VG #PV #LV VSize VFree
data-vg 2 1 300g 250g ← 多了 200G 余量
# 5. 可以继续 lvextend
$ lvextend -L +200G -r /dev/data-vg/mysql
扩 PV(底层云盘扩了之后)
# 云控制台扩了云盘从 100G 到 500G
# 1. 重新扫盘(让内核认到新大小)
$ partprobe /dev/sda
# 或:
$ echo 1 > /sys/block/sda/device/rescan
$ lsblk /dev/sda
sda 500G disk ← 看到新大小
# 2. 让 PV 用新大小
$ pvresize /dev/sda
Physical volume "/dev/sda" changed
1 physical volume(s) resized
$ pvs
PV VG PSize PFree
/dev/sda data-vg 500g 400g ← VG 自动得到新空间
# 3. lvextend + resize2fs/xfs_growfs
4. 缩容(慎用)
缩容是有风险的操作
LVM 缩 LV 操作顺序不能错,错了数据丢。
xfs 不能缩。ext4 可以但要 umount。
ext4 缩容步骤
# 1. umount(**必须**)
umount /var/lib/mysql
# 2. fsck 检查
e2fsck -f /dev/data-vg/mysql
# 3. 先缩文件系统(**必须先于 LV**)
resize2fs /dev/data-vg/mysql 40G
# 4. 再缩 LV
lvreduce -L 40G /dev/data-vg/mysql
# 提示确认
# 5. 挂回
mount /dev/data-vg/mysql /var/lib/mysql
# 6. 一致性最终检查
e2fsck -f /dev/data-vg/mysql
或者用 -r 一气:
umount /var/lib/mysql
lvreduce -L 40G -r /dev/data-vg/mysql # -r 自动处理 fs
mount /dev/data-vg/mysql /var/lib/mysql
生产几乎不缩 LV——除非真要释放空间。改"少用一点"通常更安全。
5. Snapshot(LVM 的另一杀手锏)
# 创建快照(10G 空间存储变化数据)
$ lvcreate -L 10G -s -n mysql-snap /dev/data-vg/mysql
^^
--snapshot
Logical volume "mysql-snap" created.
$ lvs
LV VG Attr LSize Pool Origin Data%
mysql data-vg owi-aos---- 50.00g
mysql-snap data-vg swi-a-s---- 10.00g mysql 0.05%
^^^^^^
snapshot 的源
Snapshot 怎么工作
graph LR
A[mysql<br>原始 LV] -->|开 snapshot 时<br>所有数据共享| B[mysql-snap<br>10G COW 空间]
A -.开始变化.-> A2[mysql 修改后<br>新数据写到 mysql]
B -.保留原始.-> B2[mysql-snap<br>仍然看 snapshot 时刻的数据]
LVM snapshot 是 Copy-On-Write (COW):
- snapshot 创建时几乎瞬间(只是记录元数据)
- 原始 LV 数据变化时、旧数据被复制到 snapshot 空间
- snapshot 大小 = 你预留的"变化空间"
用 snapshot 做一致性备份
# 1. 创建 snapshot(瞬间)
lvcreate -L 10G -s -n mysql-snap /dev/data-vg/mysql
# 2. 挂 snapshot(**read-only**)
mkdir -p /mnt/snap
mount -o ro /dev/data-vg/mysql-snap /mnt/snap
# 3. 备份 snapshot 内容
rsync -a /mnt/snap/ /backup/mysql-$(date +%F)/
# 4. 备份完成、卸载 + 删 snapshot
umount /mnt/snap
lvremove -y /dev/data-vg/mysql-snap
好处:备份时不影响线上 MySQL——MySQL 继续写、snapshot 是冻结的时间点视图。
Snapshot 空间用完会怎样
$ lvs
LV ... Data%
mysql-snap 100.00% ← 满了!
→ snapshot 失效(不再能读 snapshot 内容)→ 原始 LV 正常。
记得监控 snapshot 用量、定期清。
6. 删除操作
# 删 LV
$ umount /var/lib/mysql # 先卸载
$ lvremove /dev/data-vg/mysql
Do you really want to remove active logical volume mysql? [y/n]: y
# 从 VG 移除 PV(先确保 PV 上没数据)
$ pvmove /dev/sdb # 把数据移到其它 PV
$ vgreduce data-vg /dev/sdb # 从 VG 移除
# 删 PV 标记(让盘回归普通)
$ pvremove /dev/sdb
# 删 VG
$ vgremove data-vg # 必须先 lvremove 所有 LV
7. 生产实战场景
场景 1:根分区用 LVM、可扩容
装机时把根分区做成 LVM——以后空间不够直接扩。
# 装机时分区:
/dev/sda1 /boot 1G ext4 (普通分区、不是 LVM)
/dev/sda2 PV (rest) 99G → 整个 VG: ubuntu-vg
├─ LV: root 30G → /
├─ LV: swap 4G → swap
└─ LV: home 65G → /home
# 现在 / 满了 → 怎么办?
# 方案 A: 扩底层盘 / 加新盘到 VG、扩 LV
# 方案 B: 缩 /home、扩 /
场景 2:K8s Local PV via LVM
# 每个节点上预先用 LVM 切多个 LV
$ lvcreate -L 100G -n pv-001 data-vg
$ lvcreate -L 100G -n pv-002 data-vg
$ lvcreate -L 100G -n pv-003 data-vg
$ mkfs.ext4 /dev/data-vg/pv-001
# ... pv-002, pv-003 同
$ mount /dev/data-vg/pv-001 /mnt/disks/pv-001
# fstab 持久
# K8s Local PV
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-001-m4
spec:
capacity:
storage: 100Gi
local:
path: /mnt/disks/pv-001
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values: [m4]
场景 3:TopoLVM —— 自动用 LVM 做 K8s 动态 Local PV
TopoLVM 是 K8s CSI driver,自动:
- 节点上的 LVM VG 看作 K8s 存储池
- PVC 创建时自动 lvcreate 一个 LV
- 调度时把 pod 调度到 VG 有足够空间的节点
# StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: topolvm-ssd
provisioner: topolvm.io
parameters:
csi.storage.k8s.io/fstype: ext4
"topolvm.io/device-class": "ssd"
volumeBindingMode: WaitForFirstConsumer
生产 Local PV 自动化的最优解。
场景 4:用 snapshot 做"安全升级"
# 升级数据库之前 snapshot
lvcreate -L 50G -s -n mysql-presnap /dev/data-vg/mysql
# 升级
mysql_upgrade
# 升级失败 → 回滚(merge snapshot)
umount /var/lib/mysql
lvconvert --merge /dev/data-vg/mysql-presnap
mount /dev/data-vg/mysql /var/lib/mysql
# 升级成功 → 删 snapshot
lvremove -y /dev/data-vg/mysql-presnap
数据库 / 关键服务的"安全升级"标准动作。
8. LVM 与文件系统选择
| LV 用途 | 文件系统 | 理由 |
|---|---|---|
| 通用 / 系统 | ext4 | 稳定、生态成熟 |
| 大文件 / 数据库 | xfs | 大文件性能好、动态 inode |
| 容器 overlay | ext4 | containerd 默认 |
| 不要 | btrfs | 跟 LVM snapshot 概念重复、复杂 |
详见 mkfs.md。
9. 看 LVM 状态的命令汇总
# 简略
pvs # 物理卷
vgs # 卷组
lvs # 逻辑卷
# 详细
pvdisplay
vgdisplay
lvdisplay -m # -m 显示 segment 详情
# 看 LV 的盘 mapping(哪些 PV 上有这个 LV 的数据)
lvs -o +devices
# 看某 LV 详细 + segment 分布
lvdisplay -m /dev/data-vg/mysql
# 看一个 PV 上有哪些 LV
pvdisplay -m /dev/sda
# JSON 输出(脚本用)
lvs --reportformat json
lvs 常看的列
$ lvs -a -o +devices,segtype
LV VG Attr LSize Pool Origin Data% Devices Type
mysql data-vg -wi-ao---- 50.00g /dev/sda(0) linear
logs data-vg -wi-ao---- 30.00g /dev/sda(12800) linear
kafka data-vg Vwi-aotz-- 100.00g pool 25.00% thin
| 列 | 含义 |
|---|---|
Attr | 9 位状态码(详见下面) |
LSize | 逻辑大小 |
Pool | thin pool 的 pool name |
Origin | snapshot 的源 |
Data% | thin pool / snapshot 用量 |
Devices | 数据实际在哪些 PV 上 |
Type | linear / striped / mirror / thin |
Attr 9 位(按位置):
| 位 | 含义 | 取值 |
|---|---|---|
| 1 | volume 类型 | -=linear, s=snapshot, m=mirror, t=thin pool, V=thin volume |
| 2 | 权限 | w=可写, r=只读 |
| 3 | allocation | i=inherited, a=allocate anywhere |
| 4 | fixed minor | -=不固定 |
| 5 | 状态 | a=active, s=suspended |
| 6 | 设备打开 | o=open(被用着) |
| 7 | target type | t=thin, s=snapshot |
| 8 | 一致性 | z=zeroed |
| 9 | 健康 | p=partial, -=正常 |
实战经验:
-wi-ao----:linear、可写、active、open(正常用着)-wi-a-----:linear、可写、active、没人 mountVwi-aotz--:thin volume、可写、active、open、thin、zeroed(thin LV 正常)
10. Thin Provisioning —— 进阶
普通 LV 是 thick provision——创建时立刻占用 VG 空间。
Thin LV 是 稀疏分配——创建 100G LV 但实际只用 10G 时只占 10G。
# 1. 创建 thin pool(占用真实空间)
lvcreate -L 100G -T data-vg/thin-pool
^^
thin pool
# 2. 从 thin pool 创建 thin volumes
lvcreate -V 50G -T data-vg/thin-pool -n vol1 # 50G "虚拟"
lvcreate -V 50G -T data-vg/thin-pool -n vol2 # 50G "虚拟"
lvcreate -V 50G -T data-vg/thin-pool -n vol3 # 50G "虚拟"
# 总共 150G 声明、但 pool 只 100G
# 只要实际使用 ≤ 100G 就 OK
好处:
- 过度分配 —— 给用户 / 应用承诺更大空间、按需扩展
- 快照高效 —— Thin snapshot 占用更小
- storage class 灵活 —— 适合 K8s
风险:
- 超额使用 —— 用户真用满了、所有 LV 一起挂
- 监控关键 —— 看
Data%、超 80% 必须扩
$ lvs
LV VG Attr LSize Pool Data%
thin-pool data-vg twi-aotz-- 100.00g 75.00% ← 75% 用了
vol1 data-vg Vwi-aotz-- 50.00g thin-pool 30.00%
vol2 data-vg Vwi-aotz-- 50.00g thin-pool 40.00%
监控 Data% < 80%。满了扩 pool:
lvextend -L +50G data-vg/thin-pool
11. 反面教材
反面 1:扩 LV 但忘扩文件系统
lvextend -L +30G /dev/data-vg/mysql
df -h /var/lib/mysql
# 还是老大小
修:加 -r:
lvextend -L +30G -r /dev/data-vg/mysql
养成习惯:lvextend 永远加 -r。
反面 2:缩容顺序错
# 错的:先缩 LV
lvreduce -L 30G /dev/data-vg/mysql
# 文件系统还是 50G、但底层 LV 只 30G → 文件系统损坏
正确顺序:
umount + resize2fs 缩 fs → lvreduce
# 或用 lvreduce -r 自动处理
xfs 根本不能缩。
反面 3:snapshot 空间太小、忽然失效
lvcreate -L 1G -s -n mysql-snap /dev/data-vg/mysql
# 几个小时后 mysql 写了 2G 数据
$ lvs
mysql-snap ... 100.00% ← 满了,失效
snapshot 大小应该是预期变化量 + 余量。50G LV 一天可能变化 5-10G、snapshot 给 20G。
或者用 thin snapshot(用 thin pool 资源、不预分配)。
反面 4:误删 PV 标记
pvremove /dev/sda # 在还在用的 PV 上跑!
PV 标记没了 → VG metadata 损坏。生产事故级。
防御:pvremove 永远在 vgreduce 之后、确认没在用。
恢复:
# 看 VG 备份
ls /etc/lvm/backup/
ls /etc/lvm/archive/
# 还原 metadata(如果有备份)
vgcfgrestore -f /etc/lvm/backup/data-vg data-vg
反面 5:用 sudo 远程 lvextend 但忘了
$ ssh m4 lvextend -L +30G -r /dev/data-vg/mysql
# Permission denied
ssh m4 'sudo lvextend ...' 或者直接 ssh root 进去。
反面 6:thin pool 数据满 = 所有 thin LV 挂
$ lvs
thin-pool ... 100.00% ← 满了
vol1 ... -p-------- ← partial!数据写入失败
修:紧急扩 thin pool:
lvextend -L +50G data-vg/thin-pool
预防:监控 thin pool Data% < 80%。
反面 7:不知道 LVM 配置位置
# 系统配置 (LVM 行为):
/etc/lvm/lvm.conf
# 备份 (自动)
/etc/lvm/backup/<vg-name> ← 当前 VG 配置
/etc/lvm/archive/<vg-name> ← 历史版本
# 看 LVM 全局参数
lvmconfig
VG 损坏时 /etc/lvm/backup/ 是救命的。别误删。
12. 排查 cheatsheet
"LV 看不到 / 设备不存在"
# 1. PV / VG / LV 都正常吗
pvs
vgs
lvs
# 2. LV active 吗
$ lvs
LV Attr ...
mysql -wi-a----- ← 第 5 位 a = active;- = inactive
# 3. 激活
$ vgchange -ay data-vg # active 所有 LV
# 或
$ lvchange -ay /dev/data-vg/mysql # 单个
# 4. 设备节点
ls /dev/data-vg/
ls /dev/mapper/
"VG metadata 损坏"
# 看 VG 状态
$ vgs --foreign --readonly
$ vgs -v
# 强制激活
$ vgchange -ay --partial
# 从备份恢复
$ vgcfgrestore -f /etc/lvm/archive/data-vg_xxx.vg data-vg
最坏情况:找 LVM 专家或者从备份恢复整个盘。
"PV missing"
$ vgs
WARNING: ... PV /dev/sdb is missing
VG #PV #LV
data-vg 2 3 ← 但显示只有 1 PV 可用
# 看缺哪个
$ vgs -o +pv_uuid_attr
# 修复
$ vgreduce --removemissing data-vg # 移除 missing PV(数据会丢)
# 或
$ vgcfgrestore data-vg # 如果有备份