В наши дни управлять томами в Linux — совершенно тривиальная задача, которая не вызывает в инженере ни чувства азарта, ни жажды исследования. Наборами команд, инструкциями по созданию томов и снэпшотов — ими же кишит весь интернет. Тем не менее, беспокойный инженерный ум требует разобраться, как же это работает под капотом. А происходит это весьма интересно. Но для начала, как обычно, позвольте небольшую историческую справку (на 40 минут). Приятного чтения :-)

Как было раньше?
Если мы отмотаем время назад и посмотрим, как выглядела работа с дисками в ее первозданном виде (когда деревья были высокими, а трава зеленее), то здесь сюрпризов нет: блочное устройство разделяется на партиции, на них создается файловая система (или не создается –– работа с блочными устройствами такая же неотъемлемая часть Linux).
На этом этапе нам интересно освежить несколько моментов:
Момент 1
Если мы выполняем ls для блочных устройств, то там видны пары цифр:
# ls -la /dev/sda* brw-rw----. 1 root disk 8, 0 Apr 18 11:54 /dev/sda brw-rw----. 1 root disk 8, 1 Apr 18 11:54 /dev/sda1 brw-rw----. 1 root disk 8, 2 Apr 18 11:54 /dev/sda2 brw-rw----. 1 root disk 8, 3 Apr 18 11:54 /dev/sda3
Это не что иное как, пара major и minor номера. Major указывает на драйвер, работающий с данным устройством, а minor — порядковый номер устройства:
# grep sd /proc/devices 8 sd 65 sd 66 sd . . . .
Момент 2
Из этого происходит и полное имя блочного устройства:
sd — имя драйвера;
a — порядковый номер диска;
1, 2, 3, 4… — порядковый номер партиции.
Момент 3
Если мы посмотрим на диски пристальнее, то заметим: на каждое блочное устройство резервируется 15 minor-номеров. Один — на имя всего диска, остальные — под партиции:
[root@cluster-2 ~]# ls -la /dev/sd* brw-rw----. 1 root disk 8, 0 Apr 18 11:54 /dev/sda brw-rw----. 1 root disk 8, 1 Apr 18 11:54 /dev/sda1 brw-rw----. 1 root disk 8, 2 Apr 18 11:54 /dev/sda2 brw-rw----. 1 root disk 8, 3 Apr 18 11:54 /dev/sda3 brw-rw----. 1 root disk 8, 16 Apr 18 11:54 /dev/sdb brw-rw----. 1 root disk 8, 32 Apr 18 11:54 /dev/sdc brw-rw----. 1 root disk 8, 48 Apr 18 11:54 /dev/sdd brw-rw----. 1 root disk 8, 64 Apr 18 11:54 /dev/sde brw-rw----. 1 root disk 8, 80 Apr 18 11:54 /dev/sdf brw-rw----. 1 root disk 8, 96 Apr 18 11:54 /dev/sdg brw-rw----. 1 root disk 8, 112 Apr 18 11:54 /dev/sdh
Почему резервируется столько minor-номеров, если можно создать только четыре primary-партиции? Это связано с extended-партициями. Причем если рассматривать их максимальное количество, то вопрос «Почему так много?» сменяется противоположным «Почему так мало?». Добавим еще один диск и посмотрим, что же можно «выжать» из него:
# ls -la /dev/sd* brw-rw----. 1 root disk 8, 0 Apr 18 11:54 /dev/sda brw-rw----. 1 root disk 8, 1 Apr 18 11:54 /dev/sda1 brw-rw----. 1 root disk 8, 2 Apr 18 11:54 /dev/sda2 brw-rw----. 1 root disk 8, 3 Apr 18 11:54 /dev/sda3 brw-rw----. 1 root disk 8, 16 Apr 18 11:54 /dev/sdb brw-rw----. 1 root disk 8, 32 Apr 18 11:54 /dev/sdc brw-rw----. 1 root disk 8, 48 Apr 18 11:54 /dev/sdd brw-rw----. 1 root disk 8, 64 Apr 18 11:54 /dev/sde brw-rw----. 1 root disk 8, 80 Apr 18 11:54 /dev/sdf brw-rw----. 1 root disk 8, 96 Apr 18 11:54 /dev/sdg brw-rw----. 1 root disk 8, 112 Apr 18 11:54 /dev/sdh brw-rw----. 1 root disk 8, 128 Apr 21 18:38 /dev/sdi
Начинаем делать extended-партиции и видим, что список minor-номеров продлился:
# ls -la /dev/sdi* brw-rw----. 1 root disk 8, 128 Apr 21 18:55 /dev/sdi brw-rw----. 1 root disk 8, 129 Apr 21 18:55 /dev/sdi1 brw-rw----. 1 root disk 8, 138 Apr 21 18:55 /dev/sdi10 brw-rw----. 1 root disk 8, 139 Apr 21 18:55 /dev/sdi11 brw-rw----. 1 root disk 8, 140 Apr 21 18:55 /dev/sdi12 brw-rw----. 1 root disk 8, 141 Apr 21 18:55 /dev/sdi13 brw-rw----. 1 root disk 8, 142 Apr 21 18:55 /dev/sdi14 brw-rw----. 1 root disk 8, 143 Apr 21 18:55 /dev/sdi15 brw-rw----. 1 root disk 259, 4 Apr 21 18:55 /dev/sdi16 brw-rw----. 1 root disk 259, 5 Apr 21 18:55 /dev/sdi17 brw-rw----. 1 root disk 259, 6 Apr 21 18:55 /dev/sdi18 brw-rw----. 1 root disk 259, 7 Apr 21 18:55 /dev/sdi19 brw-rw----. 1 root disk 8, 130 Apr 21 18:55 /dev/sdi2 brw-rw----. 1 root disk 8, 131 Apr 21 18:55 /dev/sdi3 brw-rw----. 1 root disk 8, 132 Apr 21 18:55 /dev/sdi4 brw-rw----. 1 root disk 8, 133 Apr 21 18:55 /dev/sdi5 brw-rw----. 1 root disk 8, 134 Apr 21 18:55 /dev/sdi6 brw-rw----. 1 root disk 8, 135 Apr 21 18:55 /dev/sdi7 brw-rw----. 1 root disk 8, 136 Apr 21 18:55 /dev/sdi8 brw-rw----. 1 root disk 8, 137 Apr 21 18:55 /dev/sdi9
И их максимальное количество может быть 60. В исходном коде драйвера sd есть строки, описывающие механизм резервации:
Inside a major, we have 16k disks, however mapped non- * contiguously. The first 16 disks are for major0, the next * ones with major1, ... Disk 256 is for major0 again, disk 272 * for major1, ... * As we stay compatible with our numbering scheme, we can reuse * the well-know SCSI majors 8, 65--71, 136--143. */
Момент 4
Раньше мы могли создать достаточно большое количество партиций и решить проблему c делением диска классическим способом.
Примечание: Не забываем про динамическую природу именования устройств в Linux. В зависимости от последовательности, в которой ОС инициализировала устройства, их имена могут изменяться. И так то, что было, например, /dev/sdi, станет /dev/sdb, а после очередной перезагрузки — наоборот.
Как решили проблему объединения
Но что если нам, наоборот, нужно объединить несколько дисков в одно большое блочное устройство?
Итак, теперь мы решаем задачу объединения. Можно закрыть много вопросов аппаратным RAID-контроллером, но это слишком просто и, относительно 0 рублей, затрачиваемых на Software-RAID, дорого.
Долгое время штатным решением считалось использование модуля md (multiple devices) и создание, например, конкатенации (concatenation).
Примечание автора: иногда stripe и concat относят к RAID0 (например, в Solaris):
# mdadm --create --verbose /dev/md0 --level=linear --raid-devices=2 /dev/sdi1 /dev/sdi2 mdadm: Defaulting to version 1.2 metadata mdadm: array /dev/md0 started.
Посмотрим на получившееся устройство:
# ls -la /dev/md0 brw-rw----. 1 root disk 9, 0 Apr 22 14:03 /dev/md0
А кто же обслуживает major под номером 9?
# grep md /proc/devices 9 md 254 mdp
Вроде эту задача решили, и можно так жить дальше. Расходимся. К нашей радости, ИТ-мир гораздо более сознателен и не останавливается на половине пути, за что мы его и любим.
Logical Volume Manager (LVM)
В «кровавом Enterprise» работа с дисками давно была организована на принципиально новом уровне, но за деньги:
1) Мы могли разметить диски для использования LVM (инициализировать их).
2) Диски объединялись в логические группы на основании, например, приложения, работающего с ними (дисковые группы).
3) В этих логических группах создавались уже сами тома различной логики (RAID, размещение на дисках с учетом местоположения дисков на SCSI-контроллерах).
Решить все перечисленные задачи в Linux стало возможно с помощью карты device-mapper.
Примечание: device-mapper — это модуль ядра, предоставляющий фреймворк для работы с дисковыми устройствами. На основании этих карт LVM создает свои устройства.
Чем же занимается device-mapper? Он может из «кусков» блочных устройств создавать новое виртуальное устройство, которое для ОС выглядит как совершенно обычное блочное устройство. Работает это следующим образом: приложения/команды используют библиотеку libdevmapper, которая при помощи команд ioctl передает данные в устройство /dev/mapper/control
# strace dmsetup ls execve("/usr/sbin/dmsetup", ["dmsetup", "ls"], 0x7fff2bea9258 /* 25 vars */) = 0 brk(NULL) = 0x55e3c31ee000 arch_prctl(0x3001 /* ARCH_??? */, 0x7ffef62227b0) = -1 EINVAL (Invalid argument) access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=48415, ...}) = 0 mmap(NULL, 48415, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f3c1e240000 close(3) = 0 openat(AT_FDCWD, "/lib64/libdevmapper.so.1.02", O_RDONLY|O_CLOEXEC) = 3 . . . . . . . . . . . . . . mmap(NULL, 337024, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f3c1e1dd000 close(3) = 0 uname({sysname="Linux", nodename="master", ...}) = 0 stat("/dev/mapper/control", {st_mode=S_IFCHR|0600, st_rdev=makedev(0xa, 0xec), ...}) = 0 openat(AT_FDCWD, "/dev/mapper/control", O_RDWR) = 3 openat(AT_FDCWD, "/proc/devices", O_RDONLY) = 4 fstat(4, {st_mode=S_IFREG|0444, st_size=0, ...}) = 0 read(4, "Character devices:\n 1 mem\n 4 /"..., 1024) = 550 close(4) = 0

