LVM¶
本文初稿已完成,但可能仍需大幅度修改
本文将介绍常见 LVM 特性的使用与维护。
LVM(Logical Volume Manager)是 Linux 下的逻辑卷管理器,基于内核的 device mapper(dm)功能。 相比于直接在创建分区表后使用分区,LVM 提供了更加灵活的存储管理方式:
- LVM 可以管理多个硬盘(物理卷)上的存储空间
- LVM 中的逻辑卷可以跨越多个物理卷,文件系统不需要关心物理卷的位置
- LVM 的逻辑卷可以动态调整大小,而不需要移动分区的位置——移动分区的起始位置是一个危险且耗时的操作
一些 Linux 发行版的安装程序默认使用 LVM 来管理磁盘,例如 Fedora、CentOS 等。如果需要实际使用 LVM,也推荐阅读来自红帽 RHEL 的 LVM 管理指南1。
本部分无法涵盖全部内容
LVM 包含了很多功能,在本份文档中不可能面面俱到,因此我们仅介绍在 LUG 与 Vlab 项目中使用过的功能。
基础概念¶
LVM 中有三个基本概念:
- 物理卷(Physical Volume,PV):通常是一块硬盘(分区)
- 卷组(Volume Group,VG):由一个或多个物理卷组成
- 逻辑卷(Logical Volume,LV):在卷组里分配的逻辑存储空间。称之为「逻辑」,是因为它可以跨越多个物理卷,也不一定是连续的
这里我们创建三个 1GB 的文件作为物理卷,并且加入到一个卷组中:
避免在物理磁盘上创建无分区表的文件系统/物理卷
在实践中,尽管没有什么阻止这么做,但是不创建分区表、直接将整个磁盘格式化为某个文件系统,或者加入 LVM 中是不建议的。 这会给其他人带来困惑,并且如果未来有在对应磁盘上启动系统等需要多分区的需求,会带来很多麻烦(可能只能备份数据后从头再来)。
直接对物理磁盘设备格式化为文件系统也是操作时常见的输入错误:
在下面的例子中,为了简化操作,我们假设 pv[1-3].img 相当于每块硬盘上使用全部空间的分区。
$ truncate -s 1G pv1.img pv2.img pv3.img
$ sudo losetup -f --show pv1.img
/dev/loop0
$ sudo losetup -f --show pv2.img
/dev/loop1
$ sudo losetup -f --show pv3.img
/dev/loop2
$ sudo pvcreate /dev/loop0 /dev/loop1 /dev/loop2 # 创建物理卷
Physical volume "/dev/loop0" successfully created.
Physical volume "/dev/loop1" successfully created.
Physical volume "/dev/loop2" successfully created.
$ sudo vgcreate vg201-test /dev/loop0 /dev/loop1 /dev/loop2 # 创建卷组
Volume group "vg201-test" successfully created
$ sudo pvs # 查看物理卷信息
PV VG Fmt Attr PSize PFree
/dev/loop0 vg201-test lvm2 a-- 1020.00m 1020.00m
/dev/loop1 vg201-test lvm2 a-- 1020.00m 1020.00m
/dev/loop2 vg201-test lvm2 a-- 1020.00m 1020.00m
$ sudo vgs # 查看卷组信息
VG #PV #LV #SN Attr VSize VFree
vg201-test 3 0 0 wz--n- <2.99g <2.99g
$ sudo lvcreate -n lvol0 -L 2.5G vg201-test # 创建一个 2.5G 的逻辑卷
Logical volume "lvol0" created.
$ sudo lvs # 查看逻辑卷信息
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
lvol0 vg201-test -wi-a----- 2.50g
$ ls -lh /dev/mapper/
total 0
crw------- 1 root root 10, 236 Feb 11 13:30 control
lrwxrwxrwx 1 root root 7 Feb 12 00:09 vg201--test-lvol0 -> ../dm-0
$ # /dev/mapper/vg201--test-lvol0 就是我们创建的逻辑卷(块设备),可以在上面创建文件系统。
$ # 对应的设备文件也可以在 /dev/vg201-test/ 里面找到
$ sudo lvchange -an vg201-test/lvol0 # 取消激活 (disactivate) 刚才创建的逻辑卷
$ sudo lvs
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
lvol0 vg201-test -wi------- 2.50g
$ sudo losetup -D # 卸载本地回环
之后再次挂载:
$ sudo losetup -f --show pv1.img
/dev/loop0
$ sudo losetup -f --show pv2.img
/dev/loop1
$ sudo losetup -f --show pv3.img
/dev/loop2
$ sudo pvs # 可以看到物理卷被自动识别了
PV VG Fmt Attr PSize PFree
/dev/loop0 vg201-test lvm2 a-- 1020.00m 0
/dev/loop1 vg201-test lvm2 a-- 1020.00m 0
/dev/loop2 vg201-test lvm2 a-- 1020.00m 500.00m
$ sudo lvchange -ay vg201-test/lvol0 # 激活 LV
$ sudo lvs
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
lvol0 vg201-test -wi-a----- 2.50g
等等,怎么每块盘少了几 MB 空间?
其实大概可以猜到,这些空间留给了 LVM 的元数据。LVM 的元数据为纯文本格式,可以存储相对复杂的信息,但是也带来了下述两个问题:
- LVM 的元数据格式没有书面的标准,因此其他软件在解析 LVM 元数据时可能会出现问题。科大镜像站的机器就遇到过 GRUB 解析 LVM 元数据代码的问题,导致无法正确配置启动器。 这个问题直到现在都没有被 GRUB 修复,因此只能自行编译手动修复后的版本,并且固定 GRUB 版本。
- LVM 的纯文本格式导致元数据本身较大,如果预分配的元数据空间不足,并且卷组中有大量逻辑卷(默认值 + 上千个 LV 就会出现问题),那么最后会导致无法再创建/扩容逻辑卷,并且只能通过添加新的物理卷,然后由该卷存储元数据来解决问题。Vlab 项目曾遇到过这样的问题。
这里创建的逻辑卷横跨了三块盘,所以 LVM 默认是 RAID 0?
这是不正确的。这里逻辑卷的数据布局与 RAID 0 不同。RAID 0 考虑的是性能,因此数据类似于这么存储:
Disk 1 | Disk 2 | Disk 3 |
---|---|---|
0 | 1 | 2 |
3 | 4 | 5 |
6 | 7 | 8 |
... | ... | ... |
这样的话,应用程序顺序读写时,就可以利用多块盘的并行读写能力。但是 LVM 类似于这样:
Disk 1 | Disk 2 | Disk 3 |
---|---|---|
0 | 100000 | 200000 |
1 | 100001 | 200001 |
2 | 100002 | 200002 |
... | ... | ... |
每块盘是顺序填充的。这样做可以更加灵活地管理空间,但是性能不如 RAID 0。
LVM 也提供了创建 RAID 0 逻辑卷的功能,被称为「条带化」(Striped)卷,上面默认生成的被称为「线性」(Linear)卷。
创建 RAID¶
不建议使用 LVM 构建 RAID
相比于 mdadm,LVM 与 RAID 相关的概念与提供的工具更加复杂,并且这种复杂性在很多场景下没有收益。 更加常见的模式是,使用 mdadm 构建 RAID,然后使用 LVM 管理构建好的 RAID 上的逻辑卷。
LVM 与 mdadm 使用的都是内核的 md 模块。即使最终使用 LVM 构建 RAID,也建议简单阅读后一节关于 mdadm 的内容。
当然了,对于多盘场景,上面的例子显然是不满足需求的:创建出的逻辑卷仍然是有一块盘坏掉就会故障的状态。 以下展示了 RAID0, 1, 5 的创建方式:
$ sudo lvremove vg201-test/lvol0 # 删除刚才的 LV,腾出一些空间
Do you really want to remove active logical volume vg201-test/lvol0? [y/n]: y
Logical volume "lvol0" successfully removed.
$ # RAID 0 (striped)。--stripes 参数指定了条带的数量,正常情况下和盘数量一致
$ sudo lvcreate -n lvraid0 -L 0.5G --type striped --stripes 3 vg201-test
Using default stripesize 64.00 KiB.
Logical volume "lvraid0" created.
$ # RAID 1。--mirrors 参数指定了副本数量(不含本体),所以是盘数量减一
$ sudo lvcreate -n lvraid1 -L 0.5G --type raid1 --mirrors 2 vg201-test
Logical volume "lvraid1" created.
$ # 因为只有 3 块盘,这里展示 RAID 5。--stripes 参数不包含额外的验证盘。
$ sudo lvcreate -n lvraid5 -L 0.2G --type raid5 --stripes 2 vg201-test
Using default stripesize 64.00 KiB.
Rounding up size to full physical extent 208.00 MiB
Logical volume "lvraid5" created.
$ sudo lvs
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
lvraid0 vg201-test -wi-a----- 516.00m
lvraid1 vg201-test rwi-a-r--- 512.00m 100.00
lvraid5 vg201-test rwi-a-r--- 208.00m 100.00
不要使用 --type mirror
mirror
和 raid1
是两个不同的 type。除非有特殊需要,否则应该使用 --type raid1
创建 RAID 1 阵列。
可以使用 lvconvert
将 mirror 转换为 raid1。
相关讨论可查看 In what case(s) will --type mirror
continue to be a good choice / is not deprecated?。
在后文的缺盘测试中,mirror
的行为也与预期不同——LVM 默认会拒绝挂载,如果强行挂载,会直接将缺失的盘丢掉。
Extent 是多大
有时在输入错误的参数之后,会出现 extent 不足的提示,类似于这样:
$ # --mirrors 参数多了一,空间不够
$ sudo lvcreate -n lvraid1 -L 0.5G --type raid1 --mirrors 3 vg201-test
Insufficient suitable allocatable extents for logical volume lvraid1: 516 more required
但是 "extent" 是多大呢?在 LVM 中,有两种 extent: PE(Physical Extent)和 LE(Logical Extent),
对应物理卷和逻辑卷的大小参数。这里指的是 PE,可以使用 pvdisplay
或 vgdisplay
查看。
$ sudo vgdisplay
--- Volume group ---
VG Name vg201-test
System ID
Format lvm2
Metadata Areas 3
Metadata Sequence No 11
VG Access read/write
VG Status resizable
MAX LV 0
Cur LV 2
Open LV 0
Max PV 0
Cur PV 3
Act PV 3
VG Size <2.99 GiB
PE Size 4.00 MiB
Total PE 765
Alloc PE / Size 514 / <2.01 GiB
Free PE / Size 251 / 1004.00 MiB
VG UUID Ybsskf-2giI-Q5PU-LCof-Irr9-EDud-nlB0Ms
$ sudo pvdisplay
--- Physical volume ---
PV Name /dev/loop0
VG Name vg201-test
PV Size 1.00 GiB / not usable 4.00 MiB
Allocatable yes
PE Size 4.00 MiB
Total PE 255
Free PE 84
Allocated PE 171
PV UUID DBn9ke-9UfO-tZJA-ymSh-GQtP-jMq8-tSm62B
--- Physical volume ---
PV Name /dev/loop1
VG Name vg201-test
PV Size 1.00 GiB / not usable 4.00 MiB
Allocatable yes
PE Size 4.00 MiB
Total PE 255
Free PE 84
Allocated PE 171
PV UUID aut3hf-J6Tl-O5Gq-0TIw-bneD-mfzr-8wMJJx
--- Physical volume ---
PV Name /dev/loop2
VG Name vg201-test
PV Size 1.00 GiB / not usable 4.00 MiB
Allocatable yes
PE Size 4.00 MiB
Total PE 255
Free PE 83
Allocated PE 172
PV UUID AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ
可以看到 PE 是 4M,因此缺少 516 个 extent 指缺少 516 * 4M ~= 2G 空间。 这里是因为 PV 数量不足,所以无法找到能够存储第四份副本的磁盘。
lvs
支持指定参数查看 LV 的其他信息,这里我们查看逻辑卷实际使用的物理卷:
$ sudo lvs -a -o +devices vg201-test
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert Devices
lvraid0 vg201-test -wi-a----- 516.00m /dev/loop0(0),/dev/loop1(0),/dev/loop2(0)
lvraid1 vg201-test rwi-a-r--- 512.00m 100.00 lvraid1_rimage_0(0),lvraid1_rimage_1(0),lvraid1_rimage_2(0)
[lvraid1_rimage_0] vg201-test iwi-aor--- 512.00m /dev/loop0(44)
[lvraid1_rimage_1] vg201-test iwi-aor--- 512.00m /dev/loop1(44)
[lvraid1_rimage_2] vg201-test iwi-aor--- 512.00m /dev/loop2(44)
[lvraid1_rmeta_0] vg201-test ewi-aor--- 4.00m /dev/loop0(43)
[lvraid1_rmeta_1] vg201-test ewi-aor--- 4.00m /dev/loop1(43)
[lvraid1_rmeta_2] vg201-test ewi-aor--- 4.00m /dev/loop2(43)
lvraid5 vg201-test rwi-a-r--- 208.00m 100.00 lvraid5_rimage_0(0),lvraid5_rimage_1(0),lvraid5_rimage_2(0)
[lvraid5_rimage_0] vg201-test iwi-aor--- 104.00m /dev/loop0(173)
[lvraid5_rimage_1] vg201-test iwi-aor--- 104.00m /dev/loop1(173)
[lvraid5_rimage_2] vg201-test iwi-aor--- 104.00m /dev/loop2(173)
[lvraid5_rmeta_0] vg201-test ewi-aor--- 4.00m /dev/loop0(172)
[lvraid5_rmeta_1] vg201-test ewi-aor--- 4.00m /dev/loop1(172)
[lvraid5_rmeta_2] vg201-test ewi-aor--- 4.00m /dev/loop2(172)
rimage, rmeta(与 mimage, mlog)
可以观察到,列表中出现了一些默认隐藏的逻辑卷,它们是创建 RAID 1/5/6 的产物:
- rimage: "RAID image",代表了实际存储数据(以及校验信息)的逻辑卷
- rmeta: 存储了 RAID 的元数据信息
如果在创建 RAID 1 时选择了 --type mirror
,那么对应创建的是 mimage 和 mlog:
- mimage: "mirrored image",数据写入时,会向每个关联的 mimage 写入数据
- mlog: 存储了 RAID 1 的盘之间的同步状态信息
RAID 维护¶
RAID 状态与重建¶
正常情况下,lvs
返回的 RAID 1/5/6 设备的 "Cpy%Sync" 应该是 100.00,表示数据已经同步到所有盘上。
并且 health_status
属性应该为空。这里模拟强制删除一块盘的情况:
$ sudo vgchange -an vg201-test
0 logical volume(s) in volume group "vg201-test" now active
$ sudo losetup -D
$ # 接下来只挂载两块盘
$ sudo losetup -f --show pv1.img
/dev/loop0
$ sudo losetup -f --show pv2.img
/dev/loop1
$ sudo pvs
WARNING: Couldn't find device with uuid AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ.
WARNING: VG vg201-test is missing PV AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ (last written to /dev/loop2).
PV VG Fmt Attr PSize PFree
/dev/loop0 vg201-test lvm2 a-- 1020.00m 228.00m
/dev/loop1 vg201-test lvm2 a-- 1020.00m 228.00m
[unknown] vg201-test lvm2 a-m 1020.00m 224.00m
$ sudo vgchange -ay vg201-test
WARNING: Couldn't find device with uuid AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ.
WARNING: VG vg201-test is missing PV AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ (last written to /dev/loop2).
Refusing activation of partial LV vg201-test/lvraid0. Use '--activationmode partial' to override.
2 logical volume(s) in volume group "vg201-test" now active
$ sudo lvs -a -o name,copy_percent,health_status,devices vg201-test
WARNING: Couldn't find device with uuid AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ.
WARNING: VG vg201-test is missing PV AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ (last written to /dev/loop2).
LV Cpy%Sync Health Devices
lvraid0 partial /dev/loop0(0),/dev/loop1(0),[unknown](0)
lvraid1 100.00 partial lvraid1_rimage_0(0),lvraid1_rimage_1(0),lvraid1_rimage_2(0)
[lvraid1_rimage_0] /dev/loop0(44)
[lvraid1_rimage_1] /dev/loop1(44)
[lvraid1_rimage_2] partial [unknown](44)
[lvraid1_rmeta_0] /dev/loop0(43)
[lvraid1_rmeta_1] /dev/loop1(43)
[lvraid1_rmeta_2] partial [unknown](43)
lvraid5 100.00 partial lvraid5_rimage_0(0),lvraid5_rimage_1(0),lvraid5_rimage_2(0)
[lvraid5_rimage_0] /dev/loop0(173)
[lvraid5_rimage_1] /dev/loop1(173)
[lvraid5_rimage_2] partial [unknown](173)
[lvraid5_rmeta_0] /dev/loop0(172)
[lvraid5_rmeta_1] /dev/loop1(172)
[lvraid5_rmeta_2] partial [unknown](172)
可以发现:
- RAID 0 由于缺少一块盘,LVM 会拒绝激活
- RAID 1/5 可以激活,但是 health_status 为 partial,表示对应阵列处于不完整的状态
假设我们添加一块新盘,并删除旧盘,进行 RAID 1/5 的重建:
$ truncate -s 1G pv4.img
$ sudo losetup /dev/loop3 pv4.img
$ sudo pvcreate /dev/loop3
Physical volume "/dev/loop3" successfully created.
$ sudo vgextend vg201-test /dev/loop3
WARNING: Couldn't find device with uuid AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ.
WARNING: VG vg201-test is missing PV AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ (last written to /dev/loop2).
WARNING: Couldn't find device with uuid AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ.
WARNING: Couldn't find device with uuid AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ.
Volume group "vg201-test" successfully extended
$ sudo lvconvert --repair vg201-test/lvraid1
WARNING: Couldn't find device with uuid AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ.
WARNING: VG vg201-test is missing PV AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ (last written to [unknown]).
WARNING: Couldn't find device with uuid AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ.
Attempt to replace failed RAID images (requires full device resync)? [y/n]: y
WARNING: Couldn't find device with uuid AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ.
WARNING: Couldn't find device with uuid AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ.
Faulty devices in vg201-test/lvraid1 successfully replaced.
$ sudo lvconvert --repair vg201-test/lvraid5
WARNING: Couldn't find device with uuid AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ.
WARNING: VG vg201-test is missing PV AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ (last written to [unknown]).
WARNING: Couldn't find device with uuid AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ.
Attempt to replace failed RAID images (requires full device resync)? [y/n]: y
WARNING: Couldn't find device with uuid AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ.
WARNING: Couldn't find device with uuid AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ.
Faulty devices in vg201-test/lvraid5 successfully replaced.
$ sudo lvs -a -o name,copy_percent,health_status,devices vg201-test
WARNING: Couldn't find device with uuid AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ.
WARNING: VG vg201-test is missing PV AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ (last written to [unknown]).
LV Cpy%Sync Health Devices
lvraid0 partial /dev/loop0(0),/dev/loop1(0),[unknown](0)
lvraid1 100.00 lvraid1_rimage_0(0),lvraid1_rimage_1(0),lvraid1_rimage_2(0)
[lvraid1_rimage_0] /dev/loop0(44)
[lvraid1_rimage_1] /dev/loop1(44)
[lvraid1_rimage_2] /dev/loop3(1)
[lvraid1_rmeta_0] /dev/loop0(43)
[lvraid1_rmeta_1] /dev/loop1(43)
[lvraid1_rmeta_2] /dev/loop3(0)
lvraid5 100.00 lvraid5_rimage_0(0),lvraid5_rimage_1(0),lvraid5_rimage_2(0)
[lvraid5_rimage_0] /dev/loop0(173)
[lvraid5_rimage_1] /dev/loop1(173)
[lvraid5_rimage_2] /dev/loop3(130)
[lvraid5_rmeta_0] /dev/loop0(172)
[lvraid5_rmeta_1] /dev/loop1(172)
[lvraid5_rmeta_2] /dev/loop3(129)
下面展示将原始的 /dev/loop2
恢复回 vg201-test 的过程,以「恢复」最后的 RAID 0。
通过使用 vgextend
的 --restoremissing
参数,我们不需要重新初始化 /dev/loop2
,而是直接将其加入到卷组中。
只在确定原始的 PV 没有被修改的情况下才能如此操作。
$ sudo losetup /dev/loop2 pv3.img
$ sudo pvs
WARNING: ignoring metadata seqno 37 on /dev/loop2 for seqno 43 on /dev/loop0 for VG vg201-test.
WARNING: Inconsistent metadata found for VG vg201-test.
See vgck --updatemetadata to correct inconsistency.
WARNING: VG vg201-test was previously updated while PV /dev/loop2 was missing.
WARNING: VG vg201-test was missing PV /dev/loop2 AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ.
PV VG Fmt Attr PSize PFree
/dev/loop0 vg201-test lvm2 a-- 1020.00m 224.00m
/dev/loop1 vg201-test lvm2 a-- 1020.00m 224.00m
/dev/loop2 vg201-test lvm2 a-m 1020.00m 848.00m
/dev/loop3 vg201-test lvm2 a-- 1020.00m 396.00m
$ sudo vgextend vg201-test /dev/loop2 --restoremissing
WARNING: ignoring metadata seqno 37 on /dev/loop2 for seqno 43 on /dev/loop0 for VG vg201-test.
WARNING: Inconsistent metadata found for VG vg201-test.
See vgck --updatemetadata to correct inconsistency.
WARNING: VG vg201-test was previously updated while PV /dev/loop2 was missing.
WARNING: VG vg201-test was missing PV /dev/loop2 AQj8ej-EKps-ud3h-0KkP-wDxo-ZagG-ZJIdnZ.
WARNING: VG vg201-test was previously updated while PV /dev/loop2 was missing.
WARNING: updating old metadata to 44 on /dev/loop2 for VG vg201-test.
Volume group "vg201-test" successfully extended
$ sudo lvs -a -o name,copy_percent,health_status,devices vg201-test
LV Cpy%Sync Health Devices
lvraid0 /dev/loop0(0),/dev/loop1(0),/dev/loop2(0)
lvraid1 100.00 lvraid1_rimage_0(0),lvraid1_rimage_1(0),lvraid1_rimage_2(0)
[lvraid1_rimage_0] /dev/loop0(44)
[lvraid1_rimage_1] /dev/loop1(44)
[lvraid1_rimage_2] /dev/loop3(1)
[lvraid1_rmeta_0] /dev/loop0(43)
[lvraid1_rmeta_1] /dev/loop1(43)
[lvraid1_rmeta_2] /dev/loop3(0)
lvraid5 100.00 lvraid5_rimage_0(0),lvraid5_rimage_1(0),lvraid5_rimage_2(0)
[lvraid5_rimage_0] /dev/loop0(173)
[lvraid5_rimage_1] /dev/loop1(173)
[lvraid5_rimage_2] /dev/loop3(130)
[lvraid5_rmeta_0] /dev/loop0(172)
[lvraid5_rmeta_1] /dev/loop1(172)
[lvraid5_rmeta_2] /dev/loop3(129)
完整性检查¶
即使正常运行,RAID 也无法防止阵列中的某块硬盘因为某种原因数据不一致的情况(例如比特翻转), 因此定期进行完整性检查(Scrub)是非常重要的。以下展示一个没有定期 scrub 的反例:
有一个三块盘的 RAID1,因为某些神奇的误操作,其中一块盘的状态一直是 2018 年的,另两块是正确的 RAID1,然后这组盘被挪到了新机器,被重新加成了一个三盘的 RAID1,软 RAID 软件 somehow 没有做检查就跑了起来,于是读文件时有 1/3 概率读取到旧盘,也就是 ls 一下可能看到旧文件也可能看到新文件,可能这样用了很长一段时间一直没发现,昨天重启之后突然发现 glibc 回到了 2018 年
另一个没有 scrub 数据,最终导致文件丢失的例子是 Linus Tech Tips(YouTube/Bilibili, 05:15)。
下面我们向 pv4.img
中间写入 1M 的随机数据,并展示 LVM 的检查与修复功能。
$ sudo dd if=/dev/urandom of=/dev/loop3 bs=1M count=1 oseek=100
1+0 records in
1+0 records out
1048576 bytes (1.0 MB, 1.0 MiB) copied, 0.00402655 s, 260 MB/s
$ sudo lvs -o devices vg201-test
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
lvraid0 vg201-test -wi------- 516.00m
lvraid1 vg201-test rwi-a-r--- 512.00m 100.00
lvraid5 vg201-test rwi-a-r--- 208.00m 100.00
$ sudo lvchange --syncaction check vg201-test/lvraid1
$ sudo dmesg
(省略)
[1655658.162616] md: mdX: data-check done.
[1655663.533169] md: data-check of RAID array mdX
$ sudo lvs -o +raid_sync_action,raid_mismatch_count
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert SyncAction Mismatches
lvraid0 vg201-test -wi------- 516.00m
lvraid1 vg201-test rwi-a-r-m- 512.00m 100.00 idle 2048
lvraid5 vg201-test rwi-a-r--- 208.00m 100.00 idle 0
$ # 因为我们的 RAID 1 有三块盘,所以这里的「不一致」还可以修复。
$ sudo lvchange --syncaction repair vg201-test/lvraid1
$ sudo dmesg
(省略)
[1655787.490037] md: requested-resync of RAID array mdX
[1655789.691174] md: mdX: requested-resync done.
$ sudo lvs -o +raid_sync_action,raid_mismatch_count
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert SyncAction Mismatches
lvraid0 vg201-test -wi------- 516.00m
lvraid1 vg201-test rwi-a-r--- 512.00m 100.00 idle 0
lvraid5 vg201-test rwi-a-r--- 208.00m 100.00 idle 0
dm-integrity
LVM 支持在设置 RAID 时添加 integrity 功能(--raidintegrity
),这项功能为数据添加了校验和,
LVM 在发现数据不一致时会在内核日志中报告,并在可以修复的情况下自动修复。
这项功能不是 scrub 的替代品。
扩容/缩小操作¶
LVM 支持在线扩容/缩小逻辑卷,有三个相关命令:lvextend
(扩大)、lvreduce
(缩小)、lvresize
(通用)。
让我们先在 lvraid0 上创建一个 ext4 文件系统并挂载,模拟在线场景:
$ sudo mkfs.ext4 /dev/vg201-test/lvraid0
mke2fs 1.47.0 (5-Feb-2023)
Discarding device blocks: done
Creating filesystem with 132096 4k blocks and 33040 inodes
Filesystem UUID: 49e73c33-1ea2-43fa-8609-586389a11b98
Superblock backups stored on blocks:
32768, 98304
Allocating group tables: done
Writing inode tables: done
Creating journal (4096 blocks): done
Writing superblocks and filesystem accounting information: done
$ sudo mount /dev/vg201-test/lvraid0 /somewhere/you/like
$ df -h /somewhere/you/like
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/vg201--test-lvraid0 492M 152K 455M 1% /somewhere/you/like
下面展示 lvextend
和 lvreduce
。lvresize
的操作可以自行查阅。
$ sudo lvextend --size +100M /dev/vg201-test/lvraid0
Using stripesize of last segment 64.00 KiB
Rounding size (154 extents) up to stripe boundary size for segment (156 extents).
Size of logical volume vg201-test/lvraid0 changed from 516.00 MiB (129 extents) to 624.00 MiB (156 extents).
Logical volume vg201-test/lvraid0 successfully resized.
此时 lvraid0
这个 LV 已经增大了 100M,但是文件系统并没有感知到这个变化:
$ df -h /somewhere/you/like
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/vg201--test-lvraid0 492M 152K 455M 1% /somewhere/you/like
因此我们需要用文件系统提供的工具扩容。ext4 支持使用 resize2fs
在线扩容/缩小:
$ sudo resize2fs /dev/vg201-test/lvraid0
resize2fs 1.47.0 (5-Feb-2023)
Filesystem at /dev/vg201-test/lvraid0 is mounted on /somewhere/you/like; on-line resizing required
old_desc_blocks = 1, new_desc_blocks = 1
The filesystem on /dev/vg201-test/lvraid0 is now 159744 (4k) blocks long.
$ df -h /somewhere/you/like
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/vg201--test-lvraid0 600M 152K 563M 1% /somewhere/you/like
缩小操作的顺序刚好相反:需要先缩小文件系统,再缩小 LV。 否则文件系统被缩小的部分会丢失,导致文件系统损坏。 由于 ext4 文件系统不支持在线缩小,因此操作前必须卸载文件系统。 这里我们把文件系统缩到最小,然后缩小 LV,最后再扩大文件系统:
$ sudo umount /somewhere/you/like
$ sudo resize2fs -M /dev/vg201-test/lvraid0
resize2fs 1.47.0 (5-Feb-2023)
Please run 'e2fsck -f /dev/vg201-test/lvraid0' first.
$ # 在缩小前,保险起见,需要先检查文件系统的完整性
$ sudo e2fsck -f /dev/vg201-test/lvraid0
e2fsck 1.47.0 (5-Feb-2023)
Pass 1: Checking inodes, blocks, and sizes
Pass 2: Checking directory structure
Pass 3: Checking directory connectivity
Pass 4: Checking reference counts
Pass 5: Checking group summary information
/dev/vg201-test/lvraid0: 12/33040 files (0.0% non-contiguous), 6407/159744 blocks
$ sudo resize2fs -M /dev/vg201-test/lvraid0
resize2fs 1.47.0 (5-Feb-2023)
Resizing the filesystem on /dev/vg201-test/lvraid0 to 6420 (4k) blocks.
The filesystem on /dev/vg201-test/lvraid0 is now 6420 (4k) blocks long.
$ # 现在缩小 LV
$ sudo lvreduce --size -100M /dev/vg201-test/lvraid0
Rounding size (131 extents) up to stripe boundary size for segment (132 extents).
File system ext4 found on vg201-test/lvraid0.
File system size (<25.08 MiB) is smaller than the requested size (528.00 MiB).
File system reduce is not needed, skipping.
Size of logical volume vg201-test/lvraid0 changed from 624.00 MiB (156 extents) to 528.00 MiB (132 extents).
Logical volume vg201-test/lvraid0 successfully resized.
$ sudo resize2fs /dev/vg201-test/lvraid0
resize2fs 1.47.0 (5-Feb-2023)
Resizing the filesystem on /dev/vg201-test/lvraid0 to 135168 (4k) blocks.
The filesystem on /dev/vg201-test/lvraid0 is now 135168 (4k) blocks long.
$ sudo mount /dev/vg201-test/lvraid0 /somewhere/you/like
$ df -h /somewhere/you/like
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/vg201--test-lvraid0 504M 152K 471M 1% /somewhere/you/like
对于支持的文件系统,lvreduce
会检查文件系统的大小,避免数据损坏。但在操作时仍然需要谨慎。
SSD 缓存¶
LVM 支持将 SSD 作为 HDD 的缓存,以提高性能。以下介绍基于 dm-cache 的读写缓存。
dm-writecache
本部分不介绍以优化写入为目的的 dm-writecache——我们没有相关的使用场景。
缓存方案
目前内核自带这些缓存方案:
- bcache:需要将 SSD 和 HDD 对应的块设备分区使用
make-bcache
初始化后, 在 bcache 暴露的/dev/bcache0
上进行操作。 - lvmcache:需要在 LVM 的基础上进行操作。基于内核的 dm-cache。
此外,在 Linux 6.7(2024 年 1 月)之后,内核合并了 bcachefs 支持,它同样也包含了 SSD 缓存的功能。 但是 bcachefs 的稳定性仍然需要至少数年的时间来验证。ZFS 同样也包含缓存功能(ARC 与 L2ARC),将在 ZFS 中介绍。
不过,随着 SSD 单位空间成本逐渐降低,SSD 缓存的意义也在逐渐减小。
甚至也有预测表明,到 2026 年后 SSD 的成本甚至会低于 HDD。内核中 dm-cache
的开发也不活跃。
科大镜像站于 2024 年 7 月将缓存方案由 lvmcache(基于 SSD)迁移至了 ZFS ARC(基于内存),以减小下文中提到的相关问题导致的运维压力。
在考虑缓存方案的选择时,需要考虑各种因素,下文会做一些简单的介绍。
首先删除上面创建的 LV 与 PV,然后创建一个大的 image 和一个小的 image 作为 HDD 与 SSD:
$ sudo lvremove vg201-test/lvraid0 vg201-test/lvraid1 vg201-test/lvraid5
Logical volume "lvraid0" successfully removed.
Do you really want to remove active logical volume vg201-test/lvraid1? [y/n]: y
Logical volume "lvraid1" successfully removed.
Do you really want to remove active logical volume vg201-test/lvraid5? [y/n]: y
Logical volume "lvraid5" successfully removed.
$ sudo vgremove vg201-test
Volume group "vg201-test" successfully removed
$ sudo losetup -D
$ rm pv*.img
$ truncate -s 100G hdd.img
$ truncate -s 10G ssd.img
$ sudo losetup -f --show hdd.img
/dev/loop0
$ sudo losetup -f --show ssd.img
/dev/loop1
$ sudo pvcreate /dev/loop0 /dev/loop1
Physical volume "/dev/loop0" successfully created.
Physical volume "/dev/loop1" successfully created.
$ sudo vgcreate vg201-test /dev/loop0 /dev/loop1
Volume group "vg201-test" successfully created
接下来我们创建存储(后备)数据的 LV,这个 LV 只应该在 HDD 上:
$ sudo lvcreate -n lvdata -l 100%FREE vg201-test /dev/loop0
Logical volume "lvdata" created.
$ sudo lvs -o +devices vg201-test
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert Devices
lvdata vg201-test -wi-a----- <100.00g /dev/loop0(0)
之后我们需要创建缓存相关的 LV。LVM 支持两种方式:cachevol
和 cachepool
:
cachevol
在一个 LV 中包含了缓存的数据和元数据cachepool
将缓存的数据和元数据分开存储(因此可以将数据和元数据放到不同的设备上)
许多教程中都使用 cachepool
,但是很多时候是没有必要的。
下面先展示 cachevol
的操作(只需两步:创建缓存盘,然后 lvconvert
配置缓存即可):
$ sudo lvcreate -n lvdata_cache -l 100%FREE vg201-test /dev/loop1
Logical volume "lvdata_cache" created.
$ sudo lvconvert --type cache --cachevol lvdata_cache vg201-test/lvdata
Erase all existing data on vg201-test/lvdata_cache? [y/n]: y
Logical volume vg201-test/lvdata is now cached.
$ sudo lvs -a -o +devices vg201-test
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert Devices
lvdata vg201-test Cwi-a-C--- <100.00g [lvdata_cache_cvol] [lvdata_corig] 0.01 11.07 0.00 lvdata_corig(0)
[lvdata_cache_cvol] vg201-test Cwi-aoC--- <10.00g /dev/loop1(0)
[lvdata_corig] vg201-test owi-aoC--- <100.00g /dev/loop0(0)
lvs
也可以输出一些缓存相关的配置和统计信息:
$ sudo lvs -o devices,cache_policy,cachemode,cache_settings,cache_total_blocks,cache_used_blocks,cache_dirty_blocks,cache_read_hits,cache_read_misses,cache_write_hits,cache_write_misses
Devices CachePolicy CacheMode CacheSettings CacheTotalBlocks CacheUsedBlocks CacheDirtyBlocks CacheReadHits CacheReadMisses CacheWriteHits CacheWriteMisses
lvdata_corig(0) smq writethrough 163584 15 0 5 51 0 0
可以看到缓存的模式是 writethrough,策略是 smq,以及缓存的读写命中率与脏块数量。
resize
直到相对新(>= 2.03.12,发布于 2021/05/08)的 LVM 工具之前,被缓存的 LV 不能被 resize。因此 resize 之前必须先撤下缓存,resize 结束后再安回去。
因此 Debian Bookworm 的 LVM 工具支持这种情况下的 resize,而 Bullseye 不支持。
cachevol
无法修复
目前 lvconvert --repair
不支持修复 cachevol,尝试这么做会看到以下输出:
lvmcache 的缓存模式、策略与部分术语
lvmcache 支持三种缓存模式:
- passthrough: 缓存无效。此时所有读写都到后备设备。同时写命中会触发对应的块缓存失效。
- writethrough: 写入操作在缓存和后备设备上都进行,在两者均写入完成后才视作写入生效。
- writeback: 写入操作先在缓存上进行,对应的块标记为脏块(Dirty Block),推迟在后备设备上的写入。 除非被缓存的数据完全无关紧要,或者有多块 SSD 进行缓存,否则不要使用 writeback。文件系统核心元数据的损坏可能直接导致整个文件系统无法读取。
在策略一栏,我们可以看到 "smq"。事实上,lvmcache 唯一支持的有效的现代策略就是 smq(Stochastic Multi-Queue)。 另一种 cleaner 策略用于将所有脏块写回后备设备。
此外,在阅读相关资料时,可能会看到下面三个术语:
- Migration:将一块数据从一个设备复制到另一个设备
- Promotion:将一块数据从后备设备复制到缓存设备
- Demotion:将一块数据从缓存设备复制到后备设备
@taoky: 关于缓存模式
分享一个笑话,最开始 @iBug 配的缓存模式选了 passthrough,理由是:
这里的缓存模式采用 passthrough,即写入动作绕过缓存直接写回原设备(当然啦,写入都是由从上游同步产生的),另外两种 writeback 和 writethrough 都会写入缓存,不是我们想要的。
(当然,这是错的)
另外可以注意到,lvmcache
做了这么一个假设:写入的内容很快就会被读取。但是这个假设真的总是成立吗?
writearound 的做法是写入的内容会绕过缓存,当然 lvmcache 没有实现这个模式。
dm-cache-policy-smq.c
中实现的 SMQ 策略算法
核心的结构体是 smq_policy
,其中包含了三个 SMQ 队列:热点(hotspot)队列、clean 队列和 dirty 队列。
每个 SMQ 队列中包含 64 个 LRU 队列。这 64 个队列构成不同的等级,存储由热到冷的内容。
如果不关心数据卷和元数据卷的细节,创建 cachepool
的体验也类似。先把上面的 uncache 掉:
$ sudo lvconvert --uncache vg201-test/lvdata
Logical volume "lvdata_cache" successfully removed.
Logical volume vg201-test/lvdata is not cached and vg201-test/lvdata_cache is removed.
然后创建 cache-pool 类型的 LV,并且附加到后备设备上:
$ sudo lvcreate --type cache-pool -n lvdata_cache -l 100%FREE vg201-test /dev/loop1
Logical volume "lvdata_cache" created.
$ sudo lvs -a
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
lvdata vg201-test -wi-a----- <100.00g
lvdata_cache vg201-test Cwi---C--- 9.97g
[lvdata_cache_cdata] vg201-test Cwi------- 9.97g
[lvdata_cache_cmeta] vg201-test ewi------- 12.00m
[lvol0_pmspare] vg201-test ewi------- 12.00m
$ sudo lvconvert --type cache --cachepool lvdata_cache vg201-test/lvdata
Do you want wipe existing metadata of cache pool vg201-test/lvdata_cache? [y/n]: y
Logical volume vg201-test/lvdata is now cached.
$ sudo lvs -a
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
lvdata vg201-test Cwi-a-C--- <100.00g [lvdata_cache_cpool] [lvdata_corig] 0.01 11.07 0.00
[lvdata_cache_cpool] vg201-test Cwi---C--- 9.97g 0.01 11.07 0.00
[lvdata_cache_cpool_cdata] vg201-test Cwi-ao---- 9.97g
[lvdata_cache_cpool_cmeta] vg201-test ewi-ao---- 12.00m
[lvdata_corig] vg201-test owi-aoC--- <100.00g
[lvol0_pmspare] vg201-test ewi------- 12.00m
可以发现,在 lvcreate --type cache-pool
的时候,LVM 会自动创建两个 LV:lvdata_cache_cdata
和 lvdata_cache_cmeta
。
但是更老一些的教程中会介绍分别手工创建缓存和元数据 LV 的内容。让我们先把这个再拆掉(uncache),然后手工做这个过程。
根据 RHEL 6 的文档,数据和元数据的推荐大小比例是 1000:1(例如如果有 2G 的缓存,那么就需要 12M 的元数据)。 这里我们的 "SSD" 有 10G,因此大约需要 60M 的元数据,可以先创建元数据 LV,然后剩下的——全部给数据?
$ sudo lvcreate -L 60M -n lvdata_cache_meta vg201-test /dev/loop1
Logical volume "lvdata_cache_meta" created.
$ sudo lvcreate -l 100%FREE -n lvdata_cache_data vg201-test /dev/loop1
Logical volume "lvdata_cache_data" created.
$ sudo lvconvert --type cache-pool --poolmetadata lvdata_cache_meta --cachemode writethrough vg201-test/lvdata_cache_data
WARNING: Converting vg201-test/lvdata_cache_data and vg201-test/lvdata_cache_meta to cache pool's data and metadata volumes with metadata wiping.
THIS WILL DESTROY CONTENT OF LOGICAL VOLUME (filesystem etc.)
Do you really want to convert vg201-test/lvdata_cache_data and vg201-test/lvdata_cache_meta? [y/n]: y
Volume group "vg201-test" has insufficient free space (0 extents): 15 required.
Failed to set up spare metadata LV for pool.
可以看到还需要一些空间(15 个 extent),因此使用 lvreduce
留点出来:
$ sudo lvreduce -l -15 vg201-test/lvdata_cache_data
No file system found on /dev/vg201-test/lvdata_cache_data.
Size of logical volume vg201-test/lvdata_cache_data changed from <9.94 GiB (2544 extents) to <9.88 GiB (2529 extents).
Logical volume vg201-test/lvdata_cache_data successfully resized.
$ sudo lvconvert --type cache-pool --poolmetadata lvdata_cache_meta --cachemode writethrough vg201-test/lvdata_cache_data
WARNING: Converting vg201-test/lvdata_cache_data and vg201-test/lvdata_cache_meta to cache pool's data and metadata volumes with metadata wiping.
THIS WILL DESTROY CONTENT OF LOGICAL VOLUME (filesystem etc.)
Do you really want to convert vg201-test/lvdata_cache_data and vg201-test/lvdata_cache_meta? [y/n]: y
Converted vg201-test/lvdata_cache_data and vg201-test/lvdata_cache_meta to cache pool.
之后就和上面一致了:
$ sudo lvs -a
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
lvdata vg201-test -wi-a----- <100.00g
lvdata_cache_data vg201-test Cwi---C--- <9.88g
[lvdata_cache_data_cdata] vg201-test Cwi------- <9.88g
[lvdata_cache_data_cmeta] vg201-test ewi------- 60.00m
[lvol0_pmspare] vg201-test ewi------- 60.00m
$ sudo lvconvert --type cache --cachepool lvdata_cache_data vg201-test/lvdata
Do you want wipe existing metadata of cache pool vg201-test/lvdata_cache_data? [y/n]: y
Logical volume vg201-test/lvdata is now cached.
Does lvmcache scale?¶
现实中我们肯定不可能只用 10G 的 SSD 来做缓存,而在非家用的场景下,需要缓存的后备存储也不可能只有 100G 这么大。 下面考虑一个类似于目前 mirrors 的场景:1.5T 的 SSD 空间对 65T 的 HDD 空间进行缓存(比例大约 1:45)。
把已有的拆掉之后重新创建 1.5T(1536G)和 65T 的稀疏文件作为 SSD 和 HDD,加入 LVM,然后让我们试试 cachevol……
$ sudo lvconvert --type cache --cachevol lvdata_cache vg201-test/lvdata
Erase all existing data on vg201-test/lvdata_cache? [y/n]: y
Cache data blocks 3219046400 and chunk size 128 exceed max chunks 1000000.
Use smaller cache, larger --chunksize or increase max chunks setting.
(这里显示的单位是扇区,即 512 字节)
这里 cache 需要记录缓存数据的访问情况,记录的单位就是 chunk(需要是 32K 的倍数)。默认情况下,chunk 大小设置为 64KB。 并且 LVM 推荐 chunk 不超过一百万个(这也是 allocation/cache_pool_max_chunks 设置的默认值)。 换句话讲,如果保持默认设置,那么后备数据最大只能有 64KB * 1,000,000 ~= 64GB << 1.5T,这显然是不够的。
直觉来讲,既然 chunk 推荐不超过一百万个,那就拉高 chunk size?但是需要考虑这两个问题:
- 如果访问的模式不那么连续,那么更大的 chunk size 就势必导致更多的数据在缓存和后备设备之间来回传输。(命中率降低)
- SSD 的写入量是有限制的。上面一点中提到的命中率降低的问题会导致更多的写入,最终导致 SSD 的寿命缩短。
因此更好的选择是以(相对更能接受的)一些额外的 overhead 为代价,增加 chunk 数量。具体的「代价」在后续创建后,可以在内核日志里面看到:
[2030783.641796] device-mapper: cache: You have created a cache device with a lot of individual cache blocks (12578624)
All these mappings can consume a lot of kernel memory, and take some time to read/write.
Please consider increasing the cache block size to reduce the overall cache block count.
考虑到在服务器场合,内存一般都是足够(甚至过量)的,因此唯一可能需要考虑的就是额外的延迟了。
@taoky: 真实事件的教训
最开始的时候,mirrors 的 chunk size 设置为了 1M,结果过了两年多就发现 SSD 快挂了。 查看统计发现 SSD 每小时读取 0.1T,但是写 1T 数据……
因为经历过 SSD 被 lvmcache 磨没的事件,所以我个人的看法是, 在 201 中,提及这一点(以及后面会介绍 lvmcache 还有其他的坑)是很重要的——有些方案放大规模之后,就会出现意想不到的问题。
$ sudo lvconvert --type cache --cachevol lvdata_cache --config allocation/cache_pool_max_chunks=25148800 vg201-test/lvdata
Erase all existing data on vg201-test/lvdata_cache? [y/n]: y
WARNING: Configured cache_pool_max_chunks value 25148800 is higher then recommended 1000000.
Logical volume vg201-test/lvdata is now cached.
64K 也是目前 mirrors 机器使用的 chunk size。
当然也可以把 chunk size 略微调大到 128K,这样的话 chunk 就少一些:
$ sudo lvconvert --type cache --cachevol lvdata_cache --chunksize 128K --config allocation/cache_pool_max_chunks=12578624 vg201-test/lvdata
Erase all existing data on vg201-test/lvdata_cache? [y/n]: y
WARNING: Configured cache_pool_max_chunks value 12578624 is higher then recommended 1000000.
Logical volume vg201-test/lvdata is now cached.
可能需要进行一些性能测试来权衡 chunk size 带来的影响——考虑到本地测试时稀疏文件等因素,实际的性能测试可能需要在真实的环境中进行。
此外,在 lvconvert
创建缓存时,如果 SSD 设备不支持 TRIM(常见的场景是在 RAID 卡后面),那么其会清零对应的块,这个过程可能会花费超过半个小时的时间。
Too dirty to use¶
lvmcache 方案的一个无法忽视的弊端是:即使模式设置为 writethrough,如果没有干净地卸载,那么在下次加载后,缓存中所有的块都会被标记为脏块。 更加致命的是,在生产负载下,可能会出现脏块写回在默认情况下极其缓慢的问题(即使设置 policy 为 cleaner),以至于可能过了几个小时都没有迁移任何一个块。
如何实验复现「所有块被标记」的行为?
由于没有能够强制卸载本地回环的方法,因此这里可以考虑的思路是:
在虚拟机中使用本地回环设备创建 lvmcache,写入并读取一些数据(单纯的写入一次可能不会使用缓存块空间),然后使用 reboot -f
强制重启。
如果使用的块够多,并且操作及时,可能就能看到 dirty block > 0 的情况:
$ sudo losetup -f --show hdd.img
/dev/loop0
$ sudo losetup -f --show ssd.img
/dev/loop1
$ sudo lvs -a -o devices,cache_policy,cachemode,cache_settings,cache_total_blocks,cache_used_blocks,cache_dirty_blocks,cache_read_hits,cache_read_misses,cache_write_hits,cache_write_misses
Devices CachePolicy CacheMode CacheSettings CacheTotalBlocks CacheUsedBlocks CacheDirtyBlocks CacheReadHits CacheReadMisses CacheWriteHits CacheWriteMisses
lvdata_corig(0) smq writethrough 163584 62544 4918 120 0 0 0
/dev/loop1(0)
/dev/loop0(0)
另外在本地测试时发现,如果创建 cache 之后过短暂的时间后重启,LVM 调用的旧版本的 cache 检查工具可能会认为 cache LV 的结构不正确,
此时 LVM 无法挂载4 cache,只能用一些 trick 来 uncache 掉这个坏掉的 LV
(cachevol 可能会麻烦一些,需要先使用 dmsetup remove
移除多余的卷;cachepool 可以直接 uncache)。
或者将 thin-provisioning-tools 升级至 1.0.12 以上。
来自 linux-lvm 邮件列表的可能相关的故障报告参见 https://lore.kernel.org/all/[email protected]/T/。
最新的内核可能部分修复了后一个问题
参见 https://github.com/torvalds/linux/commit/1e4ab7b4c881cf26c1c72b3f56519e03475486fb。 根据该 commit 的描述,在 cleaner 状态下即使 IO idle 为 false,也会进行脏块迁移。
lvmcache 的设计
从前面的统计数据可以注意到,脏块的数量是一个指标。在 lvmcache 的设计中,存在出现模式为 writethrough 并且存在脏块的可能,所以目前程序上没有实现看到 writethrough 之后 就忽略脏块的问题。
模拟在 IO 压力下迁移缓慢的情况
格式化我们刚才创建的有 cache 的 LV,使用 fio
上点压力:
$ sudo fio --filename=./test --filesize=2G --direct=1 --rw=randrw --bs=4k --ioengine=libaio --iodepth=256 --runtime=120 --numjobs=4 --time_based --group_reporting --name=job_name --eta-newline=1
同时做切换 cachemode 的操作:
$ sudo lvchange --cachemode writeback vg201-test/lvdata
WARNING: repairing a damaged cachevol is not yet possible.
WARNING: cache mode writethrough is suggested for safe operation.
Continue using writeback without repair?y
Logical volume vg201-test/lvdata changed.
$ sudo lvchange --cachemode writethrough vg201-test/lvdata
Flushing 16401 blocks for cache vg201-test/lvdata.
Flushing 16405 blocks for cache vg201-test/lvdata.
Flushing 12309 blocks for cache vg201-test/lvdata.
Flushing 8213 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 6481 blocks for cache vg201-test/lvdata.
Flushing 2411 blocks for cache vg201-test/lvdata.
Logical volume vg201-test/lvdata changed.
可以注意到卡在了 Flushing 6481 blocks
比较长的时间。测试环境为笔记本电脑的 NVMe SSD,如果是实际的 HDD + 较大负载的话,问题会严重得多。
@taoky: 其他信息
这可能是对于较大缓存、较重的负载下 lvmcache 最大的问题了。 关于下面处理不方便的问题,我在 lvm2 仓库提交了 issue: https://github.com/lvmteam/lvm2/issues/141, 希望能够在 lvm 工具中实现绕过 flush dirty block 的操作。
不过最佳的解决方法可能还是给 kernel 交个 patch,如果目前状态是 writethrough 并且 dirty = 0 那么给某个 struct 设一个特殊的 bit。但是感觉不知道如何下手,只能以后再说了。
根据文档,migration_threshold
参数控制每次迁移脏块的扇区数量。相关的 bug report 建议将这个值设置为 chunk size 的至少 8 倍(默认为 2048,对应 1M)。
在调试问题时发现,修改这个值可以实现强制迁移(但是迁移时所有相关的 IO 操作都会暂停),脚本类似这样:
# dirty hack
sudo lvchange --cachepolicy cleaner lug/repo
for i in `seq 1 1500`; do sudo lvchange --cachesettings migration_threshold=2113536 lug/repo && sudo lvchange --cachesettings migration_threshold=16384 lug/repo && echo $i && sleep 15; done;
# 需要确认没有脏块。如果还有的话继续执行(次数调小一些)
# 如果是从 writeback 切换,需要先把模式切到 writethrough
# 然后再修改 cachepolicy 到 smq
sudo lvchange --cachepolicy smq lug/repo
在我们的配置下,写全部脏块操作(包括中间的 sleep
操作在内)需要大约 10 个小时。
GRUB 可能无法处理自定义的 migration_threshold 等属性
参考 patch: https://github.com/taoky/grub/commit/484b718831ab3ca034bb5ea3624a85efeb5bf2ba。
相关的备注信息:https://blog.taoky.moe/attachments/2021-04-17-tunight/show.html#21。
虽然这个 patch 也有问题,并且 GRUB 的开发非常不活跃,所以可能一直都要自己编译 GRUB 了。
我们目前的建议是在计划重启(维护窗口)前手动卸载缓存,在重启后再挂载(之前维护时观察到,即使正常关机,也可能出现脏块的问题)。 另一种方式是:(在确认没有事实上的脏块的前提下)手动修改 LVM 元数据,把缓存扔掉。
小心操作
对每次 LVM 操作,lvm 的工具都会在 /etc/lvm/archive
备份操作前的元数据信息,同时在 /etc/lvm/backup
存储当前的元数据,但是还是尽量小心,以免酿成悲剧。
有 cache 的 VG 元数据例子
# Generated by LVM2 version 2.03.23(2) (2023-11-21): Sun Feb 18 00:25:36 2024
contents = "Text Format Volume Group"
version = 1
description = "Created *after* executing 'lvconvert --type cache --cachevol lvdata_cache --config allocation/cache_pool_max_chunks=25148800 vg201-test/lvdata'"
creation_host = "shimarin.taoky.moe" # Linux shimarin.taoky.moe 6.6.10-arch1-1 #1 SMP PREEMPT_DYNAMIC Fri, 05 Jan 2024 16:20:41 +0000 x86_64
creation_time = 1708187136 # Sun Feb 18 00:25:36 2024
vg201-test {
id = "kDKbJ2-kebs-HaIJ-4Vfj-E4OB-aCce-yEcdAy"
seqno = 34
format = "lvm2" # informational
status = ["RESIZEABLE", "READ", "WRITE"]
flags = []
extent_size = 8192 # 4 Megabytes
max_lv = 0
max_pv = 0
metadata_copies = 0
physical_volumes {
pv0 {
id = "VfZ83M-BPwe-bIQ1-JJRO-ZBSf-ks8d-fMln8E"
device = "/dev/loop0" # Hint only
status = ["ALLOCATABLE"]
flags = []
dev_size = 139586437120 # 65 Terabytes
pe_start = 2048
pe_count = 17039359 # 65 Terabytes
}
pv1 {
id = "8hZXeS-gZJX-OfrY-vfUm-tG1R-pw3V-6c3CBI"
device = "/dev/loop1" # Hint only
status = ["ALLOCATABLE"]
flags = []
dev_size = 3221225472 # 1.5 Terabytes
pe_start = 2048
pe_count = 393215 # 1.5 Terabytes
}
}
logical_volumes {
lvdata {
id = "6BsYbX-T21Z-BgiW-pZET-a3Lf-wcSh-E5hSm2"
status = ["READ", "WRITE", "VISIBLE"]
flags = []
creation_time = 1708181300 # 2024-02-17 22:48:20 +0800
creation_host = "shimarin.taoky.moe"
segment_count = 1
segment1 {
start_extent = 0
extent_count = 17039359 # 65 Terabytes
type = "cache+CACHE_USES_CACHEVOL"
cache_pool = "lvdata_cache_cvol"
origin = "lvdata_corig"
metadata_format = 2
chunk_size = 128
cache_mode = "writethrough"
policy = "smq"
metadata_start = 0
metadata_len = 2170880
data_start = 2170880
data_len = 3219046400
}
}
lvdata_cache_cvol {
id = "RuL2He-0xlL-J4fd-T0eD-2YTr-QASe-gsWtBV"
status = ["READ", "WRITE"]
flags = ["CACHE_VOL"]
creation_time = 1708187133 # 2024-02-18 00:25:33 +0800
creation_host = "shimarin.taoky.moe"
segment_count = 1
segment1 {
start_extent = 0
extent_count = 393215 # 1.5 Terabytes
type = "striped"
stripe_count = 1 # linear
stripes = [
"pv1", 0
]
}
}
lvdata_corig {
id = "xIprwR-KD4I-f6E8-tdgp-lcjN-iUdk-rS63YR"
status = ["READ", "WRITE"]
flags = []
creation_time = 1708187136 # 2024-02-18 00:25:36 +0800
creation_host = "shimarin.taoky.moe"
segment_count = 1
segment1 {
start_extent = 0
extent_count = 17039359 # 65 Terabytes
type = "striped"
stripe_count = 1 # linear
stripes = [
"pv0", 0
]
}
}
}
}
没有 cache 的 VG 元数据例子
以下仅展示 logical_volumes
部分:
# (省略)
vg201-test {
# (省略)
logical_volumes {
lvdata {
id = "6BsYbX-T21Z-BgiW-pZET-a3Lf-wcSh-E5hSm2"
status = ["READ", "WRITE", "VISIBLE"]
flags = []
creation_time = 1708181300 # 2024-02-17 22:48:20 +0800
creation_host = "shimarin.taoky.moe"
segment_count = 1
segment1 {
start_extent = 0
extent_count = 17039359 # 65 Terabytes
type = "striped"
stripe_count = 1 # linear
stripes = [
"pv0", 0
]
}
}
lvdata_cache {
id = "RuL2He-0xlL-J4fd-T0eD-2YTr-QASe-gsWtBV"
status = ["READ", "WRITE", "VISIBLE"]
flags = []
creation_time = 1708187133 # 2024-02-18 00:25:33 +0800
creation_host = "shimarin.taoky.moe"
segment_count = 1
segment1 {
start_extent = 0
extent_count = 393215 # 1.5 Terabytes
type = "striped"
stripe_count = 1 # linear
stripes = [
"pv1", 0
]
}
}
}
}
首先对比 archive 和 backup 中的元数据,确认无误之后使用 vgcfgrestore
恢复元数据。
某些情况下,可能需要编辑元数据文件以符合实际情况(例如在创建缓存之后又做了其他操作)。
$ sudo vgcfgrestore -f /etc/lvm/archive/vg201-test_00084-792872325.vg vg201-test
Volume group vg201-test has active volume: lvdata.
Volume group vg201-test has active volume: lvdata_cache_cvol.
Volume group vg201-test has active volume: lvdata_cache_cvol.
Volume group vg201-test has active volume: lvdata_cache_cvol.
Volume group vg201-test has active volume: lvdata_corig.
WARNING: Found 5 active volume(s) in volume group "vg201-test".
Restoring VG with active LVs, may cause mismatch with its metadata.
Do you really want to proceed with restore of volume group "vg201-test", while 5 volume(s) are active? [y/n]: y
Restored volume group vg201-test.
$ sudo lvs -a
WARNING: Detected cache segment type does not match expected type striped for vg201-test/lvdata.
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
lvdata vg201-test -wi-XX--X- <65.00t
lvdata_cache vg201-test -wi-a----- <1.50t
在重新 activate 之后,状态即恢复正常:
$ sudo vgchange -an vg201-test
0 logical volume(s) in volume group "vg201-test" now active
$ sudo vgchange -ay vg201-test
2 logical volume(s) in volume group "vg201-test" now active
$ sudo lvs -a
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
lvdata vg201-test -wi-a----- <65.00t
lvdata_cache vg201-test -wi-a----- <1.50t
缓存方案比较¶
作为 SSD 缓存部分的最后一小节,本部分以表格形式介绍已有的 SSD 缓存方案(包括已经不再维护的)。 我们建议无论选择何种方案,都需要先测试其是否易于使用,是否会给运维操作带来额外的负担。
方案 | 缓存模式 | 缓存算法 | 简介 | 上次维护时间2 |
---|---|---|---|---|
lvmcache | writethrough, writeback | smq | 与 LVM 集成的缓存方案,基于内核的 dm-cache | 7 个月前 |
bcache | writethrough, writeback, writearound | lru, fifo, random | 已在内核中的稳定 cache 方案 | 2 个月前 |
ZFS ARC + L2ARC | 类似 writearound | ARC | ZFS 自带的缓存。ARC 以内存为缓存;L2ARC 作为第二级缓存,使用 SSD,用于在高负载情况下支撑 IOPS,命中率较低 | 随 ZFS 开发 |
EnhanceIO | readonly (writearound), writethrough, writeback | lru, fifo, random | 早期的 SSD 缓存方案 | ☠️ 9 年前 |
Flashcache | writethrough, writeback, writearound | fifo, lru | Facebook 开发的早期 SSD 缓存方案 | ☠️ 7 年前 |
OpenCAS | writethrough, writeback, writearound, write-invalidate, write-only | lru (?) | SPDK 的一部分 | 3 个月前 |
bcachefs | writethrough, writeback, writearound | lru3 | 由 bcache 作者开发的新 CoW 文件系统,内置 SSD 缓存支持 | 活跃开发 |
集群存储¶
LVM 支持多机共享存储。在这种场景下,集群中的服务器通过 iSCSI 等方式连接到同一台共享的存储,并且通过锁等机制实现集群内部的同步。
LVM 自带的 locking 机制为 lvmlockd
,支持 dlm
(需要配置 dlm 与 corosync 构建集群)和 sanlock
两种后端。
不过很可惜的是,我们唯一使用到集群存储的地方是 Proxmox VE 虚拟机,而 PVE 使用的是另一套方案:
PVE 自带的集群管理功能使用了 corosync
维护了一个集群内部的全局锁,所有使用 PVE 工具修改存储的操作都会先获取这个全局锁。
并且 PVE 不存在多台机器访问同一个 LV 的情况,因此这一套方案不依赖于 lvmlockd
。
确保所有访问 LVM 的机器在同一个 PVE 集群中
否则在集群外的虚拟机创建等操作不会正确获取锁,导致覆盖已有的虚拟机磁盘。 相关故障案例见 https://vlab.ibugone.com/servers/ct100/#%E6%95%85%E9%9A%9C。
LVM 集群不支持精简置备 LV
在虚拟化场景下,一个常见的节省空间的做法是使用精简置备(thin-provisioned)的存储, 这么做可以在创建 LV 时只分配少量的空间,然后在需要的时候再分配更多的空间。 但是 LVM 集群不支持这种操作(因此每个虚拟机都要实打实地占用对应的空间)。 如果需要节约空间,可能需要考虑其他方案(例如部分企业级 SAN 支持类似于「虚拟地址」的功能,可以在需要的时候再分配空间)。
-
推荐查看最新版本的 RHEL 手册进行阅读,因为新版本可能包含一些新特性,并且 Debian 的版本更新比 RHEL 更快。本链接指向目前最新的 RHEL 9 的 LVM 手册。 ↩
-
Retrieved on 2024-02-18. ↩
-
"Buckets containing only cached data are discarded as needed by the allocator in LRU order" (bcachefs: Principles of Operation 2.2.4) ↩
-
Cache 的完整性检查工具
cache_check
位于 thin-provisioning-tools 中。其 1.0 版本使用 Rust 重写后存在一个 bug,会导致即使检查失败,LVM 也会继续尝试挂载。该问题在 1.0.12 被修复。 ↩
@taoky: 另一种做法
在配置 mirrors4 服务器的缓存时,我们的文档中的做法是把 SSD 和 HDD 分别开 VG,在后面创建好之后再
vgmerge
。这么做其实是没有必要的,只要
lvcreate
的时候清醒一些就行。 况且缓存盘和后备设备必须在同一个 VG 里头。