Библиотека — Резервное копирование: стратегия, инструменты, проверка восстановления [X]
Шифр
backup-strategy
Разделы
linux tooling
Сверено
2 сентября — в этот день утверждения статьи сверялись с первоисточниками, а не правился текст
Объём
24 483 знака · 15 разделов · чтения около 25 минут

Резервное копирование: стратегия, инструменты, проверка восстановления


Обзор

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

Поэтому статья построена не вокруг инструментов, а вокруг двух вопросов: от чего именно защищает конкретная схема и как убедиться, что она работает. Инструменты — restic и borg — в середине, между моделью угроз и проверкой восстановления, и это отражает их реальный вес: выбор между ними влияет на результат заметно меньше, чем наличие или отсутствие регулярного тестового restore.

Снепшоты уже описаны в базе: ZFS — в FreeBSD: ZFS, Btrfs и LVM — в файловых системах Linux. Здесь они рассматриваются только с одной стороны: почему их недостаточно.

Версии-якоря: restic 0.19.1, borg 1.4.5 (ветка 2.0 на момент написания в бете — 2.0.0b23, для боевого применения upstream её не рекомендует), sanoid/syncoid 2.3.0.

Снепшот, реплика и бэкап — три разные вещи

Их путают постоянно, хотя защищают они от разного.

Снепшот — точка отката внутри той же файловой системы. Он мгновенный, почти бесплатный и отлично спасает от ошибки оператора: снёс не тот каталог — откатился. Но он лежит на том же пуле, что и данные, и разделяет с ними судьбу. Сгорел контроллер, посыпался диск, отформатировали не тот том, хостер удалил VPS — снепшоты уходят вместе с оригиналом. От шифровальщика, добравшегося до root, снепшот тоже не спасает: zfs destroy -r выполняется одной командой.

Реплика — копия на другой машине, поддерживаемая в актуальном состоянии. Она защищает от отказа железа, но не от ошибки: удаление реплицируется вместе со всем остальным, и часто быстрее, чем вы успеваете заметить. Реплика без истории — это RAID, растянутый по сети, а не бэкап.

Бэкап — копия на отдельном носителе, с историей версий и возможностью восстановить состояние на момент в прошлом. Именно история отличает его от реплики, а отдельный носитель — от снепшота.

Отсюда простая проверка схемы по четырём угрозам:

Угроза Снепшот Реплика Бэкап на том же сайте Offsite
Отказ диска нет да да да
Ошибка оператора да нет да да
Шифровальщик с правами root нет нет только immutable только immutable
Пожар, кража, изъятие, потеря доступа к хостеру нет если реплика в другом месте нет да

Ни одна строка не закрывается одним средством, и в этом весь смысл: схема собирается из слоёв, а не выбирается из списка.

3-2-1 и что к нему добавили

Правило 3-2-1 восходит к книге «The DAM Book» фотографа Питера Крога и звучит так: три копии данных, на двух разных типах носителей, одна из них — вне площадки. Оно старое и намеренно грубое, но проходит проверку из таблицы выше лучше любой одиночной меры.

Позже появилась надстройка 3-2-1-1-0: к прежним трём цифрам добавляются «одна копия offline или immutable» и «ноль ошибок при проверке». Стоит понимать её происхождение: это не стандарт и не рекомендация организации по стандартизации, а вендорская формулировка, разошедшаяся из маркетинговых материалов Veeam. Но оба добавленных требования по делу, и оба закрывают ровно те строки таблицы, которые классическое 3-2-1 оставляет открытыми: неудаляемая копия — единственная защита от шифровальщика с правами root, а регулярная проверка — единственный способ отличить работающий бэкап от папки с мусором.

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

restic и borg

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

borg требует, чтобы borg был установлен на обеих сторонах: клиент общается по SSH с процессом borg serve на сервере, и вся работа с репозиторием — чтение индекса, дедупликация, упаковка — распределена между ними. Это даёт эффективность и, что важнее, серверную сторону, которой можно ограничить права клиента. Но это же означает, что положить репозиторий borg в S3 нельзя: там некому запустить borg serve.

restic устроен наоборот: вся логика на клиенте, а хранилище — тупой объектный склад. Отсюда список бэкендов, которого у borg нет:

