Доброго времени суток.
Хочу поделиться с сообществом практическим опытом построения системы хранения данных для KVM с использованием md RAID + LVM.

В программе будет:

  • Сборка md RAID 1 из NVMe SSD.
  • Сборка md RAID 6 из SATA SSD и обычных дисков.
  • Особенности работы TRIM/DISCARD на SSD RAID 1/6.
  • Создание загрузочного md RAID 1/6 массива на общем наборе дисков.
  • Установка системы на NVMe RAID 1 при отсутствии поддержки NVMe в BIOS.
  • Использование LVM cache и LVM thin.
  • Использование BTRFS снимков и send/recieve для резервного копирования.
  • Использование LVM thin снимков и thin_delta для резервного копирования в стиле BTRFS.

Если заинтересовало, прошу под кат.

Заявление


Автор не несет никакой ответственности за последствия использования или не использования материалов/примеров/кода/советов/данных из этой статьи. Читая или каким-то образом используя данный материал вы берете на себя ответственность за все последствия от этих действий. К возможным последствиям относятся:

  • Зажаренные до хрустящей корочки NVMe SSD.
  • Полностью израсходованный ресурс записи и выход из строя SSD накопителей.
  • Полная потеря всех данных на всех накопителях, в том числе резервных копий.
  • Неисправное компьютерное железо.
  • Потраченное время, нервы и деньги.
  • Любые другие последствия, которые не перечислены выше.

Железо


В наличии было:


Материнская плата где-то 2013 года выпуска на чипсете Z87 в комплекте с Intel Core i7 / Haswell.

  • Процессор 4 ядра, 8 потоков
  • 32 Гигабайта оперативной памяти DDR3
  • 1 x 16 или 2 x 8 PCIe 3.0
  • 1 x 4 + 1 x 1 PCIe 2.0
  • 6 x 6 GBps SATA 3 разъема

SAS адаптер LSI SAS9211-8I перепрошитый в режим IT / HBA. Прошивка с поддержкой RAID намеренно заменена на прошивку HBA чтобы:

  1. Можно было в любой момент выкинуть этот адаптер и заменить на любой другой первый попавшийся.
  2. Нормально работал TRIM/Discard на дисках, т.к. в RAID прошивке эти команды не поддерживаются совсем, а HBA в общем-то все равно какие команды по шине передавать.

Жесткие диски, — 8 штук HGST Travelstar 7K1000 объемом 1 TB в форм-факторе 2.5, как для ноутбуков. Эти диски ранее были в RAID 6 массиве. В новой системе им тоже найдется применение. Для хранения локальных резервных копий.

Дополнительно было добавлено:


6 штук SATA SSD модели Samsung 860 QVO 2TB. От этих SSD требовался большой объем, наличие SLC кэша, желательна надежность, и, невысокая цена. Обязательна была поддержка discard/zero которая проверяется строчкой в dmesg:

kernel: ata1.00: Enabling discard_zeroes_data

2 штуки NVMe SSD модели Samsung SSD 970 EVO 500GB.

Для этих SSD важна скорость случайного чтения/записи и ресурс под ваши нужды. Радиатор к ним. Обязательно. Совсем обязательно. Иначе, — прожарите их до хрустящей корочки при первой-же синхронизации RAIDa.

Адаптер StarTech PEX8M2E2 для 2 x NVMe SSD с установкой в PCIe 3.0 8x слот. Это, опять-же, просто HBA, но для NVMe. Отличается от дешевых адаптеров отсутствием требования поддержки PCIe bifurcation от материнской платы благодаря наличию встроенного PCIe коммутатора. Будет работать даже в самой древней системе где есть PCIe, даже если это будет x1 PCIe 1.0 слот. Естественно, с соответствующей скоростью. Никаких RAIDов там нет. Встроенного BIOS на борту нет. Так что, ваша система магически не научится загружаться с NVMe и тем более делать NVMe RAID благодаря этому устройству.

Компонент этот был обусловлен исключительно наличием только одного свободного 8x PCIe 3.0 в системе, и, при наличии 2х свободных слотов, легко заменяется на два копеечных PEX4M2E1 или аналоги, которых можно купить где угодно по цене от 600 рублей.

Отказ от всевозможных аппаратных или встроенных в чипсет/BIOS RAIDов был сделан осознанно, с целью иметь возможность полностью заменить всю систему, за исключением самих SSD/HDD, сохранив все данные. В идеале, чтобы можно было сохранить даже установленную операционную систему при переезде на совсем новое/другое железо. Главное чтобы были SATA и PCIe порты. Это как live CD или загрузочная флэшка, только очень быстрая и немного габаритная.

Юмор
А то, знаете как бывает, — иногда нужно срочно взять весь массив с собой на вынос. А данные терять не хочется. Для этого все упомянутые носители удобно располагаются по салазкам в отсеках 5.25 стандартного корпуса.

Ну, и, конечно-же, для экспериментов с разными способами SSD кэширования в Linux.
Аппаратные рэйды, это скучно. Включаешь. Оно или работает, или нет. А с mdadm всегда есть варианты.

Софт


Ранее на железе была установлена Debian 8 Jessie которая близка к EOL. Был собран RAID 6 из выше упомянутых HDD в паре с LVM. На нем крутились виртуальные машины в kvm/libvirt.

Т.к. автор имеет подходящий опыт создания портативных загрузочных SATA/NVMe флэшек, а также, чтобы не рвать привычный apt-шаблон, в качестве целевой системы была выбрана Ubuntu 18.04 которая уже достаточно стабилизировалась, но до сих пор имеет 3 года поддержки в перспективе.

В упомянутой системе присутствуют все необходимые нам драйверы железа из коробки. Никакого стороннего софта и драйверов нам не потребуется.

Подготовка к установке


Для установки системы нам потребуется Ubuntu Desktop Image. У серверной системы какой-то ядреный установщик, который проявляет излишнюю не отключаемую самостоятельность обязательно впихивая UEFI системный раздел на один из дисков портя всю красоту. Соответственно устанавливается оно только в UEFI режиме. Вариантов не предлагает.

Нас это не устраивает.

Почему?
К сожалению, UEFI загрузка крайне плохо совместима с загрузочным программным RAIDом, т.к. резервирования для UEFI ESP раздела нам никто не предлагает. В сети есть рецепты, которые предлагают разместить ESP раздел на флэшке в USB порте, но, это точка отказа. Есть рецепты с использованием программного mdadm RAID 1 с метаданными версии 0.9 которые не мешают UEFI BIOS видеть этот раздел, но, это живет до счастливого момента когда BIOS или другая ОС на железе запишет что-то в ESP забыв синхронизировать на другие зеркала.

Помимо этого, загрузка UEFI зависит от NVRAM, которая не переедет вместе с дисками на новую систему, т.к. является частью материнской платы.

Так что, мы не будем изобретать новый велосипед. У нас уже есть готовый, проверенный годами дедовский велосипед ныне называемый Legacy/BIOS boot, носящий гордое имя CSM на UEFI-совместимых системах. Мы просто достанем его с полки, смажем, подкачаем колеса и протрем влажной тряпочкой.

Desktop версия Ubuntu тоже не умеет нормально ставиться с Legacy загрузчиком, но тут, как говорится, хотя-бы есть варианты.

И так, собираем железо и грузим систему с загрузочной флэшки Ubuntu Live. Нам надо будет скачивать пакеты, так что настраиваем сеть, какая у вас заработала. Если не заработала, — нужные пакеты можно подгрузить на флешку заранее.

Заходим в Desktop окружение, запускаем эмулятор терминала, и, поехали:

#sudo bash

Как...?
Строчка выше является каноническим триггером холиваров про sudo. С большими возможностями приходит и большая ответственность. Вопрос в том, сможете ли вы взять ее на себя. Многие считают что использование sudo таким образом это, по крайней мере не осторожно. Однако:


#apt-get install mdadm lvm2 thin-provisioning-tools btrfs-tools util-linux lsscsi nvme-cli mc

Почему не ZFS...?
Когда мы устанавливаем программное обеспечение на свой компьютер, — по сути, мы одалживаем погонять свое железо разработчикам этого программного обеспечения.
Когда мы доверяем этому программному обеспечению сохранность своих данных, — мы берем кредит равный стоимости восстановления этих данных, по которому когда-то придется расплачиваться.

С этой точки зрения ZFS — это Феррари, а mdadm+lvm больше походит на велосипед.

Субъективно автор предпочитает одалживать неизвестным личностям взятый в кредит велосипед вместо Феррари. Там и цена вопроса не высока. Не нужно прав. Проще ПДД. Парковки бесплатные. Проходимость лучше. К велосипеду всегда можно приделать ноги, да и починить велосипед можно своими руками.

Зачем тогда BTRFS...?
Для того чтобы загрузить операционную систему нам потребуется файловая система поддерживаемая в Legacy/BIOS GRUB из коробки, и, при этом, поддерживающая снимки на-живую. Мы будем использовать ее для /boot раздела. Помимо этого, автор предпочитает использовать эту ФС для / (корня) не забывая отметить, что для любого другого софта можно создать отдельные разделы на LVM и монтировать в нужные каталоги.

Ни образы виртуальных машин, ни базы данных мы на этой ФС хранить не будем.
Использоваться эта ФС будет только для создания мгновенных снимков системы без ее выключения с последующим перекачиванием этих снимков на резервный диск при помощи send/recieve.

Помимо этого, автор вообще предпочитает держать минимум программного обеспечения непосредственно на железе и гонять весь остальной софт в виртуальных машинах используя такие штуки как пробрасывание GPU и PCI-USB Host-контроллеров в KVM через IOMMU.

На железе остаются только — хранение данных, виртуализация и резервное копирование.

Если вы больше доверяете ZFS, то, в принципе, для указанного применения они взаимозаменяемы.

Тем не менее, автор сознательно игнорирует встроенные функции зеркалирования / RAID и избыточности которые есть в ZFS, BRTFS и LVM.

В качестве дополнительного аргумента, BTRFS имеет свойство превращать случайную запись в последовательную, что крайне позитивно сказывается на скорости синхронизации снимков / резервных копий на HDD.

Заново пересканируем все устройства:

#udevadm control --reload-rules && udevadm trigger

Осмотримся:

