概述
干了这么多年运维,磁盘空间不够用这事儿估计每个人都遇到过。半夜三点被告警电话叫醒,一看是数据库服务器磁盘快满了,这种经历真的是刻骨铭心。早些年没用LVM的时候,扩容意味着停机、备份、重新分区、恢复数据,一套流程下来少说几个小时,业务方那边催得你脑仁疼。
后来全面上了LVM,这种痛苦就少多了。LVM这东西说白了就是在物理磁盘和文件系统之间加了一层抽象,让磁盘空间的管理变得灵活很多。最爽的一点就是可以在线扩容,业务不用停,数据不用动,加完磁盘敲几条命令就搞定。
这篇文章我会把LVM在线扩容的方方面面都讲透,从基础原理到实战操作,从最佳实践到故障排查,都是这些年踩坑总结出来的经验。内容比较长,建议收藏慢慢看。
为什么选择LVM
在聊扩容之前,先说说为什么要用LVM。传统的磁盘分区方式有几个致命问题:
第一,分区大小固定。一旦分区建好,想扩容基本上要动到整个分区表,风险极高。我见过有人直接用fdisk改分区大小,结果把分区表搞坏了,数据全丢。
第二,跨磁盘不方便。如果一个分区需要100G,但是手头只有两块60G的盘,传统分区方式根本没法搞。只能做RAID0或者分成两个挂载点,都不是很优雅的解决方案。
第三,扩容必须停机。这个在生产环境简直是噩梦。现在的互联网业务哪能说停就停,一分钟的停机可能就是几十万的损失。
LVM把这些问题都解决了。它的核心思想是把物理磁盘抽象成物理卷(PV),多个物理卷组成卷组(VG),然后从卷组里划出逻辑卷(LV)给上层使用。这样一来,扩容就变得很简单:加磁盘、创建PV、扩展VG、扩展LV、扩展文件系统,全程不用停服务。
LVM架构基础
在动手之前,先把LVM的几个核心概念搞清楚,不然后面操作的时候容易懵。
物理卷(Physical Volume, PV)
物理卷是LVM的最底层,可以是整块磁盘,也可以是磁盘上的一个分区。一块磁盘要被LVM使用,首先得初始化成物理卷。这个过程会在磁盘开头写入LVM的元数据,标记这块盘已经是LVM的一部分了。
卷组(Volume Group, VG)
卷组是物理卷的集合,你可以把它理解成一个大的存储池。多个物理卷加入同一个卷组后,它们的空间就合并在一起了。后续创建逻辑卷就是从这个池子里划空间出来。
逻辑卷(Logical Volume, LV)
逻辑卷就是最终呈现给上层应用使用的块设备,你可以在上面创建文件系统、挂载使用。逻辑卷的大小可以随时调整,这就是LVM灵活性的核心所在。
物理区块(Physical Extent, PE)
PE是LVM分配空间的最小单位,默认是4MB。当你创建一个100MB的逻辑卷时,实际上是分配了25个PE。这个粒度在绝大多数场景下都够用,但如果你需要更精细的控制,创建卷组的时候可以指定更小的PE大小。
这几个概念的关系可以这么理解:物理磁盘(或分区)→ 物理卷 → 卷组 → 逻辑卷 → 文件系统。扩容的时候就是反过来:添加磁盘 → 创建PV → 扩展VG → 扩展LV → 扩展文件系统。
在线扩容的前提条件
不是所有情况都能在线扩容,有几个前提条件需要满足:
-
文件系统支持在线扩容:ext4、xfs这些主流文件系统都支持,但是老的ext2就不行。xfs甚至只能扩大不能缩小,这点要注意。
-
内核版本支持:现在主流的Linux发行版内核都没问题,RHEL 7以上、Ubuntu 16.04以上都OK。
-
有可用的磁盘空间:这是废话,但确实有人问过我"磁盘满了能不能在线扩容",兄弟,你得先有盘加进来啊。
-
LV不是快照卷:如果逻辑卷有活跃的快照,扩容可能会有问题。建议先删除快照再扩容。
详细步骤
下面进入实操环节。我会按照实际工作中最常见的场景来讲,就是给虚拟机加一块新磁盘,然后扩容现有的逻辑卷。
场景说明
假设我们有一台CentOS 8的服务器(其实Rocky Linux 9、AlmaLinux 9操作都一样),现有一个100G的逻辑卷挂载在/data目录,现在空间不够用了,需要再加100G。
当前的存储结构是这样的:
-
物理磁盘:/dev/sdb(100G) -
物理卷:/dev/sdb -
卷组:data_vg -
逻辑卷:data_lv(100G) -
挂载点:/data -
文件系统:xfs
现在要添加一块新磁盘/dev/sdc(100G),把/data扩到200G。
第一步:确认新磁盘已识别
在虚拟化环境下加完磁盘后,有时候系统不会自动识别,需要手动扫描一下。
# 查看当前磁盘
lsblk
# 如果看不到新磁盘,扫描SCSI总线
echo "- - -" > /sys/class/scsi_host/host0/scan
echo "- - -" > /sys/class/scsi_host/host1/scan
echo "- - -" > /sys/class/scsi_host/host2/scan
# 或者用这个更暴力的方式,扫描所有host
for host in /sys/class/scsi_host/host*; do
echo "- - -" > ${host}/scan
done
# 再次确认
lsblk
正常情况下你应该能看到新加的/dev/sdc了:
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 50G 0 disk
├─sda1 8:1 0 1G 0 part /boot
└─sda2 8:2 0 49G 0 part
├─centos-root 253:0 0 45G 0 lvm /
└─centos-swap 253:1 0 4G 0 lvm [SWAP]
sdb 8:16 0 100G 0 disk
└─data_vg-data_lv
253:2 0 100G 0 lvm /data
sdc 8:32 0 100G 0 disk
这里sdc就是我们新加的磁盘,还没有任何分区。
第二步:创建物理卷
把新磁盘初始化为LVM物理卷:
# 创建物理卷
pvcreate /dev/sdc
# 输出类似这样:
# Physical volume "/dev/sdc" successfully created.
# 确认物理卷创建成功
pvs
# 输出:
# PV VG Fmt Attr PSize PFree
# /dev/sda2 centos lvm2 a-- <49.00g 0
# /dev/sdb data_vg lvm2 a-- <100.00g 0
# /dev/sdc lvm2 a-- <100.00g <100.00g
# 查看详细信息
pvdisplay /dev/sdc
pvdisplay会输出更详细的信息:
"/dev/sdc" is a new physical volume of "<100.00 GiB"
--- NEW Physical volume ---
PV Name /dev/sdc
VG Name
PV Size <100.00 GiB
Allocatable NO
PE Size 0
Total PE 0
Free PE 0
Allocated PE 0
PV UUID xxxxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxxxx
注意看VG Name那里是空的,说明这个物理卷还没加入任何卷组。
第三步:扩展卷组
把新创建的物理卷加入到现有的卷组:
# 扩展卷组
vgextend data_vg /dev/sdc
# 输出:
# Volume group "data_vg" successfully extended
# 确认卷组扩展成功
vgs
# 输出:
# VG #PV #LV #SN Attr VSize VFree
# centos 1 2 0 wz--n- <49.00g 0
# data_vg 2 1 0 wz--n- 199.99g <100.00g
# 查看详细信息
vgdisplay data_vg
vgdisplay输出:
--- Volume group ---
VG Name data_vg
System ID
Format lvm2
Metadata Areas 2
Metadata Sequence No 3
VG Access read/write
VG Status resizable
MAX LV 0
Cur LV 1
Open LV 1
Max PV 0
Cur PV 2
Act PV 2
VG Size 199.99 GiB
PE Size 4.00 MiB
Total PE 51198
Alloc PE / Size 25599 / <100.00 GiB
Free PE / Size 25599 / <100.00 GiB
VG UUID xxxxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxxxx
可以看到VG Size已经变成200G了,Free PE / Size显示有100G的可用空间。
第四步:扩展逻辑卷
现在卷组有了空闲空间,可以扩展逻辑卷了。这一步有几种不同的写法:
# 方式一:指定要扩展的大小
lvextend -L +100G /dev/data_vg/data_lv
# 方式二:指定扩展后的总大小
lvextend -L 200G /dev/data_vg/data_lv
# 方式三:使用卷组中所有剩余空间
lvextend -l +100%FREE /dev/data_vg/data_lv
# 方式四:扩展到卷组的某个百分比
lvextend -l 100%VG /dev/data_vg/data_lv
我个人比较喜欢用方式三,简单直接,不用算具体数字。执行后输出:
Size of logical volume data_vg/data_lv changed from <100.00 GiB (25599 extents) to 199.99 GiB (51198 extents).
Logical volume data_vg/data_lv successfully resized.
确认一下:
lvs
# 输出:
# LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
# root centos -wi-ao---- <45.00g
# swap centos -wi-ao---- 4.00g
# data_lv data_vg -wi-ao---- 199.99g
逻辑卷已经扩到200G了,但这时候文件系统还是100G,还需要最后一步。
第五步:扩展文件系统
这一步根据文件系统类型不同,用的命令也不一样。
对于XFS文件系统:
# xfs用xfs_growfs,后面跟挂载点
xfs_growfs /data
# 输出类似:
# meta-data=/dev/mapper/data_vg-data_lv isize=512 agcount=4, agsize=6553344 blks
# = sectsz=512 attr=2, projid32bit=1
# = crc=1 finobt=1, sparse=1, rmapbt=0
# = reflink=1
# data = bsize=4096 blocks=26213376, imaxpct=25
# = sunit=0 swidth=0 blks
# naming =version 2 bsize=4096 ascii-ci=0, ftype=1
# log =internal log bsize=4096 blocks=12799, version=2
# = sectsz=512 sunit=0 blks, lazy-count=1
# realtime =none extsz=4096 blocks=0, rtextents=0
# data blocks changed from 26213376 to 52426752
对于EXT4文件系统:
# ext4用resize2fs,后面跟设备路径
resize2fs /dev/data_vg/data_lv
# 输出类似:
# resize2fs 1.45.6 (20-Mar-2020)
# Filesystem at /dev/data_vg/data_lv is mounted on /data; on-line resizing required
# old_desc_blocks = 13, new_desc_blocks = 25
# The filesystem on /dev/data_vg/data_lv is now 52426752 (4k) blocks long.
验证扩容结果:
df -h /data
# 输出:
# Filesystem Size Used Avail Use% Mounted on
# /dev/mapper/data_vg-data_lv 200G 50G 150G 25% /data
完美,/data已经变成200G了,整个过程业务完全无感知。
一条命令搞定扩容
其实从2.02.98版本开始,lvextend命令支持一个-r参数,可以在扩展逻辑卷的同时自动扩展文件系统,省去了单独执行resize2fs或xfs_growfs的步骤:
# 扩展逻辑卷的同时扩展文件系统
lvextend -r -l +100%FREE /dev/data_vg/data_lv
# 或者
lvextend --resizefs -l +100%FREE /dev/data_vg/data_lv
这个-r参数会自动检测文件系统类型并调用对应的扩容命令,非常方便。现在我基本上都用这种方式,能少敲一条命令就少敲一条。
更多实战场景
上面讲的是最基础的场景,实际工作中还会遇到各种变体情况,下面一一说明。
场景一:扩展现有磁盘而非添加新磁盘
在虚拟化环境中,有时候不是添加新磁盘,而是直接扩展现有磁盘的大小。比如把原来100G的虚拟磁盘扩到200G。这种情况下操作稍微不一样。
# 首先让系统识别到磁盘大小变化
# 对于SCSI磁盘
echo 1 > /sys/block/sdb/device/rescan
# 确认磁盘大小已更新
lsblk
# 应该能看到sdb变成200G了
# 扩展物理卷
pvresize /dev/sdb
# 输出:
# Physical volume "/dev/sdb" changed
# 1 physical volume(s) resized or updated / 0 physical volume(s) not resized
# 确认
pvs
# 应该能看到sdb的大小已经变成200G
# 后续步骤和前面一样,扩展LV和文件系统
lvextend -r -l +100%FREE /dev/data_vg/data_lv
这种方式比添加新磁盘更干净,物理卷数量不会增加。但要注意,pvresize默认会使用磁盘上所有可用空间,如果你只想用一部分,需要用--setphysicalvolumesize参数指定大小。
场景二:磁盘有分区的情况
有些环境下,物理卷不是直接建在整块磁盘上,而是建在分区上。这种情况扩容就复杂一些。
假设/dev/sdb分成了一个分区/dev/sdb1,物理卷建在sdb1上。现在把sdb从100G扩到200G。
# 让系统识别磁盘大小变化
echo 1 > /sys/block/sdb/device/rescan
# 查看分区情况
fdisk -l /dev/sdb
# 会看到sdb是200G,但sdb1还是100G
# 这时候需要扩展分区
# 使用growpart工具(需要安装cloud-utils-growpart包)
growpart /dev/sdb 1
# 或者用parted
parted /dev/sdb
# 进入parted后
(parted) resizepart 1 100%
(parted) quit
# 刷新分区表
partprobe /dev/sdb
# 确认分区已扩展
lsblk
# sdb1应该变成200G了
# 扩展物理卷
pvresize /dev/sdb1
# 后续步骤一样
lvextend -r -l +100%FREE /dev/data_vg/data_lv
说实话,我个人不太喜欢在分区上建PV,直接用整块磁盘更简单。但有些老系统就是这么设计的,你也没办法。
场景三:根分区扩容
根分区扩容是最常见也最让人紧张的场景。因为根分区不能卸载,操作不当可能导致系统无法启动。
好消息是,如果根分区是ext4或xfs,在线扩容是完全支持的。
# 假设根分区是 /dev/centos/root
# 添加新磁盘/dev/sdc后
pvcreate /dev/sdc
vgextend centos /dev/sdc
lvextend -r -l +100%FREE /dev/centos/root
# 就这么简单
不过要注意几点:
-
在RHEL/CentOS 7和8上,如果根分区是xfs,用xfs_growfs扩容时后面要跟"/"而不是设备路径。 -
某些系统根分区可能用了LVM thin provisioning,扩容方式略有不同。 -
如果根分区空间已经100%满了,可能连lvextend命令都执行不了(因为LVM操作需要临时空间)。这时候需要先清理一些空间出来。
场景四:LVM thin provisioning扩容
Thin provisioning是LVM的高级特性,可以实现存储空间的超额分配。理解起来就是你可以创建一个1TB的逻辑卷,但实际只占用10G空间,随着数据写入动态增长。
thin provisioning涉及thin pool和thin volume两层,扩容也是两步:
# 假设thin pool是 data_vg/data_pool
# thin volume是 data_vg/data_thin_lv
# 先扩展thin pool
lvextend -l +100%FREE data_vg/data_pool
# 再扩展thin volume(这一步其实是修改元数据,很快)
lvextend -L 500G data_vg/data_thin_lv
# 最后扩展文件系统
# xfs
xfs_growfs /data
# 或 ext4
resize2fs /dev/data_vg/data_thin_lv
thin provisioning的坑比较多,尤其是thin pool满了之后的处理很麻烦,这个以后可以单独写一篇。
场景五:使用系统自带磁盘的剩余空间
有些虚拟机模板默认只用了磁盘的一部分空间做LVM,剩余空间没有使用。这种情况不需要加新盘,直接用现有磁盘的剩余空间就行。
# 查看磁盘分区情况
fdisk -l /dev/sda
# 假设sda有100G,但只有sda1和sda2用了50G
# 创建新分区
fdisk /dev/sda
# 输入n创建新分区,选择默认值使用所有剩余空间
# 输入t修改分区类型为8e(Linux LVM)
# 输入w保存退出
# 刷新分区表
partprobe /dev/sda
# 创建物理卷
pvcreate /dev/sda3
# 扩展卷组
vgextend centos /dev/sda3
# 扩展逻辑卷
lvextend -r -l +100%FREE /dev/centos/root
这种方式在云环境下特别常见,因为很多云镜像默认就是这么做的。
实战案例
讲了这么多理论,来几个真实的生产案例。
案例一:MySQL数据目录扩容
这是我遇到过最多的场景。数据库跑着跑着数据量上来了,/var/lib/mysql挂载的分区快满了。
环境信息:
-
OS: Rocky Linux 9.2 -
MySQL: 8.0.35 -
当前/var/lib/mysql:500G,使用率85% -
需要扩到1TB
操作记录:
# 1. 查看当前状态
df -h /var/lib/mysql
# Filesystem Size Used Avail Use% Mounted on
# /dev/mapper/mysql_vg-mysql_lv 500G 425G 75G 85% /var/lib/mysql
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
# ├─rl-root 253:0 0 50G 0 lvm /
# └─rl-swap 253:1 0 4G 0 lvm [SWAP]
# sdb 8:16 0 500G 0 disk
# └─mysql_vg-mysql_lv 253:2 0 500G 0 lvm /var/lib/mysql
# 2. 虚拟化平台添加500G新磁盘
# 3. 扫描新磁盘
for host in /sys/class/scsi_host/host*; do
echo "- - -" > ${host}/scan
done
lsblk
# 确认sdc出现
# 4. 执行扩容
pvcreate /dev/sdc
vgextend mysql_vg /dev/sdc
lvextend -r -l +100%FREE /dev/mysql_vg/mysql_lv
# 5. 验证
df -h /var/lib/mysql
# Filesystem Size Used Avail Use% Mounted on
# /dev/mapper/mysql_vg-mysql_lv 1.0T 425G 575G 43% /var/lib/mysql
整个过程MySQL一直在正常运行,没有任何感知。我当时还在跑着一个大查询,完全没受影响。
案例二:批量扩容脚本
当你管理几百台服务器的时候,手动一台台扩容太慢了。这是我写的一个批量扩容脚本:
#!/bin/bash
#
# LVM在线扩容脚本
# 用法: lvm_extend.sh <设备> <卷组> <逻辑卷>
# 例如: lvm_extend.sh /dev/sdc data_vg data_lv
#
set -e
NEW_DISK=$1
VG_NAME=$2
LV_NAME=$3
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 参数检查
if [ -z "$NEW_DISK" ] || [ -z "$VG_NAME" ] || [ -z "$LV_NAME" ]; then
echo "用法: $0 <新磁盘设备> <卷组名> <逻辑卷名>"
echo "例如: $0 /dev/sdc data_vg data_lv"
exit 1
fi
# 检查磁盘是否存在
if [ ! -b "$NEW_DISK" ]; then
log_error "设备 $NEW_DISK 不存在"
exit 1
fi
# 检查磁盘是否已被使用
if pvs "$NEW_DISK" &>/dev/null; then
log_error "设备 $NEW_DISK 已经是物理卷"
exit 1
fi
if mount | grep -q "$NEW_DISK"; then
log_error "设备 $NEW_DISK 已经被挂载"
exit 1
fi
# 检查卷组是否存在
if ! vgs "$VG_NAME" &>/dev/null; then
log_error "卷组 $VG_NAME 不存在"
exit 1
fi
# 检查逻辑卷是否存在
if ! lvs "$VG_NAME/$LV_NAME" &>/dev/null; then
log_error "逻辑卷 $VG_NAME/$LV_NAME 不存在"
exit 1
fi
# 获取当前大小
CURRENT_SIZE=$(lvs --noheadings -o lv_size "$VG_NAME/$LV_NAME" | tr -d ' ')
NEW_DISK_SIZE=$(lsblk -b -d -n -o SIZE "$NEW_DISK" | numfmt --to=iec)
log_info "开始扩容操作"
log_info "新磁盘: $NEW_DISK ($NEW_DISK_SIZE)"
log_info "目标逻辑卷: $VG_NAME/$LV_NAME (当前: $CURRENT_SIZE)"
# 确认操作
read -p "确认执行扩容? (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
log_warn "操作已取消"
exit 0
fi
# 创建物理卷
log_info "创建物理卷..."
pvcreate "$NEW_DISK"
# 扩展卷组
log_info "扩展卷组..."
vgextend "$VG_NAME" "$NEW_DISK"
# 扩展逻辑卷
log_info "扩展逻辑卷和文件系统..."
lvextend -r -l +100%FREE "/dev/$VG_NAME/$LV_NAME"
# 验证结果
NEW_SIZE=$(lvs --noheadings -o lv_size "$VG_NAME/$LV_NAME" | tr -d ' ')
log_info "扩容完成!"
log_info "逻辑卷大小: $CURRENT_SIZE -> $NEW_SIZE"
# 显示最终状态
echo ""
echo "=== 扩容后状态 ==="
pvs
echo ""
vgs "$VG_NAME"
echo ""
lvs "$VG_NAME/$LV_NAME"
echo ""
df -h "/dev/mapper/$VG_NAME-$LV_NAME"
使用方法:
chmod +x lvm_extend.sh
./lvm_extend.sh /dev/sdc data_vg data_lv
案例三:Ansible自动化扩容
对于大规模环境,用Ansible做自动化扩容更方便:
---
# lvm_extend.yml
# 用法: ansible-playbook -i inventory lvm_extend.yml -e "new_disk=/dev/sdc vg_name=data_vg lv_name=data_lv"
- name: LVM在线扩容
hosts: all
become: yes
vars:
new_disk: "{{ new_disk }}"
vg_name: "{{ vg_name }}"
lv_name: "{{ lv_name }}"
tasks:
- name: 扫描SCSI总线识别新磁盘
shell: |
for host in /sys/class/scsi_host/host*; do
echo "- - -" > ${host}/scan
done
changed_when: false
- name: 等待磁盘识别
wait_for:
path: "{{ new_disk }}"
state: present
timeout: 30
- name: 获取扩容前状态
command: lvs --noheadings -o lv_size {{ vg_name }}/{{ lv_name }}
register: lv_size_before
changed_when: false
- name: 创建物理卷
lvg:
vg: "{{ vg_name }}"
pvs: "{{ new_disk }}"
state: present
register: pv_result
- name: 扩展卷组
lvg:
vg: "{{ vg_name }}"
pvs: "{{ lookup('pipe', 'pvs --noheadings -o pv_name -S vg_name=' + vg_name + ' 2>/dev/null | tr -d \" \" | tr \"\\n\" \",\"') }}{{ new_disk }}"
state: present
when: pv_result.changed
- name: 扩展逻辑卷
lvol:
vg: "{{ vg_name }}"
lv: "{{ lv_name }}"
size: +100%FREE
resizefs: yes
- name: 获取扩容后状态
command: lvs --noheadings -o lv_size {{ vg_name }}/{{ lv_name }}
register: lv_size_after
changed_when: false
- name: 显示扩容结果
debug:
msg: "逻辑卷 {{ vg_name }}/{{ lv_name }} 从 {{ lv_size_before.stdout | trim }} 扩展到 {{ lv_size_after.stdout | trim }}"
用Ansible的好处是可以并行在多台机器上执行,而且有幂等性保证,即使磁盘已经添加过了也不会出错。
案例四:Kubernetes PVC扩容
现在越来越多的环境用Kubernetes,PVC扩容也很常见。如果后端存储是LVM-based的(比如用了local-path-provisioner或者TopoLVM),扩容流程是这样的:
# 首先确保StorageClass允许扩容
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-lvm
provisioner: kubernetes.io/no-provisioner
allowVolumeExpansion: true # 这个必须设置为true
volumeBindingMode: WaitForFirstConsumer
# 修改PVC的大小
kubectl patch pvc mysql-data -p '{"spec":{"resources":{"requests":{"storage":"200Gi"}}}}'
# 等待扩容完成
kubectl get pvc mysql-data -w
# 某些CSI驱动需要重启Pod才能完成扩容
kubectl delete pod mysql-0
如果用的是TopoLVM,它会自动处理底层的LVM扩容操作,非常方便。
最佳实践
这些年踩的坑总结成了一些最佳实践,希望能帮你少走弯路。
容量规划
-
预留buffer空间:永远不要等到磁盘快满了才扩容。我的经验是使用率超过70%就该考虑扩容了,超过85%就必须立即行动。磁盘满了再扩容压力很大,万一出问题连救的余地都没有。
-
监控告警:配置好磁盘空间告警,阈值设置在70%、80%、90%三档。Prometheus + Alertmanager或者Zabbix都可以。
-
扩容幅度:每次扩容不要太小气,一次扩个50%到100%是比较合理的。频繁扩容也是运维负担。
操作规范
-
操作前备份:虽然在线扩容理论上很安全,但对于重要系统还是建议操作前做个快照或备份。
-
选择合适的时间:虽然可以在线扩容,但如果不是紧急情况,还是选择业务低峰期操作比较好。万一出问题影响面小。
-
记录操作日志:把所有执行的命令和输出都记录下来,出问题时方便排查。用script命令录制终端会话是个好办法:
script /var/log/lvm_extend_$(date +%Y%m%d_%H%M%S).log
# 执行操作
exit -
逐步确认:每一步操作完都确认一下结果,不要一口气执行完才发现中间某步出错了。
架构设计
-
分离数据目录:数据目录单独做逻辑卷,不要和系统盘混在一起。这样扩容更方便,也更安全。
-
卷组命名规范:用有意义的名字,比如mysql_vg、mongodb_vg,别用vg01、vg02这种。时间长了你自己都不知道是干什么的。
-
预留VG空间:创建LV时不要把VG空间用满,留10-20%的buffer。这样可以快速响应紧急扩容需求,不用等新磁盘到位。
-
使用thin provisioning要谨慎:thin provisioning是把双刃剑,用好了可以提高存储利用率,用不好可能导致thin pool满了整个LV不可用。如果用,一定要监控thin pool使用率。
多路径和RAID环境
-
多路径环境:在SAN存储多路径环境下,一定要在multipath设备上操作(/dev/mapper/mpatha),不要在单个路径上操作(/dev/sda)。
-
硬件RAID:如果底层是硬件RAID,扩容需要先在RAID卡上添加磁盘、扩展RAID组,然后才能在OS层面扩容。
-
软RAID:如果用的是mdadm软RAID,需要先扩展md设备,再pvresize。
云环境特殊考虑
-
EBS/云盘扩容:AWS EBS、阿里云云盘这些可以直接在控制台扩容,但扩容后有冷却时间(通常6小时),期间不能再次扩容。规划好一次扩够。
-
热迁移影响:某些虚拟化平台在热迁移过程中不允许磁盘操作,如果扩容卡住了检查一下是不是在迁移。
-
NVMe磁盘:NVMe磁盘的扫描命令不一样:
# NVMe设备重新扫描
nvme ns-rescan /dev/nvme0n1
# 或者
echo 1 > /sys/class/nvme/nvme0/rescan_controller
故障排查
即使是再简单的操作也可能出问题,下面是常见问题和解决方法。
问题一:新磁盘识别不到
症状:虚拟化平台已经加了磁盘,但lsblk看不到。
排查步骤:
# 1. 确认SCSI扫描已执行
for host in /sys/class/scsi_host/host*; do
echo "- - -" > ${host}/scan
done
# 2. 检查dmesg有没有磁盘识别信息
dmesg | tail -50 | grep -i sd
# 3. 检查/dev下有没有新设备
ls -la /dev/sd*
# 4. 如果是virtio磁盘,看vd设备
ls -la /dev/vd*
# 5. 如果是NVMe
ls -la /dev/nvme*
# 6. 还是不行,试试重新加载存储驱动
modprobe -r mptspi && modprobe mptspi # 具体驱动看你的环境
常见原因:
-
SCSI控制器类型配置错误 -
虚拟机配置没有保存 -
需要重启虚拟机(某些老版本vmware)
问题二:pvcreate报错Device excluded by a filter
症状:
Device /dev/sdc excluded by a filter.
解决方法:
# 1. 检查磁盘是否有分区表或文件系统
wipefs /dev/sdc
# 2. 如果有,清除(确保这块盘真的是空的!)
wipefs -a /dev/sdc
# 3. 或者强制创建(不推荐,先确认磁盘是对的)
pvcreate -f /dev/sdc
# 4. 检查lvm.conf的filter配置
grep -E "filter|global_filter" /etc/lvm/lvm.conf | grep -v "^#"
常见原因:
-
磁盘上有残留的分区表或文件系统元数据 -
LVM的filter配置排除了这个设备 -
磁盘被multipath管理了应该用/dev/mapper/xxx
问题三:vgextend报错Physical volume already in volume group
症状:
Physical volume '/dev/sdc' is already in volume group 'xxx'
解决方法: 这说明PV已经加入VG了,用pvs确认一下。如果确实已经在目标VG里,直接跳过这步执行lvextend就行。
问题四:lvextend后文件系统大小没变
症状:lvs显示LV已经扩大了,但df -h看到的大小还是原来的。
解决方法: 忘记扩展文件系统了。
# xfs
xfs_growfs /挂载点
# ext4
resize2fs /dev/mapper/vgname-lvname
问题五:resize2fs报错on-line resizing required
症状:
resize2fs: On-line shrinking not supported
解决方法: 这个报错通常是因为你在尝试缩小逻辑卷。ext4支持在线扩大但不支持在线缩小,如果要缩小必须先卸载。xfs根本不支持缩小。
问题六:xfs_growfs报错is not a mounted XFS filesystem
症状:
xfs_growfs: /dev/mapper/data_vg-data_lv is not a mounted XFS filesystem
解决方法: xfs_growfs后面要跟挂载点,不是设备路径。
# 错误
xfs_growfs /dev/mapper/data_vg-data_lv
# 正确
xfs_growfs /data # /data是挂载点
问题七:操作过程中IO错误
症状:执行LVM命令时报Input/output error。
排查步骤:
# 1. 检查dmesg
dmesg | tail -100 | grep -i error
# 2. 检查磁盘健康状态
smartctl -a /dev/sdc
# 3. 检查多路径状态(如果用了多路径)
multipath -ll
# 4. 检查存储端日志
常见原因:
-
磁盘故障 -
光纤链路问题 -
存储阵列故障 -
HBA卡问题
这种情况先别动,排查清楚故障原因再说。
问题八:扩容后数据丢失
这是最严重的问题,不过正常的在线扩容不会导致数据丢失。如果真的发生了,可能的原因:
-
操作了错误的磁盘 -
执行了减少逻辑卷大小的操作(缩容时没有先缩文件系统) -
文件系统本身有问题,扩容触发了隐藏的bug
预防措施:
-
操作前三次确认设备名 -
操作前备份或快照 -
使用脚本而非手动输入,减少人为错误
问题九:系统启动时LVM卷激活失败
症状:扩容后重启系统,发现LVM卷无法激活。
排查步骤:
# 1. 进入救援模式
# 2. 扫描PV
pvscan
# 3. 激活所有VG
vgchange -ay
# 4. 检查VG状态
vgs
# 5. 如果VG显示partial,说明有PV丢失
vgchange -ay --partial vgname
# 6. 查看哪个PV有问题
pvs -a
常见原因:
-
新加的磁盘UUID与系统里已存在的冲突 -
fstab配置错误 -
initramfs没有更新
问题十:LVM元数据损坏
症状:
Couldn't find device with uuid xxxxx.
VG data_vg is missing PV xxxxx.
解决方法:
# 1. 尝试从备份恢复元数据
vgcfgrestore -l data_vg # 列出可用备份
vgcfgrestore -f /etc/lvm/archive/data_vg_00042-xxxxxxxx.vg data_vg
# 2. 如果PV真的丢失了,减少VG
vgreduce --removemissing --force data_vg
LVM元数据损坏是很严重的问题,修复之前一定要先备份现有状态。
高级话题
LVM缓存
LVM支持使用SSD作为HDD的缓存,可以显著提升IO性能。扩容时要注意缓存配置:
# 查看是否有缓存配置
lvs -a -o +devices,cache_mode
# 如果有缓存,扩容时可能需要同时扩展缓存
LVM镜像
如果使用了LVM镜像(类似RAID1),扩容需要特殊处理:
# 查看镜像配置
lvs -a -o +devices
# 扩展镜像LV时,新空间也会自动镜像
lvextend -l +100%FREE /dev/vg/mirrored_lv
VDO压缩卷
RHEL 8/9支持VDO(Virtual Data Optimizer),可以做数据去重和压缩。VDO上的LVM扩容稍有不同:
# 查看VDO状态
vdostats --human-readable
# 扩展VDO卷(先扩VDO再扩LVM)
vdo growLogical --name=vdo_data --vdoLogicalSize=2T
lvextend -r -L 2T /dev/vg/lv_on_vdo
Stratis存储
Stratis是Red Hat主推的下一代存储管理方案,比传统LVM更简单。不过目前还不太成熟,生产环境用的不多。
# Stratis扩容
stratis pool add-data my_pool /dev/sdc
监控和自动化
Prometheus监控LVM
用node_exporter可以监控LVM状态:
# prometheus告警规则
groups:
- name: lvm
rules:
- alert: LVMVolumeHighUsage
expr: |
100 - (node_filesystem_avail_bytes{mountpoint=~"/data.*"} /
node_filesystem_size_bytes{mountpoint=~"/data.*"} * 100) > 80
for: 5m
labels:
severity: warning
annotations:
summary: "LVM卷使用率过高"
description: "{{ $labels.mountpoint }} 使用率超过80%,当前 {{ $value | printf \"%.1f\" }}%"
- alert: LVMVolumeAlmostFull
expr: |
100 - (node_filesystem_avail_bytes{mountpoint=~"/data.*"} /
node_filesystem_size_bytes{mountpoint=~"/data.*"} * 100) > 90
for: 5m
labels:
severity: critical
annotations:
summary: "LVM卷即将满"
description: "{{ $labels.mountpoint }} 使用率超过90%,当前 {{ $value | printf \"%.1f\" }}%,请立即扩容"
自动扩容
在某些场景下可以实现自动扩容,但要谨慎:
#!/bin/bash
# 自动扩容脚本示例(仅供参考,生产环境慎用)
THRESHOLD=85
VG_NAME="data_vg"
LV_NAME="data_lv"
MOUNT_POINT="/data"
# 获取当前使用率
USAGE=$(df -h "$MOUNT_POINT" | awk 'NR==2 {print $5}' | tr -d '%')
if [ "$USAGE" -gt "$THRESHOLD" ]; then
# 检查VG是否有剩余空间
FREE_PE=$(vgs --noheadings -o vg_free_count "$VG_NAME" | tr -d ' ')
if [ "$FREE_PE" -gt 0 ]; then
logger "自动扩容: $VG_NAME/$LV_NAME 使用率 ${USAGE}%,执行扩容"
lvextend -r -l +50%FREE "/dev/$VG_NAME/$LV_NAME"
# 发送通知
# send_alert "LVM自动扩容" "$LV_NAME 已自动扩容"
else
logger "警告: $VG_NAME 没有剩余空间,无法自动扩容"
# send_alert "LVM空间不足" "$VG_NAME 没有剩余空间,需要添加磁盘"
fi
fi
可以把这个脚本加入crontab定期执行,但我个人不太推荐完全自动化扩容,万一脚本有bug可能造成更大问题。我的做法是自动检测+告警,然后人工确认执行扩容。
性能考量
在线扩容对性能的影响很小,但还是有几点需要注意:
-
元数据更新:LVM元数据更新是原子操作,通常在毫秒级完成,对业务基本无感知。
-
文件系统扩展:xfs_growfs和resize2fs在扩展文件系统时会有短暂的IO增加,但不会锁定文件系统。
-
大容量扩展:扩展几TB的逻辑卷和扩展几十GB的逻辑卷,对LVM来说没什么区别,时间都很短。但文件系统扩展时间和容量相关,ext4扩展大容量时可能需要几分钟。
-
thin pool扩展:thin pool扩展会稍慢一些,因为需要更新更多元数据。
与传统方式对比
最后对比一下LVM在线扩容和传统扩容方式的区别:
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
总结
LVM在线扩容是每个Linux运维工程师必须掌握的技能。这篇文章从基础概念讲起,覆盖了各种实战场景、最佳实践、故障排查,应该能应对工作中绝大多数的扩容需求。
核心要点回顾:
-
扩容流程:添加磁盘 → pvcreate → vgextend → lvextend -r,就这四步。
-
记住-r参数:lvextend -r可以同时扩展文件系统,省一条命令。
-
不同文件系统不同命令:xfs用xfs_growfs跟挂载点,ext4用resize2fs跟设备路径。
-
做好监控:磁盘使用率70%就该考虑扩容了,别等到满了才着急。
-
操作前确认:设备名一定要核对清楚,操作错了后果很严重。
-
记录日志:所有操作都留痕,出问题好排查。
LVM还有很多高级功能这篇文章没有展开讲,比如快照、镜像、条带化、缓存等,以后有机会再单独写。有问题欢迎留言讨论。
本文基于Rocky Linux 9.3/AlmaLinux 9.3环境编写,LVM2版本2.03.23,适用于RHEL 8/9及其衍生版本,Debian/Ubuntu系列操作类似但包名可能不同。
文末福利
网络监控是保障网络系统和数据安全的重要手段,能够帮助运维人员及时发现并应对各种问题,及时发现并解决,从而确保网络的顺畅运行。
谢谢一路支持,给大家分享6款开源免费的网络监控工具,并准备了对应的资料文档,建议运维工程师收藏(文末一键领取)。

100%免费领取
一、zabbix
二、Prometheus
内容较多,6款常用网络监控工具(zabbix、Prometheus、Cacti、Grafana、OpenNMS、Nagios)不再一一介绍, 需要的朋友扫码备注【监控合集】,即可100%免费领取。
以上所有资料获取请扫码

100%免费领取
(后台不再回复,扫码一键领取)