Бэкенд Схема URL
Локальный каталог /srv/restic-repo
SFTP sftp:user@host:/srv/restic-repo
REST-сервер rest:https://host:8000/
S3 и совместимые s3:https://endpoint/bucket
Backblaze B2 b2:bucket:path
Azure Blob, Google Cloud Storage azure:container:path, gs:bucket:/
Всё остальное через rclone rclone:remote:path

Сводка различий, существенных при выборе:

restic 0.19.1 borg 1.4.5
Требует свой софт на стороне хранилища нет да (borg serve по SSH)
Объектные хранилища да, напрямую нет, только через промежуточную ФС
Шифрование всегда включено задаётся при init, вплоть до none
Где лежит ключ в репозитории, открывается паролем repokey — в репозитории, keyfile — на клиенте
Сжатие формат репозитория 2 (с 0.14.0), --compression off/fastest/auto/better/max zlib/lzma/zstd, задаётся при создании архива
Неудаляемый режим через rest-server --append-only штатный append-only на стороне репозитория или ключа
Освобождение места prune prune, затем отдельно compact

Две ловушки в этой таблице стоят отдельного упоминания.

Первая — режим keyfile в borg. Он кладёт ключ репозитория не в сам репозиторий, а в ~/.config/borg/keys/ на клиенте. Схема безопаснее (пароль без ключа бесполезен), но означает, что при потере клиентской машины репозиторий не открывается вообще, сколько бы вы ни помнили пароль. Ключ в этом режиме — такой же объект резервного копирования, как данные, и хранить его нужно отдельно. Режим repokey (и repokey-blake2, заметно быстрее на процессорах без аппаратного AES) кладёт зашифрованный ключ в репозиторий, и тогда пароля достаточно.

Вторая — borg prune не освобождает место. Он удаляет архивы из индекса, но сегменты остаются на диске до borg compact, и без него репозиторий растёт, несмотря на исправно работающую политику хранения. В restic такого разделения нет: restic forget --prune делает обе фазы.

Рекомендация. Если бэкап уезжает в объектное хранилище — restic, альтернативы фактически нет. Если это ваш собственный второй сервер и есть возможность поставить туда borg — borg, ради append-only, который у него встроенный, а не пристроенный сбоку. Смешивать оба в одной установке смысла нет: две системы вместо одной удваивают объём того, что нужно регулярно проверять.

Оба вывода привязаны к ветке 1.4; в бете 2.0 borg разворачивается ровно в обратную сторону. Репозиторий там работает через библиотеку borgstore, которая помимо file: и rest: умеет sftp:, rclone: и s3:/b2:, — то есть объектное хранилище borg получает, — но штатный append-only из репозитория убран (2.0.0b16), а транспорт ssh:// с borg serve оставлен только для доступа к старым репозиториям borg 1.x: для новых его заменяет rest://, который тоннелируется через SSH. Пока ветка в бете и upstream просит не использовать её на боевых данных, но при переходе на 2.0 выбор между инструментами придётся делать заново.

Репликация ZFS: когда её достаточно

zfs send/receive — механизм из другого класса: он передаёт не файлы, а блоки, знает, что изменилось, со времени предыдущего снепшота, и переносит датасет побайтово, вместе со свойствами. Инкрементальная передача (zfs send -i) стоит ровно столько, сколько изменилось, независимо от числа файлов — обход дерева каталогов не нужен вообще. Для больших датасетов с миллионами мелких файлов это разница на порядок. Механика подробно разобрана в статье про ZFS.

В связке с sanoid и syncoid (2.3.0) это даёт полноценную схему: sanoid держит политику снепшотов локально, syncoid гоняет инкременты на второй хост.

Границы у подхода тоже жёсткие:

  • Приёмник обязан быть ZFS. Отправить датасет в S3 можно только как поток в файл, а поток — это всё или ничего: достать из него один файл нельзя, а повреждение одного байта делает непригодным весь поток. Как формат долговременного хранения zfs send в файл — плохая идея, о чём предупреждает и сам OpenZFS.
  • Восстановление одного файла требует смонтировать снепшот на принимающей стороне. Это просто, но это не restore --include.
  • Гомогенность: схема работает, пока обе стороны ZFS. Стоит появиться Linux-хосту на ext4 — нужен второй инструмент.

Практический вывод: репликация ZFS отлично закрывает «второй сайт» для машин на ZFS, но не закрывает offsite в облако и не заменяет инструмент с файловым восстановлением. На VPS с ZFS разумна связка «syncoid на свой второй хост + restic в объектное хранилище»: разные технологии на разных плечах — это плюс, а не дублирование, потому что общая ошибка не выкосит обе копии сразу.