#lsscsi && nvme list
[0:0:0:0] disk ATA Samsung SSD 860 2B6Q /dev/sda
[1:0:0:0] disk ATA Samsung SSD 860 2B6Q /dev/sdb
[2:0:0:0] disk ATA Samsung SSD 860 2B6Q /dev/sdc
[3:0:0:0] disk ATA Samsung SSD 860 2B6Q /dev/sdd
[4:0:0:0] disk ATA Samsung SSD 860 2B6Q /dev/sde
[5:0:0:0] disk ATA Samsung SSD 860 2B6Q /dev/sdf
[6:0:0:0] disk ATA HGST HTS721010A9 A3J0 /dev/sdg
[6:0:1:0] disk ATA HGST HTS721010A9 A3J0 /dev/sdh
[6:0:2:0] disk ATA HGST HTS721010A9 A3J0 /dev/sdi
[6:0:3:0] disk ATA HGST HTS721010A9 A3B0 /dev/sdj
[6:0:4:0] disk ATA HGST HTS721010A9 A3B0 /dev/sdk
[6:0:5:0] disk ATA HGST HTS721010A9 A3B0 /dev/sdl
[6:0:6:0] disk ATA HGST HTS721010A9 A3J0 /dev/sdm
[6:0:7:0] disk ATA HGST HTS721010A9 A3J0 /dev/sdn
Node SN Model Namespace Usage Format FW Rev
---------------- -------------------- ---------------------------------------- --------- -------------------------- ---------------- --------
/dev/nvme0n1 S466NXXXXXXX15L Samsung SSD 970 EVO 500GB 1 0,00 GB / 500,11 GB 512 B + 0 B 2B2QEXE7
/dev/nvme1n1 S5H7NXXXXXXX48N Samsung SSD 970 EVO 500GB 1 0,00 GB / 500,11 GB 512 B + 0 B 2B2QEXE7

Разметка «дисков»


NVMe SSD


А вот никак мы их не будем размечать. Все равно наш BIOS не видит эти накопители. Так что, они целиком пойдут в программный RAID. Даже разделов создавать там не будем. Если хочется по «канону» или «принципиально» — создайте один большой раздел, как у HDD.

SATA HDD


Тут особо изобретать ничего не надо. Мы создадим один раздел на все. Раздел создадим потому, что эти диски видит BIOS и даже может попытаться с них загрузиться. Мы даже установим позже на эти диски GRUB чтобы у системы это внезапно получилось.

#cat >hdd.part << EOF
label: dos
label-id: 0x00000000
device: /dev/sdg
unit: sectors

/dev/sdg1 : start= 2048, size= 1953523120, type=fd, bootable
EOF
#sfdisk /dev/sdg < hdd.part
#sfdisk /dev/sdh < hdd.part
#sfdisk /dev/sdi < hdd.part
#sfdisk /dev/sdj < hdd.part
#sfdisk /dev/sdk < hdd.part
#sfdisk /dev/sdl < hdd.part
#sfdisk /dev/sdm < hdd.part
#sfdisk /dev/sdn < hdd.part

SATA SSD


Тут у нас интереснее всего.

Во-первых накопители у нас размером 2 ТБ. Это в пределах допустимого для MBR, чем мы и воспользуемся. При необходимости можно заменить на GPT. У GPT дисков есть слой совместимости который позволяет MBR-совместимым системам видеть первые 4 раздела если они расположены в пределах первых 2х терабайт. Главное, чтобы загрузочный раздел и раздел bios_grub на этих дисках были в начале. Это позволяет даже делать с GPT дисков Legacy/BIOS загрузку.

Но, это не наш случай.

Здесь мы будем создавать два раздела. Первый будет размером 1 Гб и использован для RAID 1 /boot.

Второй будет будет использоваться для RAID 6 и занимать все оставшееся свободное место за исключением небольшой не размеченной области в конце накопителя.

Что за неразмеченная область?
Согласно источникам в сети наши SATA SSD имеют на борту динамически расширяемый SLC кэш размером от 6 до 78 гигабайт. 6 гигабайт мы получаем «бесплатно» за счет разницы между «гигабайтами» и «гибибайтами» в техпаспорте накопителя. Остальные 72 гигабайта выделяются за счет неиспользуемого пространства.

Тут надо заметить, что кэш у нас SLC, а место занимается в режиме 4 bit MLC. Что для нас эффективно означает, что за каждые 4 гигабайта свободного пространства мы получим только 1 гигабайт SLC кэша.

Умножаем 72 гигабайта на 4 и получаем 288 гигабайт. Это и есть то свободное место которое мы не будем размечать, чтобы позволить накопителям на полную использовать SLC кэш.

Таким образом, мы получим эффективно до 312 гигабайт SLC кэша суммарно от шести накопителей. Из всех накопителей 2 будут использоваться в RAID для избыточности.

Такое количество кэша позволит нам крайне редко в живой практике сталкиваться с ситуацией, когда запись идет не в кэш. Это чрезвычайно хорошо компенсирует самый печальный недостаток QLC памяти, — крайне низкую скорость записи когда данные пишутся в обход кэша. Если ваши нагрузки этому не соответствуют, то, я рекомендую вам сильно задуматься о том, сколько проживут ваши SSD под такой нагрузкой учитывая TBW из техпаспорта.

#cat >ssd.part << EOF
label: dos
label-id: 0x00000000
device: /dev/sda
unit: sectors

/dev/sda1 : start= 2048, size= 2097152, type=fd, bootable
/dev/sda2 : start= 2099200, size= 3300950016, type=fd
EOF
#sfdisk /dev/sda < ssd.part
#sfdisk /dev/sdb < ssd.part
#sfdisk /dev/sdc < ssd.part
#sfdisk /dev/sdd < ssd.part
#sfdisk /dev/sde < ssd.part
#sfdisk /dev/sdf < ssd.part

Создание массивов


Для начала нам нужно переименовать машину. Нужно потому, что имя хоста является частью имени массива где-то внутри mdadm и где-то на что-то влияет. Массивы конечно можно позже переименовать, но, это лишние действия.

#mcedit /etc/hostname
#mcedit /etc/hosts
#hostname
vdesk0

NVMe SSD


#mdadm --create --verbose --assume-clean /dev/md0 --level=1 --raid-devices=2 /dev/nvme[0-1]n1

Зачем --assume-clean...?
Чтобы не инициализировать массивы. Для обоих уровней RAID 1 и 6 это допустимо. Все может работать и без инициализации, если это новый массив. Более того, инициализация массива SSD при создании — это пустая трата ресурса TBW. Мы используем TRIM/DISCARD где возможно на собранных массивах SSD чтобы их «инициализировать».

У массивов SSD RAID 1 DISCARD поддерживается из коробки.

У массивов SSD RAID 6 DISCARD надо включать в параметрах модуля ядра.

Это стоит делать только в том случае, когда вообще все SSD используемые в массивах уровней 4/5/6 в этой системе имеют работающую поддержку discard_zeroes_data. Иногда попадаются странные накопители, которые сообщают ядру о поддержке этой функции, но, по-факту, ее нет, или функция работает не всегда. На данный момент поддержка есть практически везде, однако, старые накопители и прошивки с ошибками встречаются. По этой причине поддержка DISCARD по-умолчанию выключена для RAID 6.

Внимание, следующая команда уничтожит все данные на NVMe накопителях «инициализировав» массив «нулями».

#blkdiscard /dev/md0

Если что-то пошло не так, то попробуйте указать шаг.

#blkdiscard --step 65536 /dev/md0

SATA SSD


#mdadm --create --verbose --assume-clean /dev/md1 --level=1 --raid-devices=6 /dev/sd[a-f]1
#blkdiscard /dev/md1
#mdadm --create --verbose --assume-clean /dev/md2 --chunk-size=512 --level=6 --raid-devices=6 /dev/sd[a-f]2

Зачем такой большой...?
Увеличение chunk-size позитивно влияет на скорость случайного чтения блоками до chunk-size включительно. Происходит это потому, что одна операция соответствующего размера или меньше может быть полностью выполнена на одном устройстве. Поэтому, IOPS от всех устройств суммируется. По статистике 99% IO не превышает 512K.

У RAID 6 IOPS на запись всегда меньше или равно IOPS у одного накопителя. Когда как на случайное чтение IOPS может быть больше такового у одного накопителя в несколько раз, и тут размер блока имеет ключевое значение.
Автор не видит смысла в попытках оптимизировать параметр который плох у RAID 6 by-design и вместо этого оптимизирует то, в чем RAID 6 показывает себя хорошо.
Плохую случайную запись RAID 6 мы будем компенсировать кэшем на NVMe и трюками с thin-provisioning.

Мы пока не включили DISCARD для RAID 6. Так что «инициализировать» этот массив пока не будем. Сделаем это позже, — после установки ОС.

SATA HDD


#mdadm --create --verbose --assume-clean /dev/md3 --chunk-size=512 --level=6 --raid-devices=8 /dev/sd[g-n]1

LVM на NVMe RAID


Для скорости мы хотим разместить корневую ФС на NVMe RAID 1 который /dev/md0.
Тем не менее, этот быстрый массив нам еще понадобится для других нужд, таких как swap, метаданные и кэш LVM-cache и метаданные LVM-thin, потому, на этом массиве мы создадим LVM VG.

#pvcreate /dev/md0
#vgcreate root /dev/md0


Создадим раздел для корневой ФС.

#lvcreate -L 128G --name root root

Создадим раздел для подкачки по размеру оперативной памяти.

#lvcreate -L 32G --name swap root

Установка ОС


Итого, у нас есть все необходимое, чтобы установить систему.

Запускаем мастер установки системы из окружения Ubuntu Live. Обычная установка. Только на этапе выбора дисков для установки нужно указать следующее:

  • /dev/md1, — точка монтирования /boot, ФС — BTRFS
  • /dev/root/root (a.k.a /dev/mapper/root-root), — точка монтирования / (корень), ФС — BTRFS
  • /dev/root/swap (a.k.a /dev/mapper/root-swap), — использовать как раздел подкачки
  • Загрузчик установить на /dev/sda

При выборе BTRFS в качестве корневой ФС, — установщик автоматически создаст два BTRFS-тома с именами "@" для / (корня), и "@home" для /home.

Запускаем установку…

Установка завершится модальным диалоговым окном сообщающем об ошибке установки загрузчика. К сожалению, выйти из этого диалога штатными средствами и продолжить установку не получится. Делаем логаут из системы и снова логинимся, попадая в чистый рабочий стол Ubuntu Live. Открываем терминал, и снова:

#sudo bash

Создаем chroot окружение, чтобы продолжить установку:

#mkdir /mnt/chroot
#mount -o defaults,space_cache,noatime,nodiratime,discard,subvol=@ /dev/mapper/root-root /mnt/chroot
#mount -o defaults,space_cache,noatime,nodiratime,discard,subvol=@home /dev/mapper/root-root /mnt/chroot/home
#mount -o defaults,space_cache,noatime,nodiratime,discard /dev/md1 /mnt/chroot/boot
#mount --bind /proc /mnt/chroot/proc
#mount --bind /sys /mnt/chroot/sys
#mount --bind /dev /mnt/chroot/dev

