Що спільного між LVM та матрьошкою?

Доброго вам дня.
Хочу поділитися із спільнотою практичним досвідом побудови системи зберігання даних для 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 — це Ferrari, а 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.

HDD SATA

Тут особливо винаходити нічого не треба. Ми створимо один розділ на все. Розділ створимо тому, що ці диски бачить 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 мають на борту кеш, що динамічно розширюється, розміром від 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. Отже, «ініціалізувати» цей масив поки не будемо. Зробимо це пізніше, після установки ОС.

HDD SATA

#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 (aka /dev/mapper/root-root), - точка монтування / (корінь), ФС - BTRFS
  • /dev/root/swap (aka /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=="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 на самих масивах.

Підредагуємо /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 в обхід кешу ( _corig). При цьому сам кешований пристрій все одно буде просканований через кеш (просто ).
  • пристроях, що містять метадані 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=256write --bs=256k --rw=write
--name=128write --bs=128k --rw=write
--name=64write --bs=64k --rw=write
--name=32write --bs=32k --rw=write
--name=16write --bs=16k --rw=write
--name=8write --bs=8k --rw=write
--name=4write --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}' доки не отримаємо нуль. Якщо нуля немає надто довго, його можна створити тимчасово перевівши кеш у режиміперезавантаження. Однак, враховуючи швидкісні характеристики наших масивів 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 "%fn" | 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 [email protected]
drwxr-xr-x 1 root root 516 мар 6 09:39 [email protected]
...
/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 @[email protected]
drwxr-xr-x 1 root root 22 мар 6 09:39 @[email protected]
...
drwxr-xr-x 1 root root 240 мар 6 09:39 @[email protected]
drwxr-xr-x 1 root root 240 мар 6 09:39 @[email protected]
...

#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 можуть бути фізичними томами LVM PV для інших VG. LVM може бути рекурсивний, як матрьошки. Це дає LVM надзвичайну гнучкість.

PS

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

Джерело: habr.com

Додати коментар або відгук