Обзор
Резервное копирование — единственная область администрирования, где работа считается сделанной по признаку, который никто не проверяет. Задание в кроне есть, диск не переполняется, письма об ошибках не приходят — значит, всё хорошо. Выясняется обратное ровно в тот момент, когда восстановление нужно по-настоящему, и выяснять уже поздно.
Поэтому статья построена не вокруг инструментов, а вокруг двух вопросов: от чего именно защищает конкретная схема и как убедиться, что она работает. Инструменты — 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 не задаёт: данные не просто целы, а расшифровываются и складываются в файл, идентичный оригиналу.
Учебная тревога. Раз в квартал, целиком, с секундомером:
- Взять чистую машину — VM, контейнер, что угодно, лишь бы не ту, с которой снимался бэкап.
- Восстановить репозиторий, имея при себе только то, что было бы доступно при реальной аварии: адрес хранилища и пароль. Если для доступа понадобился SSH-ключ, лежавший на погибшем сервере, — тревога уже провалена, и это её главный результат.
- Поднять сервис: развернуть конфиги, восстановить базу из дампа, запустить.
- Записать время. Не «работает» или «не работает», а сколько заняло. Это и есть ваш реальный RTO, и он обычно неприятно отличается от воображаемого.
- Записать, чего не хватило. В первый раз не хватает всегда.
Смысл пятого пункта важнее остальных: учебная тревога проверяет не инструмент, а полноту списка из раздела «что бэкапить». Инструмент почти наверняка исправен. Список — почти наверняка нет.
Мониторинг: как заметить, что бэкапа нет уже месяц
Молчание задания — не признак успеха. Задание может не запускаться вовсе: таймер выключен, диск заполнен, пароль протух, репозиторий недоступен. Ни один из этих случаев не порождает письма, если некому его отправить.
Проверять нужно не «была ли ошибка», а «когда был последний успешный снапшот» — метрику, у которой есть значение и в случае, когда ничего не происходит:
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
Связанные заметки
- FreeBSD — ZFS — снепшоты,
send/receiveи boot environments, на которые опирается схема репликации - Linux — файловые системы: ext4, XFS, Btrfs, LVM — снепшоты Btrfs и LVM и почему они не заменяют бэкап
- Linux — systemd: init, сервисы, таймеры — таймеры и credentials, которыми запускается и вооружается задание
- Обратный прокси и TLS — состояние ACME и ключи учётной записи, которые обязаны попасть в бэкап
- Медиасервер: Transmission + Sonarr/Radarr + Plex — пример установки, где данные и состояние сервисов разнесены и бэкапятся по-разному
- Наблюдаемость: Prometheus, node_exporter, Grafana, Loki — куда отдавать возраст последнего снапшота и как повесить на него алерт
- Восстановление незагружающейся системы — граница, за которой чинить дороже, чем развернуть из бэкапа
- Эксплуатация локальных ИИ-сервисов — веса моделей как кеш, а адаптеры, промпты и базы эмбеддингов как данные
- AEAD: аутентифицированное шифрование, nonce и лимиты — как устроено шифрование репозитория изнутри: nonce в формате файла и обязательство по ключу при шифровании по паролю
- KDF для паролей: почему медленно и с памятью — что происходит с паролем репозитория до того, как он откроет ключ: калибровка scrypt в restic и почему параметры меняются вместе с ключом
Из библиотеки института уносят только знания. Читателей здесь не считают: посещение не регистрируется, отметок в издании не остаётся, счётчиков нет ни у одного издания фонда.
Издание пополняется и пересверяется. Текст доступен на условиях CC BY-SA 4.0 — унесённое можно передавать дальше на тех же условиях.