Настроим сеть и hostname в chroot:

#cat /etc/hostname >/mnt/chroot/etc/hostname
#cat /etc/hosts >/mnt/chroot/etc/hosts
#cat /etc/resolv.conf >/mnt/chroot/etc/resolv.conf

Заходим в chroot окружение:

#chroot /mnt/chroot

Первым делом доставим пакеты:

apt-get install --reinstall mdadm lvm2 thin-provisioning-tools btrfs-tools util-linux lsscsi nvme-cli mc debsums hdparm

Проверим и исправим все пакеты которые криво установились из-за незаконченной установки системы:

#CORRUPTED_PACKAGES=$(debsums -s 2>&1 | awk '{print $6}' | uniq)
#apt-get install --reinstall $CORRUPTED_PACKAGES

Если что-то не срослось, возможно, вам понадобится перед этим подредактировать /etc/apt/sources.list

Поправим параметры для модуля RAID 6 чтобы включить TRIM/DISCARD:

#cat >/etc/modprobe.d/raid456.conf << EOF
options raid456 devices_handle_discard_safely=1
EOF

Немного поднастроим наши массивы:

#cat >/etc/udev/rules.d/60-md.rules << EOF
SUBSYSTEM=="block", KERNEL=="md*", ACTION=="change", TEST=="md/stripe_cache_size", ATTR{md/stripe_cache_size}="32768"
SUBSYSTEM=="block", KERNEL=="md*", ACTION=="change", TEST=="md/sync_speed_min", ATTR{md/sync_speed_min}="48000"
SUBSYSTEM=="block", KERNEL=="md*", ACTION=="change", TEST=="md/sync_speed_max", ATTR{md/sync_speed_max}="300000"
EOF
#cat >/etc/udev/rules.d/62-hdparm.rules << EOF
SUBSYSTEM=="block", ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="1", RUN+="/sbin/hdparm -B 254 /dev/%k"
EOF
#cat >/etc/udev/rules.d/63-blockdev.rules << EOF
SUBSYSTEM=="block", ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="1", RUN+="/sbin/blockdev --setra 1024 /dev/%k"
SUBSYSTEM=="block", ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="0", RUN+="/sbin/blockdev --setra 0 /dev/%k"
SUBSYSTEM=="block", ACTION=="add|change", KERNEL=="nvme[0-9]n1", RUN+="/sbin/blockdev --setra 0 /dev/%k"
SUBSYSTEM=="block", ACTION=="add|change", KERNEL=="dm-*", ATTR{queue/rotational}=="0", RUN+="/sbin/blockdev --setra 0 /dev/%k"
SUBSYSTEM=="block", ACTION=="add|change", KERNEL=="md*", RUN+="/sbin/blockdev --setra 0 /dev/%k"

EOF

Что это было..?
Мы создали набор udev правил которые будут делать следующее:

  • Выставлять адекватный для 2020-ого года размер кэша блоков для RAID 6. Значение по-умолчанию, кажется, не менялось со времен создания Linux, и уже давно не адекватно.
  • Резервировать на время проверок/синхронизаций массивов минимум IO. Это нужно, чтобы ваши массивы не застревали в состоянии вечной синхронизации под нагрузкой.
  • Ограничивать на время проверок/синхронизаций массивов максимум IO. Это нужно, чтобы синхронизация/проверка SSD RAID-ов не прожарила ваши накопители до хрустящей корочки. Особенно актуально для NVMe. ( Помните про радиатор? Я ведь не шутил. )
  • Запрещать через APM дискам останавливать вращение шпинделя (HDD) и устанавливать таймаут для сна контроллеров дисков на 7 часов. Можно совсем отключить APM если ваши диски это умеют (-B 255). Со значением по-умолчанию диски будут останавливаться через пять секунд. Потом ОС захочет сбросить дисковый кэш, диски раскрутятся снова, и, все по-новой. У дисков ограничено максимальное число раскручиваний шпинделя. Такой нехитрый цикл по-умолчанию может легко убить ваши диски за пару лет. Этим страдают не все диски, но, наши-то «ноутбучные», с соответствующими настройками по-умолчанию, которые делают из RAID-а кривое подобие mini-MAID-а.
  • Устанавливать readahead на дисках (вращающихся) в 1 мегабайт — два последовательных блока/chunk RAID 6
  • Выключать readahead на SATA SSD
  • Выключать readahead на NVMe SSD
  • Выключать readahead на всех LVM томах собранных из SSD.
  • Выключать readahead на всех RAID массивах.


Подредактируем /etc/fstab:

#cat >/etc/fstab << EOF
# /etc/fstab: static file system information.
#
# Use 'blkid' to print the universally unique identifier for a
# device; this may be used with UUID= as a more robust way to name devices
# that works even if disks are added and removed. See fstab(5).
# file-system mount-point type options dump pass
/dev/mapper/root-root / btrfs defaults,space_cache,noatime,nodiratime,discard,subvol=@ 0 1
UUID=$(blkid -o value -s UUID /dev/md1) /boot btrfs defaults,space_cache,noatime,nodiratime,discard 0 2
/dev/mapper/root-root /home btrfs defaults,space_cache,noatime,nodiratime,discard,subvol=@home 0 2
/dev/mapper/root-swap none swap sw 0 0
EOF

Почему так..?
Раздел /boot мы будем искать по UUID т.к. именование массивов теоретически может измениться.

Остальные разделы мы будем искать по LVM именам в нотации /dev/mapper/vg-lv, т.к. они достаточно уникально идентифицируют разделы.

Не используем UUID для LVM т.к. UUID у LVM томов и их снапшотов может совпадать.
Дважды монтируем /dev/mapper/root-root..?
Да. Именно так. Особенность BTRFS. Эту ФС можно монтировать несколько раз с разными subvol.

В следствии этой-же особенности рекомендую никогда не создавать LVM снапшоты активных BTRFS томов. Можете получить сюрприз при перезагрузке.

Перегенерируем конфиг mdadm:

#/usr/share/mdadm/mkconf | sed 's/#DEVICE/DEVICE/g' >/etc/mdadm/mdadm.conf

Подкорректируем настройки LVM:

#cat >>/etc/lvm/lvmlocal.conf << EOF

activation {
thin_pool_autoextend_threshold=90
thin_pool_autoextend_percent=5
}
allocation {
cache_pool_max_chunks=2097152
}
devices {
global_filter=["r|^/dev/.*_corig$|","r|^/dev/.*_cdata$|","r|^/dev/.*_cmeta$|","r|^/dev/.*gpv$|","r|^/dev/images/.*$|","r|^/dev/mapper/images.*$|","r|^/dev/backup/.*$|","r|^/dev/mapper/backup.*$|"]
issue_discards=1
}
EOF

Что это было..?
Мы включили автоматическое расширение пулов LVM thin по достижении 90% занятого места на 5% от объема.

Мы увеличили максимальное количество блоков кэша для LVM cache.

Мы запретили LVM искать LVM тома (PV) на:

  • устройствах содержащих LVM cache (cdata)
  • устройствах кэшированных при помощи LVM cache в обход кэша (<lv_name>_corig). При этом само кэшированное устройство все равно будет просканировано через кэш (просто <lv_name>).
  • устройствах содержащих метаданные LVM cache (cmeta)
  • всех устройствах в VG с названием images. Тут у нас будут образы дисков виртуальных машин, и, мы не хотим чтобы LVM на хосте активировал тома принадлежащие гостевой ОС.
  • всех устройствах в VG с названием backup. Тут у нас будут резервные копии образов виртуальных машин.
  • всех устройствах имя которых заканчивается на «gpv» ( guest physical volume )

Мы включили поддержку DISCARD при освобождении свободного пространства на LVM VG. Будьте осторожны. Это сделает удаление LV на SSD достаточно долгим. Особенно это относится к SSD RAID 6. Однако, по плану, мы будем использовать thin provisioning, так что, это нам совсем не помешает.

Обновим образ initramfs:

#update-initramfs -u -k all

Установим и сконфигурируем grub:

#apt-get install grub-pc
#apt-get purge os-prober
#dpkg-reconfigure grub-pc


Какие диски выбирать?
Все которые sd*. Система должна быть способна загрузиться с любого работающего SATA диска или SSD.

Зачем прибили os-prober..?
За излишнюю самостоятельность и шаловливые ручки.

Он не работает корректно если один из RAID-ов находится в деградированном состоянии. Он пытается искать ОС на разделах, которые используются в виртуальных машинах работающих на этом железе.

Если он вам нужен, то можете оставить, но, имейте ввиду все вышеперечисленное. Рекомендую поискать рецепты избавления от шаловливых ручек в сети.

На этом мы завершили начальную установку. Пришло время перезагрузиться в только что установленную ОС. Не забудьте вынуть загрузочный Live CD/USB.

#exit
#reboot


В качестве устройства для загрузки выбираем любой из SATA SSD.

LVM на SATA SSD


К этому моменту мы уже загрузились в новую ОС, настроили сеть, apt, открыли эмулятор терминала, и запустили:

#sudo bash

Продолжим.

«Инициализируем» массив из SATA SSD:

#blkdiscard /dev/md2

Если не прокатило, то пробуем:

#blkdiscard --step 65536 /dev/md2
Создаем LVM VG на SATA SSD:

#pvcreate /dev/md2
#vgcreate data /dev/md2


Зачем еще одна VG..?
В самом деле, у нас уже есть VG с именем root. Почему бы не добавить все в одну VG?

Если в VG есть несколько PV, то для корректной активации VG все PV должны присутствовать (online). Исключением является LVM RAID, который мы намеренно не используем.

Мы очень хотим, чтобы при отвале (читай потере данных) на любом из RAID 6 массивов операционная система загрузилась штатно и дала нам возможность решить проблему.

Для этого, на первом уровне абстракции мы будем изолировать каждый тип физического «носителя» в отдельную VG.

Если по-научному, то разные RAID массивы относятся к разным «доменам надежности». Не стоит создавать для них дополнительную общую точку отказа, запихивая в одну VG.

Наличие LVM на «железном» уровне позволит нам произвольно нарезать кусочки разных RAID массивов по-разному их комбинируя. Например, — запустить одновременно bcache + LVM thin, bcache + BTRFS, LVM cache + LVM thin, сложную конфигурацию ZFS с кэшами или любую другую адскую смесь, чтобы все это пощупать и сравнить.

На «железном» уровне мы ничего, кроме старых-добрых «толстых» LVM-томов, использовать не будем. Исключением из этого правила, возможно, будет раздел для резервного копирования.

