Главная » Хабрахабр » Бэкап для Linux, или как создать снапшот

Бэкап для Linux, или как создать снапшот

Всем привет! Я работаю в Veeam над проектом Veeam Agent for Linux. С помощью этого продукта можно бэкапить машину с ОС Linux. «Agent» в названии означает, что программа позволяет бэкапить физические машины. Виртуалки тоже бэкапит, но располагается при этом на гостевой ОС.

Вдохновением для этой статьи послужил мой доклад на конференции Linux Piter, который я решил оформить в виде статьи для всех интересующихся хабражителей.

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

Всех заинтересовавшихся прошу под кат!

Немного теории в начале

Исторически так сложилось, что есть два подхода к созданию бэкапов: File backup и Volume backup. В первом случае мы копируем каждый файл как отдельный объект, во втором – копируем всё содержимое тома в виде некоего образа.

У обоих способов есть масса своих плюсов и минусов, но мы рассмотрим их через призму восстановления после сбоя:

  • В случае File backup для полноценного восстановления сервера целиком, нам потребуется вначале установить ОС, потом — необходимые сервисы и только после этого восстановить файлы из бэкапа.
  • В случае же Volume backup для полного восстановления достаточно просто восстановить все тома машины без лишних усилий со стороны человека.

Очевидно, что в случае Volume backup восстановить систему можно быстрее, а это важная характеристика системы. Поэтому, для себя отмечаем volume backup как более предпочтительный вариант.

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

В теории всё просто: создаём неизменную копию – снапшот – и бэкапим данные с него. Чтобы избежать всех этих проблем, прогрессивное человечество придумало технологию моментального снимка – снапшота. Звучит просто, но, как водится, есть нюансы. Когда бэкап окончен – снапшот уничтожаем.

Например, решения на основе device mapper, такие, как LVM и Thin provisioning, обеспечивают полноценные снапшоты томов, но требуют специальной разметки дисков ещё на этапе установки системы, а значит, в общем случае не подходят. Из-за них было рождено множество реализаций этой технологи.

BTRFS и ZFS дают возможность создавать моментальные снимки подструктур файловой системы, что очень здорово, но на данный момент их доля на серверах невелика, а мы пытаемся сделать универсальное решение.

В этом случае мы можем использовать dm-snap (кстати, сейчас разрабатывается dm-bow), но тут — свой нюанс. Предположим, на нашем блочном девайсе есть банальная EXT. Этим путём решили пойти и мы, написав свой модуль. Нужно иметь на готове свободное блочное устройство, чтобы было куда отбрасывать данные снапшота.
Обратив внимание на альтернативные решения для бэкапа, мы заметили что они, как правило, используют свой модуль ядра для создания снапшотов блочных устройств. Решено было распространять его по GPL лицензии, так что в открытом доступе он доступен на github.

How it works — в теории

Снапшот под микроскопом

Итак, теперь рассмотрим общий принцип работы модуля и более подробно остановимся на ключевых проблемах.

По сути, veeamsnap (так мы назвали свой модуль ядра) – это фильтр драйвера блочного устройства.

Его работа заключена в перехвате запросов к драйверу блочного устройства.

Назовём эту область снапсторой. Перехватив запрос на запись, модуль производит копирование данных с оригинального блочного устройства в область данных снапшота.

Это виртуальное блочное устройство, копия оригинального устройства на конкретный момент времени. А что такое сам снапшот? При обращении к блокам данных на этом устройстве они могут быть считаны либо со снапсторы, либо с оригинального устройства.

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

Самый простой способ это сделать – воспользоваться ioctl GETFSMAP.
Данные о занятых блоках позволяют читать со снапшота только актуальные данные. Например, мы можем получить карту занятых блоков от файловой системы.

Ну, и совсем опциональное действие: проиндексировать файлы, которые попадают в бэкап, для возможности гранулярного рестора в будущем. Также, можно исключить некоторые файлы.

CoW vs RoW

Давайте немного остановимся на выборе алгоритма работы снапшота. Выбор тут не особо обширен: Copy-on-Write или Redirect-on-Write.

Замечательный алгоритм для систем хранения, построенных на базе В+ деревьев, таких, как BTRFS, ZFS и Thin Provisioning. Redirect-on-Write при перехвате запроса на запись перенаправит его в снапстору, после чего все запросы на чтение этого блока будут уходить туда же. Производительность – отличная, по сравнению с CoW. Технология стара как мир, но особенно хорошо она проявляет себя в гипервизорах, где можно создать новый файл и писать туда новые блоки на время жизни снапшота. Но есть жирный минус – структура оригинального устройства меняется, а при удалении снапшота надо скопировать все блоки из снапсторы в оригинальное место.

