Debian.pro/

Про Debian


Большой мануал: часть 23. Делаем бэкапы.

Эта статья — часть Большого Мануала по настройке lamp-сервера на debian.

Предыдущая часть цикла — Ставим phpmyadmin и делаем его чуть безопаснее.

Следующая часть цикла — Боремся с вирусней на сайтах.


disclaimer — сам скрипт я писал 7 лет назад, если бы писал сейчас — он, возможно, выглядел бы совсем по-другому, код был бы аккуратнее. С другой стороны, главный его плюс именно в том, что все эти 7 лет он работает и каши не просит =)
если лень читать теорию — cttl-F -> TLDR, там установка скрипта

Есть миллион способов делать бэкапы. И ещё примерно полмиллиона способов их не делать.
За долгие 10 лет я перепробовал множество способов резервного копирования. Многие из них подводили в самый неожиданный момент, многие были слишком тяжелыми по ресурсам, некоторые были сложными для понимания или скриптования (сложно было оценивать, сделался ли бэкап, даже глазами, не говоря уже об автоматике).

Давайте для начала перечислю, что перепробовал (точнее, чем пользовался достаточно долго, чтобы оценить).

  • Снапшоты (разного вида, в основном — LVM и btrfs) — слишком накладно по IO. Есть сложности с доставанием из бэкапа отдельного файла. Нужно писать скрипты, чревато ошибками.
  • rdiff-backup — охрененная идея, на бумаге отличная реализация, на практике достать определенное состояние каталога из прошлого ни разу не получилось — постоянно что-то ломалось в метаданных.
  • rsnapshot — прикольный, гибкий. Это его и сгубило. Каждый раз настраивать надоедает.
  • Пробовал множество обвязок над rsync (помимо rsnapshot) — встретил очень много проблем с логикой, некоторые не делают своей работы вообще. А уследить за ними тяжело
  • bacula — работает. Вроде даже данные с ней не терял. Но настраивать её для каждого маленького сервера — тот ещё геморрой

В итоге в какой-то момент я психанул на все эти утилиты. Остановился на связке tar+mysqldump. tar, само собой, не простой, а с инкрементными архивами.
У tar-а в этом месте есть множество минусов — бэкап может быть неконсистентным (если тарится долго — файл1 может отличаться от файла10000000 на час в своём состоянии. Впрочем, этим страдают все file-based утилиты для бэкапов, если вас это не устраивает — то спасут только снапшоты (и снятие копии уже со снапшота)), жрется место как не в себя. Например, если большой файл меняется каждый день — то в каждом архиве этот файл будет целиком. Работает сам tar весьма неторопливо, пару террабайт уже не забэкапишь (впрочем, в век ssd это почти не ощущается). При восстановлении отдельного каталога приходится читать весь архив. Но всё это на практике не имеет смысла против главного плюса — надежности.
Mysqldump тоже не идеальная утилита — там либо скорость, либо консистентность.
Но я рассуждал очень просто — мне нужен был максимально надежный (как в плане создания копии, так и в плане восстановления файлов из неё), максимально универсальный способ забэкапить LAMP-сервер. Там, где есть какие-то особые условия (активная запись, или очень много файлов) — я стараюсь использовать другие решения (в основном, правда, создаю LVM-снапшот, монтирую его и делаю архив, после чего удаляю снапшот, угу). Результатом изысканий табуреточного метода бэкапов и стал скрипт, про который мы поговорим ниже.

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