Думаю, к этому моменту, многие читатели уже начали что-то подозревать касательно матрешки.

LVM на SATA HDD


#pvcreate /dev/md3
#vgcreate backup /dev/md3


Опять новая VG..?
Мы очень хотим, чтобы при отвале массива дисков, который мы будем использовать для резервного копирования данных, наша операционная система продолжала работать штатно, штатно-же сохраняя доступ к нерезервным данным. Поэтому, во-избежание проблемм активации VG, — мы создаем отдельную VG.

Настройка LVM cache


Создадим LV на NVMe RAID 1 чтобы использовать его в качестве кэширующего устройства.

#lvcreate -L 70871154688B --name cache root

Чего так мало...?
Дело в том, что у наших NVMe SSD тоже есть SLC кэш. 4 гигабайта «бесплатного» и 18 гигабайт динамического за счет свободного пространства занимаемого в 3-bit MLC. По-исчерпании этого кэша NVMe SSD станут не на много быстрее нашего SATA SSD с кэшем. Собственно, по этой причине нам нет смысла делать LVM cache раздел сильно больше двукратного объема SLC кэша NVMe накопителя. Для используемых NVMe накопителей автор считает разумным сделать 32-64 гигабайта кэша.

Приведенный размер раздела необходим для организации 64 гигабайт кэша, размещения метаданнных кэша и резервной копии метаданных.

Дополнительно замечу, что после грязного выключения системы LVM пометит весь кэш как грязный и будет синхронизировать заново. Более того, это будет повторяться при каждом использовании lvchange на этом устройстве до новой перезагрузки системы. Потому, рекомендую сразу пересоздать кэш соответствующим скриптом.

Создадим LV на SATA RAID 6 чтобы использовать его в качестве кэшируемого устройства.

#lvcreate -L 3298543271936B --name cache data

Почему только три терабайта..?
Чтобы, при необходимости, можно было использовать SATA SSD RAID 6 для каких-то других нужд. Размер кэшируемого пространства можно увеличить динамически, на лету, без остановки работы системы. Для этого необходимо временно остановить и заново включить кэш, но, отличительным преимуществам LVM-cache перед, например, bcache, является то, что это можно делать на лету.

Создадим новую VG для кэширования.

#pvcreate /dev/root/cache
#pvcreate /dev/data/cache
#vgcreate cache /dev/root/cache /dev/data/cache


Создадим LV на кэшируемом устройстве.

#lvcreate -L 3298539077632B --name cachedata cache /dev/data/cache

Тут мы сразу заняли все свободное место на /dev/data/cache чтобы все остальные нужные разделы создавались сразу на /dev/root/cache. Если у вас что-то создалось не там, можно переместить при помощи pvmove.

Создадим и включим кэш:

#lvcreate -y -L 64G -n cache cache /dev/root/cache
#lvcreate -y -L 1G -n cachemeta cache /dev/root/cache
#lvconvert -y --type cache-pool --cachemode writeback --chunksize 64k --poolmetadata cache/cachemeta cache/cache
#lvconvert -y --type cache --cachepool cache/cache cache/cachedata

Почему такой chunksize..?
Методом практических экспериментов автору удалось выяснить, что наилучший результат достигается если размер блока LVM cache совпадает с размером блока LVM thin. При этом, чем меньше размер, тем лучше себя показывает конфигурация на случайной записи.

64к — это минимальный размер блока допустимый для LVM thin.

Осторожно writeback..!
Да. Этот тип кэша откладывает синхронизацию записи на кэшируемое устройство. Это приводит к тому, что, в случае потери кэша, можно потерять данные на кэшируемом устройстве. Позже автор расскажет, какие меры, помимо NVMe RAID 1 можно предпринять, чтобы компенсировать этот риск.

Данный тип кэша выбран намерено, чтобы компенсировать низкую производительность RAID 6 на случайной записи.

Проверим, что у нас получилось:

#lvs -a -o lv_name,lv_size,devices --units B cache
LV LSize Devices
[cache] 68719476736B cache_cdata(0)
[cache_cdata] 68719476736B /dev/root/cache(0)
[cache_cmeta] 1073741824B /dev/root/cache(16384)
cachedata 3298539077632B cachedata_corig(0)
[cachedata_corig] 3298539077632B /dev/data/cache(0)
[lvol0_pmspare] 1073741824B /dev/root/cache(16640)

На /dev/data/cache должен располагаться только [cachedata_corig]. Если что-то не так, то используйте pvmove.

Отключить кэш при необходимости можно одной командой:

#lvconvert -y --uncache cache/cachedata

Это делается on-line. LVM просто синхронизирует кэш на диск, удалит его и переименует cachedata_corig обратно в cachedata.

Настройка LVM thin


Приблизительно оценим сколько места нам потребуется для метаданных LVM thin:

#thin_metadata_size --block-size=64k --pool-size=6terabytes --max-thins=100000 -u bytes
thin_metadata_size - 3385794560 bytes estimated metadata area size for "--block-size=64kibibytes --pool-size=6terabytes --max-thins=100000"


Округлим до 4х гигабайт: 4294967296B

Умножим на два и прибавим 4194304B для метаданных LVM PV: 8594128896B
Создадим отдельный раздел на NVMe RAID 1 чтобы разметисть на нем метаданные LVM thin и их резервную копию:

#lvcreate -L 8594128896B --name images root

Зачем..?
Тут может возникнуть вопрос, зачем размещать метаданные LVM thin отдельно, если они все равно будут кэшироваться на NVMe и будут работать быстро.

Скорость тут хоть и является важной, но далеко не основной причиной. Все дело в том, что кэш, это точка отказа. С ним может что-то случиться, и, если метаданные LVM thin будут кэшированы, это приведет к полной потере всего. Без целых метаданных собрать тонкие тома будет практически невозможно.

Перемещая-же метаданные на отдельный не-кешированный, но быстрый, том, мы гарантируем сохранность метаданных в случае утери или повреждения кэша. В этом случае все повреждения вызванные утерей кэша будут локализованы внутри тонких томов, что на порядки упростит процедуру восстановления. С большой вероятностью эти повреждения будут восстановимы с помощью журналов ФС.

Более того, если ранее был сделан мгновенный снимок тонкого тома, и, после этого, кэш был хотябы один раз полностью синхронизирован, то, в силу особенностей внутреннего устройства LVM thin, целостность снимка будет гарантирована в случае утери кэша.

Создадим новую VG которая будет отвечать за thin-provisioning:

#pvcreate /dev/root/images
#pvcreate /dev/cache/cachedata
#vgcreate images /dev/root/images /dev/cache/cachedata

Создадим пул:

#lvcreate -L 274877906944B --poolmetadataspare y --poolmetadatasize 4294967296B --chunksize 64k -Z y -T images/thin-pool
Зачем -Z y
Помимо того, для чего это режим собственно и предназначен, — не позволять данным из одной виртуальной машины утекать в другую виртуальную машину при перераспределении пространства, — zeroing дополнительно используется для увеличения скорости случайной записи блоками меньше 64k. Любая запись меньше 64k в ранее не выделенную область тонкого тома будет превращаться в 64K выровненные по границе кэша. Это позволит выполнить операцию полностью через кэш минуя кэшируемое устройство.

Переместим LV на соответствующие PV:

#pvmove -n images/thin-pool_tdata /dev/root/images /dev/cache/cachedata
#pvmove -n images/lvol0_pmspare /dev/cache/cachedata /dev/root/images
#pvmove -n images/thin-pool_tmeta /dev/cache/cachedata /dev/root/images

Проверим:

#lvs -a -o lv_name,lv_size,devices --units B images
LV LSize Devices
[lvol0_pmspare] 4294967296B /dev/root/images(0)
thin-pool 274877906944B thin-pool_tdata(0)
[thin-pool_tdata] 274877906944B /dev/cache/cachedata(0)
[thin-pool_tmeta] 4294967296B /dev/root/images(1024)

Создадим тонкий том для тестов:

#lvcreate -V 64G --thin-pool thin-pool --name test images

Поставим пакеты для тестов и наблюдения:

#apt-get install sysstat fio

Вот так можно наблюдать за поведением нашей конфигурации хранилища в реальном времени:

#watch 'lvs --rows --reportformat basic --quiet -ocache_dirty_blocks,cache_settings cache/cachedata && (lvdisplay cache/cachedata | grep Cache) && (sar -p -d 2 1 | grep -E "sd|nvme|DEV|md1|md2|md3|md0" | grep -v Average | sort)'

Вот так можно протестировать нашу конфигурацию:

#fio --loops=1 --size=64G --runtime=4 --filename=/dev/images/test --stonewall --ioengine=libaio --direct=1 \
--name=4kQD32read --bs=4k --iodepth=32 --rw=randread \
--name=8kQD32read --bs=8k --iodepth=32 --rw=randread \
--name=16kQD32read --bs=16k --iodepth=32 --rw=randread \
--name=32KQD32read --bs=32k --iodepth=32 --rw=randread \
--name=64KQD32read --bs=64k --iodepth=32 --rw=randread \
--name=128KQD32read --bs=128k --iodepth=32 --rw=randread \
--name=256KQD32read --bs=256k --iodepth=32 --rw=randread \
--name=512KQD32read --bs=512k --iodepth=32 --rw=randread \
--name=4Kread --bs=4k --rw=read \
--name=8Kread --bs=8k --rw=read \
--name=16Kread --bs=16k --rw=read \
--name=32Kread --bs=32k --rw=read \
--name=64Kread --bs=64k --rw=read \
--name=128Kread --bs=128k --rw=read \
--name=256Kread --bs=256k --rw=read \
--name=512Kread --bs=512k --rw=read \
--name=Seqread --bs=1m --rw=read \
--name=Longread --bs=8m --rw=read \
--name=Longwrite --bs=8m --rw=write \
--name=Seqwrite --bs=1m --rw=write \
--name=512Kwrite --bs=512k --rw=write \
--name=256Kwrite --bs=256k --rw=write \
--name=128Kwrite --bs=128k --rw=write \
--name=64Kwrite --bs=64k --rw=write \
--name=32Kwrite --bs=32k --rw=write \
--name=16Kwrite --bs=16k --rw=write \
--name=8Kwrite --bs=8k --rw=write \
--name=4Kwrite --bs=4k --rw=write \
--name=512KQD32write --bs=512k --iodepth=32 --rw=randwrite \
--name=256KQD32write --bs=256k --iodepth=32 --rw=randwrite \
--name=128KQD32write --bs=128k --iodepth=32 --rw=randwrite \
--name=64KQD32write --bs=64k --iodepth=32 --rw=randwrite \
--name=32KQD32write --bs=32k --iodepth=32 --rw=randwrite \
--name=16KQD32write --bs=16k --iodepth=32 --rw=randwrite \
--name=8KQD32write --bs=8k --iodepth=32 --rw=randwrite \
--name=4kQD32write --bs=4k --iodepth=32 --rw=randwrite \
| grep -E 'read|write|test' | grep -v ioengine