Используется для создания снапшотов для LVM томов и теневых копий VSS. Copy-on-Write при перехвате запроса копирует в снапстору данные, которые должны подвергнуться изменению, после чего позволяет их перезаписать в оригинальном месте. не меняет структуру оригинального устройства, и при удалении (или аварии) снапшот можно просто отбросить, не рискуя данными. Очевидно, для создания снапшотов блочных устройств он подходит больше, т.к. Минус такого подхода – снижение производительности, так как на каждую операцию записи добавляется пара операций чтение/запись.

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

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

How it works — на практике

Согласованное состояние

Ради него всё и задумывалось.
Например, если в момент создания снапшота (в первом приближении можно считать, что создаётся он моментально) в какой-то файл будет производиться запись, то в снапшоте файл окажется недописанным, а значит — повреждённый и бессмысленный. Аналогичная ситуация и с файлам баз данных и самой файловой системой.

Есть же механизмы журналирования, предохраняющие от подобных проблем! Но мы же в 21-м веке живём! При восстановлении в согласованное состояние по журналу незавершённые операции будут отброшены, а значит — потеряны. Замечание верное, правда, есть важное “но”: эта защита не от сбоя, а от его последствий. Поэтому важно сместить приоритет на защиту от причины, а не лечить последствия.

Для этого в ядре есть функции freeze_bdev и thaw_bdev. Систему можно предупредить о том, что сейчас будет создан снапшот. При вызове первой система должна сбросить кэш, приостановить создание новых запросов к блочному устройству и дождаться завершения всех ранее сформированных запросов. Они дёргают функции файловой системы freeze_fs и unfreeze_fs. А при вызове unfreeze_fs файловая система восстанавливает своё нормальное функционирование.

А что с приложениями? Получается, что файловую систему мы можем предупредить. В то время, как в Windows существует механизм VSS, который с помощью Writer-ов обеспечивает взаимодействие с другими продуктами, в Linux каждый идёт своим путём. Тут, к сожалению, всё плохо. Со своей стороны, в ближайшем релизе мы внедрим поддержку Oracle Application Processing, как наиболее часто запрашиваемую нашими клиентами функцию. На данный момент это привело к ситуации, что задача администратора системы самостоятельно написать (скопировать, украсть, купить, etc) pre-freeze и post-thaw скрипты, которые будут подготавливать их приложение к снапшоту. Потом, возможно, будут поддержаны и другие приложения, но в целом ситуация довольно печальна.

Где расположить снапстору?

Это вторая встающая на нашем пути проблема. С первого взгляда проблема не очевидна, но, немного разобравшись, увидим, что это та ещё заноза.

Для разработчика вариант просто отличный! Конечно же, самое простое решение — расположить снапстору в RAM. Всё быстро, очень удобно делать отладку, но есть косяк: оперативка — ресурс ценный, и расположить там большую снапстору нам никто не даст.

Но возникает другая проблема – нельзя бэкапить том, на котором расположена снапстора. ОК, давайте сделаем снапстору обычным файлом. Кони бегали по кругу, по-научному — deadlock. Причина проста: мы перехватываем запросы на запись, а значит, будем и перехватывать свои собственные запросы на запись в снапстору. Работать надо уметь на том что есть. Следом возникает острое желание использовать для этого отдельный диск, но никто ради нас в сервера диски добавлять не будет.

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

Но, как правило, всё место на локальных дисках уже распределено между файловыми системами, и заодно надо крепко подумать, как обойти проблему deadlock’a. Значит, надо как-то хитро расположить снапстору на локальном диске.

Решение этой проблемы было реализовано в user-space коде, в сервисе. Направление для раздумий, в принципе, одно: надо как-то аллоцировать у файловой системы пространство, но с блочным устройством работать напрямую.

При этом фактически на файловой системе создаются только метаданные, описывающие расположение файла на томе. Существует системный вызов fallocate, который позволяет создать пустой файл нужного размера. А ioctl FIEMAP позволяет нам получить карту расположения блоков файла.

Далее при обращении к снапсторе модуль делает запросы напрямую к блочному устройству в известные нам блоки, и никаких deadlock’ов. И вуаля: мы создаём файл под снапстору с помощью fallocate, FIEMAP отдаёт нам карту расположения блоков этого файла, которую мы можем передать для работы в наш модуль veeamsnap.