Где держать пароль репозитория

Задание должно работать без человека, а значит, пароль обязан быть доступен процессу. Наивный ответ — вписать его в unit-файл через Environment= — плох тем, что окружение процесса видно любому, кто может прочитать /proc/<pid>/environ, а сам unit обычно читается всеми.

restic принимает пароль тремя способами: переменная RESTIC_PASSWORD, файл (--password-file, RESTIC_PASSWORD_FILE) и внешняя команда (--password-command, RESTIC_PASSWORD_COMMAND). У borg аналогично — BORG_PASSPHRASE и BORG_PASSCOMMAND. Вариант с командой предпочтителен: секрет не материализуется ни в конфигурации, ни в окружении.

Правильный современный ответ в systemd — механизм credentials. systemd-creds шифрует секрет ключом, привязанным к машине (TPM2 и/или host key), после чего зашифрованный блоб можно хранить хоть в самом unit-файле:

# зашифровать пароль; результат привязан к этой машине
echo -n 'пароль репозитория' | systemd-creds encrypt --name=restic-repo - -
[Service]
Type=oneshot
LoadCredentialEncrypted=restic-repo:/etc/credstore.encrypted/restic-repo
Environment=RESTIC_REPOSITORY=s3:https://s3.example.com/backups
Environment=RESTIC_PASSWORD_FILE=%d/restic-repo
ExecStart=/usr/bin/restic backup --verbose /etc /srv /home

Внутри сервиса секрет появляется файлом в каталоге, на который указывает $CREDENTIALS_DIRECTORY (в unit-файле он же доступен как спецификатор %d), с правами только для этого сервиса, и исчезает вместе с ним. Расшифровать блоб на другой машине не выйдет — это одновременно достоинство и то, о чём нужно помнить: при переезде на новый сервер credential создаётся заново, а исходный пароль должен быть у вас где-то ещё.

Где-то ещё — это менеджер паролей и бумага в другом физическом месте. Пароль от репозитория с бэкапами — единственный секрет в системе, потеря которого не имеет обходного пути: репозиторий без пароля не открывается никакими правами root. Хранить его только на бэкапируемой машине — это ровно та ошибка, от которой бэкап должен был защищать.

Автоматизация

В Linux — service плюс timer. Разбор таймеров есть в статье про systemd, здесь только специфика бэкапа: Persistent=true, чтобы пропущенный из-за выключенной машины запуск отработал при следующей загрузке, и RandomizedDelaySec=, чтобы парк машин не пришёл в хранилище одновременно.

[Unit]
Description=Ночной бэкап в offsite-репозиторий