Осторожно! Ресурс!
Этот код запустит 36 различных тестов, каждый из которых будет выполняться по 4 секунды. Половина из тестов на запись. За 4 секунды на NVMe можно успеть записать очень много. До 3х гигабайт в секунду. Так что, каждый запуск тестов на запись может скушать у вас до 216 гигабайт ресурса SSD.

Чтение и запись вперемешку?
Да. Тесты на чтение и на запись имеет смысл запускать отдельно. Более того, имеет смысл убедиться, что все кэши синхронизированы, чтобы ранее сделанная запись не влияла на чтение.

Результаты будут сильно различаться при первом запуске и последующих по мере заполнения кэша и тонкого тома, а также, в зависимости от того, успела ли система синхронизировать кэши заполненные при прошлом запуске.

Помимо прочего, рекомендую замерить скорость на уже заполненном тонком томе, с которого только что был сделан снапшот. Автор имел возможность наблюдать, как случайная запись резко ускоряется сразу после создания первого снапшота, особенно, когда кэш еще не полностью заполнен. Происходит это благодаря copy-on-write семантике записи, выравниваню блоков кэша и тонкого тома, и тому, что случайная запись на RAID 6 преврящается в случайное чтение с RAID 6 с последующей записью в кэш. В нашей же конфигурации случайное чтение с RAID 6 до 6ти раз (число SATA SSD в массиве) быстрее записи. Т.к. блоки для CoW выделяются последовательно из тонкого пула, то запись, по большей части, еще и превращается в последовательную.

Обе эти особенности можно выгодно использовать.

Кэш-«когерентные» снапшоты


Для уменьшения риска потери данных в случае повреждения/потери кэша автор предлагает ввести практику ротации снапшотов гарантирующую их целостность в этом случае.

Во-первых, благодаря тому, что метаданные тонких томов располагаются на некэшированном устройстве, метаданные будут целостны, и возможные потери будут изолированы внутри блоков данных.

Следующий цикл ротации снапшотов дает гарантию целостности данных внутри снапшотов в случае утери кэша:

  1. Для каждого тонкого тома с именем <имя> создаем снапшот с именем <имя>.cached
  2. Установим migration threshold на разумное высокое значение: #lvchange --quiet --cachesettings "migration_threshold=16384" cache/cachedata
  3. В цикле проверяем количество грязных блоков в кэше: #lvs --rows --reportformat basic --quiet -ocache_dirty_blocks cache/cachedata | awk '{print $2}' пока не получим ноль. Если ноля нет слишком долго, его можно создать временно переведя кэш в writethrough режим. Однако, учитывая скоростные характеристики наших массивов SATA и NVMe SSD, а также, их ресурс TBW, вы либо сможете достаточно быстро поймать момент и без изменения режима кэша, либо ваше железо полностью скушает весь свой ресурс за несколько дней. Из-за ограничений ресурса система в принципе не способна находиться под 100% нагрузкой на запись постоянно. Наши NVMe SSD под 100% нагрузкой на запись полностью израсходуют ресурс за 3-4 дня. SATA SSD проживут всего-то раза в два дольше. Потому, мы будем считать, что большая часть нагрузки идет на чтение, а на запись у нас, — относительно кратковременные всплески крайне высокой активности в сочетании с низкой нагрузкой в среднем.
  4. Как только поймали (или сделали) нолик — переименовываем <имя>.cached в <имя>.committed. Старый <имя>.committed при этом удаляем.
  5. Опционально, если кэш заполнен на 100%, его можно пересоздать скриптом, таким образом очистив. С полупустым кэшем система работает гораздо быстрее на запись.
  6. Установим migration threshold на ноль: #lvchange --quiet --cachesettings "migration_threshold=0" cache/cachedata Это временно запретит синхронизировать кэш на основной носитель.
  7. Ждем, пока в кэше накопится достаточно много изменений #lvs --rows --reportformat basic --quiet -ocache_dirty_blocks cache/cachedata | awk '{print $2}' или сработает таймер.
  8. Повторяем заново.

Зачем сложности с migration threshold...?
Все дело в том, что в реальной практике «случайная» запись на самом деле не совсем случайна. Если мы записали что-то в сектор размером 4 килобайта, велика вероятность того, что ближайшую пару минут будет сделана запись в этот-же или один из соседних (+- 32K) секторов.

Выставляя migration threshold в ноль мы откладываем синхронизацию записи на SATA SSD и агрегируем несколько изменений одного блока 64K в кэше. Таким образом заметно экономится ресурс SATA SSD.

А код где..?
К сожалению, автор считает себя недостаточно компетентным по части разработки bash скриптов ибо является на 100% самоучкой и практирует «google»-driven development, потому считает, что тот страшный код, который выходит из под его рук, лучше не использовать никому другому.

Думаю, что профессионалы этого дела смогут и самостоятельно изобразить всю описанную выше логику в случае необходимости, и, может быть, даже красиво оформить в виде systemd сервиса, как это попытался сделать автор.

Подобная нехитрая схема ротации снапшотов позволит нам не только постоянно иметь один полностью синхронизированный на SATA SSD снапшот, но и позволит при помощи утилиты thin_delta узнать, какие блоки были изменены после его создания, и, таким образом, локализовать повреждения на основных томах, многократно упрощая восстановление.

TRIM/DISCARD в libvirt/KVM


Т.к. хранилище данных будет использоваться для KVM под управлением libvirt, то было бы неплохо научить наши VM не только занимать свободное место, но и освобождать уже ненужное.

Это делается посредством эмуляции поддержки TRIM/DISCARD на виртуальных дисках. Для этого надо изменить тип контроллера на virtio-scsi и подредактировать xml.

#virsh edit vmname
<disk type='block' device='disk'>
<driver name='qemu' type='raw' cache='writethrough' io='threads' discard='unmap'/>
<source dev='/dev/images/vmname'/>
<backingStore/>
<target dev='sda' bus='scsi'/>
<alias name='scsi0-0-0-0'/>
<address type='drive' controller='0' bus='0' target='0' unit='0'/>
</disk>

<controller type='scsi' index='0' model='virtio-scsi'>
<alias name='scsi0'/>
<address type='pci' domain='0x0000' bus='0x04' slot='0x00' function='0x0'/>
</controller>

Подобные DISCARDы из гостевых ОС корректно обрабатываются LVMом, и блоки корректно освобождаются как в кэше, так и в тонком пуле. В нашем случае, это происходит, в основном, отложенно, при удалении очередного снапшота.

Резервное копирование BTRFS


Использовать готовые скрипты с крайней осторожностью и на свой страх и риск. Автор писал этот код сам и исключительно для себя. Уверен, что у многих опытных пользователей Linux имеются подобные костыли наработки, и копировать чужие не понадобится.

Создадим том на резервном устройстве:

#lvcreate -L 256G --name backup backup

Отформатируем в BTRFS:

#mkfs.btrfs /dev/backup/backup

Создадим точки монтирования и примонтируем корневые подразделы ФС:

#mkdir /backup
#mkdir /backup/btrfs
#mkdir /backup/btrfs/root
#mkdir /backup/btrfs/back
#ln -s /boot /backup/btrfs
# cat >>/etc/fstab << EOF

/dev/mapper/root-root /backup/btrfs/root btrfs defaults,space_cache,noatime,nodiratime 0 2
/dev/mapper/backup-backup /backup/btrfs/back btrfs defaults,space_cache,noatime,nodiratime 0 2
EOF
#mount -a
#update-initramfs -u
#update-grub

Создадим каталоги для резервных копий:

#mkdir /backup/btrfs/back/remote
#mkdir /backup/btrfs/back/remote/root
#mkdir /backup/btrfs/back/remote/boot

Создадим каталог для скриптов резервного копирования:

#mkdir /root/btrfs-backup

Скопируем скрипт:

Много страшного bash-кода. Использовать на свой страх и риск. Автору гневные письма не писать...
#cat >/root/btrfs-backup/btrfs-backup.sh << EOF
#!/bin/bash
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

SCRIPT_FILE="$(realpath $0)"
SCRIPT_DIR="$(dirname $SCRIPT_FILE)"
SCRIPT_NAME="$(basename -s .sh $SCRIPT_FILE)"

LOCK_FILE="/dev/shm/$SCRIPT_NAME.lock"
DATE_PREFIX='%Y-%m-%d'
DATE_FORMAT=$DATE_PREFIX'-%H-%M-%S'
DATE_REGEX='[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]'
BASE_SUFFIX=".@base"
PEND_SUFFIX=".@pend"
SNAP_SUFFIX=".@snap"
MOUNTS="/backup/btrfs/"
BACKUPS="/backup/btrfs/back/remote/"

function terminate ()
{
echo "$1" >&2
exit 1
}

function wait_lock()
{
flock 98
}

function wait_lock_or_terminate()
{
echo "Wating for lock..."
wait_lock || terminate "Failed to get lock. Exiting..."
echo "Got lock..."
}

function suffix()
{
FORMATTED_DATE=$(date +"$DATE_FORMAT")
echo "$SNAP_SUFFIX.$FORMATTED_DATE"
}

function filter()
{
FORMATTED_DATE=$(date --date="$1" +"$DATE_PREFIX")
echo "$SNAP_SUFFIX.$FORMATTED_DATE"
}