root@server:~# tar -czg /etc/backup/metadata/${WEEK}.metadata -f /backup/rootfs/${WEEK}/${DATE}.tar.gz --numeric-owner --exclude=/proc/* --exclude=/sys/* --exclude=/dev/* --exclude=/backup/* --exclude=/var/lib/mysql/* /
  • -c = create (бывает ещё x = extract)
  • -z — сжимать архив gzip-ом (впрочем, в самом скрипте я объебался — надо было гнать в pipe и сжимать cli gzip-ом, потому что у меня заложен nice для gzip для одноядерных систем, ну да ладно).
  • -g /etc/backup/metadata/${WEEK}.metadata — указываем, что архив будет инкрементным (ну и дальше указываем, где хранить метадату для инкремента). Тут важно понимать, что если файла с метаданными нет — то архив будет создан с нуля (то есть со всеми файлами), чем мы нагло и пользуемся, каждую неделю в первый создавая новый «базовый» архив со всеми файлами. Все остальные же архивы за неделю будут уже диффом к этому базовому. При этом не забудьте, что инкремент tar-а содержит и отметки об удаленных файлах с момента прошлого запуска.
  • -f /backup/rootfs/${WEEK}/${DATE}.tar.gz — указываем путь до архива на файловой системе.
  • —numeric-owner — uid-ы пользователей сохраняются в архиве в виде uid-ов (а не имён пользователей), что позволит потом взять архив, распаковать его целиком и чрутнуться в него, запустить весь софт. Ну или сделать lxc-контейнер из архива.
  • —exclude=/proc/* — исключаем каталоги, которые бэкапить вредно. Бэкапить /backup в /backup/чтототам.tar.gz ОЧЕНЬ вредно, сами понимаете. Ну а попытка затарить весь /proc приведет к тому, что /proc/kcore радостно сожрет сколько угодно места, например.

  • последний / означает, что мы тарим всю ФС от корня. Тут по вкусу. Я всегда тарю корень, потому что сама система занимает от силы гигабайт 5, а что-то полезное забэкапить можно забыть.

C mysqldump всё ещё проще (ну при условии, что вы создали /root/.my.cnf, как я в какой-то из частей раньше писал):

root@server:~# mysqldump dbname |
gzip -9 > /backup/mysqldumps/${DATE}.sql.gz

Если хочется, чтобы сайт при этом не падал (консистентность при этом может пострадать, хоть и дамп потом, конечно, восстановится):

root@server:~# mysqldump --single-transaction --quick dbname |
gzip -9 > /backup/mysqldumps/${DATE}.sql.gz

Теперь поговорим о восстановлении. Сразу скажу про mysqldump — с ним попроще.
Сугубо в теории восстановление из mysqldump-а выглядит так:

root@server:~# zcat /backup/mysqldumps/${DATE}.sql.gz | mysql dbname

Но это сработает только для чистой базы. Если в базе уже что-то есть — то дамп накатится поверх существующей базы. В этом нужно её дропнуть и создать заново:

root@server:~# mysql -e 'drop database `dbname`;'; mysql -e 'create database `dbname`;'

Бывают ещё случаи, когда дамп очень большой базы не восстанавливается (и вам лень разбираться почему, а база хоть в каком-то виде нужна), возможно из-за duplicate keys, возможно из-за ещё чего. В этом случае можно использовать source на дамп из самого mysql.
Для начала положим дамп туда, где пользователь mysql сможет его прочитать (как, впрочем, и другие пользователи. Если понимаете, что делаете — лучше кладите дамп в /var/lib/mysql).

root@server:~# zcat /backup/mysqldumps/${DATE}.sql.gz > /tmp/dump.sql

Можно и gunzip-ом, само собой, но мне всегда лень вспоминать его ключи (тот же —keep, чтобы исходный файл не удалялся), а zcat-у опции не нужны.
После того, как дамп уютненько расположился в /tmp у всех пользователей на виду, заходим в консоль mysql-сервера (опять же, если .my.cnf настроен, если нет — опция -p в помощь):

root@server:~# mysql

В mysql консоли делаем source:

mysql> SOURCE /tmp/dump.sql;

А дальше уже очень внимательно смотрите, что происходит. Главный плюс source — он не прекращается после первой ошибки. Можно увидеть все ошибки за один проход, поправить их, а потом уже в один заход их исправить (если вообще нужно — на всякие duplicate key пофиг, можно руками потом пофиксить на живой базе). Ну и ещё полезно, что в source практически невозможна ситуация с «не пролез запрос», если речь о действительно огромных таблицах, задампленных одной строкой.

Теперь о таре. Как вообще будут выглядеть копии файловой системы в таре (исходим из того, что вы делаете бэкап раз в сутки). В понедельник мы имеем самый обычный архив, распаковываем файлы из него обычным xvf не заморачиваясь. Во вторник мы имеем уже хитрый архив, в котором написано примерно «удалить файл ХХХ, выкатить новую версию файла УУУ, поменять права на файл ЗЗЗ». Соответственно, если нам нужно состояние каталога за вторник и дальше — придется использовать хитрую конструкцию ниже. Если же нужен отдельный файл за вторник — его, в принципе, можно тоже просто достать через xvf на отдельный архив за вторник.

Но лучше на примерах. Допустим, наш каталог с архивами за прошлую неделю выглядит так (список архивов я взял с одного из реальных бэкапов, чтобы было удобнее):

root@server:~# ls -1 /backup/rootfs
2017-12-18-10-20-02.tar.gz
2017-12-19-10-20-01.tar.gz
2017-12-20-10-20-01.tar.gz
2017-12-21-10-20-01.tar.gz
2017-12-22-10-20-01.tar.gz
2017-12-23-10-20-01.tar.gz
2017-12-24-10-20-01.tar.gz

Допустим, нам нужно распаковать каталог /var/www за понедельник в каталог /restored. Делается легко:

root@server:~# cd /restored; tar -xvf /backup/rootfs/2017-12-18-10-20-02.tar.gz var/www

Вообще здесь я упомяну, что НИКОГДА не стоит распаковывать бэкап в каталог, в котором бэкап должен… мхм… быть распакован, что ли. То есть, если хотите восстановить /var/www — сделайте /var/www2, распакуйте архив туда, а потом уже манипулируйте двумя каталогами (www и www2) до нужного результата. Например, сделайте mv www www.current; mv www2 www. Если не заработает — можно будет быстренько откатить на, хотя бы, текущее состоянии до восстановления архива. Иначе каша получится. Ну и руками совместить файлы из двух каталогов потом будет проще.

Ладно, с лирическим отступлением закончили. Теперь мы захотели посмотреть на этот же каталог за среду. Дела. Распаковать нужно 3 архива по очереди вот таким образом (cd сделать не забудьте только):

root@server:~# for i in 2017-12-18-10-20-02.tar.gz 2017-12-19-10-20-01.tar.gz 2017-12-20-10-20-01.tar.gz; do tar -xjGf "$i" var/www; done

Распакуется каталог из базового архива, а потом к нему применятся изменения, «упакованные» в двух следующих. Здесь же будет полезно напомнить, что metadata-файлы для распаковки архивов не нужны (только для создания следующего инкремента).

Ладно, знаю, утомил всех, хотя прочитать на досуге советую. Поехали в TLDR.
Качаем мой скрипт и «ставим его»:

root@server:~# wget -O /usr/bin/yabackup.sh https://debian.pro/files/backup.sh/backup.sh; chmod +x /usr/bin/yabackup.sh

Качаем пример конфига:

root@server:~# wget -O /etc/backup.conf https://debian.pro/files/backup.sh/backup.conf-example

Добавляем запуск в cron (в моём примере — на 6:00 каждого утра):

0 6 * * * root /usr/bin/backup.sh /etc/backup.conf

Типа всё. Ладно-ладно, расскажу, что там интересного есть.
Самое полезное, пожалуй — он умеет делать tar и mysqldump сразу на удаленную машину, минуя локальную ФС (полезно, если у вас мало места локально), через конструкцию вида tar … | ssh user@host «cat > /file».
Чтобы эта фигня заработала — нужно включить destination_type=»remote» (выключив local, соответственно. В примере у меня 2 строчки) и настроить переменную ssh_command= (например ssh -i /etc/backup/key -o StrictHostKeyChecking=no backupuser@backup.example.net). Только перед отдачей скрипта на откуп крона убедитесь, что ваша ssh_command выполняется (и убедитесь, что не по вашему сфорварженному ключу), а то окажется, что скрипт падает где-то в районе «add new remote ssh host key yes/no».
Есть и какой-то простенький лог — /var/log/backup.log , имеет смысл посматривать туда, особенно в первые запуски. И вообще лучше первые 2 запуска сделать руками, а не кроном, чтобы убедиться, что базовый архив и первый инкремент к нему делаются успешно.
Ну и если скрипт вас заинтриговал — прочитайте README, пример конфига, там вроде всё написано. И welcome с вопросами в комментарии.

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

28.12.2017 byinkvizitor68sl|big-manual

Комментарии (15):

  1. Artem :

    Не скачивает (не найден): https://debian.pro/files/backup.sh/yabackup.sh

    Resolving debian.pro (debian.pro)… 37.140.188.172
    Connecting to debian.pro (debian.pro)|37.140.188.172|:443… connected.
    HTTP request sent, awaiting response… 404 Not Found
    2017-12-28 22:49:41 ERROR 404: Not Found.

  2. ы… спишем на издержки торопливого ребрендинга.

  3. Поправил ссылку

  4. Tony :

    Как обычно, больше, чем ожидалось.
    Натравить конфиг на подмонтированный yandex.disk и готово счастье :)

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

  6. Endy-les :

    А если в начале недели файл с бекапом повредится, то последующие до понедельника работать не будут получается? Или есть какая-то защита от сбоев?

  7. > последующие до понедельника работать не будут получается?
    Получается, что да. Только изменившиеся файлы.

    Но повредить tar-архив не так просто, это же потоковый архив. А что бэкап в принципе сделался — нужно смотреть в логе.

  8. Jungo :

    Я пользуюсь burp
    настраивается все в одном конфиге
    он клиент серверный, в репах есть
    инкремент сжатие дедупликация
    можно подсунуть скрипты для дампа например БД

    добавлять новые хосты не очень удобно

  9. Arp :

    Посмотрите на pigz — сокращение от Parallel Implementation of GZip (параллельная реализация GZip), это полная замена GZip, способная использовать несколько процессоров (нескольких ядер) при сжатии данных, по умолчанию использует все, но это настраиваемо.

  10. Я бы рад, конечно, но:
    «# we have to nice down gzip, if we have only one cpu-kernel»
    =)

    Но в целом скрипт написан так, чтобы у него (с точки зрения debian + base-utils) не было никаких зависимостей.

  11. Иван :

    О! То что искал, просто и надежно.
    Вопрос. Если «инкрементальность» создается на основе файла *.metadata, значит можно спокойно шифровать архивы (например через pgp)? Ну конечно при распаковке их надо будет расшифровать обратно ;)

  12. Да, можно спокойно шифровать. Распаковывать только не забывать по очереди.

  13. Иван :

    Кстати, может скрипт пора на гитхаб закинуть, чтобы люди могли PR слать?

  14. Их же рассматривать придётся, а мне лень =) И тестировать потом ещё.
    Ну и прежде чем выкладывать — надо хотя бы known issues починить.

  15. timsa :

    так же вариант создания
    mkdir -p /opt/backup
    nano /opt/backup/backup.sh

    #!/bin/bash
    dirs=»/home /etc»
    out=»/opt/backup»
    day=$(date +%A-%F)
    hostname=$(hostname -s)
    archive=»$hostname-$day.tgz»

    echo «### Directory backup has been started ###»
    echo «### Creating backup archive ###»
    tar czf $out/$archive $dirs
    echo «### Backup successfully completed ###»
    date +%A-%F-%T
    ls -lh $out

    chmod +x /opt/backup/backup.sh

Написать комментарий