[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedDelaySec=30m
Persistent=true

[Install]
WantedBy=timers.target

Разумно разделить два задания: ежедневный backup и еженедельные forget --prune и check. Смешивать их в одном юните неудобно — у них разная длительность, разная цена ошибки и разная частота, а падение prune не должно выглядеть как падение бэкапа.

Во FreeBSD роль таймеров играет periodic: скрипт кладётся в /usr/local/etc/periodic/daily/, включается переменной в /etc/periodic.conf, а вывод попадает в ежедневное письмо root — то самое, которое обычно никто не читает и которое стоит начать читать именно из-за бэкапов. Для расписания, не совпадающего с суточным, остаётся обычный cron.

Ретенция

Политика хранения задаётся одинаково по смыслу в обоих инструментах: сколько последних, сколько посуточных, понедельных, помесячных, годовых. У restic это --keep-last, --keep-hourly, --keep-daily, --keep-weekly, --keep-monthly, --keep-yearly и --keep-within <срок>; у borg — те же --keep-daily, --keep-weekly, --keep-monthly, --keep-within, причём -1 означает «без ограничения».

Семантика важнее списка флагов и постоянно понимается неверно. --keep-daily 7 не значит «хранить семь дней». Оно значит: взять последние семь суток, в которых вообще были снапшоты, и оставить в каждой по одному, самому свежему. Если машина месяц стояла выключенной, эти семь снапшотов растянутся на месяц назад — что как раз правильно, но неожиданно для тех, кто читал флаг как срок.

Типичная разумная политика:

restic forget --prune \
  --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --keep-yearly 2

Перед первым применением любой политики — прогон с --dry-run. forget удаляет снапшоты, prune — данные, на которые больше никто не ссылается, и обе операции необратимы. У borg соответственно prune --list --dry-run, а после боевого прогона не забыть borg compact, иначе место не вернётся.

Во что это обходится по объёму — вопрос, на который нет универсального ответа, потому что дедупликация делает его зависящим от характера данных. Ориентир: при таком наборе правил хранится порядка двух десятков снапшотов, но за счёт дедупликации и сжатия репозиторий обычно оказывается ближе к полутора-двум объёмам исходных данных, а не к двадцати. Проверять это нужно на своих данных: restic stats --mode raw-data показывает реальный размер, borg info — то же для репозитория.

Что бэкапить

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

  • /etc целиком. Конфигурация сервисов, fstab, сеть, юниты systemd, sudoers — это часы работы, которые иначе придётся повторить по памяти.
  • Состояние ACME: выданные сертификаты и, главное, ключи учётной записи. Каталог зависит от клиента (/etc/letsencrypt, /var/lib/caddy, /var/lib/acme), см. обратный прокси и TLS. Сертификаты перевыпускаются, но упереться в лимиты выдачи в момент аварийного восстановления — отдельное удовольствие.
  • Список установленных пакетов. Не сами пакеты, а именно список: dpkg --get-selections, pacman -Qqe, pkg query %n. Он занимает килобайты и экономит день.
  • Данные сервисов из /var/lib, если они не в базе: состояние *Arr-стека, метаданные Plex и подобное (см. медиасервер).
  • Сами скрипты бэкапа и их конфигурация — иначе после восстановления не из чего восстанавливать дальше.

Не бэкапить: кэши, /proc, /sys, /tmp, содержимое торрент-каталогов, которое перекачивается, и — отдельным пунктом — сам репозиторий бэкапов, если он смонтирован локально. Последнее звучит абсурдно, но регулярно случается: путь попал под общее правило, и репозиторий начинает рекурсивно копировать сам себя.

Базы данных

Копировать файлы работающей СУБД обычным файловым бэкапом нельзя: обход каталога занимает время, за это время файлы меняются, и в копии оказывается набор кусков из разных моментов. Это не «немного неконсистентно» — это нерабочая база.

Правильных вариантов два.

Логический дамп: pg_dump, mysqldump, sqlite3 .backup. Дамп PostgreSQL внутренне консистентен — он представляет состояние базы на момент запуска команды, независимо от того, сколько потом выполнялся. Дамп переносим между версиями и читается глазами; платой становится время восстановления на больших объёмах.

Атомарный снепшот файловой системы. Здесь важна деталь, которую обычно проговаривают неточно: снепшот всего каталога данных PostgreSQL — валидный бэкап, и документация это прямо разрешает. Восстановленная копия выглядит так, будто сервер аварийно завершился, и при старте проигрывает WAL — это ожидаемое поведение, а не поломка. Но требование одно и жёсткое: снепшот должен быть одновременным для всех задействованных файловых систем. Если данные, табличные пространства и WAL разнесены по разным пулам, а снепшоты делаются по очереди, консистентности нет, и получается ровно тот случай, от которого мы уходили.

Практический вывод для одиночного сервера: дамп в файл, файл — в репозиторий restic или borg. Дедупликация неплохо справляется с тем, что дампы соседних дней похожи, а схема остаётся простой и проверяемой. Снепшоты уровня файловой системы имеет смысл добавлять, когда дамп перестаёт укладываться в окно.

Проверка восстановления

Это единственный раздел, который нельзя пропустить. Всё предыдущее — подготовка к нему.

Проверять нужно на трёх уровнях, и они не заменяют друг друга.

Целостность репозитория. restic check проверяет структуру — снапшоты, деревья, метаданные пакфайлов — но не читает сами данные. Чтобы прочитать, нужен --read-data, а это полная выкачка репозитория со всеми вытекающими по времени и трафику. Компромисс, предусмотренный upstream, — --read-data-subset, который принимает n/t (проверить группу n из t), процент или объём:

restic check                              # структура, быстро
restic check --read-data-subset=1/7       # плюс седьмая часть данных

Семь недель таких проверок покрывают репозиторий целиком, не создавая пиковой нагрузки. У borg аналог — borg check --verify-data.

Отдельно стоит знать, что restic верифицирует данные перед отправкой в репозиторий: сгенерированные файлы проверяются на декодируемость, чтобы не залить в хранилище повреждённое. Отключается это флагом --no-extra-verify, и если вы его включили ради скорости, то проверять репозиторий чтением данных нужно уже не факультативно.

Восстановление файла. Быстрая проверка, которую не жалко делать часто:

restic restore latest --target /var/tmp/restore-test --include /etc/fstab
diff /etc/fstab /var/tmp/restore-test/etc/fstab && echo OK

Она отвечает на вопрос, который check не задаёт: данные не просто целы, а расшифровываются и складываются в файл, идентичный оригиналу.

Учебная тревога. Раз в квартал, целиком, с секундомером:

  1. Взять чистую машину — VM, контейнер, что угодно, лишь бы не ту, с которой снимался бэкап.
  2. Восстановить репозиторий, имея при себе только то, что было бы доступно при реальной аварии: адрес хранилища и пароль. Если для доступа понадобился SSH-ключ, лежавший на погибшем сервере, — тревога уже провалена, и это её главный результат.
  3. Поднять сервис: развернуть конфиги, восстановить базу из дампа, запустить.
  4. Записать время. Не «работает» или «не работает», а сколько заняло. Это и есть ваш реальный RTO, и он обычно неприятно отличается от воображаемого.
  5. Записать, чего не хватило. В первый раз не хватает всегда.

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

Мониторинг: как заметить, что бэкапа нет уже месяц

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

Проверять нужно не «была ли ошибка», а «когда был последний успешный снапшот» — метрику, у которой есть значение и в случае, когда ничего не происходит:

restic snapshots --latest 1 --json | jq -r '.[0].time'

Дальше эта величина превращается в возраст последнего снапшота и попадает в мониторинг с порогом чуть больше периода запуска. Так устроен единственный работающий вид проверки — по отсутствию свежести, а не по наличию ошибки; в терминологии мониторинга это dead man's switch. Для систем с постоянным сбором метрик естественная форма — экспортировать возраст последнего снапшота и повесить на него алерт; связка с Prometheus и Alertmanager — тема отдельной статьи, а на одиночной машине достаточно проверки в periodic/cron, которая пишет в лог и шлёт письмо, когда возраст превысил порог.

Второй слой — состояние самого таймера: systemctl list-timers показывает время следующего и последнего запуска, и выключенный таймер виден сразу.

Сравнение с FreeBSD

Инструменты одни и те же: restic и borg есть в портах и работают одинаково. Различия в обвязке и в том, что даёт базовая система.

Планировщик — periodic поверх cron вместо systemd timers: скрипт в /usr/local/etc/periodic/daily/, переключатель в /etc/periodic.conf, отчёт письмом root. Механизма, аналогичного systemd credentials, в базовой системе нет — пароль хранится в файле с правами 0600, принадлежащем root, и подаётся через BORG_PASSCOMMAND или --password-file.

Зато на FreeBSD с ZFS по умолчанию доступно то, чего в Linux приходится добиваться отдельно: boot environments дают откат системы, не связанный с бэкапом данных, а zfs send — репликацию без файлового обхода. Разумное разделение ролей на такой машине: BE отвечают за откат неудачного обновления, снепшоты — за ошибку оператора, репликация — за отказ железа, restic в облако — за пожар. Каждый слой закрывает свою строку таблицы из начала статьи, и ни один не пытается закрыть чужую.

Источники

  • restic documentation — бэкенды, forget/prune, check --read-data-subset, форматы репозитория и сжатие
  • restic: Working with repositories — что именно проверяет check и чем отличается --read-data
  • Borg documentation — режимы шифрования, prune и compact, append-only
  • Borg: append-only mode — borg config REPO append_only 1, поведение delete/prune и порядок отката по журналу транзакций
  • rest-server — --append-only, --private-repos
  • PostgreSQL: Backup and Restore — консистентность pg_dump, требование одновременности снепшотов, проигрывание WAL при старте
  • systemd-creds(1) — шифрование секретов и LoadCredentialEncrypted=
  • sanoid / syncoid — политики снепшотов и репликация ZFS

Связанные заметки

Из библиотеки института уносят только знания. Читателей здесь не считают: посещение не регистрируется, отметок в издании не остаётся, счётчиков нет ни у одного издания фонда.

Издание пополняется и пересверяется. Текст доступен на условиях CC BY-SA 4.0 — унесённое можно передавать дальше на тех же условиях.