function backup()
{
SOURCE_PATH="$MOUNTS$1"
TARGET_PATH="$BACKUPS$1"
SOURCE_BASE_PATH="$MOUNTS$1$BASE_SUFFIX"
TARGET_BASE_PATH="$BACKUPS$1$BASE_SUFFIX"
TARGET_BASE_DIR="$(dirname $TARGET_BASE_PATH)"
SOURCE_PEND_PATH="$MOUNTS$1$PEND_SUFFIX"
TARGET_PEND_PATH="$BACKUPS$1$PEND_SUFFIX"
if [ -d "$SOURCE_BASE_PATH" ]
then
echo "$SOURCE_BASE_PATH found"
else
echo "$SOURCE_BASE_PATH File not found creating snapshot of $SOURCE_PATH to $SOURCE_BASE_PATH"
btrfs subvolume snapshot -r $SOURCE_PATH $SOURCE_BASE_PATH
sync
if [ -d "$TARGET_BASE_PATH" ]
then
echo "$TARGET_BASE_PATH found out of sync with source... removing..."
btrfs subvolume delete -c $TARGET_BASE_PATH
sync
fi
fi
if [ -d "$TARGET_BASE_PATH" ]
then
echo "$TARGET_BASE_PATH found"
else
echo "$TARGET_BASE_PATH not found. Synching to $TARGET_BASE_DIR"
btrfs send $SOURCE_BASE_PATH | btrfs receive $TARGET_BASE_DIR
sync
fi
if [ -d "$SOURCE_PEND_PATH" ]
then
echo "$SOURCE_PEND_PATH found removing..."
btrfs subvolume delete -c $SOURCE_PEND_PATH
sync
fi
btrfs subvolume snapshot -r $SOURCE_PATH $SOURCE_PEND_PATH
sync
if [ -d "$TARGET_PEND_PATH" ]
then
echo "$TARGET_PEND_PATH found removing..."
btrfs subvolume delete -c $TARGET_PEND_PATH
sync
fi
echo "Sending $SOURCE_PEND_PATH to $TARGET_PEND_PATH"
btrfs send -p $SOURCE_BASE_PATH $SOURCE_PEND_PATH | btrfs receive $TARGET_BASE_DIR
sync
TARGET_DATE_SUFFIX=$(suffix)
btrfs subvolume snapshot -r $TARGET_PEND_PATH "$TARGET_PATH$TARGET_DATE_SUFFIX"
sync
btrfs subvolume delete -c $SOURCE_BASE_PATH
sync
btrfs subvolume delete -c $TARGET_BASE_PATH
sync
mv $SOURCE_PEND_PATH $SOURCE_BASE_PATH
mv $TARGET_PEND_PATH $TARGET_BASE_PATH
sync
}

function list()
{
LIST_TARGET_BASE_PATH="$BACKUPS$1$BASE_SUFFIX"
LIST_TARGET_BASE_DIR="$(dirname $LIST_TARGET_BASE_PATH)"
LIST_TARGET_BASE_NAME="$(basename -s .$BASE_SUFFIX $LIST_TARGET_BASE_PATH)"
find "$LIST_TARGET_BASE_DIR" -maxdepth 1 -mindepth 1 -type d -printf "%f\n" | grep "${LIST_TARGET_BASE_NAME/$BASE_SUFFIX/$SNAP_SUFFIX}.$DATE_REGEX"
}

function remove()
{
REMOVE_TARGET_BASE_PATH="$BACKUPS$1$BASE_SUFFIX"
REMOVE_TARGET_BASE_DIR="$(dirname $REMOVE_TARGET_BASE_PATH)"
btrfs subvolume delete -c $REMOVE_TARGET_BASE_DIR/$2
sync
}

function removeall()
{
DATE_OFFSET="$2"
FILTER="$(filter "$DATE_OFFSET")"
while read -r SNAPSHOT ; do
remove "$1" "$SNAPSHOT"
done < <(list "$1" | grep "$FILTER")

}

(
COMMAND="$1"
shift

case "$COMMAND" in
"--help")
echo "Help"
;;
"suffix")
suffix
;;
"filter")
filter "$1"
;;
"backup")
wait_lock_or_terminate
backup "$1"
;;
"list")
list "$1"
;;
"remove")
wait_lock_or_terminate
remove "$1" "$2"
;;
"removeall")
wait_lock_or_terminate
removeall "$1" "$2"
;;
*)
echo "None.."
;;
esac
) 98>$LOCK_FILE

EOF


Что хоть оно делает..?
Содержит набор простейших команд для создания снапшотов BTRFS и их копирования на другую ФС посредством BTRFS send/recieve.

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

Еще один скрипт который запихнем в cron:

Еще немного bash-кода
#cat >/root/btrfs-backup/cron-daily.sh << EOF
#!/bin/bash
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

SCRIPT_FILE="$(realpath $0)"
SCRIPT_DIR="$(dirname $SCRIPT_FILE)"
SCRIPT_NAME="$(basename -s .sh $SCRIPT_FILE)"

BACKUP_SCRIPT="$SCRIPT_DIR/btrfs-backup.sh"
RETENTION="-60 day"
$BACKUP_SCRIPT backup root/@
$BACKUP_SCRIPT removeall root/@ "$RETENTION"
$BACKUP_SCRIPT backup root/@home
$BACKUP_SCRIPT removeall root/@home "$RETENTION"
$BACKUP_SCRIPT backup boot/
$BACKUP_SCRIPT removeall boot/ "$RETENTION"
EOF


Что оно делает..?
Создает и и синхронизирует на backup ФС инкрементальные снимки перечисленных BTRFS-томов. После этого удаляет все снимки созданные 60 дней назад. После запуска в подкаталогах /backup/btrfs/back/remote/ появятся датированные снимки перечисленных томов.

Дадим коду права на выполнение:

#chmod +x /root/btrfs-backup/cron-daily.sh
#chmod +x /root/btrfs-backup/btrfs-backup.sh

Проверим и запихнем в крон:

#/usr/bin/nice -n 19 /usr/bin/ionice -c 3 /root/btrfs-backup/cron-daily.sh 2>&1 | /usr/bin/logger -t btrfs-backup
#cat /var/log/syslog | grep btrfs-backup
#crontab -e
0 2 * * * /usr/bin/nice -n 19 /usr/bin/ionice -c 3 /root/btrfs-backup/cron-daily.sh 2>&1 | /usr/bin/logger -t btrfs-backup

Резервное копирование LVM thin


Создадим тонкий пул на резервном устройстве:

#lvcreate -L 274877906944B --poolmetadataspare y --poolmetadatasize 4294967296B --chunksize 64k -Z y -T backup/thin-pool

Установим ddrescue, т.к. скрипты будут использовать этот инструмент:

#apt-get install gddrescue

Создадим каталог для скриптов:

#mkdir /root/lvm-thin-backup

Скопируем скрипты:

Очень много bash внутри...
#cat >/root/lvm-thin-backup/lvm-thin-backup.sh << EOF
#!/bin/bash
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

SCRIPT_FILE="$(realpath $0)"
SCRIPT_DIR="$(dirname $SCRIPT_FILE)"
SCRIPT_NAME="$(basename -s .sh $SCRIPT_FILE)"

LOCK_FILE="/dev/shm/$SCRIPT_NAME.lock"
DATE_PREFIX='%Y-%m-%d'
DATE_FORMAT=$DATE_PREFIX'-%H-%M-%S'
DATE_REGEX='[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]'
BASE_SUFFIX=".base"
PEND_SUFFIX=".pend"
SNAP_SUFFIX=".snap"
BACKUPS="backup"
BACKUPS_POOL="thin-pool"

export LVM_SUPPRESS_FD_WARNINGS=1

function terminate ()
{
echo "$1" >&2
exit 1
}

function wait_lock()
{
flock 98
}

function wait_lock_or_terminate()
{
echo "Wating for lock..."
wait_lock || terminate "Failed to get lock. Exiting..."
echo "Got lock..."
}

function suffix()
{
FORMATTED_DATE=$(date +"$DATE_FORMAT")
echo "$SNAP_SUFFIX.$FORMATTED_DATE"
}

function filter()
{
FORMATTED_DATE=$(date --date="$1" +"$DATE_PREFIX")
echo "$SNAP_SUFFIX.$FORMATTED_DATE"
}

function read_thin_id {
lvs --rows --reportformat basic --quiet -othin_id "$1/$2" | awk '{print $2}'
}

function read_pool_lv {
lvs --rows --reportformat basic --quiet -opool_lv "$1/$2" | awk '{print $2}'
}

function read_lv_dm_path {
lvs --rows --reportformat basic --quiet -olv_dm_path "$1/$2" | awk '{print $2}'
}

function read_lv_active {
lvs --rows --reportformat basic --quiet -olv_active "$1/$2" | awk '{print $2}'
}

function read_lv_chunk_size {
lvs --rows --reportformat basic --quiet --units b --nosuffix -ochunk_size "$1/$2" | awk '{print $2}'
}

function read_lv_size {
lvs --rows --reportformat basic --quiet --units b --nosuffix -olv_size "$1/$2" | awk '{print $2}'
}

function activate_volume {
lvchange -ay -Ky "$1/$2"
}

function deactivate_volume {
lvchange -an "$1/$2"
}

function read_thin_metadata_snap {
dmsetup status "$1" | awk '{print $7}'
}

function thindiff()
{
DIFF_VG="$1"
DIFF_SOURCE="$2"
DIFF_TARGET="$3"
DIFF_SOURCE_POOL=$(read_pool_lv $DIFF_VG $DIFF_SOURCE)
DIFF_TARGET_POOL=$(read_pool_lv $DIFF_VG $DIFF_TARGET)

if [ "$DIFF_SOURCE_POOL" == "" ]
then
(>&2 echo "Source LV is not thin.")
exit 1
fi

if [ "$DIFF_TARGET_POOL" == "" ]
then
(>&2 echo "Target LV is not thin.")
exit 1
fi

if [ "$DIFF_SOURCE_POOL" != "$DIFF_TARGET_POOL" ]
then
(>&2 echo "Source and target LVs belong to different thin pools.")
exit 1
fi

DIFF_POOL_PATH=$(read_lv_dm_path $DIFF_VG $DIFF_SOURCE_POOL)
DIFF_SOURCE_ID=$(read_thin_id $DIFF_VG $DIFF_SOURCE)
DIFF_TARGET_ID=$(read_thin_id $DIFF_VG $DIFF_TARGET)
DIFF_POOL_PATH_TPOOL="$DIFF_POOL_PATH-tpool"
DIFF_POOL_PATH_TMETA="$DIFF_POOL_PATH"_tmeta
DIFF_POOL_METADATA_SNAP=$(read_thin_metadata_snap $DIFF_POOL_PATH_TPOOL)

if [ "$DIFF_POOL_METADATA_SNAP" != "-" ]
then
(>&2 echo "Thin pool metadata snapshot already exist. Assuming stale one. Will release metadata snapshot in 5 seconds.")
sleep 5
dmsetup message $DIFF_POOL_PATH_TPOOL 0 release_metadata_snap
fi

dmsetup message $DIFF_POOL_PATH_TPOOL 0 reserve_metadata_snap
DIFF_POOL_METADATA_SNAP=$(read_thin_metadata_snap $DIFF_POOL_PATH_TPOOL)

if [ "$DIFF_POOL_METADATA_SNAP" == "-" ]
then
(>&2 echo "Failed to create thin pool metadata snapshot.")
exit 1
fi

#We keep output in variable because metadata snapshot need to be released early.
DIFF_DATA=$(thin_delta -m$DIFF_POOL_METADATA_SNAP --snap1 $DIFF_SOURCE_ID --snap2 $DIFF_TARGET_ID $DIFF_POOL_PATH_TMETA)

dmsetup message $DIFF_POOL_PATH_TPOOL 0 release_metadata_snap

echo $"$DIFF_DATA" | grep -E 'different|left_only|right_only' | sed 's/</"/g' | sed 's/ /"/g' | awk -F'\"' '{print $6 "\t" $8 "\t" $11}' | sed 's/different/copy/g' | sed 's/left_only/copy/g' | sed 's/right_only/discard/g'

}

