Инкрементальные архивы берегут диск, но расплачиваются цепочкой зависимостей: пока не распакован полный архив и все ступени после него, файла из глубины истории не достать. Полные копии за каждый день восстанавливаются просто, зато место на носителе заканчивается раньше, чем появляется желание их чистить. Утилита rsnapshot снимает противоречие целиком: она держит серию снапшотов, каждый выглядит и восстанавливается как полная копия, а физически на диске лежит только изменение между соседними снапшотами. Механика построена на rsync и жёстких ссылках файловой системы, рутина ротации уходит в cron, настройка сводится к одному текстовому конфигу. Ниже разбор механизма, рабочий конфиг, расписание и честное сравнение с инкрементальными архивами.
Механика жёстких ссылок и общий inode в экономии дискового места
Жёсткая ссылка - это дополнительное имя для того же файла на уровне файловой системы. У файла в ext4 или XFS есть inode, учётная запись со счётчиком ссылок: в ней живут права, владелец, времена модификации и указатели на блоки с содержимым. Когда счётчик выше единицы, файл отвечает сразу на несколько имён, и все имена равноправны. Удаление одного имени лишь уменьшает счётчик, байты покидают диск только после обнуления, поэтому чистка одного снапшота не портит остальные копии того же файла.
Копирование командой cp с ключами -al не переносит содержимое: утилита строит новое дерево каталогов, где каждый файл привязан к прежнему inode. На этом приёме и стоит rsnapshot. Новый снапшот начинается как клон предыдущего на жёстких ссылках, после чего rsync приносит в него изменившиеся файлы.
Арифметика выходит наглядной. Сервер с деревом на 40 ГБ, из которого за сутки меняется 200 МБ, получит дневной снапшот весом около 200 МБ реальных блоков, хотя любой просмотрщик покажет полные 40 ГБ. Неделя истории из семи полных на вид копий займёт примерно 40 ГБ плюс суммарная дельта, а не 280 ГБ семи независимых зеркал.
Подсчёт места таит нюансы. du по каждому снапшоту в отдельности покажет полный размер, утилита не знает о соседях, а один вызов du по корню хранилища посчитает каждый inode единожды, как и df по файловой системе. Общие для соседей файлы легко найти командой find с ключом -samefile:
find /var/cache/rsnapshot -samefile /var/cache/rsnapshot/hourly.0/etc/hostname
Одинаковый inode у файла в соседних снапшотах и есть экономия в чистом виде.
Установка Rsnapshot и синтаксис конфига с табуляцией вместо пробелов
Пакет находится в репозиториях большинства дистрибутивов: в Debian и Ubuntu он устанавливается командой apt install rsnapshot, в Fedora через dnf install rsnapshot, для производных Red Hat Enterprise Linux пакет приезжает из репозитория EPEL, в Arch Linux хватает pacman -S rsnapshot. Конфиг живёт в /etc/rsnapshot.conf, рядом обычно лежит файл с примером и комментариями, с которого удобно начинать. Для копирования с удалённых машин предусмотрена директива cmd_ssh, но локальная схема не требует ни сети, ни ключей.
Главная ловушка синтаксиса - разделители. Поля внутри конфига обязаны разделяться табуляцией, а не пробелами, и rsnapshot принципиален на этот счёт: случайный пробел между директивой и значением превращается в ошибку, которую поймает проверка configtest. Минимальный рабочий набор выглядит так:
config_version 1.2
snapshot_root /var/cache/rsnapshot/
cmd_rsync /usr/bin/rsync
cmd_du /usr/bin/du
cmd_logger /usr/bin/logger
lockfile /var/run/rsnapshot.pid
loglevel 3
logfile /var/log/rsnapshot.log
retain hourly 6
retain daily 7
retain weekly 4
retain monthly 3
exclude /proc/
exclude /sys/
exclude /dev/
exclude /run/
exclude lost+found/
backup /etc/ localhost/
backup /home/ localhost/
backup /var/www/ localhost/
Директива snapshot_root задаёт корень хранилища, снапшоты появятся прямо в нём. Ключевые слова cmd_* указывают пути к rsync, du и logger, lockfile не даёт двум запускам ротации пересечься, loglevel и logfile управляют журналом. По умолчанию rsync получает короткий ключ -a и длинные --delete --numeric-ids --relative --delete-excluded, набор меняют директивой rsync_long_args. Псевдофайловые системы исключаются из копии: их содержимое меняется быстрее, чем читается.
Строка backup описывает точку копирования: сначала источник, затем префикс, под которым он попадёт в снапшот, так /etc/ ляжет в подкаталог localhost/etc. Базы данных копируются отдельной директивой backup_script: скрипт запускается во временном каталоге, и всё, что он там создаст, уходит в снапшот под этим именем. Дамп снимается средствами самой СУБД, потому что сырые файлы работающей базы копируются неконсистентно:
backup_script /usr/local/bin/dump_databases.sh databases/
Содержимое скрипта сводится к вызову pg_dumpall -f databases.sql: утилита дампа пишет файл в текущий каталог.
Интервалы retain и расчёт глубины истории из двадцати снапшотов
Директива retain задаёт имя интервала и число хранимых копий. Интервалы перечисляются строго от частых к редким: hourly выше daily, daily выше weekly, порядок строк в файле обязан это отражать. Имена произвольны, в примерах из документации встречаются alpha, beta и gamma, но привычные hourly и daily удобнее: они совпадают со строками cron и командами запуска.
Глубина истории считается без хитростей. При запуске раз в четыре часа шесть часовых снапшотов покрывают сутки, семь дневных дают неделю, четыре недельных закрывают месяц, три месячных добавляют ещё квартал. Суммарно 20 каталогов, а место на диске равно одной полной копии плюс накопленные дельты.
Каждое имя интервала превращается в команду: rsnapshot hourly создаёт новый часовой снапшот, rsnapshot weekly - недельный. Убрать или добавить уровень просто: строка retain и строка в cron меняются парой, поэтому глубину истории легко подстраивать под реальные запросы из практики "вернуть как было позавчера".
Ротация каталогов при каждом запуске и продвижение копии по уровням
При каждом вызове rsnapshot hourly утилита выполняет одну и ту же последовательность шагов:
- удаляет самый старый каталог hourly.5 вместе с содержимым;
- переименовывает hourly.4 в hourly.5, hourly.3 в hourly.4 и далее по цепочке до сдвига hourly.0 в hourly.1;
- строит новый hourly.0 как дерево жёстких ссылок на файлы hourly.1;
- запускает rsync с ключом --link-dest, который приносит изменившиеся файлы и зеркалит удаления источника;
- пишет отчёт в журнал и снимает lockfile, освобождая путь следующему запуску.
Шаги 3 и 4 выполняет rsync с ключом --link-dest, в старых сборках та же работа делалась связкой cp -al и rsync. Ручной эквивалент одного прохода выглядит так:
rsync -a --delete --link-dest=../hourly.1 /etc/ hourly.0/
Ключ --delete переносит актуальные удаления источника в свежий снапшот, но старые снапшоты продолжают хранить удалённые файлы: история не подчиняется зеркалу, в этом и смысл ротации.
Продвижение между уровнями устроено одинаково. Запуск rsnapshot daily сначала сдвигает дневную цепочку, затем переименовывает самый старый часовой каталог hourly.5 в daily.0. Копия, родившаяся как hourly.0, за сутки добирается до края часовой ветки и вечером уезжает в daily.0, неделей позже попадает в weekly.0, а через месяц уходит в monthly.0. Снапшот не пересоздаётся на новых уровнях - он переименовывается вместе со своими inode. Первый запуск особый: ссылаться не на что, полная копия пишется целиком, поэтому её выполняют вручную в тихое окно, до включения расписания.
Расписание cron и проверочные прогоны configtest до передачи рутины планировщику
Задачи cron разводятся по минутам, чтобы часовые и дневные запуски не сталкивались:
0 */4 * * * /usr/bin/rsnapshot hourly
50 23 * * * /usr/bin/rsnapshot daily
40 23 * * 0 /usr/bin/rsnapshot weekly
30 23 1 * * /usr/bin/rsnapshot monthly
Дневной запуск в 23:50 идёт до полуночного часового, самый старый часовой снапшот успевает уехать в дневную цепочку до того, как очередной часовой запуск удалит его. Недельный назначен на вечер воскресенья, месячный - на первое число. Времена подстраиваются под нагрузку, важен принцип: редкий интервал забирает самую старую копию частого, и минуты решают, произойдёт ли это до удаления.
До передачи рутины планировщику схему прогоняют руками. Команда rsnapshot configtest проверяет синтаксис, rsnapshot -t hourly печатает план действий без выполнения, rsnapshot -V hourly ведёт подробный лог первого реального прогона. После запуска смотрят хвост журнала и df до и после: честный прирост места равен дельте, а не размеру дерева.
Директивы sync_first и use_lazy_deletes меняют ритм. Первая выносит тяжёлую передачу файлов в отдельную команду rsnapshot sync, интервальные вызовы превращает в короткие переименования. Вторая отправляет удаление самого старого снапшота в фон, сокращая паузу между запусками.
Восстановление из любого снапшота без разборки цепочки архивов
Каждый снапшот - обычный каталог, специальные утилиты для чтения не нужны. Один файл возвращается копированием, дерево - вызовом rsync:
cp -a /var/cache/rsnapshot/hourly.2/etc/nginx/nginx.conf /etc/nginx/nginx.conf
rsync -a /var/cache/rsnapshot/daily.3/etc/ /etc/
Самодостаточность - главное отличие от цепочки инкрементальных архивов: восстановление не требует ни полного архива, ни порядка распаковки, повреждённый или случайно удалённый снапшот не мешает остальным. Скорость определяется локальным диском: единичный файл возвращается за секунды, дерево - за время обычного копирования.
Интереснее поведение при ошибках. Файл, испорченный в новой копии неосторожным редактированием, не затрагивает старые снапшоты: редакторы создают новый файл и меняют имя, прежний inode остаётся у старых копий. Удаление файла из одного снапшота тоже безопасно, содержимое держат остальные имена. Одна ситуация требует дисциплины: запись внутрь снапшота "на месте", дозапись перенаправлением вывода или через dd уйдёт в общий inode и проявится во всех связанных копиях. Правило простое: снапшот читают, а правят восстановленную копию.
Сравнение с инкрементальными бэкапами и границы схемы на жёстких ссылках
Классический инкрементальный архив tar со списком изменений снимается двумя командами:
tar -czf /srv/backup/etc-0.tar.gz -g /srv/backup/etc.snar /etc
tar -czf /srv/backup/etc-1.tar.gz -g /srv/backup/etc.snar /etc
Первый вызов пишет полную копию и создаёт файл состояния, второй складывает только изменившиеся файлы со времени прошлой отметки. Место экономится похоже на снапшоты, но восстановление из глубины требует распаковать полный архив и каждую ступень после него в строгом порядке, а повреждение промежуточного архива обесценивает всё записанное позже. У rsnapshot цепочки нет: любой снапшот самодостаточен, восстановление сводится к одному копированию, а проверка отдельной копии - к штатному сравнению каталогов.
Экономия места у схем разная по характеру. Жёсткие ссылки хранят новые версии изменившихся файлов целиком, для /etc, домашних каталогов и кода это дёшево. Для гигантских файлов с малыми правками, дисков виртуальных машин или больших баз выигрывают блок-ориентированные средства: они сохраняют только изменённые участки. По сети rsync передаёт дельту, но в хранилище новая версия ложится целиком, и это честная цена самодостаточности каждой копии.
Границы схемы стоит знать заранее. Жёсткие ссылки не пересекают файловую систему, весь snapshot_root обязан жить на одном томе, разнести часовые и месячные ветки по разным устройствам не получится. Перенос хранилища на другой том превращает экономию в дублирование: копирование между носителями создаёт независимые файлы. Отдельный нюанс - права и владелец: атрибуты хранятся в inode и общи для всех ссылок, когда у файла на источнике меняется только владелец или права, rsync обновляет атрибуты существующего файла на месте, и изменение проявится в каждом снапшоте, где файл не пересоздавался. Для большинства деревьев это незаметно, но при жёстких требованиях к истории прав поведение проверяют заранее.
Все снапшоты делят один носитель, отказ устройства затрагивает историю целиком, поэтому локальная схема не отменяет внешней копии. В своей нише rsnapshot держит редкий баланс: глубокая история, мгновенный доступ к любой точке прошлого и расход места, близкий к инкрементальным архивам. Вечер на конфиг и расписание окупается первым же восстановленным файлом, а понимание механики inode подсказывает, во что обойдётся рост дерева.