По сути, каждый блок вашего виртуального устройства может быть произвольным блоком физического. Главное — правильно описать его в карте, чтобы избежать повторов и т.д.

Карты device-mapper
Мы можем создать файл-описание нашего виртуального устройства со следующим содержимым:
0 100000 linear 8:64 0
Это следует читать так: «создаем виртуальное устройство, с нулевого блока этого виртуального устройства используем 100 000 блоков последовательно (линейно) c устройства 8:64 (major:minor) с его начала, без отступа (offset)». Небольшая подсказка про то, какое устройство будет использовано:
# ls -la /dev/sd* brw-rw---- 1 root disk 8, 0 May 8 13:24 /dev/sda brw-rw---- 1 root disk 8, 1 May 8 13:24 /dev/sda1 brw-rw---- 1 root disk 8, 2 May 8 13:24 /dev/sda2 brw-rw---- 1 root disk 8, 16 May 8 13:24 /dev/sdb brw-rw---- 1 root disk 8, 32 May 8 13:24 /dev/sdc brw-rw---- 1 root disk 8, 48 May 8 13:24 /dev/sdd brw-rw---- 1 root disk 8, 64 May 18 22:01 /dev/sde brw-rw---- 1 root disk 8, 80 May 18 22:01 /dev/sdf echo "0 100000 linear 8:64 0" > /tmp/my-linear-device
Теперь выполняем команду dmsetup:
[root@oel78 ~]# dmsetup create my-linear-device /tmp/my-linear-device
Проверяем, появилось ли новое устройство:
#ls -la /dev/mapper/ total 0 drwxr-xr-x 2 root root 120 May 18 22:21 . drwxr-xr-x 19 root root 3400 May 18 22:21 .. crw------- 1 root root 10, 236 May 8 13:24 control lrwxrwxrwx 1 root root 7 May 18 22:21 my-linear-device -> ../dm-2 lrwxrwxrwx 1 root root 7 May 8 13:24 cl-root -> ../dm-0 lrwxrwxrwx 1 root root 7 May 8 13:24 cl-swap -> ../dm-1
Помните — ядро позволяет задавать произвольные имена, но делает это как symlink на последовательно создаваемые устройства dm-*.
Теперь с этим устройством можно работать как с обычным диском. Как вы видите, 100 000 блоков на месте:
# fdisk /dev/mapper/my-linear-device Welcome to fdisk (util-linux 2.23.2). Changes will remain in memory only, until you decide to write them. Be careful before using the write command. Command (m for help): p Disk /dev/mapper/my-linear-device: 51 MB, 51200000 bytes, 100000 sectors Units = sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disk label type: dos Disk identifier: 0x4abaf638 Device Boot Start End Blocks Id System Command (m for help):
Мы также можем сделать файловую систему на нем:
[root@oel78 ~]# mkfs.xfs -f /dev/mapper/my-linear-device meta-data=/dev/mapper/my-linear-device isize=256 agcount=2, agsize=6250 blks = sectsz=512 attr=2, projid32bit=1 = crc=0 finobt=0, sparse=0 data = bsize=4096 blocks=12500, imaxpct=25 = sunit=0 swidth=0 blks naming =version 2 bsize=4096 ascii-ci=0 ftype=1 log =internal log bsize=4096 blocks=853, version=2 = sectsz=512 sunit=0 blks, lazy-count=1 realtime =none extsz=4096 blocks=0, rtextents=0
смонтировать ее:
# mount /dev/mapper/my-linear-device /mnt
Проверить ее размер:
# df -h Filesystem Size Used Avail Use% Mounted on devtmpfs 12G 0 12G 0% /dev tmpfs 12G 0 12G 0% /dev/shm tmpfs 12G 42M 12G 1% /run tmpfs 12G 0 12G 0% /sys/fs/cgroup /dev/mapper/cl-root 33G 31G 2.0G 94% / /dev/mapper/my-linear-device 46M 2.6M 43M 6% /mnt tmpfs 2.4G 44K 2.4G 1% /run/user/0 /dev/sr0 4.6G 4.6G 0 100% /run/media/root/OL-7.9 Server.x86_64
В списке карт device-mapper есть запись и о нашей карте:
# dmsetup table my-linear-device: 0 100000 linear 8:64 0 cl-swap: 0 2097152 linear 8:2 2048 cl-root: 0 122904576 linear 8:2 2099200 lab-test--vol: 0 4194304 linear 8:112 2048
В системе уже есть linear устройства. Они созданы при инсталляции в VG с именем cl, а также во второй VG с именем lab уже руками.
Главное различие карт device-mapper, созданных вручную и через LVM, заключается в том, что созданное вручную живет до первой перезагрузки, а LVMсохраняет свою конфигурацию:
# ls -la /etc/lvm/ total 132 drwxr-xr-x. 6 root root 100 Jun 6 2024 . drwxr-xr-x. 152 root root 12288 Nov 20 23:17 .. drwx------. 2 root root 4096 Oct 30 11:47 archive drwx------. 2 root root 4096 Oct 30 11:47 backup drwx------. 2 root root 6 Mar 11 2021 cache -rw-r--r--. 1 root root 103867 Mar 11 2021 lvm.conf -rw-r--r--. 1 root root 2301 Mar 11 2021 lvmlocal.conf drwxr-xr-x. 2 root root 245 Jun 6 2024 profile # ls -la /etc/lvm/backup/ total 56 drwx------. 2 root root 4096 Oct 30 11:47 . drwxr-xr-x. 6 root root 100 Jun 6 2024 .. -rw-------. 1 root root 1736 Jun 6 2024 cl -rw------- 1 root root 1297 Oct 30 11:47 lab
Заглянем внутрь файла /etc/lvm/backup/lab:
Скрытый текст
# Generated by LVM2 version 2.03.11(2)-RHEL8 (2021-01-28): Wed Oct 30 11:47:48 2024 contents = "Text Format Volume Group" version = 1 description = "Created *after* executing 'pvscan --cache --activate ay 8:112'" creation_host = "master" # Linux master 4.18.0-348.7.1.el8_5.x86_64 #1 SMP Wed Dec 22 13:25:12 UTC 2021 x86_64 creation_time = 1730278068 # Wed Oct 30 11:47:48 2024 lab { id = "GIYURR-zK0a-WPp2-Grqh-3OTM-Likw-4hvWEO" seqno = 4 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 = "twNfpj-oa9b-IscO-WNB0-Bhqu-eCQE-y3R1c6" device = "/dev/sdh" # Hint only status = ["ALLOCATABLE"] flags = [] dev_size = 46137344 # 22 Gigabytes pe_start = 2048 pe_count = 5631 # 21.9961 Gigabytes } } logical_volumes { test-vol { id = "CPAXjT-gLOV-yrF6-cR8D-6X3x-6NyT-zNyihQ" status = ["READ", "WRITE", "VISIBLE"] flags = [] creation_time = 1730213672 # 2024-10-29 17:54:32 +0300 creation_host = "master" segment_count = 1 segment1 { start_extent = 0 extent_count = 512 # 2 Gigabytes type = "striped" stripe_count = 1 # linear stripes = [ "pv0", 0
И теперь для наглядности еще раз конфигурация карты device-mapper:
lab-test--vol: 0 4194304 linear 8:112 2048
Легко заметить аналогии, а если заняться математикой, то их станет еще больше, т.к. 2 GiB (2147483648) / 512 = 4194304
# ls -la /dev/sdh brw-rw---- 1 root disk 8, 112 Nov 20 22:43 /dev/sdh
Т.е. LVM просто хранит конфигурацию VG в файле. Но только ли в файле?
LVM умеет передавать VGмежду хостами (export/import). Логично предположить, что конфигурация где-то записывается на диске. Так и есть:
Скрытый текст
# dd if=/dev/sdh of=/tmp/header count=1 bs=1M 1+0 records in 1+0 records out 1048576 bytes (1.0 MB, 1.0 MiB) copied, 0.0250576 s, 41.8 MB/s # strings /tmp/header LABELONE LVM2 001twNfpjoa9bIscOWNB0BhqueCQEy3R1c6 e LVM2 x[5A%r0N*> pt-c lab { id = "GIYURR-zK0a-WPp2-Grqh-3OTM-Likw-4hvWEO" seqno = 1 format = "lvm2" status = ["RESIZEABLE", "READ", "WRITE"] flags = [] extent_size = 8192 max_lv = 0 max_pv = 0 metadata_copies = 0 physical_volumes { pv0 { id = "twNfpj-oa9b-IscO-WNB0-Bhqu-eCQE-y3R1c6" device = "/dev/sdh" status = ["ALLOCATABLE"] flags = [] dev_size = 46137344 pe_start = 2048 pe_count = 5631 # Generated by LVM2 version 2.03.11(2)-RHEL8 (2021-01-28): Tue Oct 29 17:53:27 2024 contents = "Text Format Volume Group" version = 1 description = "Write from vgcreate lab /dev/sdh." creation_host = "master" # Linux master 4.18.0-348.7.1.el8_5.x86_64 #1 SMP Wed Dec 22 13:25:12 UTC 2021 x86_64 creation_time = 1730213607 # Tue Oct 29 17:53:27 2024 lab { id = "GIYURR-zK0a-WPp2-Grqh-3OTM-Likw-4hvWEO" seqno = 2 format = "lvm2" status = ["RESIZEABLE", "READ", "WRITE"] flags = [] extent_size = 8192 max_lv = 0 max_pv = 0 metadata_copies = 0 physical_volumes { pv0 { id = "twNfpj-oa9b-IscO-WNB0-Bhqu-eCQE-y3R1c6" device = "/dev/sdh" status = ["ALLOCATABLE"] flags = [] dev_size = 46137344 pe_start = 2048 pe_count = 5631 logical_volumes { test-vol { id = "CPAXjT-gLOV-yrF6-cR8D-6X3x-6NyT-zNyihQ" status = ["READ", "WRITE", "VISIBLE"] flags = [] creation_time = 1730213672 creation_host = "master" segment_count = 1 segment1 { start_extent = 0 extent_count = 512 type = "striped" stripe_count = 1 stripes = [ "pv0", 0
и так далее... Т.е. конфигурация хранится еще и на самом диске.
Типы карт device-mapper
Теперь стоит уделить внимание, какими бывают device-mapper. Сейчас карты бывают:
linear
striped
mirror
snapshot и snapshot-origin
error
zero
multipath
crypt и др.
И если большинство из них интуитивно понятны, то некоторые требуют пояснения. Давайте сначала посмотрим на простую карту, а потом обсудим уже различные типы карт и заглянем за кулисы.
Striped — карта, позволяющая получить stripe-устройство (RAID0).
Mirror — классический RAID1.
snapshot и snapshot-origin — карты, участвующие в создании snapshot какого-либо тома. Например:
# lvcreate -s -L 500M -n test-vol-snap lab/test-vol Logical volume "test-vol-snap" created. # lvs LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert root cl -wi-ao---- <58.61g swap cl -wi-ao---- 1.00g test-vol lab owi-aos--- 2.00g test-vol-snap lab swi-a-s--- 500.00m test-vol 0.00
и если опять обратиться к картам, то увидим нечто не очень привычное:
# dmsetup table . . . . . . . . . . . . . . . . . . . . . . . . . . . . lab-test--vol: 0 4194304 snapshot-origin 253:4 lab-test--vol--snap-cow: 0 1024000 linear 8:112 4196352 lab-test--vol--snap: 0 4194304 snapshot 253:4 253:5 P 8 lab-test--vol-real: 0 4194304 linear 8:112 2048
RedHat в документации объясняет, откуда все это берется:
When you create the first LVM snapshot of a volume, four Device Mapper devices are used:
1. A device with a linear mapping containing the original mapping table of the source volume.
2. A device with a linear mapping used as the copy-on-write (COW) device for the source volume; for each write, the original data is saved in the COW device of each snapshot to keep its visible content unchanged (until the COW device fills up).
3. A device with a snapshot mapping combining #1 and #2, which is the visible snapshot volume
4. The "original" volume (which uses the device number used by the original source volume), whose table is replaced by a "snapshot-origin" mapping from device #1.
Вот почему их четыре. Идем дальше.
Multipath — позволяет создать устройство, доступное по нескольким путям с различными настройками:
# multipath -ll mpathb (360014057cac55ee036b4bb084f42edf0) dm-2 LIO-ORG,test-vol size=2.0G features='0' hwhandler='1 alua' wp=rw |-+- policy='service-time 0' prio=50 status=active | `- 34:0:0:0 sdb 8:16 active ready running `-+- policy='service-time 0' prio=50 status=enabled `- 33:0:0:0 sdc 8:32 active ready running # dmsetup table mpathb: 0 4194304 multipath 0 1 alua 2 1 service-time 0 1 2 8:16 1 1 service-time 0 1 2 8:32 1 1
Информацию об остальных типах можно найти здесь, а полный список тут.
Могло ли все быть по-другому?
В Linux существует, как всегда, несколько проектов/решений, и развитие может свернуть на какую-то другую дорогу (привет, SystemD). Так и LVM мог проиграть конкуренцию EVMS, разработанную IBM, но победил.
Вывод
Теперь мы с вами еще немного глубже понимаем работу Linux, что не может нас не радовать. Ну и конечно, плюсик в карму нам обеспечен.