function thinsync()
{
SYNC_VG="$1"
SYNC_PEND="$2"
SYNC_BASE="$3"
SYNC_TARGET="$4"
SYNC_PEND_POOL=$(read_pool_lv $SYNC_VG $SYNC_PEND)
SYNC_BLOCK_SIZE=$(read_lv_chunk_size $SYNC_VG $SYNC_PEND_POOL)
SYNC_PEND_PATH=$(read_lv_dm_path $SYNC_VG $SYNC_PEND)

activate_volume $SYNC_VG $SYNC_PEND

while read -r SYNC_ACTION SYNC_OFFSET SYNC_LENGTH ; do
SYNC_OFFSET_BYTES=$((SYNC_OFFSET * SYNC_BLOCK_SIZE))
SYNC_LENGTH_BYTES=$((SYNC_LENGTH * SYNC_BLOCK_SIZE))
if [ "$SYNC_ACTION" == "copy" ]
then
ddrescue --quiet --force --input-position=$SYNC_OFFSET_BYTES --output-position=$SYNC_OFFSET_BYTES --size=$SYNC_LENGTH_BYTES "$SYNC_PEND_PATH" "$SYNC_TARGET"
fi

if [ "$SYNC_ACTION" == "discard" ]
then
blkdiscard -o $SYNC_OFFSET_BYTES -l $SYNC_LENGTH_BYTES "$SYNC_TARGET"
fi
done < <(thindiff "$SYNC_VG" "$SYNC_PEND" "$SYNC_BASE")
}

function discard_volume()
{
DISCARD_VG="$1"
DISCARD_LV="$2"
DISCARD_LV_PATH=$(read_lv_dm_path "$DISCARD_VG" "$DISCARD_LV")
if [ "$DISCARD_LV_PATH" != "" ]
then
echo "$DISCARD_LV_PATH found"
else
echo "$DISCARD_LV not found in $DISCARD_VG"
exit 1
fi
DISCARD_LV_POOL=$(read_pool_lv $DISCARD_VG $DISCARD_LV)
DISCARD_LV_SIZE=$(read_lv_size "$DISCARD_VG" "$DISCARD_LV")
lvremove -y --quiet "$DISCARD_LV_PATH" || exit 1
lvcreate --thin-pool "$DISCARD_LV_POOL" -V "$DISCARD_LV_SIZE"B --name "$DISCARD_LV" "$DISCARD_VG" || exit 1
}

function backup()
{
SOURCE_VG="$1"
SOURCE_LV="$2"
TARGET_VG="$BACKUPS"
TARGET_LV="$SOURCE_VG-$SOURCE_LV"
SOURCE_BASE_LV="$SOURCE_LV$BASE_SUFFIX"
TARGET_BASE_LV="$TARGET_LV$BASE_SUFFIX"
SOURCE_PEND_LV="$SOURCE_LV$PEND_SUFFIX"
TARGET_PEND_LV="$TARGET_LV$PEND_SUFFIX"
SOURCE_BASE_LV_PATH=$(read_lv_dm_path "$SOURCE_VG" "$SOURCE_BASE_LV")
SOURCE_PEND_LV_PATH=$(read_lv_dm_path "$SOURCE_VG" "$SOURCE_PEND_LV")
TARGET_BASE_LV_PATH=$(read_lv_dm_path "$TARGET_VG" "$TARGET_BASE_LV")
TARGET_PEND_LV_PATH=$(read_lv_dm_path "$TARGET_VG" "$TARGET_PEND_LV")

if [ "$SOURCE_BASE_LV_PATH" != "" ]
then
echo "$SOURCE_BASE_LV_PATH found"
else
echo "Source base not found creating snapshot of $SOURCE_VG/$SOURCE_LV to $SOURCE_VG/$SOURCE_BASE_LV"
lvcreate --quiet --snapshot --name "$SOURCE_BASE_LV" "$SOURCE_VG/$SOURCE_LV" || exit 1
SOURCE_BASE_LV_PATH=$(read_lv_dm_path "$SOURCE_VG" "$SOURCE_BASE_LV")
activate_volume "$SOURCE_VG" "$SOURCE_BASE_LV"
echo "Discarding $SOURCE_BASE_LV_PATH as we need to bootstrap."
SOURCE_BASE_POOL=$(read_pool_lv $SOURCE_VG $SOURCE_BASE_LV)
SOURCE_BASE_CHUNK_SIZE=$(read_lv_chunk_size $SOURCE_VG $SOURCE_BASE_POOL)
discard_volume "$SOURCE_VG" "$SOURCE_BASE_LV"
sync
if [ "$TARGET_BASE_LV_PATH" != "" ]
then
echo "$TARGET_BASE_LV_PATH found out of sync with source... removing..."
lvremove -y --quiet $TARGET_BASE_LV_PATH || exit 1
TARGET_BASE_LV_PATH=$(read_lv_dm_path "$TARGET_VG" "$TARGET_BASE_LV")
sync
fi
fi
SOURCE_BASE_SIZE=$(read_lv_size "$SOURCE_VG" "$SOURCE_BASE_LV")
if [ "$TARGET_BASE_LV_PATH" != "" ]
then
echo "$TARGET_BASE_LV_PATH found"
else
echo "$TARGET_VG/$TARGET_LV not found. Creating empty volume."
lvcreate --thin-pool "$BACKUPS_POOL" -V "$SOURCE_BASE_SIZE"B --name "$TARGET_BASE_LV" "$TARGET_VG" || exit 1
echo "Have to rebootstrap. Discarding source at $SOURCE_BASE_LV_PATH"
activate_volume "$SOURCE_VG" "$SOURCE_BASE_LV"
SOURCE_BASE_POOL=$(read_pool_lv $SOURCE_VG $SOURCE_BASE_LV)
SOURCE_BASE_CHUNK_SIZE=$(read_lv_chunk_size $SOURCE_VG $SOURCE_BASE_POOL)
discard_volume "$SOURCE_VG" "$SOURCE_BASE_LV"
TARGET_BASE_POOL=$(read_pool_lv $TARGET_VG $TARGET_BASE_LV)
TARGET_BASE_CHUNK_SIZE=$(read_lv_chunk_size $TARGET_VG $TARGET_BASE_POOL)
TARGET_BASE_LV_PATH=$(read_lv_dm_path "$TARGET_VG" "$TARGET_BASE_LV")
echo "Discarding target at $TARGET_BASE_LV_PATH"
discard_volume "$TARGET_VG" "$TARGET_BASE_LV"
sync
fi
if [ "$SOURCE_PEND_LV_PATH" != "" ]
then
echo "$SOURCE_PEND_LV_PATH found removing..."
lvremove -y --quiet "$SOURCE_PEND_LV_PATH" || exit 1
sync
fi
lvcreate --quiet --snapshot --name "$SOURCE_PEND_LV" "$SOURCE_VG/$SOURCE_LV" || exit 1
SOURCE_PEND_LV_PATH=$(read_lv_dm_path "$SOURCE_VG" "$SOURCE_PEND_LV")
sync
if [ "$TARGET_PEND_LV_PATH" != "" ]
then
echo "$TARGET_PEND_LV_PATH found removing..."
lvremove -y --quiet $TARGET_PEND_LV_PATH
sync
fi
lvcreate --quiet --snapshot --name "$TARGET_PEND_LV" "$TARGET_VG/$TARGET_BASE_LV" || exit 1
TARGET_PEND_LV_PATH=$(read_lv_dm_path "$TARGET_VG" "$TARGET_PEND_LV")
SOURCE_PEND_LV_SIZE=$(read_lv_size "$SOURCE_VG" "$SOURCE_PEND_LV")
lvresize -L "$SOURCE_PEND_LV_SIZE"B "$TARGET_PEND_LV_PATH"
activate_volume "$TARGET_VG" "$TARGET_PEND_LV"
echo "Synching $SOURCE_PEND_LV_PATH to $TARGET_PEND_LV_PATH"
thinsync "$SOURCE_VG" "$SOURCE_PEND_LV" "$SOURCE_BASE_LV" "$TARGET_PEND_LV_PATH" || exit 1
sync

TARGET_DATE_SUFFIX=$(suffix)
lvcreate --quiet --snapshot --name "$TARGET_LV$TARGET_DATE_SUFFIX" "$TARGET_VG/$TARGET_PEND_LV" || exit 1
sync
lvremove --quiet -y "$SOURCE_BASE_LV_PATH" || exit 1
sync
lvremove --quiet -y "$TARGET_BASE_LV_PATH" || exit 1
sync
lvrename -y "$SOURCE_VG/$SOURCE_PEND_LV" "$SOURCE_BASE_LV" || exit 1
lvrename -y "$TARGET_VG/$TARGET_PEND_LV" "$TARGET_BASE_LV" || exit 1
sync
deactivate_volume "$TARGET_VG" "$TARGET_BASE_LV"
deactivate_volume "$SOURCE_VG" "$SOURCE_BASE_LV"
}

function verify()
{
SOURCE_VG="$1"
SOURCE_LV="$2"
TARGET_VG="$BACKUPS"
TARGET_LV="$SOURCE_VG-$SOURCE_LV"
SOURCE_BASE_LV="$SOURCE_LV$BASE_SUFFIX"
TARGET_BASE_LV="$TARGET_LV$BASE_SUFFIX"
TARGET_BASE_LV_PATH=$(read_lv_dm_path "$TARGET_VG" "$TARGET_BASE_LV")
SOURCE_BASE_LV_PATH=$(read_lv_dm_path "$SOURCE_VG" "$SOURCE_BASE_LV")

if [ "$SOURCE_BASE_LV_PATH" != "" ]
then
echo "$SOURCE_BASE_LV_PATH found"
else
echo "$SOURCE_BASE_LV_PATH not found"
exit 1
fi
if [ "$TARGET_BASE_LV_PATH" != "" ]
then
echo "$TARGET_BASE_LV_PATH found"
else
echo "$TARGET_BASE_LV_PATH not found"
exit 1
fi
activate_volume "$TARGET_VG" "$TARGET_BASE_LV"
activate_volume "$SOURCE_VG" "$SOURCE_BASE_LV"
echo Comparing "$SOURCE_BASE_LV_PATH" with "$TARGET_BASE_LV_PATH"
cmp "$SOURCE_BASE_LV_PATH" "$TARGET_BASE_LV_PATH"
echo Done...
deactivate_volume "$TARGET_VG" "$TARGET_BASE_LV"
deactivate_volume "$SOURCE_VG" "$SOURCE_BASE_LV"
}