Системный вызов fallocate поддерживается только XFS, EXT4 и BTRFS. Но тут есть нюанс. На функционале это сказывается увеличением времени на подготовку снапсторы, но выбирать не приходится. Для остальных файловых систем вроде EXT3 для аллокации файла его приходится полностью записывать. Опять таки, работать надо уметь на том что есть.

Это реальность NTFS и FAT32, где нет даже поддержки древнего FIBMAP. А что, если ioctl FIEMAP тоже не поддерживается? В двух словах алгоритм такой: Пришлось реализовать некий generic алгоритм, работа которого не зависит от особенностей файловой системы.

  1. Сервис создаёт файл и начинает записывать в него определённый паттерн.
  2. Модуль перехватывает запросы на запись, проверяет записываемые данные.
  3. Если данные блока соответствуют заданному паттерну, то блок помечается как относящийся к снапсторе.

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

Переполнение снапшота

Вернее, заканчивается место, которые мы выделили под снапстору. Суть проблемы в том, что новые данные некуда отбрасывать, а значит, снапшот становится непригоден для использования.
Что делать?

А насколько? Очевидно, надо увеличивать размер снапсторы. Для тома в 20 TB 10% будет 2TB – что очень много для ненагруженного сервера. Самый простой способ задания размера снапсторы – это определить процент от свободного места на томе (как сделано для VSS). А есть ещё тонкие тома… Для тома в 200 GB 10% составит 20GB, что может оказаться слишком мало для сервера, интенсивно обновляющего свои данные.

Это не соотвествует принципу «It just work». В общем, заранее прикинуть оптимальный размер требуемой снапсторы может только системный администратор сервера, то есть придётся заставить человака подумать и выдать своё экспертное мнение.

Идея состоит в разбиении снапсторы на порции. Для решения этой проблемы мы разработали алгоритм stretch snapshot. При этом, новые порции создаются уже после создания снапшота по мере необходимости.

Опять же коротенько алгоритм:

  1. Перед созданием снапшота создаётся первая порция снапсторы и отдаётся модулю.
  2. Когда снапшот создан, порция начнёт заполняться.
  3. Как только половина порции оказывается заполнена, посылается запрос сервису на создание новой.
  4. Сервис создаёт её, отдаёт данные модулю.
  5. Модуль начинает заполнять следующую порцию.
  6. Алгоритм повторяется пока или бэкап не завершится, или пока не упрёмся в лимит использования свободного места на диске.

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

Пытаемся угадать необходимый размер и создаём всю снапстору целиком. Что делать в других случаях? EXT3 встречается на старых машинах. Но по нашей статистике, подавляющее большинство Linux серверов сейчас используют EXT4 и XFS. Зато в SLES/openSUSE можно наткнуться на BTRFS.

Change Block Tracking (CBT)

Инкрементальный или дифференциальный бэкап (кстати, слаще хрен редьки или нет, предлагаю читать тут) – без него нельзя представить ни один взрослый продукт для бэкапа. А чтобы это работало, нужен CBT. Если кто-то пропустил: CBT позволяет отслеживать изменения и записывать в бэкап только изменённые с последнего бэкапа данные.

Например, в VMware vSphere эта функция доступна с 4-ой версии в 2009 году. Свои наработки в этой области есть у многих. Поэтому, для нашего модуля мы не стали оригинальничать и использовали уже работающие алгоритмы.
Коротенько о том, как это работает. В Hyper-V поддержка внедрена с Windows Server 2016, а для поддержки более ранних релизов был разработотан собственный драйвер VeeamFCT ещё в 2012-м.

Модуль просто отслеживает все запросы на запись, помечая изменившиеся блоки в таблице. Весь отслеживаемый том разбит на блоки. Таким образом, зная номера текущего снапшота и того, с которого был сделан предыдущий успешный бэкап, можно вычислить карту расположения изменившихся блоков. Фактически, таблица CBT – это массив байт, где каждый байт соответствует блоку и содержит номер снапшота, в котором он был изменён.
Во время бэкапа номер снапшота записывается в метаданные бэкапа.

Тут есть два нюанса.

При достижении этого порога таблица сбрасывается и происходит полный бэкап. Как я сказал, под номер снапшота в таблице CBT выделен один байт, значит, максимальная длина инкрементальной цепочки не может быть больше 255. А значит, при перезагрузке целевой машины или выгрузке модуля она будет сброшена, и опять-таки, понадобится создавать полный бэкап. Может показаться неудобным, но на самом деле цепочка в 255 инкрементов – далеко не самое лучшее решение при создании бэкап-плана.
Вторая особенность – это хранение CBT таблицы только в оперативной памяти. Кроме того, отпадает необходимость сохранять CBT таблицы при выключении системы. Такое решение позволяет не решать проблему старта модуля при старте системы.

