Как мы в ISPsystem резервные копии делали. Часть вторая
Продолжение истории о похождениях отдельной задачи в ISPsystem. Рассказывает руководитель разработки Александр Брюханов. Первая часть здесь.
Лучшее — враг хорошего
Написание резервного копирования или установки и настройки ПО у нас всегда были расстрельными задачами. Когда ставишь что-либо из репозиториев, не можешь быть до конца уверен в результате. Да даже если всё сделано идеально, мэнтейнеры рано или поздно что-нибудь сломают. Что же касается резервных копий: о них вспоминают, когда возникают проблемы. Люди уже на взводе, а если еще что-то идет не так, как они ожидали… ну вы поняли.
Подходов к резервному копированию существует довольно много, но каждый преследует одну цель: сделать процесс как можно более быстрым и при этом максимально дешевым.
Попытка угодить всем
На дворе 2011 год. Прошел не один год с тех пор, как тотальное резервное копирование серверов кануло в Лету. Нет, резервные копии виртуальных серверов делали, делают их и сейчас. Например, на WHD.moscow мне рассказали по-настоящему элегантный способ резервного копирования виртуальных серверов через живую миграцию. И все равно сейчас это не происходит так массово, как 10–15 лет назад.
Мы начали разработку пятой версии наших продуктов на основе собственного фреймворка, в котором была реализована мощная система событий и внутренних вызовов.
Решено было реализовать по-настоящему гибкий и универсальный подход к настройке резервного копирования, чтобы пользователи могли настраивать время, выбирать тип и содержимое резервных копий, раскладывать их в различные хранилища. Да еще мы задумали растянуть это решение на несколько продуктов.
Кроме того, цели резервного копирования тоже могут существенно отличаться. Кто-то делает резервные копии для защиты от сбоев оборудования, кто-то страхуется от потери данных по вине администратора. Наивные, мы хотели угодить всем.
Со стороны наша попытка сделать гибкую систему выглядела так:
Носком правой ноги вы давите окурок. Добавили пользовательские хранилища. Ведь в чем проблема: лить готовые архивы в два места? На самом деле, проблема есть: если архив невозможно залить в одно из хранилищ, можно ли считать резервное копирование успешным?Второй окурок вы давите носком левой ноги. Ломаем копья, реализуя шифрование архивов. Всё просто, пока вы не думаете о том, что должно произойти когда пользователь захочет сменить пароль.
А теперь оба окурка вы давите вместе!
К чему я это? Безумная гибкость породила бесконечное количество сценариев использования, и все их протестировать стало практически невозможно. Поэтому мы решили пойти по пути упрощения. Зачем спрашивать у пользователя, хочет ли он сохранять метаданные, если они занимают несколько килобайт. Или, к примеру, вам правда интересно, какой архиватор мы используем?
Еще одна забавная ошибка: нашелся пользователь, который ограничил время работы резервного копирования с 4:00 до 8:00. Проблема была в том, что сам процесс запускался через планировщик ежедневно в 3:00 (стандартная настройка @daily). Процесс запускался, определял, что в это время ему запрещено работать, и выходил. Резервные копии не делались.
Пишем свой велосипед dar
В середине 10x стал нарастать хайп про кластеры, а следом облака. Появилась тенденция — давайте будем управлять уже не одним сервером, а группой серверов и назовем это облаком :) Коснулось это и ISPmanager.
И, раз у нас появилось много серверов, возродилась идея вынести сжатие данных на отдельный сервер. Как и много лет назад, мы предприняли попытку найти готовое решение. Как ни странно, обнаружили bacula живой, но такой же сложной. Чтобы ей управлять, надо было, пожалуй, отдельную панель писать. И тут мне на глаза попался dar, реализовавший многие идеи, которые когда-то вкладывались в ispbackup. Казалось, вот оно, счастье! Ан нет, опыт! идеальное решение, которое позволит управлять процессом резервного копирования как нам захочется.
В 2014 году решение с использованием dar было написано. Но оно содержало две серьезные проблемы: во-первых, полученные dar архивы можно распаковать только оригинальным архиватором (т. е. самим dar); во-вторых, dar формирует листинг файлов в памяти в XML, мать его!, формате.
Именно благодаря этой утилите я узнал, что выделяя память в Си маленькими блоками (на centos 7 блок должен быть меньше 120 байт), невозможно вернуть её системе, не завершив процесс.
Но в остальном, он был мне очень симпатичен. Поэтому в 2015 году мы решили написать свой велосипедdar — isptar. Как вы, наверное, догадались, был выбран формат tar.gz — довольно легко реализуемый. Со всевозможными PAX headers я разобрался еще когда писал ispbackup.
Надо сказать, что документации по данному вопросу немного. Поэтому в свое время, мне пришлось потратить время на изучение того, как tar работает с длинными именами файлов и большими размерами, ограничения на которые были изначально заложены в tar-формате. 100 байт на длину имени файла, 155 на каталог, 12 байт на десятичную запись размера файла и т. п. Ну да, 640 килобайт хватит всем! Ха! Ха! Ха!
Оставалось решить несколько проблем. Первая — быстрое получение листинга файлов без необходимости полной распаковки архива. Вторая — возможность извлечь произвольный файл, опять же, без полной распаковки. Третья — сделать так, чтобы это был всё еще tgz, который может быть развёрнут любым архиватором. Каждую из этих проблем мы решили!
Как начать распаковку архива с определенного смещения?
Оказывается, gz потоки можно склеивать! Простой скрипт докажет вам это:
cat 1.gz 2.gz | gunzip -
Вы получаете склеенное содержимое файлов без каких-либо ошибок. Если каждый файл писать так, как будто это отдельный поток, то проблема решится. Конечно, это уменьшает степень сжатия, но не очень значительно.
Получение листинга — еще проще.
Давайте положим листинг в конец архива как обычный файл. А в листинг, еще и смещения файлов в архиве запишем (кстати, dar тоже листинг в конце архива хранит).
Почему в конце? Когда вы делаете резервную копию размером в сотни гигабайт, у вас может не оказаться достаточно места для хранения всего архива. Поэтому по мере создания вы сливаете его в хранилище по частям. Самое замечательное, что если вам надо достать один файл, вам необходимы лишь листинг и та часть, которая содержит данные.
Осталась только одна проблема: как получить смещение самого листинга?
Для этого в конец самого листинга я сложил служебную информацию об архиве, включая запакованный размер самого листинга, а в самый конец служебной информации, в виде отдельного gz потока, запакованный размер самой служебной информации (это всего две цифры). Для быстрого получения листинга достаточно прочитать последних несколько байт и распаковать их. Потом прочитать служебную информацию (смещение относительно конца файла мы теперь знаем), а потом и сам листинг (смещение которого мы взяли из служебной информации).
Простой пример листинга. Разными цветами выделены отдельные gz потоки. Соответственно, вначале мы распаковываем красный (просто анализируя последние 20–40 байт). Затем распаковываем 68 байт, содержащих запакованную короткую информацию (выделенную синим). И, наконец, распаковываем еще 6247 байт, чтобы прочитать листинг, реальный размер которого 33522 байта.
etc/.billmgr-backup root#0 root#0 488 dir
etc/.billmgr-backup/.backups_cleancache root#0 root#0 420 file 1487234390 0
etc/.billmgr-backup/.backups_imported root#0 root#0 420 file 1488512406 92 0:1:165:0
etc/.billmgr-backup/backups root#0 root#0 488 dir
etc/.billmgr-backup/plans root#0 root#0 488 dir
…
listing_header=512
listing_real_size=33522
listing_size=6247
header_size=68
Звучит немного заморочено, мне даже пришлось заглянуть в исходник, чтобы вспомнить, как я это делаю. Вы тоже можете взглянуть на исходник isptar, который, как и исходники ispbackup, я выложил на github.
Ну, а история на этом, конечно же, не заканчивается. Можно вечно смотреть на огонь, паркующуюся женщину и на то, как люди при помощи одних костылей пытаются победить другие.