function resync()
{
SOURCE_VG="$1"
SOURCE_LV="$2"
TARGET_VG="$BACKUPS"
TARGET_LV="$SOURCE_VG-$SOURCE_LV"
SOURCE_BASE_LV="$SOURCE_LV$BASE_SUFFIX"
TARGET_BASE_LV="$TARGET_LV$BASE_SUFFIX"
TARGET_BASE_LV_PATH=$(read_lv_dm_path "$TARGET_VG" "$TARGET_BASE_LV")
SOURCE_BASE_LV_PATH=$(read_lv_dm_path "$SOURCE_VG" "$SOURCE_BASE_LV")

if [ "$SOURCE_BASE_LV_PATH" != "" ]
then
echo "$SOURCE_BASE_LV_PATH found"
else
echo "$SOURCE_BASE_LV_PATH not found"
exit 1
fi
if [ "$TARGET_BASE_LV_PATH" != "" ]
then
echo "$TARGET_BASE_LV_PATH found"
else
echo "$TARGET_BASE_LV_PATH not found"
exit 1
fi
activate_volume "$TARGET_VG" "$TARGET_BASE_LV"
activate_volume "$SOURCE_VG" "$SOURCE_BASE_LV"
SOURCE_BASE_POOL=$(read_pool_lv $SOURCE_VG $SOURCE_BASE_LV)
SYNC_BLOCK_SIZE=$(read_lv_chunk_size $SOURCE_VG $SOURCE_BASE_POOL)

echo Syncronizing "$SOURCE_BASE_LV_PATH" to "$TARGET_BASE_LV_PATH"

CMP_OFFSET=0
while [[ "$CMP_OFFSET" != "" ]] ; do
CMP_MISMATCH=$(cmp -i "$CMP_OFFSET" "$SOURCE_BASE_LV_PATH" "$TARGET_BASE_LV_PATH" | grep differ | awk '{print $5}' | sed 's/,//g' )
if [[ "$CMP_MISMATCH" != "" ]] ; then
CMP_OFFSET=$(( CMP_MISMATCH + CMP_OFFSET ))
SYNC_OFFSET_BYTES=$(( ( CMP_OFFSET / SYNC_BLOCK_SIZE ) * SYNC_BLOCK_SIZE ))
SYNC_LENGTH_BYTES=$(( SYNC_BLOCK_SIZE ))
echo "Synching $SYNC_LENGTH_BYTES bytes at $SYNC_OFFSET_BYTES from $SOURCE_BASE_LV_PATH to $TARGET_BASE_LV_PATH"
ddrescue --quiet --force --input-position=$SYNC_OFFSET_BYTES --output-position=$SYNC_OFFSET_BYTES --size=$SYNC_LENGTH_BYTES "$SOURCE_BASE_LV_PATH" "$TARGET_BASE_LV_PATH"
else
CMP_OFFSET=""
fi
done
echo Done...
deactivate_volume "$TARGET_VG" "$TARGET_BASE_LV"
deactivate_volume "$SOURCE_VG" "$SOURCE_BASE_LV"
}

function list()
{
LIST_SOURCE_VG="$1"
LIST_SOURCE_LV="$2"
LIST_TARGET_VG="$BACKUPS"
LIST_TARGET_LV="$LIST_SOURCE_VG-$LIST_SOURCE_LV"
LIST_TARGET_BASE_LV="$LIST_TARGET_LV$SNAP_SUFFIX"
lvs -olv_name | grep "$LIST_TARGET_BASE_LV.$DATE_REGEX"
}

function remove()
{
REMOVE_TARGET_VG="$BACKUPS"
REMOVE_TARGET_LV="$1"
lvremove -y "$REMOVE_TARGET_VG/$REMOVE_TARGET_LV"
sync
}

function removeall()
{
DATE_OFFSET="$3"
FILTER="$(filter "$DATE_OFFSET")"
while read -r SNAPSHOT ; do
remove "$SNAPSHOT"
done < <(list "$1" "$2" | grep "$FILTER")

}

(
COMMAND="$1"
shift

case "$COMMAND" in
"--help")
echo "Help"
;;
"suffix")
suffix
;;
"filter")
filter "$1"
;;
"backup")
wait_lock_or_terminate
backup "$1" "$2"
;;
"list")
list "$1" "$2"
;;
"thindiff")
thindiff "$1" "$2" "$3"
;;
"thinsync")
thinsync "$1" "$2" "$3" "$4"
;;
"verify")
wait_lock_or_terminate
verify "$1" "$2"
;;
"resync")
wait_lock_or_terminate
resync "$1" "$2"
;;
"remove")
wait_lock_or_terminate
remove "$1"
;;
"removeall")
wait_lock_or_terminate
removeall "$1" "$2" "$3"
;;
*)
echo "None.."
;;
esac
) 98>$LOCK_FILE

EOF


Что оно делает...?
Содержит набор команд для манипуляции тонкими снимками и синхронизации разницы между двумя тонкими снапшотами, полученной через thin_delta, на другое блочное устройство с использованием ddrescue и blkdiscard.

Еще один скрипт, который мы запихнем в крон:

Еще немного bash
#cat >/root/lvm-thin-backup/cron-daily.sh << EOF
#!/bin/bash
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

SCRIPT_FILE="$(realpath $0)"
SCRIPT_DIR="$(dirname $SCRIPT_FILE)"
SCRIPT_NAME="$(basename -s .sh $SCRIPT_FILE)"

BACKUP_SCRIPT="$SCRIPT_DIR/lvm-thin-backup.sh"
RETENTION="-60 days"

$BACKUP_SCRIPT backup images linux-dev
$BACKUP_SCRIPT backup images win8
$BACKUP_SCRIPT backup images win8-data
#etc

$BACKUP_SCRIPT removeall images linux-dev "$RETENTION"
$BACKUP_SCRIPT removeall images win8 "$RETENTION"
$BACKUP_SCRIPT removeall images win8-data "$RETENTION"
#etc

EOF


Что оно делает...?
Использует предидущий скрипт, чтобы создать и синхронизировать резервные копии перечисленных тонких томов. Скрипт оставит неактивные снапшоты перечисленных томов, которые нужны для отслеживания изменений с последней синхронизации.

Этот скрипт надо подредактировать, указывая список тонких томов, для которых требуется делать резервные копии. Приведенные названия даны только для примера. При желании можно написать скрипт, который будет синхронизировать все тома.

Дадим права:

#chmod +x /root/lvm-thin-backup/cron-daily.sh
#chmod +x /root/lvm-thin-backup/lvm-thin-backup.sh


Проверим и запихнем в крон:

#/usr/bin/nice -n 19 /usr/bin/ionice -c 3 /root/lvm-thin-backup/cron-daily.sh 2>&1 | /usr/bin/logger -t lvm-thin-backup
#cat /var/log/syslog | grep lvm-thin-backup
#crontab -e
0 3 * * * /usr/bin/nice -n 19 /usr/bin/ionice -c 3 /root/lvm-thin-backup/cron-daily.sh 2>&1 | /usr/bin/logger -t lvm-thin-backup

Первый запуск будет долгим, т.к. тонкие тома будут полностью синхронизированы копированием всего используемого пространства. Благодаря метаданным LVM thin мы знаем какие блоки используются на самом деле, так что, копироваться будут только реально используемые блоки тонких томов.

Последующие запуски будут копировать данные инкрементально благодаря отслеживанию изменений через метаданные LVM thin.

Посмотрим, что получилось:


#time /root/btrfs-backup/cron-daily.sh
real 0m2,967s
user 0m0,225s
sys 0m0,353s

#time /root/lvm-thin-backup/cron-daily.sh
real 1m2,710s
user 0m12,721s
sys 0m6,671s

#ls -al /backup/btrfs/back/remote/*
/backup/btrfs/back/remote/boot:
total 0
drwxr-xr-x 1 root root 1260 мар 26 09:11 .
drwxr-xr-x 1 root root 16 мар 6 09:30 ..
drwxr-xr-x 1 root root 322 мар 26 02:00 .@base
drwxr-xr-x 1 root root 516 мар 6 09:39 .@snap.2020-03-06-09-39-37
drwxr-xr-x 1 root root 516 мар 6 09:39 .@snap.2020-03-06-09-39-57
...
/backup/btrfs/back/remote/root:
total 0
drwxr-xr-x 1 root root 2820 мар 26 09:11 .
drwxr-xr-x 1 root root 16 мар 6 09:30 ..
drwxr-xr-x 1 root root 240 мар 26 09:11 @.@base
drwxr-xr-x 1 root root 22 мар 26 09:11 @home.@base
drwxr-xr-x 1 root root 22 мар 6 09:39 @home.@snap.2020-03-06-09-39-35
drwxr-xr-x 1 root root 22 мар 6 09:39 @home.@snap.2020-03-06-09-39-57
...
drwxr-xr-x 1 root root 240 мар 6 09:39 @.@snap.2020-03-06-09-39-26
drwxr-xr-x 1 root root 240 мар 6 09:39 @.@snap.2020-03-06-09-39-56
...

#lvs -olv_name,lv_size images && lvs -olv_name,lv_size backup
LV LSize
linux-dev 128,00g
linux-dev.base 128,00g
thin-pool 1,38t
win8 128,00g
win8-data 2,00t
win8-data.base 2,00t
win8.base 128,00g
LV LSize
backup 256,00g
images-linux-dev.base 128,00g
images-linux-dev.snap.2020-03-08-10-09-11 128,00g
images-linux-dev.snap.2020-03-08-10-09-25 128,00g
...
images-win8-data.base 2,00t
images-win8-data.snap.2020-03-16-14-11-55 2,00t
images-win8-data.snap.2020-03-16-14-19-50 2,00t
...
images-win8.base 128,00g
images-win8.snap.2020-03-17-04-51-46 128,00g
images-win8.snap.2020-03-18-03-02-49 128,00g
...
thin-pool <2,09t

Причем тут матрешки?


Скорее всего при том, что логические тома LVM LV могут быть физическими томами LVM PV для других VG. LVM может быть рекурсивен, как матрешки. Это дает LVM чрезвычайную гибкость.

P.S.


В следующей статье мы с вами попробуем использовать несколько подобных мобильных СХД/KVM как основу для создания гео-распределенного storage/vm-кластера с резервированем на несколькольких континентах посредством домашних десктопов, домашнего-же интернета и P2P сетей.