Проблема производительности

Бекап — это всегда хорошая такая нагрузка на IO вашего оборудования. Если на нём и так хватает активных задач, то процесс резервного копирования может превратить вашу систему в этакого ленивца.
Давайте посмотрим, почему.

Скорость записи в таком случае максимальна, все задержки минимизированы, производительность стремится к максимуму. Представим, что сервер просто линейно пишет какие-то данные. И не забывайте, что для бэкапа надо ещё читать данные с этого же тома. Теперь добавим сюда процесс бэкапа, которому при каждой записи надо ещё успеть выполнить алгоритм Copy-on-Write, а это дополнительная операция чтения с последующей записью. Словом, ваш красивый linear access превращается в беспощадный random access со всеми вытекающими.

Работает это так. С этим явно надо что-то делать, и мы реализовали конвейер, чтобы обрабатывать запросы не по одному, а целыми пачками.

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

Throttling

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

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

Deadlock

Я думаю, надо немного подробней объяснить, что это такое.

Уже на этапе тестирования мы стали сталкиваться с ситуациями полного повисания системы с диагнозом: семь бед – один ресет.

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

Device mapper перенаправит этот запрос на блочное устройство. В данной ситуации при перехвате запроса на запись, модуль, копируя данные в снапстору, отправит запрос на запись LVM-тому. Но новый запрос не может быть обработан, пока не обработан предыдущий. Запрос от device mapper-а снова будет перехвачен модулем. В итоге, обработка запросов заблокирована, вас приветствует deadlock.

Это позволяет выявить deadlock и аварийно завершить бэкап. Чтобы не допустить подобной ситуации, в самом модуле ядра предусмотрен таймаут для операции копирования данных в снапстору. Логика здесь всё та же: лучше не сделать бэкап, чем подвесить сервер.

Round Robin Database

Это уже проблема, подкинутая пользователями после релиза первой версии.
Оказалось, есть такие сервисы, которые только и занимаются тем, что постоянно перезаписывают одни и те же блоки. Яркий пример – сервисы мониторинга, которые постоянно генерируют данные о состоянии системы и перезаписывают их по кругу. Для таких задач используют специализированные циклические базы данных (RRD).
Выяснилось, что при бэкапе таких баз снапшот гарантированно переполнится. При детальном изучении проблемы мы обнаружили недочёт в реализации CoW алгоритма. Если перезаписывался один и тот же блок, то в снапстору каждый раз копировались данные. Результат: дублирование данных в снапсторе.

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

Выбор размера блока

Теперь, когда снапстора разбита на блоки встает вопрос: а какого, собственно, размера делать блоки для разбиения снапсторы?

Если блок сделать большим, им легче оперировать, но при изменении хотя-бы одного сектора, придётся отправить весь блок в снапостору и, как следствие, повышаются шансы на переполнение снапсторы. Проблема двоякая.

Очевидно, что чем меньше размер блока, тем больший процент полезных данных отправляется в снапстору, но как это ударит по производительности?

Также отмечу, что в Windows VSS тоже используются блоки в 16 KiB. Правду искали эмпирическим путём и пришли в результату в 16KiB.

Вместо заключения

На этом пока всё. За бортом оставлю множество других, не менее интересных проблем, таких, как зависимость от версий ядра, выбор вариантов распространения модуля, kABI совместимость, работа в условиях бекпортов и т.д. Статья и так получилась объёмной, поэтому я решил остановиться на самых интересных проблемах.

0, код модуля лежит на github, и каждый желающий может использовать его в рамках GPL лицензии. Сейчас мы готовим к релизу версию 3.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Сегодня MIPS стал Open Source, против RISC/V и ARM. Как Россия повлияла на стратегию американской процессорной компании

То, о чем говорили сторонники Open Source с 1980-х — свершилось! Сегодня архитектура процессоров MIPS стала Open Source. Учитывая, что такие компании как Broadcom, Cavium, китайский ICT и Ingenic платили MIPS за архитектурную лицензию (право сделать совместимую по системе команд ...

Вышла новая версия Unity 2018.3

Вышла новая версия Unity, которая уже доступна для пользователей. Unity 2018.3 содержит более 2000 новых функций, исправлений и улучшений, включая улучшенный воркфлоу префабов, Visual Effect Graph (Preview) и обновленную систему Terrain, которые дают разработчикам возможность повысить производительность и создавать многогранные ...