7. 设备映射器Device Mapper与LVM实现
7.1 Device Mapper核心架构与设计思想
7.1.1 DM在IO栈中的位置
通用块层(BIO/request) → 【Device Mapper层】 → 块设备驱动 → 物理存储硬件
- 向上:DM层作为通用块层的“消费者”,接收通用块层下发的IO请求,将其转换为对底层物理设备的IO请求;
- 向下:DM层作为块设备驱动的“调用者”,将处理后的IO请求下发给对应的物理设备驱动,完成最终的硬件读写;
- 核心价值:在不修改上层应用、不改动块设备驱动的前提下,实现存储功能的灵活扩展(如加密、快照、条带化),以及物理设备的动态管理(如设备替换、容量扩容)。
7.1.2 DM的核心架构组件
1. DM驱动(DM Core)
- 接收用户态工具(dmsetup)的命令,创建、删除、修改DM设备;
- 解析映射表,维护逻辑地址与物理地址的映射关系;
- 接收通用块层下发的IO请求,根据映射表将其转发到对应的目标设备(target)进行处理;
- 管理目标设备(target)的加载、卸载与生命周期,支持模块化扩展新的target类型;
- 处理IO请求的错误、超时,以及设备的热插拔、故障恢复。
2. DM设备(DM Device)
3. 映射表(Mapping Table)
<逻辑起始扇区> <逻辑扇区数量> <target类型> <target参数> ...
- 逻辑起始扇区:DM设备的逻辑地址起始位置(单位512字节扇区);
- 逻辑扇区数量:该映射段对应的逻辑扇区长度,即该段逻辑地址的范围;
- target类型:该映射段使用的目标设备类型(如linear、striped、snapshot);
- target参数:针对该target的具体配置(如底层物理设备路径、物理起始扇区、条带大小等)。
0 20971520 linear /dev/sda2 0
- 逻辑起始扇区0,逻辑扇区数量20971520(对应10GB);
- target类型为linear(线性映射);
- target参数为/dev/sda2(底层物理设备)和0(物理起始扇区);
- 含义:DM设备的0~20971520逻辑扇区,线性映射到底层/dev/sda2设备的0~20971520物理扇区。
4. 目标设备(Target)
- 模块化:每个target是一个独立的内核模块(如dm-linear.ko、dm-stripe.ko),可动态加载、卸载,支持灵活扩展;
- 可组合:一个DM设备的映射表中可以包含多个target,IO请求会按映射表的顺序依次经过多个target处理,实现复杂功能(如“加密+快照”组合);
- 透明性:target对上层应用和底层驱动完全透明,上层无需关心target的存在,底层驱动无需适配target的逻辑。
7.1.3 DM的核心数据结构拆解(Linux 6.6内核)
1. DM设备结构体:struct dm_dev
struct dm_dev {
// 物理设备的路径(如"/dev/sda2")
char *name;
// 物理设备的block_device实例指针
struct block_device *bdev;
// 引用计数,管理dm_dev的生命周期
refcount_t refcnt;
// 设备的打开计数,记录有多少DM映射段使用该设备
int open_count;
// 保护该结构体的自旋锁
spinlock_t lock;
// 设备的IO统计信息
struct dm_io_stats stats;
// 设备的私有数据,用于target存储自定义信息
void *private;
};
- DM层通过dm_dev结构体间接操作底层物理设备,不直接操作block_device,避免破坏底层设备的状态;
- 引用计数refcnt确保物理设备在被DM使用期间,不会被意外卸载或删除;
- open_count记录该物理设备被多少个DM映射段引用,只有当open_count为0时,才能释放该dm_dev实例。
2. Target结构体:struct dm_target
struct dm_target {
// target的名称(如"linear"、"striped")
const char *name;
// target的版本号
unsigned int version[3];
// IO请求处理函数,核心入口:接收DM层转发的IO请求,进行处理
int (*map)(struct dm_target *ti, struct bio *bio);
// IO完成回调函数,target处理完IO后触发
void (*end_io)(struct dm_target *ti, struct bio *bio, blk_status_t status);
// target初始化函数,创建target时调用
int (*init)(struct dm_target *ti);
// target清理函数,删除target时调用
void (*exit)(struct dm_target *ti);
// 处理ioctl命令,用于target的配置与查询
int (*ioctl)(struct dm_target *ti, unsigned int cmd, unsigned long arg);
// target的私有数据,存储target的配置参数(如物理设备、扇区偏移)
void *private;
// 关联的DM设备(dm_table)
struct dm_table *table;
// 该target对应的逻辑扇区范围(起始扇区、扇区数量)
sector_t start;
sector_t len;
};
- map函数是target的核心入口,所有IO请求都会经过该函数处理,实现地址转换、功能扩展(如加密、快照);
- private字段存储target的具体配置,不同类型的target,private的结构不同(如linear target存储物理设备和物理起始扇区,striped target存储条带大小、物理设备列表);
- 每个target必须实现map函数,其他函数(end_io、init、exit)可根据需求选择性实现。
3. 映射表结构体:struct dm_table
struct dm_table {
// 该映射表对应的DM设备
struct dm_device *dev;
// 映射段(dm_target)链表,存储所有映射段
struct list_head targets;
// 映射表的逻辑扇区总大小(即DM设备的总容量)
sector_t size;
// 映射表的引用计数
refcount_t refcnt;
// 保护映射表的读写信号量
struct rw_semaphore rwsem;
// 映射表的标志位(如只读、可写、快照等)
unsigned long flags;
// 该映射表使用的所有底层物理设备(dm_dev链表)
struct list_head devices;
// 映射表的私有数据,用于存储全局配置
void *private;
};
- targets链表存储了该DM设备的所有映射段,DM层会遍历该链表,根据IO请求的逻辑地址找到对应的dm_target;
- size字段定义了DM设备的总容量,即逻辑扇区的总数量,对应/dev/mapper/设备的大小;
- devices链表存储了该映射表使用的所有底层物理设备,用于管理物理设备的引用与生命周期。
4. DM设备结构体:struct dm_device
struct dm_device {
// DM设备的名称(如"centos-root")
char *name;
// DM设备的UUID,唯一标识该设备
char *uuid;
// DM设备的主设备号和次设备号
dev_t dev;
// 该设备当前生效的映射表
struct dm_table *table;
// 该设备的gendisk实例,用于向通用块层注册块设备
struct gendisk *disk;
// 该设备的request_queue队列,用于接收通用块层的IO请求
struct request_queue *queue;
// 保护该设备的自旋锁
spinlock_t lock;
// 设备的状态标志(如激活、未激活、只读)
unsigned int flags;
// 设备的引用计数
refcount_t refcnt;
// 设备的私有数据,用于存储设备的全局配置
void *private;
};
- disk和queue字段是DM设备能被通用块层识别的关键,DM设备本质上是一个由DM层创建的虚拟块设备,通过gendisk向内核注册;
- table字段指向当前生效的映射表,当映射表修改后(如扩容、快照),会替换该字段,实现DM设备的动态配置;
- dev字段是DM设备的设备号,对应/dev/mapper/设备节点的设备号,与普通块设备一致。
7.1.4 DM的IO请求完整处理流程
1. 通用块层下发IO请求 → 2. DM设备接收IO请求 → 3. 映射表解析与target匹配 → 4. target处理IO请求 → 5. 下发IO到物理设备驱动 → 6. IO完成与回调
阶段1:通用块层下发IO请求
阶段2:DM设备接收IO请求
阶段3:映射表解析与target匹配
- 若IO请求的逻辑地址范围完全落在某个映射段内,直接匹配该映射段的target;
- 若IO请求的逻辑地址范围跨多个映射段,DM驱动会将IO请求拆分为多个子请求,分别匹配对应的target;
- 匹配完成后,将IO请求转发给对应的target处理。
阶段4:target处理IO请求
- 地址转换:将IO请求的逻辑地址,转换为底层物理设备的物理地址(如linear target直接映射,striped target计算条带对应的物理地址);
- 功能扩展:根据target类型执行额外处理(如snapshot target实现写时复制,dm-crypt target实现数据加密/解密);
- 请求转发:将处理后的IO请求,转发到底层物理设备的block_device实例,进入块设备驱动层。
阶段5:下发IO到物理设备驱动
阶段6:IO完成与回调
7.2 Device Mapper核心Target详解
7.2.1 linear(线性映射)Target
核心原理
- 每个linear映射段对应一个底层物理设备,多个映射段按逻辑地址顺序排列,形成连续的逻辑空间;
- 例如:两个物理设备/dev/sda2(10GB)和/dev/sdb1(20GB),通过linear target串联,DM设备的0~20971520逻辑扇区映射到/dev/sda2,20971521~62914560逻辑扇区映射到/dev/sdb1,最终DM设备总容量为30GB。
核心实现(源码片段)
// linear target的私有数据结构体
struct linear_c {
// 底层物理设备的dm_dev实例
struct dm_dev *dev;
// 物理设备的起始扇区(即该映射段对应的物理起始地址)
sector_t start;
};
// linear target的IO处理函数(map函数)
static int linear_map(struct dm_target *ti, struct bio *bio)
{
struct linear_c *lc = ti->private;
sector_t sector = bio->bi_sector;
// 计算IO请求在物理设备上的起始扇区
bio->bi_sector = lc->start + (sector - ti->start);
// 将IO请求的目标设备设置为底层物理设备
bio->bi_bdev = lc->dev->bdev;
// 转发IO请求到底层物理设备
return DM_MAPIO_REMAPPED;
}
- linear_map函数仅做简单的地址转换,将DM设备的逻辑扇区转换为底层物理设备的物理扇区,修改BIO的bi_bdev和bi_sector字段,然后将BIO转发到底层物理设备;
- 无额外的IO处理开销,性能接近直接操作物理设备,是LVM、根文件系统分区的默认target。
适用场景
- 多块物理设备合并为一个逻辑设备,提升存储容量;
- 底层物理设备类型一致(如都是SSD),无需条带化、镜像等高级功能;
- 生产环境中最广泛的场景:LVM的物理卷(PV)组合、根文件系统的逻辑卷(LV)。
7.2.2 striped(条带化)Target
核心原理
- 计算IO请求的逻辑扇区对应的“条带索引”:striped_index = (逻辑扇区 - 映射段起始扇区) / 条带扇区数;
- 计算对应的物理设备索引:dev_index = striped_index % 物理设备数量;
- 计算该物理设备上的物理起始扇区:物理扇区 = 物理设备起始扇区 + (striped_index / 物理设备数量) * 条带扇区数 + (逻辑扇区 % 条带扇区数)。
- 条带索引 = 200 / 128 = 1;
- 设备索引 = 1 % 3 = 1(对应第2块物理设备);
- 物理扇区 = 物理设备起始扇区 + (1 / 3) * 128 + (200 % 128) = 物理设备起始扇区 + 0 + 72 = 物理设备起始扇区 + 72。
- 顺序IO请求会被拆分为多个子请求,并行下发到多块物理设备,吞吐量接近多块设备的总和;
- 适用于大文件顺序读写场景,如HDFS、视频存储、日志服务。
核心实现(源码片段)
// striped target的私有数据结构体
struct stripe_c {
// 底层物理设备列表(dm_dev实例数组)
struct dm_dev **devs;
// 物理设备数量
unsigned int num_devs;
// 条带大小(单位:扇区数)
sector_t stripe_size;
// 条带大小的移位值(用于快速计算)
unsigned int stripe_shift;
// 每个物理设备的起始扇区
sector_t *dev_starts;
};
// striped target的IO处理函数(map函数)
static int stripe_map(struct dm_target *ti, struct bio *bio)
{
struct stripe_c *sc = ti->private;
sector_t sector = bio->bi_sector - ti->start;
sector_t stripe_index, offset_in_stripe, dev_offset;
unsigned int dev_idx;
// 计算条带索引、扇区在条带内的偏移量
stripe_index = sector >> sc->stripe_shift;
offset_in_stripe = sector & ((1 << sc->stripe_shift) - 1);
// 计算对应的物理设备索引
dev_idx = stripe_index % sc->num_devs;
// 计算该物理设备上的偏移量
dev_offset = (stripe_index / sc->num_devs) << sc->stripe_shift;
dev_offset += offset_in_stripe;
// 设置IO请求的目标物理设备和物理扇区
bio->bi_bdev = sc->devs[dev_idx]->bdev;
bio->bi_sector = sc->dev_starts[dev_idx] + dev_offset;
return DM_MAPIO_REMAPPED;
}
- 通过条带大小的移位运算,快速计算条带索引、设备索引和物理扇区,减少计算开销;
- 对于跨条带的IO请求,DM驱动会将其拆分为多个子请求,分别下发到对应的物理设备,实现并行处理;
- 条带大小的选择会影响性能:条带过大会导致单设备IO压力过大,条带过小会增加IO拆分开销,生产环境中通常设置为64KB~256KB。
适用场景
- 顺序IO密集型场景:大数据、视频存储、日志服务、备份服务;
- 多块物理设备(如SSD、RAID阵列),需要提升顺序读写吞吐量;
- 注意:striped target不提供数据冗余,任何一块物理设备故障都会导致整个DM设备数据丢失,通常与RAID、mirror target配合使用。
7.2.3 mirror(镜像)Target
核心原理
- 主镜像设备(primary device):默认的读IO来源设备;
- 从镜像设备(secondary device):用于数据冗余,与主设备保持数据一致;
- 日志设备(log device):用于记录未完成的IO请求,防止系统崩溃时数据不一致,支持“磁盘日志”和“内存日志”两种模式。
- 初始化阶段:将主镜像设备的数据同步到从镜像设备,确保初始数据一致;
- 运行阶段:所有写IO同时写入主、从设备,日志设备记录IO状态;
- 故障恢复阶段:当某一块镜像设备故障时,替换故障设备后,通过日志设备同步未完成的IO,恢复数据一致性。
核心实现要点
- 写IO处理:将写IO请求复制多份,分别下发到所有镜像设备,等待所有设备IO完成后,才返回IO成功;
- 读IO处理:默认从主镜像设备读取,可通过配置实现读负载均衡(轮流从不同镜像设备读取);
- 日志处理:使用环形日志记录未完成的IO请求,系统崩溃后,重启时通过日志恢复数据,避免数据不一致;
- 同步处理:提供“全量同步”(初始化时)和“增量同步”(故障恢复时)两种模式,减少同步开销。
适用场景
- 对数据安全性要求高的场景:数据库、核心业务存储、金融交易数据;
- 高可用架构:需要避免物理设备故障导致的数据丢失;
- 注意:mirror target会增加写IO开销(写IO需要同时写入多块设备),读IO性能略有提升,适合读多写少的场景。
7.2.4 snapshot(快照)Target
核心原理
- 快照创建时,不复制原设备的任何数据,仅创建一个“快照映射表”,记录原设备与快照设备的地址映射关系,此时原设备(源设备)和快照设备共享所有数据块;
- 当对源设备执行写IO时,DM层会先将该数据块的原始数据复制到“快照存储设备”(通常是一块独立的物理设备),然后再将新数据写入源设备;
- 对快照设备执行读IO时,若数据块未被修改(未触发COW),则直接从源设备读取;若数据块已被修改(触发COW),则从快照存储设备读取原始数据;
- 快照设备的容量由“快照存储设备”的大小决定,用于存储被修改的数据块,若快照存储设备满,快照会变为只读状态,无法继续记录修改。
- 快照创建速度极快(无需复制数据),占用空间小(仅存储被修改的数据块);
- 源设备与快照设备相互独立,源设备的修改不会影响快照,快照的读取也不会影响源设备;
- 支持多快照:一个源设备可以创建多个快照,记录不同时间点的数据状态。
核心实现(源码片段)
// snapshot target的私有数据结构体
struct snapshot_c {
// 源设备(被快照的DM设备)的dm_dev实例
struct dm_dev *origin;
// 快照存储设备(用于存储COW数据块)的dm_dev实例
struct dm_dev *cow;
// 写时复制的块大小(默认4KB)
unsigned int block_size;
// 块大小的移位值(用于快速计算)
unsigned int block_shift;
// 快照的状态(激活、只读、满)
unsigned int status;
// 保护快照的自旋锁
spinlock_t lock;
// COW映射表,记录被修改的数据块的地址映射
struct dm_cow_table *cow_table;
};
// snapshot target的写IO处理函数(map函数)
static int snapshot_map(struct dm_target *ti, struct bio *bio)
{
struct snapshot_c *sc = ti->private;
sector_t sector = bio->bi_sector - ti->start;
unsigned int block = sector >> sc->block_shift;
struct dm_cow_entry *entry;
if (bio_data_dir(bio) == WRITE) {
// 写IO:触发写时复制
spin_lock(&sc->lock);
// 检查该数据块是否已被修改(是否存在COW条目)
entry = dm_cow_table_lookup(sc->cow_table, block);
if (!entry) {
// 未修改:复制原始数据到快照存储设备,创建COW条目
if (dm_cow_copy_block(sc, block)) {
spin_unlock(&sc->lock);
return DM_MAPIO_ERROR;
}
entry = dm_cow_table_lookup(sc->cow_table, block);
}
spin_unlock(&sc->lock);
// 将写IO转发到源设备(新数据写入源设备)
bio->bi_bdev = sc->origin->bdev;
bio->bi_sector = sc->origin->bdev->bd_start_sect + sector;
} else {
// 读IO:判断是否需要从快照存储设备读取
spin_lock(&sc->lock);
entry = dm_cow_table_lookup(sc->cow_table, block);
spin_unlock(&sc->lock);
if (entry) {
// 已修改:从快照存储设备读取原始数据
bio->bi_bdev = sc->cow->bdev;
bio->bi_sector = sc->cow->bdev->bd_start_sect + entry->cow_block;
} else {
// 未修改:从源设备读取数据
bio->bi_bdev = sc->origin->bdev;
bio->bi_sector = sc->origin->bdev->bd_start_sect + sector;
}
}
return DM_MAPIO_REMAPPED;
}
- 写IO时,先检查数据块是否已触发COW,未触发则复制原始数据到快照存储设备,再写入新数据;
- 读IO时,根据数据块是否被修改,选择从源设备或快照存储设备读取;
- COW映射表记录被修改的数据块的地址,确保快照设备能正确读取原始数据;
- 快照存储设备的容量决定了快照能记录的修改量,生产环境中需根据源设备的修改频率,合理设置快照存储设备的大小。
适用场景
- 数据备份:创建快照后,将快照设备挂载,备份数据,不影响源设备的正常运行;
- 故障恢复:当源设备数据损坏时,可通过快照设备恢复到之前的时间点;
- 测试验证:基于快照设备进行测试,测试过程中对快照的修改不会影响源设备;
- 注意:快照设备仅记录源设备的修改,若源设备故障(如磁盘损坏),快照设备也无法恢复数据,需与mirror target配合使用,实现高可用备份。
7.2.5 crypt(加密)Target
核心原理
- 初始化时,用户设置加密密钥(通过cryptsetup工具),密钥被安全存储在内核内存中,不会写入磁盘;
- 写IO时,DM层将IO请求的数据通过加密算法(如AES-256)加密,然后将加密后的数据写入底层物理设备;
- 读IO时,DM层从底层物理设备读取加密数据,通过相同的密钥和加密算法解密,然后将解密后的数据返回给上层应用;
- 加密粒度为“扇区”,每个扇区独立加密,使用不同的初始化向量(IV),避免相同明文加密后产生相同的密文,提升加密安全性。
核心实现要点
- 加密算法:支持AES、DES、3DES等多种对称加密算法,可通过用户态工具配置,默认使用AES-256;
- 密钥管理:密钥由用户提供,通过cryptsetup工具导入内核,内核使用密钥环(keyring)安全存储密钥,避免密钥泄露;
- 性能优化:使用内核加密API(Crypto API),支持硬件加速(如CPU的AES-NI指令集),减少加密/解密的性能开销;
- 透明性:上层应用无需修改任何代码,即可使用加密功能,DM层自动完成加密/解密,对上层完全透明。
适用场景
- 敏感数据存储:金融数据、个人隐私数据、企业核心数据;
- 合规要求:满足数据安全合规(如GDPR、等保),确保数据泄露后无法被破解;
- 注意:密钥丢失会导致数据无法解密,生产环境中需做好密钥的备份与管理,避免密钥丢失。
7.3 LVM实现原理与核心组件
7.3.1 LVM的核心组件与层级关系
1. 物理卷(Physical Volume,PV)
- 物理卷可以是整个物理硬盘(如/dev/sda)、硬盘分区(如/dev/sda2)、RAID阵列(如/dev/md0),只要是块设备,均可格式化为PV;
- 格式化PV时,会在物理设备的开头创建一个“PV头”(PV Header),记录PV的基本信息(如PV名称、UUID、大小、VG关联信息等);
- PV被划分为多个“物理扩展块”(Physical Extent,PE),PE是LVM空间分配的最小单位,默认大小为4MB,可在创建PV时配置(通常设置为4MB、8MB、16MB);
- 一个PV可以加入多个VG(但同一时间只能属于一个VG),加入VG后,其PE会被VG管理,用于分配给LV。
struct physical_volume {
// PV的名称(如"/dev/sda2")
char *name;
// PV的UUID,唯一标识该PV
char *uuid;
// PV的总大小(单位:字节)
uint64_t size;
// PE的大小(单位:字节)
uint64_t pe_size;
// PV的总PE数量
uint64_t pe_count;
// 已分配的PE数量
uint64_t pe_allocated;
// 关联的VG(卷组)指针
struct volume_group *vg;
// PV的状态(激活、未激活、故障)
enum pv_state state;
// PV的元数据信息(存储在PV头中)
struct pv_metadata metadata;
};
2. 卷组(Volume Group,VG)
- 一个VG可以包含多个PV(最少1个,最多无限制,取决于系统配置),所有PV的PE会被合并为一个连续的PE池,LV从PE池中分配PE;
- VG有一个“物理扩展块大小”(PE Size),所有加入该VG的PV,其PE大小必须与VG的PE大小一致,否则无法加入;
- VG的总容量 = 所有PV的容量之和,VG的可用容量 = 总容量 - 已分配给LV的容量;
- 可以动态向VG中添加PV(扩展VG容量),也可以从VG中移除未被使用的PV(缩减VG容量);
- VG的元数据(Metadata)记录了VG的配置信息(如VG名称、UUID、PE大小、PV列表、LV列表等),元数据默认存储在每个PV的PV头中,实现冗余备份。
struct volume_group {
// VG的名称(如"centos")
char *name;
// VG的UUID,唯一标识该VG
char *uuid;
// PE的大小(单位:字节)
uint64_t pe_size;
// VG的总PE数量(所有PV的PE之和)
uint64_t total_pe;
// VG的可用PE数量
uint64_t free_pe;
// PV列表(该VG包含的所有PV)
struct list_head pv_list;
// LV列表(该VG下的所有LV)
struct list_head lv_list;
// VG的状态(激活、未激活、只读)
enum vg_state state;
// VG的元数据信息(存储在PV头中)
struct vg_metadata metadata;
// 关联的DM设备(用于VG的虚拟化)
struct dm_device *dm_dev;
};
3. 逻辑卷(Logical Volume,LV)
- LV的大小 = 分配的PE数量 × PE大小,可动态调整(扩容:增加PE数量;缩减:减少PE数量);
- LV对应DM设备,其底层由DM的target实现(默认linear,可配置为striped、mirror等);
- 一个VG可以包含多个LV,每个LV独立使用一部分PE池,LV之间的空间相互独立;
- LV支持高级功能:快照(创建LV的时间点快照)、条带化(跨多个PV实现IO并行)、镜像(实现数据冗余);
- LV的名称通常为“VG名称-LV名称”(如centos-root),对应/dev/mapper/centos-root设备节点。
struct logical_volume {
// LV的名称(如"root")
char *name;
// LV的UUID,唯一标识该LV
char *uuid;
// LV的大小(单位:字节)
uint64_t size;
// 分配给该LV的PE数量
uint64_t pe_count;
// 关联的VG(卷组)指针
struct volume_group *vg;
// LV的状态(激活、未激活、只读、快照)
enum lv_state state;
// LV对应的DM设备(dm_device实例)
struct dm_device *dm_dev;
// LV的DM映射表(由LVM自动生成)
struct dm_table *dm_table;
// LV的类型(线性、条带化、镜像、快照)
enum lv_type type;
// 快照相关信息(若为快照LV)
struct lv_snapshot *snapshot;
};
7.3.2 LVM的核心工作机制
1. 核心概念:PE与LE
- 物理扩展块(PE):PV的最小空间分配单位,也是VG的最小管理单位,默认4MB,所有PV的PE大小必须与所属VG的PE大小一致;
- 逻辑扩展块(LE):LV的最小空间分配单位,LE的大小与所属VG的PE大小完全一致,一个LE对应一个PE;
- 映射关系:LV的每个LE,都会映射到VG的PE池中一个未被分配的PE,LV的大小由LE的数量决定,扩容/缩减LV本质上就是增加/减少LE的数量,并调整LE与PE的映射关系。
2. LVM的IO请求流转机制
- 上层应用(如文件系统)向LV(/dev/mapper/centos-root)发起IO请求,通用块层将其封装为BIO/request请求;
- 通用块层根据设备号找到对应的DM设备(LV对应的DM设备),将IO请求下发到DM层;
- DM层解析LV对应的映射表(由LVM自动生成),该映射表的target类型由LV的类型决定(默认linear);
- target(如linear)将IO请求的逻辑地址(LV的逻辑扇区)转换为底层PV的物理地址(PE对应的物理扇区);
- DM层将处理后的IO请求下发到底层PV对应的物理设备驱动,执行实际的读写操作;
- IO完成后,驱动通过DM层将结果返回给上层应用,完成IO闭环。
3. LVM元数据管理
- 默认存储在每个PV的PV头中(位于PV的开头,占用少量空间),实现冗余备份,即使某一个PV故障,也可从其他PV中恢复元数据;
- 可配置为“集中存储”(如存储在独立的磁盘分区),适用于大规模LVM部署场景。
- PV信息:PV名称、UUID、大小、PE大小、关联的VG;
- VG信息:VG名称、UUID、PE大小、总PE数量、可用PE数量、PV列表、LV列表;
- LV信息:LV名称、UUID、大小、PE数量、LE与PE的映射关系、LV类型、关联的DM设备。
- 自动备份:LVM每次修改配置(如创建PV、VG、LV,扩容/缩减LV)时,都会自动备份元数据到/etc/lvm/backup/目录,备份文件以VG名称命名;
- 手动备份:可通过vgsave命令手动备份VG元数据;
- 恢复:当元数据丢失或损坏时,可通过vgrestore命令,从备份文件中恢复元数据,恢复LVM的配置。
7.4 LVM高级功能实现与操作实践(续)
7.4.1 动态扩容与缩减(核心功能)
(续)动态扩容LV实现原理
1.lvextend命令的底层动作:
- 首先检查VG的可用PE数量,确认满足扩容需求后,从VG的PE池中分配指定数量的PE,更新LV的pe_count(PE数量)和size(大小)字段;
- 自动更新LV对应的DM映射表,新增对应的linear(或其他target)映射段,将新增的PE对应的物理地址,添加到DM设备的逻辑地址空间;
- 通知DM层重新加载映射表,使扩容后的空间生效,此时LV的物理容量已完成扩展,但文件系统还未识别到新增空间。
2.文件系统扩展的核心逻辑:
- resize2fs(ext4)、xfs_growfs(xfs)等命令,会扫描LV的新增空间,调整文件系统的inode、块位图、超级块等元数据,将新增空间纳入文件系统的管理范围;
- 在线扩容时,文件系统会在不中断IO的情况下,完成元数据调整,确保数据安全,这也是LVM扩容的核心优势之一。
2. 动态缩减LV(需谨慎操作)
- 文件系统需支持在线缩减(ext4支持,xfs不支持xfs文件系统的LV缩减,需卸载后操作);
- LV的实际使用空间需小于缩减后的目标大小,避免文件系统数据被截断;
- LV需处于激活状态,但建议卸载文件系统后操作,降低数据丢失风险。
# 1. 备份数据(关键步骤,避免操作失误导致数据丢失)
cp -r /mnt/data /backup/ # 假设LV挂载在/mnt/data
# 2. 卸载LV(建议操作,若ext4支持在线缩减,可跳过,但不推荐)
umount /mnt/data
# 3. 检查文件系统完整性(避免文件系统损坏导致缩减失败)
e2fsck -f /dev/mapper/centos-data # ext4文件系统,-f强制检查
# 4. 收缩文件系统(将文件系统大小缩减到20GB,需小于LV当前大小)
resize2fs /dev/mapper/centos-data 20G
# 5. 缩减LV(将LV大小缩减到20GB,与文件系统大小一致)
lvreduce -L 20G /dev/mapper/centos-data
# 方式2:指定缩减的大小(-10G表示减少10GB)
# lvreduce -L -10G /dev/mapper/centos-data
# 6. 重新挂载LV,验证缩减结果
mount /dev/mapper/centos-data /mnt/data
df -h # 查看文件系统大小
lvdisplay /dev/mapper/centos-data # 查看LV大小
vgs # 查看VG的可用PE数量(应增加)
- 文件系统收缩(resize2fs):先扫描文件系统的空闲空间,将文件系统的元数据(inode、块位图)调整到目标大小,释放多余的块空间,确保文件系统数据不会被截断;
- LV缩减(lvreduce):检查LV的实际使用空间,确认小于目标大小后,移除LV中多余的PE,将这些PE归还到VG的PE池,更新LV的pe_count和size字段;
- 更新DM映射表:删除LV对应的DM映射表中多余的映射段,通知DM层重新加载映射表,使缩减后的LV大小生效;
- 关键注意点:LV的缩减大小必须与文件系统的收缩大小一致,若LV缩减大小小于文件系统大小,会导致文件系统损坏,数据丢失。
7.4.2 LVM快照功能(数据备份与恢复)
1. 核心特性与分类
- 按存储方式分类:
- 内部快照(Internal Snapshot):快照数据与源LV存储在同一个VG中,无需独立的PV,适用于临时快照、测试场景,优点是配置简单,缺点是会占用源LV所在VG的空间;
- 外部快照(External Snapshot):快照数据存储在独立的PV(快照存储设备)中,不占用源LV所在VG的空间,适用于长期快照、备份场景,优点是不影响源LV的空间使用,缺点是需要额外的存储设备。
- 关键特性:
- 快照创建速度极快(无需复制源数据,仅创建映射表),占用空间小(仅存储源LV被修改的数据块);
- 快照LV可挂载、读写(默认只读,可配置为可写),挂载后可直接备份数据;
- 一个源LV可创建多个快照LV,记录不同时间点的数据状态;
- 若快照存储空间(COW空间)满,快照LV会变为只读状态,无法继续记录源LV的修改,需及时清理或扩容快照空间。
2. 实现原理(结合DM snapshot target)
- 创建快照时,LVM会创建一个快照LV(对应DM的snapshot target),并指定源LV(origin LV)和快照存储设备(COW设备);
- LVM自动生成DM映射表,将快照LV的逻辑地址映射到源LV和快照存储设备,此时快照LV与源LV共享所有数据块,无额外空间占用;
- 当源LV执行写IO时,DM层触发COW机制:先将该数据块的原始数据复制到快照存储设备,再将新数据写入源LV;
- 当快照LV执行读IO时,若数据块未被修改(未触发COW),直接从源LV读取;若数据块已被修改(触发COW),从快照存储设备读取原始数据;
- 快照LV的大小由快照存储设备的大小决定,用于存储被修改的数据块,若快照存储设备满,快照LV变为只读,防止数据溢出。
3. 完整操作流程(生产环境常用)
# 1. 查看源LV信息(假设源LV为centos-data,挂载在/mnt/data)
lvdisplay /dev/mapper/centos-data
# 2. (可选)创建独立的PV和VG,用于存储快照数据(外部快照)
pvcreate /dev/sdd1 # 格式化物理设备为PV
vgcreate snapshot_vg /dev/sdd1 # 创建快照专用VG
# 3. 创建快照LV(外部快照,大小10GB,快照名称为data_snap,源LV为centos-data)
lvcreate -L 10G -s -n data_snap /dev/mapper/centos-data -V snapshot_vg
# 选项说明:
# -L 10G:快照LV大小(COW空间大小)
# -s:表示创建快照
# -n data_snap:快照LV名称
# -V snapshot_vg:指定快照存储的VG(外部快照),省略则为内部快照
# 4. 查看快照LV信息
lvdisplay /dev/mapper/centos-data_snap
# 5. 挂载快照LV,备份数据(快照默认只读,可通过-o ro挂载)
mkdir /mnt/snap_data
mount -o ro /dev/mapper/centos-data_snap /mnt/snap_data
cp -r /mnt/snap_data/* /backup/data_snap/ # 备份数据
# 6. (可选)测试快照恢复(假设源LV数据损坏,从快照恢复)
umount /mnt/data # 卸载源LV
umount /mnt/snap_data # 卸载快照LV
lvconvert --merge /dev/mapper/centos-data_snap # 将快照合并到源LV(恢复数据)
mount /dev/mapper/centos-data /mnt/data # 重新挂载源LV,验证数据
# 7. 删除快照LV(备份完成后,及时删除,释放空间)
umount /mnt/snap_data
lvremove /dev/mapper/centos-data_snap
4. 生产环境注意事项
- 快照COW空间大小配置:根据源LV的修改频率,合理设置快照大小,一般建议为源LV大小的10%~30%,若修改频率高(如数据库LV),需适当增大COW空间;
- 快照生命周期管理:快照仅用于短期备份,不建议长期保留,长期保留会导致COW空间满,且占用存储资源,备份完成后及时删除;
- 数据一致性:创建快照时,若源LV正在进行写IO,可能导致快照数据不一致,建议在业务低峰期创建快照,或先暂停业务再创建;
- 快照只读设置:生产环境中,快照LV建议设置为只读,防止误操作修改快照数据,影响备份有效性。
7.4.3 条带化LV(提升顺序IO吞吐量)
1. 核心特性与原理
- 核心优势:顺序IO请求会被拆分为多个子请求,并行下发到多块物理设备,吞吐量接近多块设备的总和,大幅提升顺序读写性能;
- 条带大小(Stripe Size):默认64KB,可在创建LV时配置(通常设置为64KB~256KB),条带大小决定了每次IO在单个PV上写入的数据量,影响IO性能;
- 无数据冗余:条带化LV仅实现IO并行,不提供数据冗余,任何一块PV故障都会导致整个LV数据丢失,通常与RAID、mirror LV配合使用,实现高可用;
- PE分配方式:条带化LV的PE会按条带大小,依次分配到多个PV上,形成条带分布,确保IO请求能分散到多块设备。
2. 实现原理(结合DM striped target)
- 创建条带化LV时,LVM会指定多个PV(至少2个),并设置条带大小,自动生成DM映射表,target类型为striped;
- DM的striped target根据条带大小,将LV的逻辑地址转换为多个PV的物理地址,实现IO请求的拆分与并行下发;
- 例如:3块PV,条带大小64KB,LV的逻辑地址0~63KB(128扇区)映射到PV1,64~127KB映射到PV2,128~191KB映射到PV3,192~255KB映射到PV1,以此类推,实现IO并行。
3. 完整操作流程
# 1. 准备多个PV(至少2个,假设为/dev/sda2、/dev/sdb1、/dev/sdc1)
pvcreate /dev/sda2 /dev/sdb1 /dev/sdc1
# 2. 创建VG(包含上述3个PV,PE大小设为64KB,与条带大小匹配)
vgcreate -s 64K striped_vg /dev/sda2 /dev/sdb1 /dev/sdc1
# 3. 创建条带化LV(大小30GB,条带大小64KB,使用3个PV)
lvcreate -L 30G -i 3 -I 64K -n striped_lv striped_vg
# 选项说明:
# -i 3:指定使用3个PV(条带数量)
# -I 64K:指定条带大小为64KB
# -n striped_lv:条带化LV名称
# 4. 格式化LV,挂载使用
mkfs.ext4 /dev/mapper/striped_vg-striped_lv
mkdir /mnt/striped_data
mount /dev/mapper/striped_vg-striped_lv /mnt/striped_data
# 5. 验证条带化效果(查看LV的条带配置)
lvdisplay /dev/mapper/striped_vg-striped_lv # 查看Stripes和Stripe Size字段
dmsetup table striped_vg-striped_lv # 查看DM映射表,确认target为striped
4. 生产环境最佳实践
- 条带大小选择:根据业务IO大小选择,大文件顺序读写(如视频、日志)建议设置为64KB~256KB,小文件随机读写建议设置为较小的条带大小(如32KB),减少IO拆分开销;
- PV选择:建议使用同型号、同容量、同性能的物理设备(如相同型号的SSD),避免因设备性能差异导致IO瓶颈;
- 高可用配合:条带化LV无数据冗余,生产环境中需配合RAID阵列(如RAID 0+1)或mirror LV,实现数据冗余与高可用;
- 性能监控:通过iostat、dstat等工具监控各PV的IO负载,确保IO请求均匀分布在多块设备上,避免单设备负载过高。
7.4.4 镜像LV(数据冗余与高可用)
1. 核心组件与特性
- 核心组件:
- 源LV(Primary LV):主镜像设备,默认的读IO来源设备;
- 镜像LV(Mirror LV):从镜像设备,与源LV保持数据一致,用于数据冗余;
- 日志设备(Log Device):用于记录未完成的IO请求,防止系统崩溃时数据不一致,支持“磁盘日志”(存储在独立PV)和“内存日志”(存储在内存),生产环境建议使用磁盘日志。
- 核心特性:
- 双写机制:所有写IO请求会同时下发到源LV和镜像LV,只有所有镜像设备都写入成功,才算IO完成;
- 读负载均衡:读IO请求可从任意一块镜像设备读取,默认从源LV读取,可配置为轮流读取,提升读IO性能;
- 故障自动切换:当某一块镜像设备故障时,LVM会自动将IO请求切换到其他正常的镜像设备,不影响业务运行;
- 数据同步:初始化时,将源LV的数据同步到镜像LV;故障恢复时,替换故障设备后,通过日志设备同步未完成的IO,恢复数据一致性。
2. 实现原理(结合DM mirror target)
- 创建镜像LV时,LVM会指定源LV、镜像LV和日志设备,自动生成DM映射表,target类型为mirror;
- 写IO处理:DM的mirror target将写IO请求复制多份,分别下发到源LV和所有镜像LV,等待所有设备IO完成后,返回IO成功;
- 读IO处理:默认从源LV读取,可通过配置实现读负载均衡,轮流从不同镜像设备读取;
- 日志处理:使用环形日志记录未完成的IO请求,系统崩溃后,重启时通过日志恢复数据,避免数据不一致;
- 同步处理:初始化时执行全量同步,将源LV的数据复制到镜像LV;故障恢复时执行增量同步,仅同步未完成的IO和被修改的数据块,减少同步开销。
3. 完整操作流程
# 1. 准备多个PV(至少2个,用于源LV和镜像LV,假设为/dev/sda2、/dev/sdb1)
pvcreate /dev/sda2 /dev/sdb1 /dev/sdc1 # /dev/sdc1用于日志设备
# 2. 创建VG(包含上述3个PV)
vgcreate mirror_vg /dev/sda2 /dev/sdb1 /dev/sdc1
# 3. 创建镜像LV(大小20GB,1个镜像设备,日志设备为/dev/sdc1)
lvcreate -L 20G -m 1 -n mirror_lv mirror_vg /dev/sda2 /dev/sdb1 --logdevice /dev/sdc1
# 选项说明:
# -m 1:指定镜像设备数量(1个镜像,共2个设备:源+镜像)
# --logdevice /dev/sdc1:指定日志设备,避免使用内存日志
# 最后两个参数:源LV对应的PV和镜像LV对应的PV
# 4. 查看镜像LV信息(确认镜像状态、日志设备)
lvdisplay /dev/mapper/mirror_vg-mirror_lv
# 5. 格式化LV,挂载使用
mkfs.ext4 /dev/mapper/mirror_vg-mirror_lv
mkdir /mnt/mirror_data
mount /dev/mapper/mirror_vg-mirror_lv /mnt/mirror_data
# 6. 故障恢复测试(模拟镜像设备故障)
# 标记/dev/sdb1(镜像设备)为故障状态
pvchange -x n /dev/sdb1 # 禁止PV写入
lvdisplay /dev/mapper/mirror_vg-mirror_lv # 查看镜像状态变为"Degraded"(降级)
# 替换故障设备(假设新设备为/dev/sdd1)
pvcreate /dev/sdd1
vgextend mirror_vg /dev/sdd1
lvconvert --replace /dev/sdb1 /dev/mapper/mirror_vg-mirror_lv # 替换故障设备
lvdisplay /dev/mapper/mirror_vg-mirror_lv # 查看镜像状态恢复为"Active"(激活)
# 7. 移除镜像(若无需冗余,可移除镜像,恢复为普通LV)
lvconvert -m 0 /dev/mapper/mirror_vg-mirror_lv
4. 生产环境注意事项
- 日志设备配置:生产环境建议使用独立的PV作为日志设备,避免使用内存日志(系统崩溃时日志丢失,导致数据不一致);
- 镜像设备选择:建议使用同型号、同容量、同性能的物理设备,确保镜像同步的效率和数据一致性;
- 性能影响:镜像LV会增加写IO开销(写IO需同时写入多块设备),读IO性能略有提升,适合读多写少的场景(如数据库读库);
- 故障监控:通过lvs、lvdisplay等命令监控镜像LV的状态,及时发现故障设备,避免多块设备同时故障导致数据丢失。
7.5 LVM元数据备份与恢复(生产环境必备)
7.5.1 元数据的存储与自动备份
1.存储位置:
- 默认存储在每个PV的PV头中(位于PV的开头,占用少量空间),实现冗余备份,即使某一个PV故障,也可从其他PV中恢复元数据;
- 可通过lvmetad服务(LVM元数据守护进程),将元数据缓存到内存中,提升LVM操作的效率,适用于大规模LVM部署场景。
2.自动备份:
- LVM每次修改配置(如创建PV、VG、LV,扩容/缩减LV,创建/删除快照)时,都会自动备份元数据到/etc/lvm/backup/目录,备份文件以VG名称命名(如centos);
- 自动备份文件包含该VG的所有配置信息(PV、LV、PE映射等),保留历史版本,可用于恢复到之前的配置状态。
7.5.2 手动备份与恢复操作
1. 手动备份元数据
# 1. 备份指定VG的元数据(推荐方式)
vgsave centos -f /backup/lvm/centos_vg_backup # 将centos VG的元数据备份到指定文件
# 2. 备份所有VG的元数据
vgsave --all -f /backup/lvm/all_vg_backup
# 3. 备份单个PV的元数据
pvsave /dev/sda2 -f /backup/lvm/sda2_pv_backup
2. 元数据恢复(分场景)
场景1:VG元数据丢失,但PV未损坏
# 1. 扫描系统中的所有PV(识别未被管理的PV)
pvscan # 查看所有PV,确认丢失元数据的PV
# 2. 恢复VG元数据(从自动备份文件恢复)
vgrestore centos -f /etc/lvm/backup/centos # 从默认备份文件恢复
# 3. 激活VG和LV
vgchange -ay centos # 激活centos VG
lvchange -ay /dev/mapper/centos-root # 激活指定LV
# 4. 验证恢复结果
vgs # 查看VG是否正常
lvs # 查看LV是否正常
mount /dev/mapper/centos-root /mnt # 验证LV可正常挂载
场景2:PV损坏,元数据丢失(需从备份恢复)
# 1. 替换损坏的PV(假设损坏的PV为/dev/sda2,新PV为/dev/sdd1)
pvcreate /dev/sdd1
# 2. 恢复VG元数据(从手动备份文件恢复)
vgrestore centos -f /backup/lvm/centos_vg_backup
# 3. 将新PV替换损坏的PV
vgextend centos /dev/sdd1 # 将新PV添加到VG
vgreduce centos /dev/sda2 # 移除损坏的PV
# 4. 激活VG和LV,验证数据
vgchange -ay centos
lvdisplay /dev/mapper/centos-root
场景3:元数据损坏,无法识别VG(紧急恢复)
# 1. 强制扫描所有PV,收集元数据碎片
pvscan --cache --force
# 2. 尝试恢复VG元数据(从最近的自动备份)
vgcfgrestore centos -f /etc/lvm/backup/centos
# 3. 若恢复失败,使用元数据修复工具
lvmdiskscan # 扫描所有LVM相关设备
vgcfgrestore --force centos # 强制恢复
# 4. 激活VG和LV,检查数据完整性
vgchange -ay centos
e2fsck -f /dev/mapper/centos-root # 检查文件系统完整性
7.5.3 生产环境元数据管理最佳实践
- 定期手动备份:每周/每月手动备份一次元数据,存储到独立的备份服务器,避免与源数据存储在同一设备;
- 保留自动备份:不要删除/etc/lvm/backup/目录下的自动备份文件,保留至少3个月的历史版本;
- 元数据监控:通过lvs、vgs、pvs等命令定期检查LVM配置,发现元数据异常及时处理;
- 应急方案:提前制定元数据丢失的应急恢复流程,明确备份文件的存储位置、恢复步骤,避免故障时手忙脚乱。
7.6 生产环境LVM与DM避坑指南
7.6.1 核心避坑要点(高频错误)
1.LV扩容/缩减的致命错误:
- 错误1:未扩展文件系统直接扩容LV,导致新增空间无法被识别,虽不影响现有数据,但空间浪费;
- 错误2:未收缩文件系统直接缩减LV,导致文件系统损坏,数据丢失(最常见、最严重的错误);
- 错误3:LV缩减大小小于文件系统实际使用大小,导致文件系统截断,数据丢失。
2.快照功能的常见问题:
- 问题1:快照COW空间满,导致快照变为只读,无法继续记录源LV的修改,需及时删除快照或扩容COW空间;
- 问题2:创建快照时,源LV正在进行大量写IO,导致快照数据不一致,备份无效;
- 问题3:长期保留快照,占用大量存储资源,且增加源LV的写IO开销(每次写都需触发COW)。
3.DM与LVM的兼容性问题:
- 错误1:手动修改DM映射表,导致LVM配置与DM映射表不一致,LVM操作失败;
- 错误2:使用废弃的DM target(如旧版本的raid1 target),导致驱动加载失败,DM设备无法激活;
- 错误3:内核版本升级后,未更新LVM工具,导致LVM操作与内核DM层不兼容,出现异常。
4.物理设备管理错误:
- 错误1:未卸载LV、未停用VG,直接移除物理设备,导致元数据损坏、数据丢失;
- 错误2:将不同PE大小的PV加入同一个VG,导致PV添加失败;
- 错误3:镜像LV的镜像设备与源设备性能差异过大,导致镜像同步卡顿,IO延迟升高。
7.6.2 生产环境最佳实践
1.规划先行:
- PV规划:根据业务需求,合理划分物理设备为PV,避免将不同用途、不同性能的物理设备混为一个PV;
- VG规划:按业务类型划分VG(如数据库VG、日志VG、备份VG),避免单个VG过大,便于管理和故障隔离;
- PE大小规划:根据LV的大小和用途选择PE大小,小LV(如小于10GB)建议使用4MB PE,大LV(如大于100GB)建议使用16MB~64MB PE,提升空间分配效率。
2.操作规范:
- 所有LVM操作前,必须备份数据和元数据,尤其是缩减、删除、替换设备等高危操作;
- 扩容/缩减LV时,严格遵循“先文件系统、后LV”的顺序(扩容:LV→文件系统;缩减:文件系统→LV);
- 避免在业务高峰期执行LVM高危操作(如扩容、缩减、快照创建),减少对业务的影响。
3.监控与运维:
- 定期监控LVM状态:使用lvs、vgs、pvs查看PV、VG、LV的状态,关注可用空间、镜像状态、快照状态;
- 监控IO性能:使用iostat、dstat、iotop监控LV、PV的IO负载,及时发现IO瓶颈;
- 定期检查文件系统完整性:使用e2fsck(ext4)、xfs_check(xfs)检查文件系统,避免文件系统损坏。
4.高可用设计:
- 关键业务的LV,建议使用镜像LV或配合RAID阵列,实现数据冗余;
- 快照用于短期备份,长期备份建议使用专业的备份工具(如rsync、backupninja),将数据备份到独立的备份服务器;
- 配置LV的监控告警,当VG可用空间不足、快照COW空间满、镜像状态异常时,及时触发告警。
7.6.3 故障排查方法
1.LV无法激活:
- 排查步骤:使用lvdisplay查看LV状态,使用dmesg查看内核日志,检查PV是否正常、元数据是否损坏、DM驱动是否加载;
- 解决方法:修复损坏的PV、恢复元数据、加载DM驱动,若LV状态为“inactive”,使用lvchange -ay激活。
2.元数据损坏:
- 排查步骤:使用pvscan、vgscan扫描PV、VG,查看元数据是否完整,检查备份文件是否存在;
- 解决方法:从自动备份或手动备份中恢复元数据,若备份丢失,使用vgcfgrestore --force强制恢复。
3.IO异常(延迟高、吞吐量低):
- 排查步骤:使用iostat查看PV、LV的IO负载,使用dmsetup status查看DM设备状态,检查target配置是否合理;
- 解决方法:调整条带大小、优化镜像同步策略、替换性能较差的物理设备,若为快照导致的IO延迟,及时删除快照。
4.快照COW空间满:
- 排查步骤:使用lvdisplay查看快照LV的空间使用情况,使用df -h查看快照存储设备的空间;
- 解决方法:删除无用的快照、扩容快照COW空间,或清理源LV的无用数据,减少修改量。
4189



被折叠的 条评论
为什么被折叠?



